feat(cowork): experimental support for Microsoft 365 Copilot Cowork custom skills#926
feat(cowork): experimental support for Microsoft 365 Copilot Cowork custom skills#926sergio-sisternes-epam merged 10 commits intomainfrom
Conversation
0b2564d to
087238c
Compare
There was a problem hiding this comment.
Pull request overview
Adds an experimentally-gated cowork install target so APM can deploy SKILL.md files into the Microsoft 365 Copilot Cowork OneDrive folder, including config/CLI support for persisting an override path, lockfile URI handling, and docs/tests.
Changes:
- Introduces a new
coworkTargetProfilewith dynamic user-scope deploy root resolution and experimental flag gating. - Adds Cowork OneDrive path resolution +
cowork://skills/...lockfile encoding/decoding and wires it into integration + uninstall flows. - Adds
apm config set/get/unset cowork-skills-dir, extensive unit coverage, and new Cowork integration documentation + changelog entry.
Reviewed changes
Copilot reviewed 29 out of 30 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/integration/cowork_paths.py |
Implements OneDrive skills dir resolution and lockfile path translation for cowork://. |
src/apm_cli/integration/targets.py |
Adds cowork target, dynamic-root support, and flag-gated target selection. |
src/apm_cli/integration/base_integrator.py |
Extends path validation/partitioning/removal to support cowork:// managed entries. |
src/apm_cli/integration/skill_integrator.py |
Routes skill deployment to resolved_deploy_root for dynamic-root targets. |
src/apm_cli/core/experimental.py |
Registers the cowork experimental flag and hint text. |
src/apm_cli/config.py |
Adds persistent config helpers for cowork_skills_dir and unsetting config keys. |
src/apm_cli/commands/config.py |
Adds CLI UX for cowork-skills-dir and config unset. |
src/apm_cli/install/phases/targets.py |
Handles Cowork resolution errors, explicit-target gating, and project-scope rejection. |
src/apm_cli/install/phases/integrate.py |
Adds warn-only Cowork caps checks (skill count and SKILL.md size). |
src/apm_cli/install/services.py |
Records out-of-tree deployed files via cowork://... and emits once-per-run cowork warnings. |
src/apm_cli/install/template.py |
Plumbs install context into integration services for cowork warnings. |
src/apm_cli/install/context.py |
Adds cowork once-per-run warning guard state. |
tests/unit/integration/test_cowork_paths.py |
Tests resolution precedence + path-safety and lockfile translation. |
tests/unit/integration/test_cowork_target.py |
Tests cowork flag gating, dynamic-root semantics, and targets phase behaviors. |
tests/unit/integration/test_base_integrator.py |
Tests cowork path validation, managed file partitioning, and removal behavior. |
tests/unit/integration/test_skill_integrator.py |
Tests cowork deployment routing and sub-skill promotion behavior. |
tests/unit/install/phases/test_targets_phase.py |
Tests project-scope gate and error handling for cowork resolution failures. |
tests/unit/install/phases/test_integrate_phase.py |
Tests warn-only cap checks for cowork installs. |
tests/unit/install/test_services.py |
Tests lockfile deployed-path encoding and once-per-run cowork warning emission. |
tests/unit/test_config_command.py |
Adds storage + CLI tests for cowork-skills-dir and config unsetting. |
tests/unit/core/test_experimental.py |
Verifies cowork flag registration, defaults, and hint URL shape. |
tests/unit/core/test_scope.py |
Updates expected KNOWN_TARGETS set to include cowork. |
docs/src/content/docs/integrations/cowork.md |
New Cowork integration page (setup, resolution, install behavior, caps, lockfile). |
docs/src/content/docs/reference/experimental.md |
Documents the new cowork experimental flag. |
docs/src/content/docs/reference/cli-commands.md |
Updates CLI reference for --target cowork and cowork-skills-dir config. |
docs/src/content/docs/integrations/ide-tool-integration.md |
Mentions Cowork as a user-scope deployment target (experimental). |
docs/astro.config.mjs |
Adds Cowork page to docs sidebar navigation. |
packages/apm-guide/.apm/skills/apm-usage/commands.md |
Updates the embedded CLI reference skill content for new cowork features. |
CHANGELOG.md |
Adds an Unreleased entry for the experimental Cowork target/config. |
tests/unit/install/phases/__init__.py |
Added/updated as part of the new phase test suite. |
APM Review Panel VerdictDisposition: REQUEST_CHANGES (one required pre-merge fix; all other findings are minor or follow-up) Per-persona findingsPython Architect: This PR introduces APM's first dynamic-root deployment target. The architecture is sound and the implementation is tidy. Key design choices reviewed below. 1. OO / class diagramclassDiagram
direction LR
class TargetProfile {
<<ValueObject / DataClass>>
+name str
+root_dir str
+primitives Dict
+requires_flag Optional[str]
+resolved_deploy_root Optional[Path]
+user_root_resolver Optional[Callable]
+for_scope(user_scope) TargetProfile
+deploy_path(project_root) Path
}
class PrimitiveMapping {
<<ValueObject>>
+subdir str
+extension str
+format_id str
+deploy_root Optional[str]
}
class CoworkPaths {
<<IOBoundary / PureModule>>
+resolve_cowork_skills_dir() Path
+to_lockfile_path(abs, root) str
+from_lockfile_path(lp, root) Path
+is_cowork_path(lp) bool
}
class CoworkResolutionError {
<<Exception>>
}
class BaseIntegrator {
<<AbstractBase>>
+validate_deploy_path(rel, root, targets) bool
+sync_remove_files(root, mf, prefix) dict
+partition_managed_files(mf, targets) dict
}
class SkillIntegrator {
<<ConcreteIntegrator>>
+integrate_package_skills(info, root, ...) IntegrationResult
}
class KNOWN_TARGETS {
<<Registry>>
copilot, claude, cursor, codex, cowork
}
TargetProfile *-- PrimitiveMapping : contains
TargetProfile ..> CoworkPaths : resolver delegates
CoworkPaths ..> CoworkResolutionError : raises
BaseIntegrator ..> CoworkPaths : calls validate+translate
SkillIntegrator --|> BaseIntegrator
KNOWN_TARGETS o-- TargetProfile : entries
class TargetProfile:::touched
class CoworkPaths:::touched
class CoworkResolutionError:::touched
class BaseIntegrator:::touched
classDef touched fill:#fff3b0,stroke:#d47600
2. Execution flow diagramflowchart TD
A["apm install --target cowork --global"] --> B["phases/targets.py: run(ctx)"]
B --> C["resolve_targets(home, user_scope=True, explicit='cowork')"]
C --> D["active_targets_user_scope('cowork')"]
D --> E{_flag_gated cowork?}
E -- "No" --> F["[FS] phases/targets.py: error + SystemExit(1)"]
E -- "Yes" --> G["for_scope(user_scope=True)"]
G --> H["[I/O] cowork_paths.resolve_cowork_skills_dir()"]
H -- "env APM_COWORK_SKILLS_DIR" --> I["[FS] validate_path_segments + expanduser"]
H -- "config cowork_skills_dir" --> J["[I/O] get_cowork_skills_dir() via config cache"]
H -- "macOS auto-detect" --> K["[FS] ~/Library/CloudStorage/OneDrive* glob"]
H -- "Windows auto-detect" --> L["[FS] ONEDRIVECOMMERCIAL / ONEDRIVE env"]
H -- "None (unavailable)" --> M["phases/targets.py: error + SystemExit(1)"]
I & J & K & L --> N["TargetProfile with resolved_deploy_root set"]
N --> O["phases/integrate.py: run(ctx) -- per-package loop"]
O --> P["services.integrate_package_primitives(...)"]
P --> Q{cowork active + non-skill primitives?}
Q -- "Yes, first time" --> R["[LOG] warn once via ctx.logger.warning"]
Q --> S["SkillIntegrator._integrate_native_skill / _promote_sub_skills"]
S --> T["[FS] target.resolved_deploy_root / skill_name / SKILL.md"]
T --> U["services._deployed_path_entry(target_path, project_root, targets)"]
U --> V["cowork_paths.to_lockfile_path(abs, cowork_root)"]
V --> W{"ensure_path_within OK?"}
W -- "Yes" --> X["[LOCK] lockfile entry: cowork://skills/name/SKILL.md"]
W -- "No (PathTraversalError)" --> Y["except Exception: pass -- [BUG] absolute path fallback"]
X --> Z["integrate.py: _check_cowork_caps warn-only"]
3. Design patternsDesign patterns
Issues:
CLI Logging Expert: All user-facing cowork messages route correctly through the
No issues found. DevX UX Expert: The command surface is clean and follows established npm/pip/cargo conventions.
Supply Chain Security Expert: This PR writes user files to a path outside the project tree (OneDrive). The key threat surfaces reviewed:
No new credential leakage surfaces, no token over-scope, no install-time code execution. Auth Expert: Not activated -- the PR touches no file in the fast-path trigger list and changes no authentication behavior, token management, credential resolution, or OSS Growth Hacker: This PR has a strong growth angle that deserves to be called out explicitly in the release narrative.
Side-channel to CEO: This feature is directly aligned with the Lorenzo Storelli enterprise AI Controls prototype (github/agents/discussions/637). When the
CEO arbitrationThe panel is in agreement: the implementation is architecturally sound, security-conscious, and well-tested (141 new tests, 5517 passing). The strategic call to ship this behind One required fix before merge: The Growth Hacker's side-channel is noted: when Required actions before merge
Optional follow-ups
|
1402090 to
45913a2
Compare
The cowork target was registered in the integration layer and gated
behind an experimental flag, but TargetParamType.convert rejected it
at parse time because it was missing from VALID_TARGET_VALUES.
This made the runtime enable-hint in phases/targets.py unreachable.
Add EXPERIMENTAL_TARGETS as a separate parser-layer set so the parser
accepts the token while runtime gating in _flag_gated() and
phases/targets.py continues to enforce the flag. parse_target_arg("all")
expansion is unchanged.
Refs #926
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update — parser-layer fix for
|
…reproofing Rename the experimental cowork feature to copilot-cowork to leave room for future variants (e.g. claude-cowork). Pure rename - no behaviour change. PR #926 is unmerged so no backward-compat shims are needed. User-facing surface: - Experimental flag: cowork -> copilot-cowork - Target name: cowork -> copilot-cowork - Env var: APM_COWORK_SKILLS_DIR -> APM_COPILOT_COWORK_SKILLS_DIR - Config key (CLI): cowork-skills-dir -> copilot-cowork-skills-dir - Config key (store): cowork_skills_dir -> copilot_cowork_skills_dir Internal symbols (also renamed for symmetry with future claude-cowork): - src/apm_cli/integration/cowork_paths.py -> src/apm_cli/integration/copilot_cowork_paths.py - resolve_cowork_skills_dir -> resolve_copilot_cowork_skills_dir - _resolve_cowork_root -> _resolve_copilot_cowork_root - _COWORK_SKILLS_SUBDIR -> _COPILOT_COWORK_SKILLS_SUBDIR - get/set/unset_cowork_skills_dir -> get/set/unset_copilot_cowork_skills_dir - 3 test files renamed via git mv Preserved (concept-level / shared across all future cowork variants): - CoworkResolutionError exception class - Lockfile prefix and URI scheme constants - Cap helpers and warn flags Docs: - docs/src/content/docs/integrations/cowork.md -> docs/src/content/docs/integrations/copilot-cowork.md - Sidebar slug, cross-references, CLI examples, env var, and the apm-usage skill (packages/apm-guide) all updated. Validation: 5592/5592 unit tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename:
|
| Surface | Before | After |
|---|---|---|
| Experimental flag | cowork |
copilot-cowork |
| Target | --target cowork |
--target copilot-cowork |
| Env var | APM_COWORK_SKILLS_DIR |
APM_COPILOT_COWORK_SKILLS_DIR |
| Config key (CLI) | cowork-skills-dir |
copilot-cowork-skills-dir |
| Config key (storage) | cowork_skills_dir |
copilot_cowork_skills_dir |
Internal module/symbols also renamed for symmetry (copilot_cowork_paths.py, resolve_copilot_cowork_skills_dir, etc.). CoworkResolutionError and lockfile prefix/URI constants are kept generic — they describe shared behaviour that any future cowork variant would reuse.
Docs (integrations page, references, sidebar slug, apm-usage skill) updated to match.
Validation: full unit suite green (5592/5592). Branch is now 3 commits ahead of 45913a2.
Pre-merge polish: applied all panel follow-upsPushed Findings addressed in this commit
Already addressed in earlier commits (re-verified clean)
Validation
Out of scope
PR is ready for re-review. |
New target mandatory test requirement:report back with a proof of integration test ran locally across all APM commands and functionalities touching the target, including:
The test results are to be passed to the review panel for approval and posted here as a comment that shows a table of all cases tested above and the results |
Local integration test report —
|
| # | Case | Scope | Result | Evidence |
|---|---|---|---|---|
| 1 | apm install --target copilot-cowork — package with skills primitive |
user (--global) |
PASS | 2 skills deployed to <OneDrive>/Documents/Cowork/skills/<skill-a>/SKILL.md and <skill-b>/SKILL.md; lockfile records cowork://skills/<skill-a> + cowork://skills/<skill-b> |
| 2 | Transitive dependencies installed via cowork target | user (--global) |
PASS | A test package pulled in 41 APM deps including 32 transitive skill files; all deployed to cowork root and tracked in ~/.apm/apm.lock.yaml |
| 3 | instructions / subagents / hooks / MCP primitives via copilot-cowork |
both | N/A — target by design only deploys skills. Other primitives continue to install via their existing targets in the same apm install invocation. |
|
| 4 | Idempotent re-install (no changes) | user | PASS | apm.lock.yaml unchanged -- skipping write on second invocation; no file rewrites |
| 5 | apm uninstall <pkg> — removes lockfile entries |
user (--global) |
PASS | Both cowork://skills/... lockfile entries removed; apm.yml updated |
| 6 | apm uninstall <pkg> — removes deployed files from OneDrive disk |
user (--global) |
PASS (after fix) | [+] Cleaned up 32 integrated skills; ls of <OneDrive>/Documents/Cowork/skills/ confirms both skill directories deleted. This was the bug found on first E2E run; fixed across commits e85534c -> 1446734 -> a8bfec2. |
| 7 | Re-install after uninstall (recovers cleanly) | user | PASS | Skill directories re-deployed with fresh timestamps; no stale state |
| 8 | apm install (project scope, no --global) with --target copilot-cowork |
project | NOT YET TESTED — will run as part of follow-up. The target writes to OneDrive regardless of scope (because OneDrive is a user-level location), so I want to confirm the project-scope behaviour explicitly. | |
| 9 | apm compile produces AGENTS.md correctly with copilot-cowork enabled |
user | NOT YET TESTED — will run as part of follow-up. copilot-cowork does not contribute to AGENTS.md (it only deploys skills, not prompts/instructions); expect zero impact but will verify. |
|
| 10 | Marketplace operations (apm marketplace) with cowork-deployed packages |
user | NOT YET TESTED — will run as part of follow-up. Marketplace operates on packages/registries; the cowork target is purely the deploy destination. No mechanical interaction expected but will validate. | |
| 11 | OneDrive auto-detection on macOS | user | PASS | OneDrive mount auto-detected; <OneDrive>/Documents/Cowork/skills/ resolved without APM_COPILOT_COWORK_SKILLS_DIR override |
| 12 | APM_COPILOT_COWORK_SKILLS_DIR env-var override |
user | PASS | Used in 3 E2E runs to redirect cowork root to /tmp/... for isolation |
| 13 | copilot_cowork_skills_dir config override (apm config set ...) |
user | NOT YET RUN END-TO-END — implementation present at src/apm_cli/integration/copilot_cowork_paths.py:110-124, behaviour identical to env var, covered by unit tests. Will exercise as part of follow-up. |
|
| 14 | Pre-existing skills outside APM are not touched on uninstall | user | PASS — verified across 3 E2E runs: pre-existing user-authored skills retained throughout install + uninstall + re-install cycles |
Bugs found and fixed locally before posting
The local testing surfaced three coupled bugs in the uninstall path that are not covered by unit tests alone — the bugs only manifest in real apm uninstall --global flows. All three are fixed and shipped on this PR:
| Commit | Layer | Root cause |
|---|---|---|
e85534c |
cleanup.py + targets.py |
get_integration_prefixes gated cowork prefix on resolved_deploy_root (always None on the static registry) instead of user_root_resolver |
1446734 |
skill_integrator.py + uninstall guard |
SkillIntegrator.sync_integration prefix-tuple did not include cowork://; _skill_dirs_exist guard checked only local project_root subdirs so cowork-only state never triggered the sync call |
a8bfec2 |
uninstall partition + targets | _buckets["skills"] was always empty for cowork because partition_managed_files uses resolved_deploy_root; even when populated, passing targets=_resolved_targets excluded copilot-cowork from prefix construction inside sync_integration |
Without these fixes, apm uninstall silently left skill directories orphaned on disk in OneDrive forever.
Test artefacts
Test commands used (HEAD a8bfec2):
apm experimental enable copilot-cowork
APM_COPILOT_COWORK_SKILLS_DIR=/tmp/cowork-e2e-$$ \
apm install --target copilot-cowork --global
# verify deploy, lockfile, idempotency
apm uninstall <pkg> --global
# verify on-disk deletion + lockfile cleanup
apm install --target copilot-cowork --global # re-install verification- Unit suite: 5606/5606 passing at
a8bfec2(excluding 3 pre-existing failures unrelated to this PR:test_audit_report.pyPython 3.11 f-string SyntaxError,test_helpers.pysystempythonbinary missing,test_runtime_factory.pysystemllmbinary missing). - Cowork-specific unit + integration regression suite: 13/13 passing.
Follow-up
Running #8, #9, #10, #13 next, then re-rebasing onto the latest main and re-running the unit suite. I will post a complete final matrix in a follow-up comment. PR remains in this state until then.
The cowork target was registered in the integration layer and gated
behind an experimental flag, but TargetParamType.convert rejected it
at parse time because it was missing from VALID_TARGET_VALUES.
This made the runtime enable-hint in phases/targets.py unreachable.
Add EXPERIMENTAL_TARGETS as a separate parser-layer set so the parser
accepts the token while runtime gating in _flag_gated() and
phases/targets.py continues to enforce the flag. parse_target_arg("all")
expansion is unchanged.
Refs #926
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
a8bfec2 to
53d5b62
Compare
…reproofing Rename the experimental cowork feature to copilot-cowork to leave room for future variants (e.g. claude-cowork). Pure rename - no behaviour change. PR #926 is unmerged so no backward-compat shims are needed. User-facing surface: - Experimental flag: cowork -> copilot-cowork - Target name: cowork -> copilot-cowork - Env var: APM_COWORK_SKILLS_DIR -> APM_COPILOT_COWORK_SKILLS_DIR - Config key (CLI): cowork-skills-dir -> copilot-cowork-skills-dir - Config key (store): cowork_skills_dir -> copilot_cowork_skills_dir Internal symbols (also renamed for symmetry with future claude-cowork): - src/apm_cli/integration/cowork_paths.py -> src/apm_cli/integration/copilot_cowork_paths.py - resolve_cowork_skills_dir -> resolve_copilot_cowork_skills_dir - _resolve_cowork_root -> _resolve_copilot_cowork_root - _COWORK_SKILLS_SUBDIR -> _COPILOT_COWORK_SKILLS_SUBDIR - get/set/unset_cowork_skills_dir -> get/set/unset_copilot_cowork_skills_dir - 3 test files renamed via git mv Preserved (concept-level / shared across all future cowork variants): - CoworkResolutionError exception class - Lockfile prefix and URI scheme constants - Cap helpers and warn flags Docs: - docs/src/content/docs/integrations/cowork.md -> docs/src/content/docs/integrations/copilot-cowork.md - Sidebar slug, cross-references, CLI examples, env var, and the apm-usage skill (packages/apm-guide) all updated. Validation: 5592/5592 unit tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Local integration test report —
|
| # | Case | Result | Evidence |
|---|---|---|---|
| 1 | install (user --global), package with skills |
PASS | 2 skills deployed, lockfile records cowork://skills/... |
| 2 | install with transitive deps | PASS | 41 deps + 32 transitive skills tracked in lockfile |
| 3 | other primitive types via copilot-cowork |
N/A by design — target deploys only skills |
|
| 4 | idempotent re-install | PASS | apm.lock.yaml unchanged -- skipping write |
| 5 | uninstall removes lockfile entries | PASS | cowork://... entries cleared |
| 6 | uninstall removes deployed skills on disk | PASS (after fixes e2f541a -> 44b7da6 -> 53d5b62) |
[+] Cleaned up N integrated skills confirmed by ls |
| 7 | re-install after uninstall | PASS | fresh deploy timestamps |
| 8 | install without --global (project scope) |
PASS | Guard fires at install: [x] The 'copilot-cowork' target requires --global (user scope). Run: apm install --target copilot-cowork --global — exit 1, deploy dir never created, zero cowork pollution |
| 9 | apm compile with cowork active |
PASS | AGENTS.md generated cleanly (5222 bytes); zero "cowork" references in output. Cowork is a deploy target only and contributes nothing to compiled context — confirmed by inspection |
| 10 | marketplace ops with cowork-deployed packages | PASS | apm marketplace list/browse/add all run cleanly; marketplace state lives in ~/.apm/marketplaces.json and is fully orthogonal to cowork lockfile entries — no cross-talk |
| 11 | OneDrive auto-detection (macOS) | PASS | mount auto-resolved without override |
| 12 | APM_COPILOT_COWORK_SKILLS_DIR env-var override |
PASS | exercised in every E2E run |
| 13 | apm config set copilot-cowork-skills-dir end-to-end |
PASS | set / get / install resolution / unset all behave correctly. Config-only resolution (env var unset) deploys to the configured path |
| 14 | pre-existing skills outside APM untouched | PASS | verified across all E2E runs |
Test environment
- Branch HEAD:
af48756(rebased ontoupstream/main5c0976b, 8 commits replayed cleanly, 6 conflicts resolved acrossCHANGELOG.md, two doc files,services.py,template.py, two test files) - Unit suite: 5753 / 5753 passing, 26 subtests, no regressions from
main - CI: all checks green on Linux (CI / Deploy Docs / CodeQL)
- Cowork-specific regression suite: 13 / 13 passing
- Real OneDrive cowork dir: empty before, during, after — verified
Notes / minor observations surfaced during testing
These are not blockers for this PR (none are introduced by copilot-cowork), but worth flagging:
--globaluninstall scope hint:apm uninstall <pkg> --globalinvoked from a subdirectory with a localapm.ymloperates on~/.apm/apm.ymlwith only an[i]info log. Worth strengthening to a[!]warning + confirmation prompt in a follow-up (pre-existing, repo-wide behaviour).- Config key casing: CLI accepts
copilot-cowork-skills-dir(hyphens); JSON storescopilot_cowork_skills_dir(underscores).apm config getrejects the underscore form. Minor UX nit. - 50-skill cap is advisory-only by design — it warns but does not fail the install (documented behaviour).
Verdict
All cells in the matrix accounted for. Three bugs found locally (uninstall path) were fixed and shipped on this PR before this report; no further functional gaps observed. Ready for panel review.
…ustom skills (#913) Adds a first-class, experimentally-gated 'cowork' target so APM users on the Microsoft 365 Copilot Frontier preview can deploy their skills to Cowork (https://learn.microsoft.com/microsoft-365/copilot/cowork) with a single 'apm install --target cowork --global'. Behind 'apm experimental enable cowork'. Skills only; non-skill primitives skipped with one summary warning. User scope only; project-scope rejected. Path-safety guards on the OneDrive resolver and all lockfile path I/O. Caps (50 skills / 1 MiB SKILL.md) are warn-only. Resolution order for the OneDrive skills directory: APM_COWORK_SKILLS_DIR env > apm config cowork-skills-dir > macOS CloudStorage glob > Windows ONEDRIVECOMMERCIAL/ONEDRIVE > none. Adds 'apm config set/get/unset cowork-skills-dir' (set gated on flag; get/unset always available as a safety valve). Lockfile entries use a synthetic 'cowork://' URI scheme translated at I/O boundaries. Closes #913. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The cowork target was registered in the integration layer and gated
behind an experimental flag, but TargetParamType.convert rejected it
at parse time because it was missing from VALID_TARGET_VALUES.
This made the runtime enable-hint in phases/targets.py unreachable.
Add EXPERIMENTAL_TARGETS as a separate parser-layer set so the parser
accepts the token while runtime gating in _flag_gated() and
phases/targets.py continues to enforce the flag. parse_target_arg("all")
expansion is unchanged.
Refs #926
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…reproofing Rename the experimental cowork feature to copilot-cowork to leave room for future variants (e.g. claude-cowork). Pure rename - no behaviour change. PR #926 is unmerged so no backward-compat shims are needed. User-facing surface: - Experimental flag: cowork -> copilot-cowork - Target name: cowork -> copilot-cowork - Env var: APM_COWORK_SKILLS_DIR -> APM_COPILOT_COWORK_SKILLS_DIR - Config key (CLI): cowork-skills-dir -> copilot-cowork-skills-dir - Config key (store): cowork_skills_dir -> copilot_cowork_skills_dir Internal symbols (also renamed for symmetry with future claude-cowork): - src/apm_cli/integration/cowork_paths.py -> src/apm_cli/integration/copilot_cowork_paths.py - resolve_cowork_skills_dir -> resolve_copilot_cowork_skills_dir - _resolve_cowork_root -> _resolve_copilot_cowork_root - _COWORK_SKILLS_SUBDIR -> _COPILOT_COWORK_SKILLS_SUBDIR - get/set/unset_cowork_skills_dir -> get/set/unset_copilot_cowork_skills_dir - 3 test files renamed via git mv Preserved (concept-level / shared across all future cowork variants): - CoworkResolutionError exception class - Lockfile prefix and URI scheme constants - Cap helpers and warn flags Docs: - docs/src/content/docs/integrations/cowork.md -> docs/src/content/docs/integrations/copilot-cowork.md - Sidebar slug, cross-references, CLI examples, env var, and the apm-usage skill (packages/apm-guide) all updated. Validation: 5592/5592 unit tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Apply remaining APM Review Panel follow-ups on top of the rename: P2 (perf): hoist resolve_copilot_cowork_skills_dir() out of the per-path loop in BaseIntegrator.sync_remove_files. Lazy resolution on first cowork:// path encountered, cached for the rest of the loop. Zero cost when no cowork:// paths are present. P3 (UX): Linux-specific error wording when the copilot-cowork resolver returns None. Linux has no OneDrive auto-detection by design; the previous message implied detection had failed. macOS / Windows wording preserved. P4 (visibility): emit a one-time [!] warning from sync_remove_files when copilot-cowork:// lockfile entries are encountered but the resolver returns None. Previously these orphans were silently skipped, leaving stale entries in apm.lock with no user signal. Adds an optional logger= kwarg with a _rich_warning fallback so existing call sites need no change. 13 new unit tests cover the three behaviours. Full unit suite: 5603 passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
apm uninstall left cowork-deployed skills on disk in OneDrive. Subsequent apm install runs did not clean them up either, so stale skill directories accumulated indefinitely. Two coupled defects in the cleanup pipeline: 1. get_integration_prefixes (integration/targets.py) gated the 'cowork://skills/' allowed-prefix on resolved_deploy_root, but that attribute is transient per-install state and is always None on the static KNOWN_TARGETS registry instance that cleanup uses (targets=None). Replaced with the capability flag user_root_resolver, which IS set on the static definition. The normal install path is unaffected because per-install targets also have a non-None user_root_resolver. 2. remove_stale_deployed_files (integration/cleanup.py) computed project_root / 'cowork://skills/...' for cowork lockfile entries, producing a nonsensical filesystem path that always failed .exists() and was silently classified as 'already gone'. Added explicit cowork:// handling: resolves the OneDrive root once (lazy, cached for the rest of the call), uses from_lockfile_path to translate the URI, then deletes the real file. Edge cases: - Cowork root resolves but file is gone -> idempotent no-op, lockfile entry still removed. - Cowork root cannot be resolved (no env var, no config, Linux without auto-detect) -> file NOT deleted, lockfile entry NOT removed (so a later configured install can clean it up), one- time [!] warning naming the count + recovery commands. - from_lockfile_path raises (containment violation, malformed URI) -> entry counted as failed, one-time warning, lockfile entry retained. 12 new unit tests cover the two fixes plus an integration-style regression test for the original reproducer (drawio uninstall + reinstall, real temp cowork root, assert skill dir is gone). Full unit suite: 5614 passed (up from 5603). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rator The previous fix (e85534c) targeted remove_stale_deployed_files, but apm uninstall routes through SkillIntegrator.sync_integration, which had two coupled gaps that left cowork skills orphaned on disk: 1. Prefix mismatch in skill_integrator.sync_integration: the prefix tuple only contained local skill dirs (.github/skills/, etc.), so cowork://skills/* lockfile entries failed startswith() and were silently skipped. Now extends the prefix tuple with COWORK_LOCKFILE_PREFIX when cowork:// entries are present and resolves them via lazy resolve_copilot_cowork_skills_dir() + from_lockfile_path translation. Edge cases mirror cleanup.py: resolver None -> one-time warn + skip, translation error -> counted as error, missing file -> idempotent. 2. Guard bypass in commands/uninstall/engine.py: _skill_dirs_exist only checked local project_root subdirs, so when only cowork entries existed, sync_integration was never called at all. Guard extended to also fire when the skills bucket contains cowork:// entries. Adds 9 new tests (7 unit + 2 integration regression). Full unit suite 5622 passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two coupled gaps in _sync_integrations_after_uninstall caused apm uninstall to silently skip cowork skill deletion even after the SkillIntegrator was updated in 1446734: Gap 1: The _has_cowork_skills guard checked _buckets["skills"] for cowork:// paths, but partition_managed_files() never routes cowork:// URIs there. The function uses resolved_deploy_root to detect dynamic-root targets; the static KNOWN_TARGETS entry for copilot-cowork always has resolved_deploy_root=None (it is set only after for_scope() resolves OneDrive at install time). Result: skill bucket was always empty, guard always returned False. Gap 2: Even if cowork paths had reached sync_integration, they would have failed the startswith(skill_prefix_tuple) guard inside SkillIntegrator.sync_integration because _resolved_targets=[copilot] produces skill_prefix_tuple=('.copilot/skills/',) -- no cowork prefix. The COWORK_LOCKFILE_PREFIX is only added when a target with user_root_resolver is present, which requires copilot-cowork to be in the source targets list. Fix (engine.py, _sync_integrations_after_uninstall): - Scan sync_managed directly for cowork:// paths (bypasses partition). - Merge found paths into _buckets["skills"] before the sync call. - Pass targets=None when cowork entries are present, so sync_integration uses KNOWN_TARGETS as source (which includes copilot-cowork with user_root_resolver set, causing COWORK_LOCKFILE_PREFIX to be added to skill_prefix_tuple). Verified end-to-end: apm uninstall drawio --global now deletes cowork://skills/drawiodiagram-ops and cowork://skills/drawiogenerate-diagram from disk and prints "Cleaned up 2 integrated skills". Unit suite: 5606 passed (1239 integration/install), 0 new failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Linux emits "Cowork has no auto-detection on Linux." while macOS emits "no OneDrive path detected" from _resolve_copilot_cowork_root. Tests hard-coded the macOS variant and broke CI on Linux. Accept either form. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
af48756 to
371b8e0
Compare
APM Review Panel VerdictDisposition: REQUEST_CHANGES (two required pre-merge fixes; the rest of the implementation is sound and merge-ready once these are addressed) Per-persona findingsPython Architect: This is a major architectural change: 1. OO / class diagramclassDiagram
direction LR
class TargetProfile {
<<ValueObject>>
+name str
+root_dir str
+primitives dict
+user_root_dir str
+user_supported bool
+requires_flag str
+user_root_resolver Callable
+resolved_deploy_root Path
+deploy_path(project_root, *parts) Path
+for_scope(user_scope) TargetProfile
+supports(primitive) bool
}
class PrimitiveMapping {
<<ValueObject>>
+name str
+subdir str
+deploy_root str
}
class BaseIntegrator {
<<Abstract>>
+validate_deploy_path(rel_path, project_root, targets) bool
+sync_remove_files(project_root, managed_files, prefix, targets, logger) dict
+partition_managed_files(managed_files, source) dict
+cleanup_empty_parents(deleted, stop_at)
}
class SkillIntegrator {
+integrate_package_skill(pkg, project_root, targets, managed_files) IntegrationResult
+sync_integration(apm_package, project_root, managed_files) dict
}
class CopilotCoworkPaths {
<<IOBoundary>>
+resolve_copilot_cowork_skills_dir() Path
+to_lockfile_path(absolute, cowork_root) str
+from_lockfile_path(lockfile_path, cowork_root) Path
+is_cowork_path(lockfile_path) bool
COWORK_URI_SCHEME = "cowork://"
COWORK_LOCKFILE_PREFIX = "cowork://skills/"
}
class CoworkResolutionError {
<<Exception>>
}
class Config {
<<IOBoundary>>
+get_copilot_cowork_skills_dir() str
+set_copilot_cowork_skills_dir(path) void
+unset_copilot_cowork_skills_dir() void
}
class ExperimentalFlag {
<<ValueObject>>
+name str
+description str
+default bool
+hint str
}
TargetProfile *-- PrimitiveMapping : primitives
SkillIntegrator --|> BaseIntegrator
TargetProfile ..> CopilotCoworkPaths : user_root_resolver calls
CopilotCoworkPaths ..> CoworkResolutionError : raises
CopilotCoworkPaths ..> Config : reads
BaseIntegrator ..> CopilotCoworkPaths : URI translation
class TargetProfile:::touched
class CopilotCoworkPaths:::touched
class BaseIntegrator:::touched
class SkillIntegrator:::touched
class Config:::touched
class ExperimentalFlag:::touched
classDef touched fill:#fff3b0,stroke:#d47600
note for CopilotCoworkPaths "Adapter: absolute path <-> cowork:// URI"
note for TargetProfile "Strategy: user_root_resolver callable\nenables dynamic-root targets without\nchanging BaseIntegrator"
2. Execution flow diagramflowchart TD
A["apm install --target copilot-cowork --global"]
B["phases/targets.py::run()"]
C{"_flag_gated(copilot-cowork)?"}
D["_resolve_targets(project_root, user_scope=True)"]
E["TargetProfile.for_scope(user_scope=True)"]
F["[FS] user_root_resolver() -> _resolve_copilot_cowork_root()"]
G["[FS] copilot_cowork_paths::resolve_copilot_cowork_skills_dir()"]
H{"env APM_COPILOT_COWORK_SKILLS_DIR set?"}
I["[FS] config::get_copilot_cowork_skills_dir()"]
J{"macOS / Windows?"}
K["[FS] ~/Library/CloudStorage/OneDrive* glob"]
L["resolved_deploy_root = Path"]
M["phases/integrate.py::run()"]
N["services::integrate_package_primitives()"]
O{"target.resolved_deploy_root is not None?"}
P["[FS] skill_integrator: target_skill_dir = resolved_deploy_root/skill_name"]
Q["[FS] shutil.copytree -> OneDrive path"]
R["services::_deployed_path_entry()"]
S{"path.relative_to(project_root) OK?"}
T["[LOCK] to_lockfile_path() -> cowork://skills/..."]
U["[LOCK] apm.lock.yaml updated"]
V["phases/integrate.py::_check_cowork_caps() warn-only"]
A --> B --> C
C -- "flag ON" --> D --> E --> F --> G
G --> H
H -- "yes" --> L
H -- "no" --> I
I -- "set" --> L
I -- "not set" --> J
J -- "macOS" --> K --> L
J -- "Windows" --> L
L --> M --> N --> O
O -- "yes (cowork)" --> P --> Q --> R
R --> S
S -- "ValueError (outside tree)" --> T
T --> U --> V
Design patterns
CLI Logging Expert: Overall output architecture is correct -- new paths use
All other messages are well-formed: exact counts, names the thing, includes the fix. DevX UX Expert: The new One blocking UX issue: in Secondary (non-blocking): Docs ( Supply Chain Security Expert: Security posture is strong. All path resolution uses One required security fix: from apm_cli.utils.path_security import validate_path_segments, PathTraversalError
try:
validate_path_segments(path, context="copilot-cowork-skills-dir config")
except PathTraversalError as exc:
raise ValueError(f"Invalid path: {exc}") from excAdd this before the Documentation gap (follow-up, not blocking): macOS OneDrive glob results are filesystem-local and not a realistic attacker surface at user-scope. Windows env vars are correctly validated before construction. Auth Expert: Not activated -- the PR adds a file-deployment target routed through locally-synced OneDrive paths; no auth tokens, credential resolution, OSS Growth Hacker: The "APM's first target outside the IDE" milestone is a genuine story beat. The positioning in the PR body -- "one manifest now governs both your coding agent and your enterprise AI assistant" -- is sharp and repeatable. The experimental gate is the right call: it protects adoption credibility (no broken first-run for M365 users on non-OneDrive machines). Two growth observations fed to CEO:
CEO arbitrationThe panel reached broad agreement: this is a well-architected, well-tested addition (+141 tests, +5635 LOC) with a clean security posture. The two blocking issues are unambiguous: (1) missing Required actions before merge
Optional follow-ups
|
|
Hi @danielmeppiel — quick status update. All panel feedback from the pre-merge review has been applied, and the branch is now rebased onto the latest Addressed in this round:
Current state:
Ready for another look whenever you have a moment. Thanks! |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Promotes [Unreleased] to [0.10.0] - 2026-04-27. Each PR since v0.9.4 gets one 'so what' line: - #926 Microsoft 365 Cowork target ships impl - #790 marketplace authoring CLI (init, package add/set, build, check, outdated, doctor, publish) -- collapsed from 20+ bullets to one - #722 marketplace plugin -> package rename + --help sectioning -- collapsed - #980 README 'Coming from npx skills add' conversion block - #981 docs auto-deploy on tag push (real fix for the #953 attempt) - #985 pr-description-skill evals suite - #984 pr-description-skill mermaid hardening - #989 cowork sys.platform mock for Windows CI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
Adds a first-class, experimentally-gated
coworktarget so APM users on the Microsoft 365 Copilot Frontier preview can deploy their skills to Cowork (Microsoft docs) with a singleapm install --target cowork --global. Today they copySKILL.mdfiles into OneDrive by hand.Behind
apm experimental enable cowork(off by default). User scope only; project-scope rejected. Skills only; non-skill primitives skipped with one summary warning. Path-safety guards (validate_path_segments+ensure_path_within) on the OneDrive resolver and allcowork://lockfile path I/O. Caps (50 skills / 1 MiBSKILL.md) are warn-only.OneDrive resolution order:
APM_COWORK_SKILLS_DIRenv var >apm config cowork-skills-dir> macOS~/Library/CloudStorage/OneDrive*glob > Windows%ONEDRIVECOMMERCIAL%then%ONEDRIVE%> none. Linux is env/config only by design (no auto-detect).Also adds
apm config set/get/unset cowork-skills-dirfor persistent path overrides —setgated on thecoworkflag;getandunsetalways available as a safety valve.This is APM's first target outside the IDE — one manifest now governs both your coding agent and your enterprise AI assistant. Enable the flag, try it, and tell us what breaks.
Reviewed by the APM Expert Review Panel (Python Architect, CLI Logging, DevX UX, Supply Chain Security, OSS Growth Hacker, CEO arbitration). Verdict: REQUEST-CHANGES with 7 required fixes — all landed in this PR. 10 lower-severity items deferred per CEO ruling; security follow-ups are hard gates on promoting the flag out of experimental.
Fixes #913
Type of change
Testing
Full unit suite: 5517 pass / 0 fail (+141 new tests vs
upstream/mainbaseline). Targeted suites for cowork paths, target gating, install phases, and config command all green. Docs build (cd docs && npm run build) green.