@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.35 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/bin/adl +2 -2
  2. package/bin/reconcile +3 -11
  3. package/bin/setup +15 -15
  4. package/package.json +1 -1
  5. package/packages/agent-hooks/deploy-profile.json +1 -1
  6. package/packages/agent-hooks/package.json +1 -1
  7. package/packages/agent-hooks/src/main.ts +23 -2
  8. package/packages/agent-hooks/src/pre-tool-use/handler.ts +2 -2
  9. package/packages/agent-hooks/src/rules/impl/adl-build-run-role.ts +116 -0
  10. package/packages/agent-hooks/src/rules/impl/adl-git-user-override-block.ts +159 -0
  11. package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +22 -12
  12. package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +9 -9
  13. package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +6 -5
  14. package/packages/agent-hooks/src/rules/impl/adl-run-required.ts +11 -5
  15. package/packages/agent-hooks/src/rules/impl/adl-workitem-role.ts +18 -5
  16. package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +30 -25
  17. package/packages/agent-hooks/src/rules/impl/git-core.ts +4 -8
  18. package/packages/agent-hooks/src/rules/impl/git-target.ts +5 -0
  19. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +4 -2
  20. package/packages/agent-hooks/src/rules/impl/tool-frontend.ts +230 -11
  21. package/packages/agent-hooks/src/rules/registry.ts +11 -2
  22. package/packages/agent-hooks/src/rules/shell/git-identity.ts +1 -1
  23. package/packages/agent-hooks/src/rules/shell/invocation.ts +14 -2
  24. package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +10 -7
  25. package/packages/agent-settings/assets/TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md +2 -2
  26. package/packages/agent-settings/package.json +1 -1
  27. package/packages/agent-settings/src/deploy/agents-deploy.ts +19 -1
  28. package/packages/agent-settings/src/deploy/index.ts +1 -0
  29. package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +4 -6
  30. package/packages/agent-skills/assets/code-review/SKILL.md +84 -66
  31. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +23 -2
  32. package/packages/agent-skills/assets/diagnosing-bugs/references/FEEDBACK-LOOP-TACTICS.md +1 -1
  33. package/packages/agent-skills/assets/explore-codebase/SKILL.md +3 -2
  34. package/packages/agent-skills/assets/finalize-feature/SKILL.md +63 -0
  35. package/packages/agent-skills/assets/fix-ci/SKILL.md +58 -0
  36. package/packages/agent-skills/assets/handoff/SKILL.md +1 -1
  37. package/packages/agent-skills/assets/implement-feature/SKILL.md +150 -0
  38. package/packages/agent-skills/assets/implement-feature/references/briefs.md +94 -0
  39. package/packages/agent-skills/assets/implement-item/SKILL.md +71 -0
  40. package/packages/agent-skills/assets/mechanics/SKILL.md +38 -0
  41. package/packages/agent-skills/assets/plan-feature/SKILL.md +141 -0
  42. package/packages/agent-skills/assets/prepare-promotion/SKILL.md +90 -0
  43. package/packages/agent-skills/assets/ralplan/SKILL.md +149 -0
  44. package/packages/agent-skills/assets/refactor-safely/SKILL.md +4 -3
  45. package/packages/agent-skills/assets/report-bug/SKILL.md +72 -0
  46. package/packages/agent-skills/assets/review-change/SKILL.md +82 -0
  47. package/packages/agent-skills/assets/review-plan/SKILL.md +124 -0
  48. package/packages/agent-skills/assets/triage-workitems/SKILL.md +99 -0
  49. package/packages/agent-skills/assets/ultragoal/SKILL.md +77 -0
  50. package/packages/agent-skills/assets/validate-design/SKILL.md +64 -0
  51. package/packages/agent-skills/assets/verify-item/SKILL.md +71 -0
  52. package/packages/agent-skills/assets/wayfinder/SKILL.md +1 -1
  53. package/packages/agent-skills/assets/workitems-process/SKILL.md +145 -0
  54. package/packages/agent-skills/assets/workitems-process/references/ado-and-git.md +227 -0
  55. package/packages/agent-skills/assets/workitems-process/references/evidence.md +74 -0
  56. package/packages/agent-skills/assets/workitems-process/references/workitem-states.md +92 -0
  57. package/packages/agent-skills/assets/write-feature/SKILL.md +94 -0
  58. package/packages/agent-skills/dist/deploy.d.ts +52 -0
  59. package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
  60. package/packages/agent-skills/dist/deploy.js +218 -7
  61. package/packages/agent-skills/dist/deploy.js.map +1 -1
  62. package/packages/agent-skills/dist/index.d.ts +1 -1
  63. package/packages/agent-skills/dist/index.d.ts.map +1 -1
  64. package/packages/agent-skills/dist/index.js +1 -1
  65. package/packages/agent-skills/dist/index.js.map +1 -1
  66. package/packages/agent-skills/package.json +1 -1
  67. package/packages/agent-skills/src/deploy.ts +288 -6
  68. package/packages/agent-skills/src/index.ts +10 -1
  69. package/packages/azdo/README.md +13 -4
  70. package/packages/azdo/bunfig.toml +6 -0
  71. package/packages/azdo/package.json +1 -1
  72. package/packages/azdo/scripts/probe-ado-rules.ts +1 -1
  73. package/packages/azdo/src/activity-log.ts +118 -0
  74. package/packages/azdo/src/azdo-attachments.ts +109 -0
  75. package/packages/azdo/src/azdo-pl.ts +100 -3
  76. package/packages/azdo/src/azdo-pr.ts +376 -17
  77. package/packages/azdo/src/azdo-wit.ts +594 -34
  78. package/packages/azdo/src/cli.ts +119 -12
  79. package/packages/azdo/src/lib/client.ts +95 -17
  80. package/packages/azdo/src/lib/validate-pr-metadata.ts +14 -5
  81. package/packages/azdo/src/mcp-server.ts +121 -8
  82. package/packages/azdo/src/tool-dispatch.ts +107 -23
  83. package/packages/azdo/src/workflow-ladder.csv +125 -107
  84. package/packages/azdo/src/workflow-rules.ts +254 -97
  85. package/packages/fleet/src/install-adl-package.ts +4 -3
  86. package/packages/repo-controls/README.md +12 -4
  87. package/packages/repo-controls/assets/promote.ts +1 -0
  88. package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +5 -5
  89. package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +42 -13
  90. package/packages/repo-controls/assets/scaffold/root/CONTRIBUTING.md +5 -5
  91. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +0 -11
  92. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +4 -6
  93. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +15 -0
  94. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +1 -17
  95. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/no-type.md +1 -1
  96. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-experiment.md +1 -1
  97. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-research.md +1 -1
  98. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-key.md +1 -1
  99. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-keys.md +1 -1
  100. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-created.md +1 -1
  101. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-description.md +1 -1
  102. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-last-update.md +1 -1
  103. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-status.md +1 -1
  104. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-title.md +1 -1
  105. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/optional-keys.md +1 -1
  106. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/research-ok.md +1 -1
  107. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +3 -22
  108. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +39 -182
  109. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/fixtures/open-work-cap-clean/plans/features/az601-clean/intent.md +17 -0
  110. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +90 -321
  111. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +31 -5
  112. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +282 -54
  113. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/design.md +35 -0
  114. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/file-map-reference.md +19 -0
  115. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/intent.md +25 -0
  116. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/items/az351-restore-entrypoint.md +25 -0
  117. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/requirements.md +44 -0
  118. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-violation/plans/features/az347-test-coverage/requirements.md +44 -0
  119. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +646 -53
  120. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +3 -3
  121. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +1 -12
  122. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +1 -1
  123. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +1 -1
  124. package/packages/repo-controls/package.json +1 -1
  125. package/packages/repo-controls/src/config.ts +21 -2
  126. package/packages/repo-controls/src/doctor.ts +9 -14
  127. package/packages/repo-controls/src/findings-ratchet.ts +12 -0
  128. package/packages/repo-controls/src/hook.ts +61 -4
