@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.
- package/bin/adl +2 -2
- package/bin/reconcile +3 -11
- package/bin/setup +15 -15
- package/package.json +1 -1
- package/packages/agent-hooks/deploy-profile.json +1 -1
- package/packages/agent-hooks/package.json +1 -1
- package/packages/agent-hooks/src/main.ts +23 -2
- package/packages/agent-hooks/src/pre-tool-use/handler.ts +2 -2
- package/packages/agent-hooks/src/rules/impl/adl-build-run-role.ts +116 -0
- package/packages/agent-hooks/src/rules/impl/adl-git-user-override-block.ts +159 -0
- package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +22 -12
- package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +9 -9
- package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +6 -5
- package/packages/agent-hooks/src/rules/impl/adl-run-required.ts +11 -5
- package/packages/agent-hooks/src/rules/impl/adl-workitem-role.ts +18 -5
- package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +30 -25
- package/packages/agent-hooks/src/rules/impl/git-core.ts +4 -8
- package/packages/agent-hooks/src/rules/impl/git-target.ts +5 -0
- package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +4 -2
- package/packages/agent-hooks/src/rules/impl/tool-frontend.ts +230 -11
- package/packages/agent-hooks/src/rules/registry.ts +11 -2
- package/packages/agent-hooks/src/rules/shell/git-identity.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/invocation.ts +14 -2
- package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +10 -7
- package/packages/agent-settings/assets/TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md +2 -2
- package/packages/agent-settings/package.json +1 -1
- package/packages/agent-settings/src/deploy/agents-deploy.ts +19 -1
- package/packages/agent-settings/src/deploy/index.ts +1 -0
- package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +4 -6
- package/packages/agent-skills/assets/code-review/SKILL.md +84 -66
- package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +23 -2
- package/packages/agent-skills/assets/diagnosing-bugs/references/FEEDBACK-LOOP-TACTICS.md +1 -1
- package/packages/agent-skills/assets/explore-codebase/SKILL.md +3 -2
- package/packages/agent-skills/assets/finalize-feature/SKILL.md +63 -0
- package/packages/agent-skills/assets/fix-ci/SKILL.md +58 -0
- package/packages/agent-skills/assets/handoff/SKILL.md +1 -1
- package/packages/agent-skills/assets/implement-feature/SKILL.md +150 -0
- package/packages/agent-skills/assets/implement-feature/references/briefs.md +94 -0
- package/packages/agent-skills/assets/implement-item/SKILL.md +71 -0
- package/packages/agent-skills/assets/mechanics/SKILL.md +38 -0
- package/packages/agent-skills/assets/plan-feature/SKILL.md +141 -0
- package/packages/agent-skills/assets/prepare-promotion/SKILL.md +90 -0
- package/packages/agent-skills/assets/ralplan/SKILL.md +149 -0
- package/packages/agent-skills/assets/refactor-safely/SKILL.md +4 -3
- package/packages/agent-skills/assets/report-bug/SKILL.md +72 -0
- package/packages/agent-skills/assets/review-change/SKILL.md +82 -0
- package/packages/agent-skills/assets/review-plan/SKILL.md +124 -0
- package/packages/agent-skills/assets/triage-workitems/SKILL.md +99 -0
- package/packages/agent-skills/assets/ultragoal/SKILL.md +77 -0
- package/packages/agent-skills/assets/validate-design/SKILL.md +64 -0
- package/packages/agent-skills/assets/verify-item/SKILL.md +71 -0
- package/packages/agent-skills/assets/wayfinder/SKILL.md +1 -1
- package/packages/agent-skills/assets/workitems-process/SKILL.md +145 -0
- package/packages/agent-skills/assets/workitems-process/references/ado-and-git.md +227 -0
- package/packages/agent-skills/assets/workitems-process/references/evidence.md +74 -0
- package/packages/agent-skills/assets/workitems-process/references/workitem-states.md +92 -0
- package/packages/agent-skills/assets/write-feature/SKILL.md +94 -0
- package/packages/agent-skills/dist/deploy.d.ts +52 -0
- package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
- package/packages/agent-skills/dist/deploy.js +218 -7
- package/packages/agent-skills/dist/deploy.js.map +1 -1
- package/packages/agent-skills/dist/index.d.ts +1 -1
- package/packages/agent-skills/dist/index.d.ts.map +1 -1
- package/packages/agent-skills/dist/index.js +1 -1
- package/packages/agent-skills/dist/index.js.map +1 -1
- package/packages/agent-skills/package.json +1 -1
- package/packages/agent-skills/src/deploy.ts +288 -6
- package/packages/agent-skills/src/index.ts +10 -1
- package/packages/azdo/README.md +13 -4
- package/packages/azdo/bunfig.toml +6 -0
- package/packages/azdo/package.json +1 -1
- package/packages/azdo/scripts/probe-ado-rules.ts +1 -1
- package/packages/azdo/src/activity-log.ts +118 -0
- package/packages/azdo/src/azdo-attachments.ts +109 -0
- package/packages/azdo/src/azdo-pl.ts +100 -3
- package/packages/azdo/src/azdo-pr.ts +376 -17
- package/packages/azdo/src/azdo-wit.ts +594 -34
- package/packages/azdo/src/cli.ts +119 -12
- package/packages/azdo/src/lib/client.ts +95 -17
- package/packages/azdo/src/lib/validate-pr-metadata.ts +14 -5
- package/packages/azdo/src/mcp-server.ts +121 -8
- package/packages/azdo/src/tool-dispatch.ts +107 -23
- package/packages/azdo/src/workflow-ladder.csv +125 -107
- package/packages/azdo/src/workflow-rules.ts +254 -97
- package/packages/fleet/src/install-adl-package.ts +4 -3
- package/packages/repo-controls/README.md +12 -4
- package/packages/repo-controls/assets/promote.ts +1 -0
- package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +5 -5
- package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +42 -13
- package/packages/repo-controls/assets/scaffold/root/CONTRIBUTING.md +5 -5
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +0 -11
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +4 -6
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +15 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +1 -17
- 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
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-key.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-keys.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-created.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-description.md +1 -1
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-status.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-title.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/optional-keys.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/research-ok.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +3 -22
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +39 -182
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +90 -321
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +31 -5
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +282 -54
- 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
- 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
- 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
- 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
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +646 -53
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +3 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +1 -12
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +1 -1
- package/packages/repo-controls/package.json +1 -1
- package/packages/repo-controls/src/config.ts +21 -2
- package/packages/repo-controls/src/doctor.ts +9 -14
- package/packages/repo-controls/src/findings-ratchet.ts +12 -0
- 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":"
|
|
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
|
-
|
|
199
|
-
|
|
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}`);
|