@@ -0,0 +1,74 @@
1
+ # Run reports and evidence
2
+
3
+ Part of [`workitems-process`](../SKILL.md). Tags: **[pending X]** = change row `X`, not built.
4
+
5
+ ## Principle
6
+
7
+ - **The dispatcher parses nothing and writes nothing.** Whether a target is done is decided from the board and the PR (the target's done proof), not from a comment.
8
+ - Evidence is for humans, for the next sub-agent, and for a resumed run. It binds to what it ran against: the head SHA, or plan revision N. A later commit voids earlier test and review evidence for what it touches; the Feature-final check at the final head is what the merge relies on.
9
+ - **Evidence is the item comment plus the PR.** The run-level report is the run's last comment: on the Feature for a planning or implementation run, on the Bug for a per-Bug run, never on a PBI. Its first line is one machine line, `adl-evidence v1 <run> <verdict> <head_sha>` (e.g. `adl-evidence v1 implementation MERGED 3f9c2ab`), then the report.
10
+
11
+ ## Where evidence lives
12
+
13
+ | Record | Written by | Where |
14
+ |---|---|---|
15
+ | Sub-agent report | the sub-agent returns it; the **lead** posts it | item comment (Feature comment for the Feature-final and finalize lanes) |
16
+ | Run report, planning and implementation | the lead (`planner`, `lead`) | one comment as its last action: on the Feature, or on the Bug in a per-Bug run |
17
+ | Run report, `workitem-triage` | `triager` | the activity log (`adl activity add`) plus one comment on each touched item |
18
+ | PR body | the lead; on an implementation PR the text comes from `writer` | the PR |
19
+ | Plan snapshot, implementation report | content from the lead; attached by `pr_create_or_update` | Feature attachments ([`ado-and-git.md` § Plan files](ado-and-git.md#4-plan-files)) |
20
+ | Review findings | review sub-agents via the lead | item comments and the implementation report; never PR threads or votes |
21
+
22
+ - Reading earlier comments needs **T1 (pending)**. A resumed run that cannot read them works from the board states and the PR, and re-runs any verify or review whose head is not the current head.
23
+ - Evidence never holds secrets: links, repo-relative paths, commands with exit codes, run ids, SHAs.
24
+
25
+ ## Sub-agent report (the common shape)
26
+
27
+ ````yaml
28
+ lane: validate | design-update | implement | fix-ci | verify | verify-final | review-code | review-architecture | finalize | review-plan-architect | review-plan-critic
29
+ target: <item id | feature id>
30
+ base_sha: <integration base sha>
31
+ head_sha: <worktree HEAD the report applies to; plan reviews name revision N>
32
+ verdict: <set per lane, below>
33
+ findings: # required for every non-pass verdict
34
+ - id: F1
35
+ severity: critical | high | medium | low
36
+ where: path:line
37
+ what: "<issue and risk>"
38
+ fix: "<concrete change>"
39
+ artifacts: # files reviewed, commands with exit codes, run ids, AC-n -> test map, commit SHAs
40
+ - ...
41
+ ````
42
+
43
+ A negative verdict with empty `findings`, or a finding without `where` and `fix`, is invalid. `REQUEST_CHANGES` and `BLOCK` send the item back into the loop; `WATCH` and medium or low findings are recorded, not blocking. The Critic's verdict for revision N counts only after the Architect's review of the same revision.
44
+
45
+ ## Verdicts by lane and by run
46
+
47
+ | Lane or run | Verdicts |
48
+ |---|---|
49
+ | plan review, architect and critic | `APPROVE` · `ITERATE` · `REJECT` |
50
+ | validate (`design.md` and its `file-map-reference.md`) | `FRESH` · `DRIFT` (exact delta) |
51
+ | design update (planner sub-agent) | `UPDATED` |
52
+ | implement | `DONE` · `DRIFT` · `PARK` |
53
+ | verify, verify-final | `PASS` · `FAIL` |
54
+ | review, code | `APPROVE` · `REQUEST_CHANGES` |
55
+ | review, architecture | `CLEAR` · `WATCH` · `BLOCK` |
56
+ | finalize | `DONE` · `REWORK` (finding `docs-missing`) |
57
+ | fix-ci | `FIXED` · `INFRA` |
58
+ | run: workitem-triage | `TRIAGED` |
59
+ | run: planning | `DONE` (every in-scope item at Awaiting Approval and the planning PR raised; the report names the critic outcome, `APPROVE` or the cap) · `PARK` (the work is obsolete: the planner moved the in-scope items to Parked for Removal, each with a reason comment; G3 decides) |
60
+ | run: implementation | `MERGED` · `PR_OPEN` (CI unfinished at session end; the next dispatch resumes) |
61
+
62
+ No lane or run has a `BLOCKED` verdict. A failed precondition, a tool refusal or a cap follows its path in [No block](../SKILL.md#no-block).
63
+
64
+ ## Run report content
65
+
66
+ Posted by the lead as its last action, then the one-line handoff `<target> <verdict> <key kind> <id> @ <sha>`.
67
+
68
+ | Target | The report holds |
69
+ |---|---|
70
+ | workitem-triage | per swept item: applied, proposed (comment posted, item unchanged) or unchanged, with reasoning and evidence (`file:line`, commit, test output); split-off ids; rolled-up Feature ids; Bugs filed through `report-bug`; Features at New with no plan yet (informational); base SHA |
71
+ | planning | revision N, baseline SHA, plan hash, review rounds, critic outcome, open findings, planning PR URL, allocated item ids |
72
+ | implementation | per item: final state, commits (`AB#` ids), sub-agent verdicts and head SHAs; design updates; Feature-final result at the final head; CI rounds; PR link; verdict |
73
+
74
+ Every negative result names `where` and `fix`.
@@ -0,0 +1,92 @@
1
+ # Work item states, moves and links
2
+
3
+ Part of [`workitems-process`](../SKILL.md). Tags: **[live]** = enforced today; **[pending X]** = change row `X`, not built. Never act as if a pending change is live.
4
+
5
+ ## 1. PBI states
6
+
7
+ | State | Owner | Entered when | Exit |
8
+ |---|---|---|---|
9
+ | New | planning run | the item is allocated under its Feature | → In Planning |
10
+ | In Planning | planning run | the planner moves it, after allocating any new PBIs and before writing the files | → Awaiting Approval |
11
+ | Awaiting Approval | Tarun (**G1**) | the planning PR is raised | → Approved, In Planning (rework) or New (requeue) |
12
+ | Approved | implementation run | Tarun approves the plan revision | → Design Validation |
13
+ | Design Validation | implementation run | the lead's first act; re-entered from In Implementation on drift | → In Implementation |
14
+ | In Implementation, In Testing, In Review | implementation run | the loop | the loop |
15
+ | Updating Docs | implementation run | the loop; the implementation PR is raised | the loop, or → Done |
16
+ | Done | — | the Feature's implementation PR, linked to the item, merges into `integration` | terminal |
17
+ | Parked for Removal | Tarun (**G3**) | a lead parks the item with a reason | → Removed, or → New (revive) |
18
+ | Removed | — | Tarun confirms the removal | terminal |
19
+
20
+ - Up to Design Validation the order is strict.
21
+ - **The loop is non-linear.** After Design Validation, In Implementation, In Testing, In Review and Updating Docs are free moves in either direction, per PBI. The usual order is In Testing before In Review, so a reviewer sees green code; a default, not a rule.
22
+ - **Fence today [live]:** among the loop states the fence allows only In Review → In Implementation (`workflow-rules.ts:81`). Every other loop move (In Testing → In Implementation, Updating Docs → In Implementation or In Review, the skips) is **[pending F1]**, as is the drift loop-back In Implementation → Design Validation (ADO allows it, the fence does not yet). On a refusal: post the exact text as an item comment and take the path the refusal names; do not work around it.
23
+ - **Drift loops back; it never leaves the run.** In Implementation → Design Validation; the lead re-validates `design.md`; a drifted `design.md` is fixed in the run by a planner sub-agent. Items never go to New or In Planning for drift. Drift that would need an approved `AC-n` changed follows [No block](../SKILL.md#no-block).
24
+ - Later commits void earlier test and review evidence, so the Feature-final check at the final head is what the merge relies on.
25
+
26
+ ## 2. Who moves what
27
+
28
+ | Move | Who | Status |
29
+ |---|---|---|
30
+ | New → In Planning | planning lead: after allocating any new PBIs, it moves every in-scope PBI (existing and new), then writes the files; all are at In Planning before the planning PR is raised | live |
31
+ | In Planning → Awaiting Approval | `pr_create_or_update`, when it raises the planning PR | pending P2 |
32
+ | Awaiting Approval → Approved / In Planning (rework) / New (requeue) | Tarun (G1) | live |
33
+ | Approved → Design Validation | implementation lead, first act | live |
34
+ | Design Validation → In Implementation | implementation lead, when an item's build starts | live |
35
+ | Free moves among the four loop states | implementation lead | pending F1 |
36
+ | In Implementation → Design Validation | implementation lead, on mid-loop drift | pending F1 |
37
+ | Loop → Updating Docs | `pr_create_or_update`, when it raises the implementation PR | pending P2 |
38
+ | Updating Docs → Done | ADO completion of the linked implementation PR (`transitionWorkItems`) | pending P1, P2, V2 |
39
+ | Any non-terminal → Parked for Removal | any lead, with a reason | live |
40
+ | Parked for Removal → Removed | Tarun (G3) | live |
41
+ | Parked for Removal → New (revive) | Tarun | human-only; the fence lets agents make it today, and agents never do |
42
+
43
+ - Both G1 rejection paths force a new G1 pass.
44
+ - **Agents never set Done, Approved or Removed.** Done follows the merge of the implementation PR, which `pr_create_or_update` links to the items ([`ado-and-git.md` § PR wiring](ado-and-git.md#6-pr-wiring)). A planning PR's merge never moves items.
45
+ - **Agents pass nothing for the PR-time moves** and make none of them.
46
+ - Feature roll-ups are `workitem-triage`'s (§3).
47
+
48
+ ## 3. Feature states
49
+
50
+ `New → In Progress → Awaiting Release → Done`, plus `Removed`. Linear, one step at a time, never backward; Done only from Awaiting Release **[pending F3, F4]**.
51
+
52
+ | State | Entered when | Moved by |
53
+ |---|---|---|
54
+ | New | the Feature is created, only at New | — |
55
+ | In Progress | at least one child has left New | `workitem-triage` roll-up |
56
+ | Awaiting Release | every child is Done or Removed (its implementation PR merged into `integration`) | `workitem-triage` roll-up |
57
+ | Done | the `integration → main` PR carrying the children is merged (G2) | `workitem-triage` roll-up |
58
+ | Removed | Tarun removes the Feature | Tarun, human-only |
59
+
60
+ - **"Closed" = Awaiting Release or later.** No child may be added to a closed Feature **[pending L1]**; work found later goes into a new Feature or a Fixes Bug.
61
+ - **`Fixes to closed Features`** (one per Epic, 548–554) stays **In Progress** permanently; agents may not move it **[pending F5]**. It is exempt from the predecessor rule and the roll-ups.
62
+
63
+ ## 4. Links
64
+
65
+ - **Parent:** a PBI or Bug's parent is its Feature; a Feature's parent is an Epic. In an item file, `feature:` is the Feature's full board URL.
66
+ - **Predecessors live on Features only:** at least 1, except the very first Feature. Soft warning first, hard refusal later **[pending L2]**. A Feature is **ready** when every predecessor is at Awaiting Release or Done.
67
+ - **PBIs carry no predecessors** **[pending L3]**; item order inside a Feature is the implementation lead's choice.
68
+ - **Fixes Bugs are the one exception:** a Bug under `Fixes to closed Features` has a predecessor (another Bug there) and a **Related** link to the Responsible Feature, in any state (a red pipeline's Responsible Feature can be open) **[pending L4]**. The first Bug in a Fixes Feature has no predecessor, and none is accepted; every later Bug needs one.
69
+ - **Findings go through `report-bug`:** a defect in an open Feature (before Awaiting Release) updates that Feature's PBIs and creates no Bug; a defect in a closed Feature creates a Bug under that Epic's `Fixes to closed Features`.
70
+
71
+ ## 5. Bug states
72
+
73
+ Every Bug is a Fixes Bug under a `Fixes to closed Features` Feature. **Never planned:** no In Planning, Awaiting Approval or Design Validation, no plan hash, no `requirements.md`, `design.md` or `file-map-reference.md`.
74
+
75
+ `New → Approved → In Reproduction → In Fixing ↔ In Testing ↔ In Review ↔ Updating Docs → Done`, plus `Parked for Removal → Removed` and `Parked for Removal → New`.
76
+
77
+ | Move | Who |
78
+ |---|---|
79
+ | created at New | `report-bug`, on the board only; its Description holds the bugfix shape ([`ado-and-git.md` § Item files](ado-and-git.md#5-item-files)) |
80
+ | New → Approved | Tarun (**G1**), approving that Description; human-only |
81
+ | Approved → In Reproduction | implementation lead, first act of the per-Bug run **[pending L5, X2]** |
82
+ | In Reproduction → In Fixing | implementation lead, once the executor (reproduce mode) has committed the item file and the regression test marked as an expected failure (the framework's xfail or `test.fails` form), with its unmarked run red on the base posted on the Bug as evidence; the reproduce commit stays green, and the fix commit removes the mark |
83
+ | Free moves among In Fixing, In Testing, In Review, Updating Docs | implementation lead (the PBI loop with In Fixing in place of In Implementation) |
84
+ | Loop → Updating Docs | `pr_create_or_update`, when it raises the Bug's implementation PR |
85
+ | Updating Docs → Done | ADO completion of the linked implementation PR; agents never move to Done |
86
+ | In Reproduction → Parked for Removal | implementation lead, when the Bug won't reproduce: it posts what was tried as a comment on the Bug |
87
+ | Any non-terminal → Parked for Removal | any lead, with a reason (New and Approved included, to park a duplicate) |
88
+ | Parked for Removal → Removed | Tarun (**G3**), human-only |
89
+ | Parked for Removal → New (revive) | Tarun, human-only |
90
+
91
+ - **Per-Bug run:** branch `fix/az<bug id>-<slug>`. The executor's first commit is the Bug's item file (the Description's sections plus `## Predecessors` mirroring the board link), with its LEDGER row `az<id>-bug`; then the regression test. **The reproduce commit stays green.** The executor runs the regression test unmarked on the base and proves it red; that run output goes into a comment on the Bug as evidence (the executor returns it, the lead posts it). It commits the test marked as an expected failure (the framework's xfail or `test.fails` form), so lint, typecheck, build and the suite pass. The fix commit removes the expected-failure mark, so the same test now passes normally. Every commit stays independently green, with no exception. The lead writes no files.
92
+ - Every other Bug move is refused. The fence's Bug table is **[pending F9]**.
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: write-feature
3
+ description: Live, human-attended session that creates a Feature on the ADO board, parent Epic chosen by the human and at least one predecessor Feature unless it is the very first, and writes its intent.md in the human's own words, then lands it through a docs branch and PR. Use when Tarun asks to start, create or write up a new Feature or its intent. Never dispatched.
4
+ metadata:
5
+ step: "live"
6
+ role: "session agent with the human"
7
+ states: "Feature:New"
8
+ ---
9
+
10
+ # Write Feature (live session)
11
+
12
+ Load `workitems-process` first. This card adds only what the live Feature session does.
13
+
14
+ ## When to use
15
+
16
+ - The human starts a new Feature in a session they attend. This is stage 1 of the Feature flow.
17
+ - **Never dispatched.** No dispatch record, no worktree from the dispatcher, no run report. If you were started by the dispatcher, stop: this skill is wrong for the run.
18
+ - Not for planning (`plan-feature`) and not for a Bug (`report-bug`).
19
+
20
+ ## Inputs (all from the human)
21
+
22
+ | Input | Rule |
23
+ |---|---|
24
+ | The Feature in their words | Why, Outcome, Constraints, Non-goals; Open questions optional |
25
+ | Parent Epic | **The human chooses.** Never guess, never create |
26
+ | Predecessor Features | At least 1, unless the human says this is the very first Feature |
27
+ | Title and slug | Human confirms both |
28
+
29
+ ## Preconditions
30
+
31
+ 1. You are in a checkout of the repo that will hold the plan, the integration base is current, clean tree.
32
+ 2. The human gave an Epic id. Confirm it parents a Feature: a wrong-typed parent is refused by name at create. Do not read or edit the Epic.
33
+
34
+ ## Procedure
35
+
36
+ Ask **one question at a time**. Lead with what you already know.
37
+
38
+ 1. **Listen.** Take the human's words. Do not rewrite, extend or "improve" them. Do not add a solution.
39
+ 2. **Fill gaps, not prose.** For each required section the human left empty (`## Why`, `## Outcome`, `## Constraints`, `## Non-goals`), ask for it. `## Open questions` is optional; add it only if the human has some.
40
+ 3. **Predecessors.** List candidate Features (`workitem_list_or_get --type Feature`) and let the human pick. A Feature needs at least 1 predecessor Feature; only the very first has none. The human says which case applies; you do not infer it. Enforcement is soft then hard: pending L2 in adl-agent-control.
41
+ 4. **Allocate the Feature on the board first.** `workitem_create_or_update_feature --title "<title>" --parent-id <epic> --predecessors <ids> --description "<Why, Outcome, Constraints, Non-goals>" --priority <1|2|3|4>`. State stays New (the tool refuses any other create state: pending F3). Mandatory at create: `title`, `parentId` (Epic), `description` (must carry the four intent.md headings `## Why` / `## Outcome` / `## Constraints` / `## Non-goals`), and `priority` ∈ `{1, 2, 3, 4}` (1=Very High, 2=High, 3=Medium, 4=Low). State-conditional gate — when moving off `New` (e.g. to `In Progress`), the call also requires `acceptanceCriteria` plus the three plan fields (`--plan-revision`, `--plan-hash`, `--baseline-sha`); create at `New` does not. Read it back: type Feature, state New, parent = the Epic, predecessors present. A refusal is the answer: report the exact text, stop, create nothing.
42
+ - **Description = copy of `intent.md`.** Pass the human's own `## Why`, `## Outcome`, `## Constraints`, `## Non-goals` text, verbatim and identical to what step 5 writes, as `--description` at create. Written once, here. Agents never edit it afterwards, the same as `intent.md`. The approved plan lives in the `plan-r<N>-<sha7>.md` attachment, not in the Description.
43
+ - Do not touch Features 548-554 ("Fixes to closed Features"); their description is already set.
44
+ - Never create an Epic. Never set `epic:` from tool output; it is the human's value.
45
+ 5. **Write `plans/features/az<id>-<slug>/intent.md`.** The id is allocated before the file exists. Layout below. No other file in the folder.
46
+ 6. **Read it back to the human.** Commit only after an explicit yes. After that, agents never edit `intent.md`; only the human does.
47
+ 7. **Land it.** An ordinary docs change. You create the intent branch from the integration base: the one exception to "agents never create a branch." Add the `plans/LEDGER.md` row `az<id>-intent`. Commit `docs(plans): add intent for az<id> <slug>`. Push. Open the PR with `pr_create_or_update`, with the five PR body sections (Summary, Why, Changes, Test plan, Security impact considered); `## Test plan` and `## Security impact considered` say "none (docs-only)". The branch is not the planning branch and the Feature has no item at Approved or later (PBIs the human already made sit at New), so this is an ordinary PR: the tool links nothing, moves nothing, attaches nothing, and it needs no implementation report. Never rebase or force-push. No reviewer, no vote ([Human gates](../workitems-process/SKILL.md#human-gates)).
48
+ 8. **Stop.** Report: Feature URL, branch, PR URL. Say the Feature is at New and the planning trigger ([Targets and triggers](../workitems-process/SKILL.md#targets-and-triggers)) fires on it once the PR has merged. Do not start planning.
49
+
50
+ ## `intent.md` layout
51
+
52
+ ```markdown
53
+ ---
54
+ type: intent
55
+ title: <Feature title>
56
+ status: New
57
+ work_item: https://dev.azure.com/<org>/<project>/_workitems/edit/<feature id>
58
+ last_update: <YYYY-MM-DDTHH:MM:SSZ>
59
+ ---
60
+
61
+ ## Why
62
+ - <the human's words>
63
+
64
+ ## Outcome
65
+ - <what is true when it is done>
66
+
67
+ ## Constraints
68
+ - <...>
69
+
70
+ ## Non-goals
71
+ - <...>
72
+
73
+ ## Open questions
74
+ - <optional; omit the section if empty>
75
+ ```
76
+
77
+ - Required sections: Why, Outcome, Constraints, Non-goals. Short bullets, the human's own wording.
78
+ - `status` mirrors the Feature's board state (New). `work_item` URL id equals the folder id. Optional `sources:` only if the human names documents.
79
+ - The check for these sections is pending R4; the Feature-state `status` values are pending R1.
80
+ - Owner of the layout: [ado-and-git.md](../workitems-process/references/ado-and-git.md#4-plan-files).
81
+
82
+ ## Stop rules
83
+
84
+ - Epic not given, or the human cannot say whether a predecessor exists → stop and ask; never default.
85
+ - A refused create or a failed read-back → stop with the exact error; no file before the id.
86
+ - Same command fails twice for the same reason → stop and report.
87
+
88
+ ## Never
89
+
90
+ - Never write, extend or paraphrase the human's intent for them.
91
+ - Never choose the Epic or a predecessor.
92
+ - Never create PBIs here; planning creates them. PBIs the human already made stay at New and planning reads them.
93
+ - Never move a state, never plan, never post a run report.
94
+ - Never edit `intent.md` after the human approved it.
@@ -6,6 +6,16 @@ export interface DeploySkillsOptions {
6
6
  readonly profile?: DeployProfile;
7
7
  /** Computes the exact same written/unchanged/removed/conflicts/retiredRemovals result without writing anything (independent review, 2026-09-29, finding 6). */
8
8
  readonly dryRun?: boolean;
9
+ /**
10
+ * When set, deploy only the named surface (profile.surfaces[].name) —
11
+ * the deploy half of the per-surface knob (az567). Other surfaces are
12
+ * left untouched: their files are not written, their conflicts are not
13
+ * reported, their retired-folder cleanup does not run. The deploy
14
+ * profile's `retired` list is the closed set of "skill folders no
15
+ * surface reads any more"; for surface-prepare this list is also
16
+ * filtered to the active surfaces.
17
+ */
18
+ readonly surfaceFilter?: string;
9
19
  }
10
20
  export interface SkillConflict {
11
21
  readonly folder: string;
@@ -27,5 +37,47 @@ export interface DeploySkillsResult {
27
37
  */
28
38
  readonly retiredRemovals: readonly string[];
29
39
  }
40
+ /**
41
+ * True when `skillDirAbs` holds exactly the asset skill's files — same
42
+ * relative paths, same bytes, nothing extra. Such a folder carries no user
43
+ * content, so deploy may adopt it (Tarun, 2026-10-01).
44
+ */
45
+ export declare function folderIdenticalToAsset(skillDirAbs: string, assetDirAbs: string, sourceFiles: readonly string[]): boolean;
46
+ export declare function removeEmptyParents(root: string, dir: string): void;
47
+ export interface SkillRefusal {
48
+ readonly folder: string;
49
+ readonly skill: string;
50
+ /** Repo-relative posix path to the entry the primitive refused to remove. */
51
+ readonly path: string;
52
+ /** Human-readable reason the entry was refused. */
53
+ readonly reason: string;
54
+ }
55
+ export interface RemoveDeployedSkillFoldersOptions {
56
+ readonly assetsDir: string;
57
+ readonly targetRepo: string;
58
+ /** Defaults to loadProfile() (the package's own deploy-profile.json) — pass explicitly when the caller has already resolved real on-disk paths (see profile.ts's header). */
59
+ readonly profile?: DeployProfile;
60
+ /** When set, only the skill folders for this surface (profile.surfaces[].name) are in scope. */
61
+ readonly surfaceFilter?: string;
62
+ /** When set, only this skill id (assets/ skill folder name) is in scope. */
63
+ readonly skillFilter?: string;
64
+ /** Computes the exact same removed/absent/refused result without unlinking anything. */
65
+ readonly dryRun?: boolean;
66
+ }
67
+ export interface RemoveDeployedSkillFoldersResult {
68
+ /** Repo-relative posix paths the primitive actually removed (or would have, in --dry-run). Sorted, deduplicated. */
69
+ readonly removed: readonly string[];
70
+ /** (surface, skill) pairs the deploy profile resolves to that are in scope but were already absent on disk. Sorted, deduplicated, "<folder>/<skill>". */
71
+ readonly absent: readonly string[];
72
+ /** Entries the manifest did not record but which live inside a managed skill dir or surface folder — never removed, always reported. */
73
+ readonly refused: readonly SkillRefusal[];
74
+ }
75
+ /**
76
+ * The deployed-skill removal primitive (az696). Removes every file the
77
+ * deploy manifest records, restricted by `surfaceFilter`/`skillFilter`
78
+ * (intersection) and the repo's `[assets.skills]` subset. Idempotent: a
79
+ * second call with the same arguments reports every target in `absent`.
80
+ */
81
+ export declare function removeDeployedSkillFolders(options: RemoveDeployedSkillFoldersOptions): RemoveDeployedSkillFoldersResult;
30
82
  export declare function deploySkills(options: DeploySkillsOptions): DeploySkillsResult;
31
83
  //# sourceMappingURL=deploy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../src/deploy.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAE,KAAK,aAAa,EAA+B,MAAM,cAAc,CAAC;AAI/E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,6KAA6K;IAC7K,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC,+JAA+J;IAC/J,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,CAAC;IAC7C,8JAA8J;IAC9J,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AA+BD,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CAiK7E"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../src/deploy.ts"],"names":[],"mappings":"AAiCA,OAAO,EAAE,KAAK,aAAa,EAA+B,MAAM,cAAc,CAAC;AAI/E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,6KAA6K;IAC7K,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC,+JAA+J;IAC/J,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;;OAQG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,CAAC;IAC7C,8JAA8J;IAC9J,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CASxH;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAWlE;AA0BD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,6KAA6K;IAC7K,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC,gGAAgG;IAChG,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,wFAAwF;IACxF,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,gCAAgC;IAC/C,oHAAoH;IACpH,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,yJAAyJ;IACzJ,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,wIAAwI;IACxI,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;CAC3C;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,iCAAiC,GAAG,gCAAgC,CAoIvH;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CA4O7E"}
@@ -17,7 +17,9 @@
17
17
  * When `profile.legacyLock` names a lock the OLD repo-controls engine
18
18
  * left behind, a same-name folder it fully wrote (per legacy-lock.ts) is
19
19
  * taken over on sight rather than reported as a conflict — see
20
- * legacy-lock.ts for the exact ownership rule and lock rewrite.
20
+ * legacy-lock.ts for the exact ownership rule and lock rewrite. A legacy
21
+ * folder whose skill retired from assets/ is removed under the same
22
+ * ownership rule (never a hand-modified folder).
21
23
  *
22
24
  * Fails loud: assets/target validation, an invalid deploy profile, an
23
25
  * invalid adl-config.toml subset, or an unwritable target all throw. There
@@ -37,7 +39,7 @@ import { listSourceSkillNames } from "./skill-meta.js";
37
39
  * relative paths, same bytes, nothing extra. Such a folder carries no user
38
40
  * content, so deploy may adopt it (Tarun, 2026-10-01).
39
41
  */
40
- function folderIdenticalToAsset(skillDirAbs, assetDirAbs, sourceFiles) {
42
+ export function folderIdenticalToAsset(skillDirAbs, assetDirAbs, sourceFiles) {
41
43
  let targetFiles;
42
44
  try {
43
45
  targetFiles = walkFiles(skillDirAbs);
@@ -49,7 +51,7 @@ function folderIdenticalToAsset(skillDirAbs, assetDirAbs, sourceFiles) {
49
51
  return false;
50
52
  return sourceFiles.every((file) => Buffer.compare(readFileSync(join(skillDirAbs, file)), readFileSync(join(assetDirAbs, file))) === 0);
51
53
  }
52
- function removeEmptyParents(root, dir) {
54
+ export function removeEmptyParents(root, dir) {
53
55
  let current = dir;
54
56
  while (current !== root && current.startsWith(root)) {
55
57
  if (!existsSync(current)) {
@@ -62,6 +64,136 @@ function removeEmptyParents(root, dir) {
62
64
  current = dirname(current);
63
65
  }
64
66
  }
67
+ /**
68
+ * The deployed-skill removal primitive (az696). Removes every file the
69
+ * deploy manifest records, restricted by `surfaceFilter`/`skillFilter`
70
+ * (intersection) and the repo's `[assets.skills]` subset. Idempotent: a
71
+ * second call with the same arguments reports every target in `absent`.
72
+ */
73
+ export function removeDeployedSkillFolders(options) {
74
+ const { assetsDir, targetRepo } = options;
75
+ const dryRun = options.dryRun === true;
76
+ if (!existsSync(assetsDir) || !statSync(assetsDir).isDirectory()) {
77
+ throw new Error(`agent-skills assets directory not found: ${assetsDir}`);
78
+ }
79
+ if (!existsSync(targetRepo) || !statSync(targetRepo).isDirectory()) {
80
+ throw new Error(`uninstall target is not a directory: ${targetRepo}`);
81
+ }
82
+ const profile = options.profile ?? loadProfile();
83
+ const availableSkills = listSourceSkillNames(assetsDir);
84
+ const selection = resolveSkillSelection(targetRepo, availableSkills);
85
+ if (!selection.ok)
86
+ throw new Error(selection.error);
87
+ const selectedSkills = [...new Set(selection.skills)].sort();
88
+ const manifest = readManifest(targetRepo);
89
+ // Build the in-scope (surface, skill) set, applying both filters as
90
+ // intersection. An unknown surfaceFilter or skillFilter is silently
91
+ // out-of-scope — the caller (the CLI) validates the names first and
92
+ // the primitive trusts its inputs.
93
+ const inScopeSurfaces = profile.surfaces.filter((surface) => options.surfaceFilter === undefined || surface.name === options.surfaceFilter);
94
+ const inScopeSkills = selectedSkills.filter((skill) => options.skillFilter === undefined || skill === options.skillFilter);
95
+ // Build the manifest-ownership set restricted to the in-scope
96
+ // (surface, skill) pairs. Everything the manifest records under those
97
+ // pairs is removable; everything else is not in scope.
98
+ const ownedPaths = new Set();
99
+ for (const surface of inScopeSurfaces) {
100
+ for (const skill of inScopeSkills) {
101
+ const prefix = `${surface.folder}/${skill}/`;
102
+ for (const file of manifest.files) {
103
+ if (file.startsWith(prefix))
104
+ ownedPaths.add(file);
105
+ }
106
+ }
107
+ }
108
+ const removed = [];
109
+ for (const relPath of [...ownedPaths].sort()) {
110
+ const absolute = join(targetRepo, relPath);
111
+ if (existsSync(absolute)) {
112
+ if (!dryRun) {
113
+ unlinkSync(absolute);
114
+ removeEmptyParents(targetRepo, dirname(absolute));
115
+ }
116
+ }
117
+ removed.push(relPath);
118
+ }
119
+ // Safety check: every path inside a managed skill dir or surface
120
+ // folder that is NOT in the manifest is reported as a refusal. The
121
+ // surface folder itself (e.g. `.claude/skills`) is reported when it
122
+ // exists and is not under any managed skill dir — the agent never
123
+ // removes the surface root on its own, only the skill dirs under it.
124
+ const refused = [];
125
+ for (const surface of inScopeSurfaces) {
126
+ const surfaceAbs = join(targetRepo, surface.folder);
127
+ if (!existsSync(surfaceAbs))
128
+ continue;
129
+ const inScope = options.skillFilter === undefined
130
+ ? inScopeSkills
131
+ : inScopeSkills.filter((skill) => skill === options.skillFilter);
132
+ // Hand-authored skill dirs the manifest did not write.
133
+ for (const name of directoryNames(surfaceAbs)) {
134
+ if (inScope.includes(name))
135
+ continue;
136
+ // A skill the manifest owns under this surface is in-scope; an
137
+ // unrecorded same-name dir is user-authored and refused.
138
+ const isManaged = manifest.files.some((file) => file.startsWith(`${surface.folder}/${name}/`));
139
+ if (isManaged)
140
+ continue; // already handled in the removal pass above
141
+ refused.push({
142
+ folder: surface.folder,
143
+ skill: name,
144
+ path: `${surface.folder}/${name}`,
145
+ reason: "folder is not recorded in the deploy manifest; refusing to remove user-authored content",
146
+ });
147
+ }
148
+ // For each in-scope skill dir, any file inside that is not in the
149
+ // manifest is reported and never removed.
150
+ for (const skill of inScope) {
151
+ const skillDirAbs = join(surfaceAbs, skill);
152
+ if (!existsSync(skillDirAbs))
153
+ continue;
154
+ const isManaged = manifest.files.some((file) => file.startsWith(`${surface.folder}/${skill}/`));
155
+ if (!isManaged)
156
+ continue; // already refused above as user-authored
157
+ for (const relFile of walkFiles(skillDirAbs)) {
158
+ const relPath = `${surface.folder}/${skill}/${relFile}`;
159
+ if (ownedPaths.has(relPath))
160
+ continue;
161
+ refused.push({
162
+ folder: surface.folder,
163
+ skill,
164
+ path: relPath,
165
+ reason: "file is inside a managed skill dir but is not recorded in the deploy manifest; refusing to remove user-authored content",
166
+ });
167
+ }
168
+ }
169
+ }
170
+ refused.sort((a, b) => a.path.localeCompare(b.path));
171
+ // The "absent" report: every (surface, skill) pair the deploy profile
172
+ // resolves to, filtered to in-scope, that has no files on disk and no
173
+ // manifest record. This is the idempotency signal a second run will
174
+ // see.
175
+ const absent = [];
176
+ for (const surface of inScopeSurfaces) {
177
+ for (const skill of inScopeSkills) {
178
+ const skillDirAbs = join(targetRepo, surface.folder, skill);
179
+ const prefix = `${surface.folder}/${skill}/`;
180
+ const hasManifestEntry = manifest.files.some((file) => file.startsWith(prefix));
181
+ if (!hasManifestEntry && !existsSync(skillDirAbs)) {
182
+ absent.push(`${surface.folder}/${skill}`);
183
+ }
184
+ }
185
+ }
186
+ absent.sort();
187
+ // Rewrite the manifest to drop the paths we removed (and only those).
188
+ // The surviving set keeps every path the manifest recorded that is
189
+ // outside the in-scope (surface, skill) pairs — those belong to other
190
+ // surfaces/skills the user did not ask to uninstall.
191
+ if (!dryRun) {
192
+ const surviving = manifest.files.filter((file) => !ownedPaths.has(file));
193
+ writeManifest(targetRepo, surviving);
194
+ }
195
+ return { removed: removed.sort(), absent, refused };
196
+ }
65
197
  export function deploySkills(options) {
66
198
  const { assetsDir, targetRepo } = options;
67
199
  const dryRun = options.dryRun === true;
@@ -91,8 +223,51 @@ export function deploySkills(options) {
91
223
  // conflict, with a reason naming what disqualified it.
92
224
  const takeoverFiles = new Set();
93
225
  const takeoverConflictReasons = new Map();
226
+ /** `${folder}/${skill}` in a retired folder that the legacy lock wrote and assets/ retired — removed by the retired-folder cleanup below. */
227
+ const legacyRetiredInRetired = new Set();
94
228
  if (legacyLock !== null) {
229
+ // Legacy retirement: a folder the old engine fully wrote (per the
230
+ // lock) whose skill no longer ships in assets/ is folded into
231
+ // takeoverFiles so the removal pass below deletes it — the header's
232
+ // "dropped from assets/ is removed" promise extended to old-engine
233
+ // folders. A repo-subset exclusion keeps the folder (the subset only
234
+ // limits what THIS deploy writes, it does not retract skills assets
235
+ // still ships). A folder failing the ownership check (hand-modified,
236
+ // extra file) is left untouched: it is becoming user-owned, the same
237
+ // disposition as the takeover conflict case. In retired folders there
238
+ // is no takeover pass: the folder is only recorded, and the
239
+ // retired-folder cleanup removes it under the same conditions.
240
+ const legacyRetired = (folder, foldIntoTakeover) => {
241
+ const prefix = `${folder}/`;
242
+ for (const skill of new Set([...legacyLock.files.keys()]
243
+ .filter((relPath) => relPath.startsWith(prefix))
244
+ .map((relPath) => relPath.slice(prefix.length).split("/", 1)[0]))) {
245
+ if (availableSkills.includes(skill))
246
+ continue; // still shipped — selected or subset path owns it
247
+ const skillDirRel = `${folder}/${skill}`;
248
+ const skillDirAbs = join(targetRepo, skillDirRel);
249
+ if (!existsSync(skillDirAbs))
250
+ continue;
251
+ if (manifest.files.some((file) => file.startsWith(`${skillDirRel}/`)))
252
+ continue; // already ours
253
+ const check = checkLegacyOwnership(legacyLock, targetRepo, skillDirRel, skillDirAbs);
254
+ if (!check.owned)
255
+ continue;
256
+ if (foldIntoTakeover) {
257
+ for (const relFile of walkFiles(skillDirAbs))
258
+ takeoverFiles.add(`${skillDirRel}/${relFile}`);
259
+ }
260
+ else {
261
+ legacyRetiredInRetired.add(skillDirRel);
262
+ }
263
+ }
264
+ };
95
265
  for (const surface of profile.surfaces) {
266
+ // az567 surfaceFilter: skip non-target surfaces entirely. The
267
+ // legacy-takeover pass for the non-target surface is irrelevant
268
+ // when only one surface is being deployed.
269
+ if (options.surfaceFilter !== undefined && surface.name !== options.surfaceFilter)
270
+ continue;
96
271
  for (const skill of selectedSkills) {
97
272
  const skillDirRel = `${surface.folder}/${skill}`;
98
273
  const skillDirAbs = join(targetRepo, skillDirRel);
@@ -109,10 +284,24 @@ export function deploySkills(options) {
109
284
  takeoverConflictReasons.set(skillDirRel, check.reason);
110
285
  }
111
286
  }
287
+ legacyRetired(surface.folder, true);
288
+ }
289
+ for (const folder of retiredFolders(profile)) {
290
+ // az567 surfaceFilter: retired-folder cleanup is also filtered to
291
+ // the active surfaces. A non-target surface's retired folder is
292
+ // left exactly as found.
293
+ if (options.surfaceFilter !== undefined) {
294
+ const isTargetActive = profile.surfaces.some((s) => s.folder === folder && s.name === options.surfaceFilter);
295
+ if (!isTargetActive)
296
+ continue;
297
+ }
298
+ legacyRetired(folder, false);
112
299
  }
113
300
  }
114
301
  const manifestFiles = new Set([...manifest.files, ...takeoverFiles]);
115
302
  const adopted = [];
303
+ // Total over `selectedSkills`: every loop below iterates that same array, so
304
+ // `sourceFilesBySkill.get(skill)` is never undefined and needs no fallback.
116
305
  const sourceFilesBySkill = new Map();
117
306
  for (const skill of selectedSkills)
118
307
  sourceFilesBySkill.set(skill, walkFiles(join(assetsDir, skill)));
@@ -121,6 +310,11 @@ export function deploySkills(options) {
121
310
  const conflicts = [];
122
311
  const keptPaths = new Set();
123
312
  for (const surface of profile.surfaces) {
313
+ // az567 surfaceFilter: only iterate the named surface. Other
314
+ // surfaces' deploy paths, conflicts, and removal decisions are out
315
+ // of scope and never appear in this result.
316
+ if (options.surfaceFilter !== undefined && surface.name !== options.surfaceFilter)
317
+ continue;
124
318
  for (const skill of selectedSkills) {
125
319
  const skillDirRel = `${surface.folder}/${skill}`;
126
320
  const skillDirAbs = join(targetRepo, skillDirRel);
@@ -129,7 +323,7 @@ export function deploySkills(options) {
129
323
  // An unrecorded folder whose whole tree is byte-identical to the asset
130
324
  // is adopted: its files fall through to the write loop below as
131
325
  // unchanged and so land in the manifest. Anything else stays a conflict.
132
- if (skillDirExists && !managedByUs && folderIdenticalToAsset(skillDirAbs, join(assetsDir, skill), sourceFilesBySkill.get(skill) ?? [])) {
326
+ if (skillDirExists && !managedByUs && folderIdenticalToAsset(skillDirAbs, join(assetsDir, skill), sourceFilesBySkill.get(skill))) {
133
327
  managedByUs = true;
134
328
  adopted.push(skillDirRel);
135
329
  }
@@ -142,7 +336,7 @@ export function deploySkills(options) {
142
336
  });
143
337
  continue; // never write into it, never claim its files in the manifest
144
338
  }
145
- for (const relFile of sourceFilesBySkill.get(skill) ?? []) {
339
+ for (const relFile of sourceFilesBySkill.get(skill)) {
146
340
  const targetRel = `${skillDirRel}/${relFile}`;
147
341
  keptPaths.add(targetRel);
148
342
  const targetAbs = join(targetRepo, targetRel);
@@ -168,6 +362,13 @@ export function deploySkills(options) {
168
362
  // already satisfy that by construction.
169
363
  if (!profile.surfaces.some((surface) => relPath.startsWith(`${surface.folder}/`)))
170
364
  continue;
365
+ // az567 surfaceFilter: paths under a non-target surface are not in
366
+ // scope and must not be removed during a per-surface deploy.
367
+ if (options.surfaceFilter !== undefined) {
368
+ const inTargetSurface = profile.surfaces.some((surface) => surface.name === options.surfaceFilter && relPath.startsWith(`${surface.folder}/`));
369
+ if (!inTargetSurface)
370
+ continue;
371
+ }
171
372
  const absolute = join(targetRepo, relPath);
172
373
  if (existsSync(absolute)) {
173
374
  if (!dryRun) {
@@ -191,12 +392,22 @@ export function deploySkills(options) {
191
392
  const retiredRemovals = [];
192
393
  const availableSkillSet = new Set(availableSkills);
193
394
  for (const folder of retiredFolders(profile)) {
395
+ // az567 surfaceFilter: only clean up retired folders that are NOT
396
+ // tied to a non-target surface. A retired folder is per-profile
397
+ // (the active-surface filters above do not reduce the retired list);
398
+ // when a surfaceFilter is set, retired-folder cleanup runs the same
399
+ // way it does for a full deploy, because retired folders are not
400
+ // tied to a particular active surface.
194
401
  const folderAbs = join(targetRepo, folder);
195
402
  if (!existsSync(folderAbs))
196
403
  continue;
197
404
  for (const name of directoryNames(folderAbs)) {
198
- if (!availableSkillSet.has(name))
199
- continue; // not ours to manage — left byte-for-byte
405
+ // Ours to remove when the skill still ships in assets/ (a stale
406
+ // duplicate of a live skill) or the legacy lock proves the old
407
+ // engine wrote this copy of a now-retired skill. Anything else was
408
+ // never this package's to manage — left byte-for-byte.
409
+ if (!availableSkillSet.has(name) && !legacyRetiredInRetired.has(`${folder}/${name}`))
410
+ continue;
200
411
  if (!dryRun)
201
412
  removeDirRecursive(join(folderAbs, name));
202
413
  retiredRemovals.push(`${folder}/${name}`);