@ikon85/agent-workflow-kit 0.39.0 → 0.40.0
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/.agents/skills/grill-me/SKILL.md +1 -1
- package/.agents/skills/grill-with-docs/SKILL.md +1 -1
- package/.agents/skills/orchestrate-wave/SKILL.md +4 -4
- package/.agents/skills/setup-workflow/SKILL.md +50 -0
- package/.agents/skills/setup-workflow/worktree-lifecycle.md +23 -1
- package/.claude/skills/grill-me/SKILL.md +1 -1
- package/.claude/skills/grill-with-docs/SKILL.md +1 -1
- package/.claude/skills/orchestrate-wave/SKILL.md +4 -4
- package/.claude/skills/setup-workflow/SKILL.md +50 -0
- package/.claude/skills/setup-workflow/worktree-lifecycle.md +23 -1
- package/README.md +25 -0
- package/agent-workflow-kit.package.json +28 -12
- package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +84 -0
- package/package.json +1 -1
- package/scripts/test_worktree_ignore_seed.py +320 -0
- package/scripts/worktree-lifecycle/README.md +18 -0
- package/scripts/worktree-lifecycle/ignore_seed.py +226 -0
- package/scripts/worktree-lifecycle/plan-artifacts.json +37 -0
- package/src/lib/bundle.mjs +6 -0
|
@@ -52,7 +52,7 @@ For a cross-cutting plan, run `python3 .claude/hooks/drift-guard.py --census-sta
|
|
|
52
52
|
|
|
53
53
|
## Plan-lock — writing PLAN.md
|
|
54
54
|
|
|
55
|
-
Once all decisions are made (plan locked, before sign-off/handoff) and the session runs in a **worktree** → write the locked plan as `PLAN.md` to the worktree root
|
|
55
|
+
Once all decisions are made (plan locked, before sign-off/handoff) and the session runs in a **worktree** → write the locked plan as `PLAN.md` to the worktree root. `PLAN.md` is **expected** to be ignored by git so it never travels over git — the kit never edits your `.gitignore` by itself, but `/setup-workflow` offers to add that rule (consistent with CLAUDE.md "plan inside the worktree"). This way the locked plan survives a session cut and `to-prd` finds its default source. Continuing same-session work **without** a worktree stays allowed (conversation = source); but for a deliberate **session cut** before `to-prd`, `PLAN.md` is mandatory. (The `-codex` variant already writes `PLAN.md` anyway.)
|
|
56
56
|
|
|
57
57
|
## Re-grill reconcile — execute-ready (Wave 26)
|
|
58
58
|
|
|
@@ -40,7 +40,7 @@ For a cross-cutting plan, run `python3 .claude/hooks/drift-guard.py --census-sta
|
|
|
40
40
|
|
|
41
41
|
## Plan-lock — writing PLAN.md
|
|
42
42
|
|
|
43
|
-
Once all decisions are made (plan locked, before sign-off/handoff) and the session runs in a **worktree** → write the locked plan as `PLAN.md` to the worktree root
|
|
43
|
+
Once all decisions are made (plan locked, before sign-off/handoff) and the session runs in a **worktree** → write the locked plan as `PLAN.md` to the worktree root. `PLAN.md` is **expected** to be ignored by git so it never travels over git — the kit never edits your `.gitignore` by itself, but `/setup-workflow` offers to add that rule (consistent with CLAUDE.md "plan inside the worktree"). This way the locked plan survives a session cut and `to-prd` finds its default source. Continuing same-session work **without** a worktree stays allowed (conversation = source); but for a deliberate **session cut** before `to-prd`, `PLAN.md` is mandatory. (The `-codex` variant already writes `PLAN.md` anyway.)
|
|
44
44
|
|
|
45
45
|
</plan-lock>
|
|
46
46
|
|
|
@@ -146,8 +146,8 @@ replace the ~30-second heartbeat; otherwise the standing heartbeat remains requi
|
|
|
146
146
|
points there). Bring its branch to current `main`:
|
|
147
147
|
`git -C <wave> merge --ff-only origin/main` (if your repo guards destructive
|
|
148
148
|
git, ff-merge is the safe path — not `reset --hard`). Install dependencies with
|
|
149
|
-
your package manager after (the lockfile may have moved). A
|
|
150
|
-
survives the merge.
|
|
149
|
+
your package manager after (the lockfile may have moved). A plan doc the repository
|
|
150
|
+
ignores survives the merge; a tracked one may not (`/setup-workflow` offers those rules).
|
|
151
151
|
5. **Run project setup steps (`§Setup`)** the later verify needs — e.g. a DB
|
|
152
152
|
tunnel or service the live-verify depends on. Absent layer → start whatever your
|
|
153
153
|
live-verify environment requires before Phase 4.
|
|
@@ -332,8 +332,8 @@ written · merge order documented.
|
|
|
332
332
|
- **Release this run's slice claims.** A landed slice's PR supersedes its claim;
|
|
333
333
|
for a pulled or abandoned slice remove the claim this run planted (unassign +
|
|
334
334
|
`claim released` comment) so it stays grabbable. Foreign ones stay untouched.
|
|
335
|
-
- **Before removing any slice worktree, read its `ANNAHMEN.md`** (an assumptions
|
|
336
|
-
|
|
335
|
+
- **Before removing any slice worktree, read its `ANNAHMEN.md`** (an assumptions log at
|
|
336
|
+
the worktree root, **expected** to be ignored by git — `/setup-workflow` offers that rule) and propagate each build-time assumption marker
|
|
337
337
|
to the sibling issue it carries. A hand-driven multi-PR / migration landing does
|
|
338
338
|
NOT run `wrapup`'s assumption-propagation step — this is the only place it
|
|
339
339
|
happens; `worktree remove` deletes the log.
|
|
@@ -37,6 +37,7 @@ This is a prompt-driven skill, not a deterministic script. Explore, present what
|
|
|
37
37
|
| `docs/agents/workflow-capabilities.json` + capability assets | consumer-owned Worktree Lifecycle, Memory Lifecycle, Workflow Advisories, and Safety Guardrails choices (Sections A4–A7) |
|
|
38
38
|
| `.claude/settings.json` | additive activation of the advisory kit-origin Edit/Write hint (Section A8) |
|
|
39
39
|
| user-local routing profile | familiar agent surfaces + switching autonomy (Section A9; never package- or repository-owned) |
|
|
40
|
+
| `.gitignore` | **offered only** — one previewed, append-only marker block with the ignore rules for the planning artifacts the skills write (Section A11) |
|
|
40
41
|
|
|
41
42
|
## Idempotency contract — read before writing anything
|
|
42
43
|
|
|
@@ -405,6 +406,55 @@ machine, checkout path, repository name, credentials, or current GitHub login.
|
|
|
405
406
|
Even a valid capability authorizes local preparation only; a pull request,
|
|
406
407
|
push, publish, or merge remains a separate explicit approval.
|
|
407
408
|
|
|
409
|
+
### 2j. Section A11 — Planning-artifact ignore rules (offered)
|
|
410
|
+
|
|
411
|
+
> The planning skills write `PLAN.md`, `PLAN-REVIEW-LOG.md`, and `ANNAHMEN.md`
|
|
412
|
+
> into a session worktree. `.gitignore` is the consumer's file: the kit does not
|
|
413
|
+
> own it and never touches it during `init` or `update`, so those artifacts are
|
|
414
|
+
> ignored only if this repository already says so. Without the rules every
|
|
415
|
+
> planning worktree reports a permanently dirty tree, the Worktree Lifecycle
|
|
416
|
+
> cleanup refuses to remove it, and a plan doc can be committed by accident.
|
|
417
|
+
|
|
418
|
+
Read the current state first. The preview is read-only and writes nothing:
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
python3 scripts/worktree-lifecycle/ignore_seed.py preview
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
- `nothing-to-do` — this repository already ignores every declared artifact.
|
|
425
|
+
Report it and ask nothing.
|
|
426
|
+
- `blocked` — the marker block exists but no longer covers everything. That
|
|
427
|
+
block is consumer-owned: name the uncovered artifacts, let the user edit
|
|
428
|
+
`.gitignore` themselves, and never repair or rewrite it.
|
|
429
|
+
- `append` — show the exact lines the preview printed, the whole marker block
|
|
430
|
+
verbatim and nothing summarized, then ask: *"Should setup add these ignore
|
|
431
|
+
rules to `.gitignore`?"*
|
|
432
|
+
|
|
433
|
+
Offer exactly two choices:
|
|
434
|
+
|
|
435
|
+
- **Add the rules** — run the seeder once. It appends that one marker block and
|
|
436
|
+
touches nothing else:
|
|
437
|
+
|
|
438
|
+
```bash
|
|
439
|
+
python3 scripts/worktree-lifecycle/ignore_seed.py apply
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
- **Not now** — write nothing. Explain that the planning skills still work, that
|
|
443
|
+
the artifacts will keep showing up as untracked changes, and that re-running
|
|
444
|
+
`/setup-workflow` offers this again.
|
|
445
|
+
|
|
446
|
+
The write is append-only inside one idempotent marker block: it never rewrites,
|
|
447
|
+
reorders, or removes an existing line; a re-run after an approval is a
|
|
448
|
+
byte-identical no-op; and a decline leaves the file exactly as it was. An
|
|
449
|
+
artifact already committed to history is reported separately — an ignore rule
|
|
450
|
+
cannot untrack it, and setup never runs `git rm`. Only this explicit, approved
|
|
451
|
+
step may write `.gitignore`; `init` and `update` reconciliation never reach the
|
|
452
|
+
seeder.
|
|
453
|
+
|
|
454
|
+
Report `appended · nothing to do (already ignored) · declined · blocked
|
|
455
|
+
(consumer-edited block)`. After an approval — and only then — the Section A4
|
|
456
|
+
`scratchPatterns` derivation has real ignored planning artefacts to read.
|
|
457
|
+
|
|
408
458
|
### 3. Section B — Triage labels
|
|
409
459
|
|
|
410
460
|
> When `triage` processes an incoming issue it applies labels (or your tracker's equivalent). It needs strings you've actually configured, or it creates duplicates.
|
|
@@ -82,12 +82,34 @@ The exact kit-owned commands are:
|
|
|
82
82
|
Preserve unrelated settings, hook groups, profile sections, and unknown keys.
|
|
83
83
|
Repeated reconciliation with the same choice is byte-identical.
|
|
84
84
|
|
|
85
|
+
## Planning-artifact ignore rules (offered, never installed)
|
|
86
|
+
|
|
87
|
+
The planning artefacts the shipped skills write live in the worktree, but
|
|
88
|
+
`.gitignore` is a consumer file the kit does not own: `init` and `update` never
|
|
89
|
+
touch it. Setup may therefore only **offer** the rules, from Section A11, using
|
|
90
|
+
`python3 scripts/worktree-lifecycle/ignore_seed.py`. The kit-owned declaration
|
|
91
|
+
of which artefacts those are is `scripts/worktree-lifecycle/plan-artifacts.json`.
|
|
92
|
+
|
|
93
|
+
| State | Setup action |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `append` | Show the exact marker block, then ask. |
|
|
96
|
+
| approve | Run `ignore_seed.py apply` once: it appends that one block, append-only. |
|
|
97
|
+
| decline | Write nothing; a later rerun offers it again. |
|
|
98
|
+
| already ignored | Report `nothing to do`; ask nothing and write nothing. |
|
|
99
|
+
| re-run after approval | Byte-identical no-op; never a second block. |
|
|
100
|
+
| consumer-edited block | `blocked` — report the uncovered artefacts and leave the file untouched. |
|
|
101
|
+
|
|
102
|
+
The seeder never rewrites, reorders, or removes an existing line, and a tracked
|
|
103
|
+
artefact is reported rather than untracked for the consumer. Only this explicit,
|
|
104
|
+
approved step may write `.gitignore`.
|
|
105
|
+
|
|
85
106
|
## Scratch classification and sweep
|
|
86
107
|
|
|
87
108
|
Reconcile an explicit `scratchPatterns` array when enabling a new profile.
|
|
88
109
|
Derive its glob values from the consumer's ignored planning artefacts; an empty
|
|
89
110
|
array is valid. Existing values are consumer-owned and remain byte-identical on
|
|
90
|
-
adoption or rerun. Core never supplies filename defaults
|
|
111
|
+
adoption or rerun. Core never supplies filename defaults — the offered ignore
|
|
112
|
+
rules above are what give that derivation something real to read.
|
|
91
113
|
|
|
92
114
|
Also reconcile an explicit
|
|
93
115
|
`wrapup.landingGeneratedArtifactPatterns` array. Derive candidates from the
|
|
@@ -52,7 +52,7 @@ For a cross-cutting plan, run `python3 .claude/hooks/drift-guard.py --census-sta
|
|
|
52
52
|
|
|
53
53
|
## Plan-lock — writing PLAN.md
|
|
54
54
|
|
|
55
|
-
Once all decisions are made (plan locked, before sign-off/handoff) and the session runs in a **worktree** → write the locked plan as `PLAN.md` to the worktree root
|
|
55
|
+
Once all decisions are made (plan locked, before sign-off/handoff) and the session runs in a **worktree** → write the locked plan as `PLAN.md` to the worktree root. `PLAN.md` is **expected** to be ignored by git so it never travels over git — the kit never edits your `.gitignore` by itself, but `/setup-workflow` offers to add that rule (consistent with CLAUDE.md "plan inside the worktree"). This way the locked plan survives a session cut and `to-prd` finds its default source. Continuing same-session work **without** a worktree stays allowed (conversation = source); but for a deliberate **session cut** before `to-prd`, `PLAN.md` is mandatory. (The `-codex` variant already writes `PLAN.md` anyway.)
|
|
56
56
|
|
|
57
57
|
## Re-grill reconcile — execute-ready (Wave 26)
|
|
58
58
|
|
|
@@ -40,7 +40,7 @@ For a cross-cutting plan, run `python3 .claude/hooks/drift-guard.py --census-sta
|
|
|
40
40
|
|
|
41
41
|
## Plan-lock — writing PLAN.md
|
|
42
42
|
|
|
43
|
-
Once all decisions are made (plan locked, before sign-off/handoff) and the session runs in a **worktree** → write the locked plan as `PLAN.md` to the worktree root
|
|
43
|
+
Once all decisions are made (plan locked, before sign-off/handoff) and the session runs in a **worktree** → write the locked plan as `PLAN.md` to the worktree root. `PLAN.md` is **expected** to be ignored by git so it never travels over git — the kit never edits your `.gitignore` by itself, but `/setup-workflow` offers to add that rule (consistent with CLAUDE.md "plan inside the worktree"). This way the locked plan survives a session cut and `to-prd` finds its default source. Continuing same-session work **without** a worktree stays allowed (conversation = source); but for a deliberate **session cut** before `to-prd`, `PLAN.md` is mandatory. (The `-codex` variant already writes `PLAN.md` anyway.)
|
|
44
44
|
|
|
45
45
|
</plan-lock>
|
|
46
46
|
|
|
@@ -146,8 +146,8 @@ replace the ~30-second heartbeat; otherwise the standing heartbeat remains requi
|
|
|
146
146
|
points there). Bring its branch to current `main`:
|
|
147
147
|
`git -C <wave> merge --ff-only origin/main` (if your repo guards destructive
|
|
148
148
|
git, ff-merge is the safe path — not `reset --hard`). Install dependencies with
|
|
149
|
-
your package manager after (the lockfile may have moved). A
|
|
150
|
-
survives the merge.
|
|
149
|
+
your package manager after (the lockfile may have moved). A plan doc the repository
|
|
150
|
+
ignores survives the merge; a tracked one may not (`/setup-workflow` offers those rules).
|
|
151
151
|
5. **Run project setup steps (`§Setup`)** the later verify needs — e.g. a DB
|
|
152
152
|
tunnel or service the live-verify depends on. Absent layer → start whatever your
|
|
153
153
|
live-verify environment requires before Phase 4.
|
|
@@ -332,8 +332,8 @@ written · merge order documented.
|
|
|
332
332
|
- **Release this run's slice claims.** A landed slice's PR supersedes its claim;
|
|
333
333
|
for a pulled or abandoned slice remove the claim this run planted (unassign +
|
|
334
334
|
`claim released` comment) so it stays grabbable. Foreign ones stay untouched.
|
|
335
|
-
- **Before removing any slice worktree, read its `ANNAHMEN.md`** (an assumptions
|
|
336
|
-
|
|
335
|
+
- **Before removing any slice worktree, read its `ANNAHMEN.md`** (an assumptions log at
|
|
336
|
+
the worktree root, **expected** to be ignored by git — `/setup-workflow` offers that rule) and propagate each build-time assumption marker
|
|
337
337
|
to the sibling issue it carries. A hand-driven multi-PR / migration landing does
|
|
338
338
|
NOT run `wrapup`'s assumption-propagation step — this is the only place it
|
|
339
339
|
happens; `worktree remove` deletes the log.
|
|
@@ -37,6 +37,7 @@ This is a prompt-driven skill, not a deterministic script. Explore, present what
|
|
|
37
37
|
| `docs/agents/workflow-capabilities.json` + capability assets | consumer-owned Worktree Lifecycle, Memory Lifecycle, Workflow Advisories, and Safety Guardrails choices (Sections A4–A7) |
|
|
38
38
|
| `.claude/settings.json` | additive activation of the advisory kit-origin Edit/Write hint (Section A8) |
|
|
39
39
|
| user-local routing profile | familiar agent surfaces + switching autonomy (Section A9; never package- or repository-owned) |
|
|
40
|
+
| `.gitignore` | **offered only** — one previewed, append-only marker block with the ignore rules for the planning artifacts the skills write (Section A11) |
|
|
40
41
|
|
|
41
42
|
## Idempotency contract — read before writing anything
|
|
42
43
|
|
|
@@ -405,6 +406,55 @@ machine, checkout path, repository name, credentials, or current GitHub login.
|
|
|
405
406
|
Even a valid capability authorizes local preparation only; a pull request,
|
|
406
407
|
push, publish, or merge remains a separate explicit approval.
|
|
407
408
|
|
|
409
|
+
### 2j. Section A11 — Planning-artifact ignore rules (offered)
|
|
410
|
+
|
|
411
|
+
> The planning skills write `PLAN.md`, `PLAN-REVIEW-LOG.md`, and `ANNAHMEN.md`
|
|
412
|
+
> into a session worktree. `.gitignore` is the consumer's file: the kit does not
|
|
413
|
+
> own it and never touches it during `init` or `update`, so those artifacts are
|
|
414
|
+
> ignored only if this repository already says so. Without the rules every
|
|
415
|
+
> planning worktree reports a permanently dirty tree, the Worktree Lifecycle
|
|
416
|
+
> cleanup refuses to remove it, and a plan doc can be committed by accident.
|
|
417
|
+
|
|
418
|
+
Read the current state first. The preview is read-only and writes nothing:
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
python3 scripts/worktree-lifecycle/ignore_seed.py preview
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
- `nothing-to-do` — this repository already ignores every declared artifact.
|
|
425
|
+
Report it and ask nothing.
|
|
426
|
+
- `blocked` — the marker block exists but no longer covers everything. That
|
|
427
|
+
block is consumer-owned: name the uncovered artifacts, let the user edit
|
|
428
|
+
`.gitignore` themselves, and never repair or rewrite it.
|
|
429
|
+
- `append` — show the exact lines the preview printed, the whole marker block
|
|
430
|
+
verbatim and nothing summarized, then ask: *"Should setup add these ignore
|
|
431
|
+
rules to `.gitignore`?"*
|
|
432
|
+
|
|
433
|
+
Offer exactly two choices:
|
|
434
|
+
|
|
435
|
+
- **Add the rules** — run the seeder once. It appends that one marker block and
|
|
436
|
+
touches nothing else:
|
|
437
|
+
|
|
438
|
+
```bash
|
|
439
|
+
python3 scripts/worktree-lifecycle/ignore_seed.py apply
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
- **Not now** — write nothing. Explain that the planning skills still work, that
|
|
443
|
+
the artifacts will keep showing up as untracked changes, and that re-running
|
|
444
|
+
`/setup-workflow` offers this again.
|
|
445
|
+
|
|
446
|
+
The write is append-only inside one idempotent marker block: it never rewrites,
|
|
447
|
+
reorders, or removes an existing line; a re-run after an approval is a
|
|
448
|
+
byte-identical no-op; and a decline leaves the file exactly as it was. An
|
|
449
|
+
artifact already committed to history is reported separately — an ignore rule
|
|
450
|
+
cannot untrack it, and setup never runs `git rm`. Only this explicit, approved
|
|
451
|
+
step may write `.gitignore`; `init` and `update` reconciliation never reach the
|
|
452
|
+
seeder.
|
|
453
|
+
|
|
454
|
+
Report `appended · nothing to do (already ignored) · declined · blocked
|
|
455
|
+
(consumer-edited block)`. After an approval — and only then — the Section A4
|
|
456
|
+
`scratchPatterns` derivation has real ignored planning artefacts to read.
|
|
457
|
+
|
|
408
458
|
### 3. Section B — Triage labels
|
|
409
459
|
|
|
410
460
|
> When `triage` processes an incoming issue it applies labels (or your tracker's equivalent). It needs strings you've actually configured, or it creates duplicates.
|
|
@@ -82,12 +82,34 @@ The exact kit-owned commands are:
|
|
|
82
82
|
Preserve unrelated settings, hook groups, profile sections, and unknown keys.
|
|
83
83
|
Repeated reconciliation with the same choice is byte-identical.
|
|
84
84
|
|
|
85
|
+
## Planning-artifact ignore rules (offered, never installed)
|
|
86
|
+
|
|
87
|
+
The planning artefacts the shipped skills write live in the worktree, but
|
|
88
|
+
`.gitignore` is a consumer file the kit does not own: `init` and `update` never
|
|
89
|
+
touch it. Setup may therefore only **offer** the rules, from Section A11, using
|
|
90
|
+
`python3 scripts/worktree-lifecycle/ignore_seed.py`. The kit-owned declaration
|
|
91
|
+
of which artefacts those are is `scripts/worktree-lifecycle/plan-artifacts.json`.
|
|
92
|
+
|
|
93
|
+
| State | Setup action |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `append` | Show the exact marker block, then ask. |
|
|
96
|
+
| approve | Run `ignore_seed.py apply` once: it appends that one block, append-only. |
|
|
97
|
+
| decline | Write nothing; a later rerun offers it again. |
|
|
98
|
+
| already ignored | Report `nothing to do`; ask nothing and write nothing. |
|
|
99
|
+
| re-run after approval | Byte-identical no-op; never a second block. |
|
|
100
|
+
| consumer-edited block | `blocked` — report the uncovered artefacts and leave the file untouched. |
|
|
101
|
+
|
|
102
|
+
The seeder never rewrites, reorders, or removes an existing line, and a tracked
|
|
103
|
+
artefact is reported rather than untracked for the consumer. Only this explicit,
|
|
104
|
+
approved step may write `.gitignore`.
|
|
105
|
+
|
|
85
106
|
## Scratch classification and sweep
|
|
86
107
|
|
|
87
108
|
Reconcile an explicit `scratchPatterns` array when enabling a new profile.
|
|
88
109
|
Derive its glob values from the consumer's ignored planning artefacts; an empty
|
|
89
110
|
array is valid. Existing values are consumer-owned and remain byte-identical on
|
|
90
|
-
adoption or rerun. Core never supplies filename defaults
|
|
111
|
+
adoption or rerun. Core never supplies filename defaults — the offered ignore
|
|
112
|
+
rules above are what give that derivation something real to read.
|
|
91
113
|
|
|
92
114
|
Also reconcile an explicit
|
|
93
115
|
`wrapup.landingGeneratedArtifactPatterns` array. Derive candidates from the
|
package/README.md
CHANGED
|
@@ -469,6 +469,31 @@ the old way. Decision record:
|
|
|
469
469
|
|
|
470
470
|
## Release notes
|
|
471
471
|
|
|
472
|
+
### 0.40.0 — planning ignore rules are offered, never installed
|
|
473
|
+
|
|
474
|
+
Nothing in this release requires a consumer action. `kit-update` reconciles it.
|
|
475
|
+
|
|
476
|
+
Several skills used to state as fact that `PLAN.md`, `PLAN-REVIEW-LOG.md` and
|
|
477
|
+
`ANNAHMEN.md` are gitignored and live on disk only. In your repository that was
|
|
478
|
+
simply untrue: neither `init` nor `update` has ever touched your `.gitignore`,
|
|
479
|
+
and no installed file added those rules — so planning worktrees looked
|
|
480
|
+
permanently dirty, cleanup refused to remove them, and a plan document could be
|
|
481
|
+
committed by accident.
|
|
482
|
+
|
|
483
|
+
- **The prose is honest now.** The affected skills say the artifacts are
|
|
484
|
+
*expected* to be ignored and name who can add the rule, instead of asserting
|
|
485
|
+
an installed guarantee.
|
|
486
|
+
- **`/setup-workflow` offers the rules.** It previews the exact lines, asks
|
|
487
|
+
first, and appends one idempotent marker block. Declining changes nothing; a
|
|
488
|
+
re-run is a byte-identical no-op; a block you edited yourself is reported, not
|
|
489
|
+
repaired; an artifact already tracked by git is reported separately, because
|
|
490
|
+
an ignore rule cannot untrack it.
|
|
491
|
+
- **`.gitignore` stays yours.** It has no manifest entry, no baseline hash and
|
|
492
|
+
no three-way reconcile — a file the kit cannot reconcile is one it must not
|
|
493
|
+
write uninvited. Ordinary `update` reconciliation never reaches the seeder.
|
|
494
|
+
Decision record:
|
|
495
|
+
[`docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md`](docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md).
|
|
496
|
+
|
|
472
497
|
### 0.39.0 — self-explaining guards, one glob dialect, a board you can create
|
|
473
498
|
|
|
474
499
|
Nothing in this release requires a consumer action. `kit-update` reconciles it.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kitVersion": "0.
|
|
2
|
+
"kitVersion": "0.40.0",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": ".agents/skills/ask-matt/SKILL.md",
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
"ownerSkill": "grill-me",
|
|
208
208
|
"surface": "codex",
|
|
209
209
|
"installRole": "consumer",
|
|
210
|
-
"sha256": "
|
|
210
|
+
"sha256": "b13087563adda186f62aa6206a9233034310ff2458535362246294a45e03079f",
|
|
211
211
|
"mode": 420,
|
|
212
212
|
"origin": "kit"
|
|
213
213
|
},
|
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
"ownerSkill": "grill-with-docs",
|
|
248
248
|
"surface": "codex",
|
|
249
249
|
"installRole": "consumer",
|
|
250
|
-
"sha256": "
|
|
250
|
+
"sha256": "93694d55fe8d164c4c0e0eef27529bd1211acdaddc18224a921c2b0f3b5a10ed",
|
|
251
251
|
"mode": 420,
|
|
252
252
|
"origin": "kit"
|
|
253
253
|
},
|
|
@@ -447,7 +447,7 @@
|
|
|
447
447
|
"ownerSkill": "orchestrate-wave",
|
|
448
448
|
"surface": "codex",
|
|
449
449
|
"installRole": "consumer",
|
|
450
|
-
"sha256": "
|
|
450
|
+
"sha256": "fab4fb2f4d6fee820c9b7a737fa955a5ea2cd398987246bd7a04c5fba2c75b26",
|
|
451
451
|
"mode": 420,
|
|
452
452
|
"origin": "kit"
|
|
453
453
|
},
|
|
@@ -687,7 +687,7 @@
|
|
|
687
687
|
"ownerSkill": "setup-workflow",
|
|
688
688
|
"surface": "codex",
|
|
689
689
|
"installRole": "consumer",
|
|
690
|
-
"sha256": "
|
|
690
|
+
"sha256": "29c61411aa42a3f456b3f27fdd15552654b64ef2d72b321bc9c9e11229745626",
|
|
691
691
|
"mode": 420,
|
|
692
692
|
"origin": "kit"
|
|
693
693
|
},
|
|
@@ -757,7 +757,7 @@
|
|
|
757
757
|
"ownerSkill": "setup-workflow",
|
|
758
758
|
"surface": "codex",
|
|
759
759
|
"installRole": "consumer",
|
|
760
|
-
"sha256": "
|
|
760
|
+
"sha256": "f9fb7ef6d4f4e1ed603a5a480d8f97508fd6c99def8194c7ba0a95c62fd3c743",
|
|
761
761
|
"mode": 420,
|
|
762
762
|
"origin": "kit"
|
|
763
763
|
},
|
|
@@ -1489,7 +1489,7 @@
|
|
|
1489
1489
|
"ownerSkill": "grill-me",
|
|
1490
1490
|
"surface": "claude",
|
|
1491
1491
|
"installRole": "consumer",
|
|
1492
|
-
"sha256": "
|
|
1492
|
+
"sha256": "a63331339ed4e018d770face4e7cfe483225ce758a4a1302f39a50239e8187e1",
|
|
1493
1493
|
"mode": 420,
|
|
1494
1494
|
"origin": "kit"
|
|
1495
1495
|
},
|
|
@@ -1569,7 +1569,7 @@
|
|
|
1569
1569
|
"ownerSkill": "grill-with-docs",
|
|
1570
1570
|
"surface": "claude",
|
|
1571
1571
|
"installRole": "consumer",
|
|
1572
|
-
"sha256": "
|
|
1572
|
+
"sha256": "0cb1b41fc6bde94d3e71f3377cbb15b9d7a9a4089df66f591fc907dc19b6c1b5",
|
|
1573
1573
|
"mode": 420,
|
|
1574
1574
|
"origin": "kit"
|
|
1575
1575
|
},
|
|
@@ -1769,7 +1769,7 @@
|
|
|
1769
1769
|
"ownerSkill": "orchestrate-wave",
|
|
1770
1770
|
"surface": "claude",
|
|
1771
1771
|
"installRole": "consumer",
|
|
1772
|
-
"sha256": "
|
|
1772
|
+
"sha256": "fab4fb2f4d6fee820c9b7a737fa955a5ea2cd398987246bd7a04c5fba2c75b26",
|
|
1773
1773
|
"mode": 420,
|
|
1774
1774
|
"origin": "kit"
|
|
1775
1775
|
},
|
|
@@ -2029,7 +2029,7 @@
|
|
|
2029
2029
|
"ownerSkill": "setup-workflow",
|
|
2030
2030
|
"surface": "claude",
|
|
2031
2031
|
"installRole": "consumer",
|
|
2032
|
-
"sha256": "
|
|
2032
|
+
"sha256": "29c61411aa42a3f456b3f27fdd15552654b64ef2d72b321bc9c9e11229745626",
|
|
2033
2033
|
"mode": 420,
|
|
2034
2034
|
"origin": "kit"
|
|
2035
2035
|
},
|
|
@@ -2099,7 +2099,7 @@
|
|
|
2099
2099
|
"ownerSkill": "setup-workflow",
|
|
2100
2100
|
"surface": "claude",
|
|
2101
2101
|
"installRole": "consumer",
|
|
2102
|
-
"sha256": "
|
|
2102
|
+
"sha256": "f9fb7ef6d4f4e1ed603a5a480d8f97508fd6c99def8194c7ba0a95c62fd3c743",
|
|
2103
2103
|
"mode": 420,
|
|
2104
2104
|
"origin": "kit"
|
|
2105
2105
|
},
|
|
@@ -2815,6 +2815,22 @@
|
|
|
2815
2815
|
"mode": 420,
|
|
2816
2816
|
"origin": "kit"
|
|
2817
2817
|
},
|
|
2818
|
+
{
|
|
2819
|
+
"path": "scripts/worktree-lifecycle/ignore_seed.py",
|
|
2820
|
+
"kind": "script",
|
|
2821
|
+
"installRole": "consumer",
|
|
2822
|
+
"sha256": "defd667ed94301cffb0f08e0a910873c246be267cb458adf40f5e7b2109ae4d0",
|
|
2823
|
+
"mode": 493,
|
|
2824
|
+
"origin": "kit"
|
|
2825
|
+
},
|
|
2826
|
+
{
|
|
2827
|
+
"path": "scripts/worktree-lifecycle/plan-artifacts.json",
|
|
2828
|
+
"kind": "doc",
|
|
2829
|
+
"installRole": "consumer",
|
|
2830
|
+
"sha256": "fe7f805ca9c698f9d77bdda4372f6f5a516b1e60c46ae732a94de184cb0ce3b4",
|
|
2831
|
+
"mode": 420,
|
|
2832
|
+
"origin": "kit"
|
|
2833
|
+
},
|
|
2818
2834
|
{
|
|
2819
2835
|
"path": "scripts/worktree-lifecycle/profile.py",
|
|
2820
2836
|
"kind": "script",
|
|
@@ -2827,7 +2843,7 @@
|
|
|
2827
2843
|
"path": "scripts/worktree-lifecycle/README.md",
|
|
2828
2844
|
"kind": "doc",
|
|
2829
2845
|
"installRole": "consumer",
|
|
2830
|
-
"sha256": "
|
|
2846
|
+
"sha256": "0dbba4a87d41171f3fce22a4842eecd2a989ea6a5ddf44832d7b2aed9feb4226",
|
|
2831
2847
|
"mode": 420,
|
|
2832
2848
|
"origin": "kit"
|
|
2833
2849
|
},
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Planning ignore rules are offered, never installed
|
|
2
|
+
|
|
3
|
+
Status: accepted (2026-07-26, #255)
|
|
4
|
+
|
|
5
|
+
The shipped planning skills write `PLAN.md`, `PLAN-REVIEW-LOG.md`, and
|
|
6
|
+
`ANNAHMEN.md` into a session worktree, and several of them told the agent those
|
|
7
|
+
files were gitignored and lived on disk only. In a consumer repository that was
|
|
8
|
+
simply untrue: `init` and `update` never touch `.gitignore`, and no installed
|
|
9
|
+
file adds the rules. The consequence mirrors what this repository fixed for
|
|
10
|
+
itself in #254 — every planning worktree reports a permanently dirty tree, the
|
|
11
|
+
shipped Worktree Lifecycle cleanup refuses to remove it, genuinely unlanded work
|
|
12
|
+
becomes indistinguishable from plan scratch, and a consumer who never notices
|
|
13
|
+
can commit a plan doc into their history that the prose implied could not be
|
|
14
|
+
committed.
|
|
15
|
+
|
|
16
|
+
The constraint is the consumer contract. Consumer-owned files are written once
|
|
17
|
+
and never overwritten by ordinary reconciliation, and `.gitignore` is stronger
|
|
18
|
+
still: the kit does not own it at all and has no manifest entry, no baseline
|
|
19
|
+
hash, and no three-way reconcile for it. A file the kit cannot reconcile is a
|
|
20
|
+
file the kit must not write uninvited.
|
|
21
|
+
|
|
22
|
+
We decided to close the gap from both ends:
|
|
23
|
+
|
|
24
|
+
1. **`/setup-workflow` offers the rules as an explicit, previewed, approved
|
|
25
|
+
step.** Section A11 previews with
|
|
26
|
+
`python3 scripts/worktree-lifecycle/ignore_seed.py preview`, shows the exact
|
|
27
|
+
lines verbatim, asks, and only an approval runs `apply`. The write is
|
|
28
|
+
append-only inside one idempotent marker block. It never rewrites, reorders,
|
|
29
|
+
or removes an existing line; a re-run is a byte-identical no-op; a decline
|
|
30
|
+
changes nothing; a repository that already ignores the artefacts reports
|
|
31
|
+
`nothing to do`; and a marker block the consumer has since edited is
|
|
32
|
+
`blocked`, never repaired. Nothing in `init` or `update` can reach the
|
|
33
|
+
seeder.
|
|
34
|
+
2. **Shipped skill prose states the assumption instead of asserting the fact.**
|
|
35
|
+
A skill now says these artefacts are *expected* to be ignored and names
|
|
36
|
+
`/setup-workflow` as the thing that can make it true, rather than claiming
|
|
37
|
+
the kit already did. The claim is pinned by a census lint over both skill
|
|
38
|
+
trees, so the assertion cannot silently return.
|
|
39
|
+
|
|
40
|
+
This is the same shape the kit shipped for the GitHub-Projects board in 0.39.0
|
|
41
|
+
(#24): offer, preview, obtain an explicit yes, never write an outward or
|
|
42
|
+
consumer-owned artefact silently.
|
|
43
|
+
|
|
44
|
+
Which artefacts the offer covers is a kit-owned fact — the kit knows which files
|
|
45
|
+
its own skills write — declared once in
|
|
46
|
+
`scripts/worktree-lifecycle/plan-artifacts.json`. That is deliberately not the
|
|
47
|
+
consumer's `worktreeLifecycle.scratchPatterns`, which is derived *from* the
|
|
48
|
+
consumer's ignored planning artefacts and would otherwise be circular: before
|
|
49
|
+
this decision there was nothing ignored for it to be derived from.
|
|
50
|
+
|
|
51
|
+
## Considered options
|
|
52
|
+
|
|
53
|
+
- **Append-only seed at `init`:** rejected. It respects append-only mechanics
|
|
54
|
+
but still writes a file the kit does not own, uninvited, on first install —
|
|
55
|
+
the one action the consumer contract exists to prevent.
|
|
56
|
+
- **Readiness capability only:** rejected. Reporting the gap and letting the
|
|
57
|
+
affected skills degrade honestly leaves the friction in place and fixes
|
|
58
|
+
nothing for the consumer who wants the promise to hold.
|
|
59
|
+
- **Prose-only fix:** rejected as insufficient on its own. It removes the false
|
|
60
|
+
claim but leaves every consumer with dirty planning worktrees and a cleanup
|
|
61
|
+
that refuses to run. It is kept as one half of the decision, not the whole.
|
|
62
|
+
- **Deriving the rules from `worktreeLifecycle.scratchPatterns`:** rejected as
|
|
63
|
+
circular, and because that array is consumer-owned deletion-adjacent policy
|
|
64
|
+
that Core never supplies defaults for.
|
|
65
|
+
- **Untracking an already-committed artefact:** rejected. An ignore rule cannot
|
|
66
|
+
untrack a tracked file, and running `git rm` on consumer history is far past
|
|
67
|
+
what a setup step may do. The seeder reports the tracked artefact and stops.
|
|
68
|
+
|
|
69
|
+
## Consequences
|
|
70
|
+
|
|
71
|
+
- A consumer who runs `/setup-workflow` and approves gets the guarantee the
|
|
72
|
+
skills describe; a consumer who declines keeps a byte-identical `.gitignore`
|
|
73
|
+
and loses nothing but the convenience.
|
|
74
|
+
- The never-overwrite contract is untouched: the only write is an append behind
|
|
75
|
+
an explicit yes, and `update` reconciliation still never touches
|
|
76
|
+
`.gitignore`.
|
|
77
|
+
- The offer reaches only consumers who run setup again. That is accepted: it is
|
|
78
|
+
the price of not writing an unowned file, and re-running `/setup-workflow` is
|
|
79
|
+
already the documented way to pick up a new project-layer capability.
|
|
80
|
+
- After an approval, `scratchPatterns` derivation finally has real ignored
|
|
81
|
+
planning artefacts to read, so the Worktree Lifecycle scratch classification
|
|
82
|
+
stops being vacuous in a fresh consumer repository.
|
|
83
|
+
- A consumer who edits or deletes the marker block owns the outcome; the kit
|
|
84
|
+
reports and stops rather than restoring its own block.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikon85/agent-workflow-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Consumer ignore-gap contract (#255).
|
|
3
|
+
|
|
4
|
+
Shipped skills write `PLAN.md`, `PLAN-REVIEW-LOG.md`, and `ANNAHMEN.md` into a
|
|
5
|
+
session worktree, but `.gitignore` is a consumer file the kit does not own, so
|
|
6
|
+
`init`/`update` never seed the matching rules. ADR 0008 resolves the gap two
|
|
7
|
+
ways, and both are pinned here:
|
|
8
|
+
|
|
9
|
+
1. `/setup-workflow` may **offer** the rules through
|
|
10
|
+
`scripts/worktree-lifecycle/ignore_seed.py`. The helper is append-only
|
|
11
|
+
inside one idempotent marker block: it never rewrites, reorders, or removes
|
|
12
|
+
an existing line, it is a no-op on re-run, and it is unreachable from
|
|
13
|
+
`init`/`update` reconciliation.
|
|
14
|
+
2. Shipped skill prose states the assumption instead of asserting the ignore
|
|
15
|
+
state as a fact the kit cannot guarantee.
|
|
16
|
+
|
|
17
|
+
Run: python3 scripts/test_worktree_ignore_seed.py
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import importlib.util
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
import tempfile
|
|
28
|
+
import unittest
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
32
|
+
LIFECYCLE = REPO / "scripts/worktree-lifecycle"
|
|
33
|
+
HELPER = LIFECYCLE / "ignore_seed.py"
|
|
34
|
+
ARTIFACT_MANIFEST = LIFECYCLE / "plan-artifacts.json"
|
|
35
|
+
SKILL_TREES = (REPO / ".claude/skills", REPO / ".agents/skills")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_helper():
|
|
39
|
+
spec = importlib.util.spec_from_file_location("wl_ignore_seed", HELPER)
|
|
40
|
+
module = importlib.util.module_from_spec(spec)
|
|
41
|
+
sys.modules[spec.name] = module
|
|
42
|
+
spec.loader.exec_module(module)
|
|
43
|
+
return module
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def git(repo: Path, *args: str) -> subprocess.CompletedProcess:
|
|
47
|
+
return subprocess.run(
|
|
48
|
+
["git", *args], cwd=repo, capture_output=True, text=True, check=True,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def make_repo(stack, gitignore: str | None = None) -> Path:
|
|
53
|
+
repo = Path(stack.enter_context(tempfile.TemporaryDirectory()))
|
|
54
|
+
git(repo, "init", "-q")
|
|
55
|
+
git(repo, "config", "user.email", "test@example.com")
|
|
56
|
+
git(repo, "config", "user.name", "test")
|
|
57
|
+
if gitignore is not None:
|
|
58
|
+
(repo / ".gitignore").write_text(gitignore, encoding="utf-8")
|
|
59
|
+
return repo
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ArtifactManifestTest(unittest.TestCase):
|
|
63
|
+
"""The kit declares which planning artifacts its own skills write."""
|
|
64
|
+
|
|
65
|
+
def test_manifest_declares_the_three_planning_artifacts(self):
|
|
66
|
+
document = json.loads(ARTIFACT_MANIFEST.read_text(encoding="utf-8"))
|
|
67
|
+
paths = [entry["path"] for entry in document["artifacts"]]
|
|
68
|
+
self.assertEqual(
|
|
69
|
+
paths, ["PLAN.md", "PLAN-REVIEW-LOG.md", "ANNAHMEN.md"],
|
|
70
|
+
)
|
|
71
|
+
for entry in document["artifacts"]:
|
|
72
|
+
self.assertTrue(entry["writtenBy"], entry["path"])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PlanTest(unittest.TestCase):
|
|
76
|
+
"""`plan()` reports the exact append, never a rewrite."""
|
|
77
|
+
|
|
78
|
+
def setUp(self):
|
|
79
|
+
self.helper = load_helper()
|
|
80
|
+
self.stack = __import__("contextlib").ExitStack()
|
|
81
|
+
self.addCleanup(self.stack.close)
|
|
82
|
+
|
|
83
|
+
def test_fresh_repo_lists_every_artifact_as_pending(self):
|
|
84
|
+
repo = make_repo(self.stack, "node_modules/\n")
|
|
85
|
+
plan = self.helper.plan(repo)
|
|
86
|
+
self.assertEqual(
|
|
87
|
+
plan.pending, ("PLAN.md", "PLAN-REVIEW-LOG.md", "ANNAHMEN.md"),
|
|
88
|
+
)
|
|
89
|
+
self.assertEqual(plan.already_ignored, ())
|
|
90
|
+
self.assertEqual(plan.status, "append")
|
|
91
|
+
for path in plan.pending:
|
|
92
|
+
self.assertIn(f"\n{path}\n", plan.block)
|
|
93
|
+
self.assertTrue(plan.block.startswith(self.helper.BLOCK_START))
|
|
94
|
+
self.assertTrue(plan.block.rstrip("\n").endswith(self.helper.BLOCK_END))
|
|
95
|
+
|
|
96
|
+
def test_missing_gitignore_is_reported_as_a_create(self):
|
|
97
|
+
repo = make_repo(self.stack)
|
|
98
|
+
plan = self.helper.plan(repo)
|
|
99
|
+
self.assertEqual(plan.status, "append")
|
|
100
|
+
self.assertFalse(plan.gitignore_exists)
|
|
101
|
+
|
|
102
|
+
def test_consumer_rules_already_covering_everything_are_nothing_to_do(self):
|
|
103
|
+
repo = make_repo(
|
|
104
|
+
self.stack, "PLAN.md\nPLAN-REVIEW-LOG.md\nANNAHMEN.md\n",
|
|
105
|
+
)
|
|
106
|
+
plan = self.helper.plan(repo)
|
|
107
|
+
self.assertEqual(plan.pending, ())
|
|
108
|
+
self.assertEqual(plan.status, "nothing-to-do")
|
|
109
|
+
self.assertIsNone(plan.block)
|
|
110
|
+
|
|
111
|
+
def test_partial_coverage_pends_only_the_missing_rules(self):
|
|
112
|
+
repo = make_repo(self.stack, "PLAN.md\n")
|
|
113
|
+
plan = self.helper.plan(repo)
|
|
114
|
+
self.assertEqual(plan.pending, ("PLAN-REVIEW-LOG.md", "ANNAHMEN.md"))
|
|
115
|
+
self.assertEqual(plan.already_ignored, ("PLAN.md",))
|
|
116
|
+
self.assertNotIn("\nPLAN.md\n", plan.block)
|
|
117
|
+
|
|
118
|
+
def test_a_wildcard_consumer_rule_counts_as_covered(self):
|
|
119
|
+
repo = make_repo(self.stack, "*.md\n")
|
|
120
|
+
plan = self.helper.plan(repo)
|
|
121
|
+
self.assertEqual(plan.pending, ())
|
|
122
|
+
|
|
123
|
+
def test_a_tracked_artifact_is_named_because_a_rule_cannot_untrack_it(self):
|
|
124
|
+
repo = make_repo(self.stack, "node_modules/\n")
|
|
125
|
+
(repo / "PLAN.md").write_text("plan\n", encoding="utf-8")
|
|
126
|
+
git(repo, "add", "PLAN.md")
|
|
127
|
+
git(repo, "commit", "-qm", "add plan")
|
|
128
|
+
plan = self.helper.plan(repo)
|
|
129
|
+
self.assertEqual(plan.tracked, ("PLAN.md",))
|
|
130
|
+
self.assertIn("PLAN.md", plan.pending)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ApplyTest(unittest.TestCase):
|
|
134
|
+
"""`apply()` is append-only, idempotent, and never rewrites."""
|
|
135
|
+
|
|
136
|
+
def setUp(self):
|
|
137
|
+
self.helper = load_helper()
|
|
138
|
+
self.stack = __import__("contextlib").ExitStack()
|
|
139
|
+
self.addCleanup(self.stack.close)
|
|
140
|
+
|
|
141
|
+
def test_apply_appends_and_preserves_the_existing_bytes_verbatim(self):
|
|
142
|
+
original = "node_modules/\n\n# my rules\ndist/\n"
|
|
143
|
+
repo = make_repo(self.stack, original)
|
|
144
|
+
result = self.helper.apply(repo)
|
|
145
|
+
self.assertEqual(result.status, "appended")
|
|
146
|
+
text = (repo / ".gitignore").read_text(encoding="utf-8")
|
|
147
|
+
self.assertTrue(text.startswith(original))
|
|
148
|
+
self.assertIn(self.helper.BLOCK_START, text)
|
|
149
|
+
for path in ("PLAN.md", "PLAN-REVIEW-LOG.md", "ANNAHMEN.md"):
|
|
150
|
+
self.assertIn(f"\n{path}\n", text)
|
|
151
|
+
|
|
152
|
+
def test_rerun_is_a_byte_identical_no_op(self):
|
|
153
|
+
repo = make_repo(self.stack, "node_modules/\n")
|
|
154
|
+
self.helper.apply(repo)
|
|
155
|
+
first = (repo / ".gitignore").read_bytes()
|
|
156
|
+
second_result = self.helper.apply(repo)
|
|
157
|
+
self.assertEqual(second_result.status, "nothing-to-do")
|
|
158
|
+
self.assertEqual((repo / ".gitignore").read_bytes(), first)
|
|
159
|
+
self.assertEqual(first.decode().count(self.helper.BLOCK_START), 1)
|
|
160
|
+
|
|
161
|
+
def test_decline_path_writes_nothing(self):
|
|
162
|
+
repo = make_repo(self.stack, "node_modules/\n")
|
|
163
|
+
before = (repo / ".gitignore").read_bytes()
|
|
164
|
+
self.helper.plan(repo)
|
|
165
|
+
self.assertEqual((repo / ".gitignore").read_bytes(), before)
|
|
166
|
+
|
|
167
|
+
def test_already_covered_repo_is_left_untouched(self):
|
|
168
|
+
original = "PLAN.md\nPLAN-REVIEW-LOG.md\nANNAHMEN.md\n"
|
|
169
|
+
repo = make_repo(self.stack, original)
|
|
170
|
+
result = self.helper.apply(repo)
|
|
171
|
+
self.assertEqual(result.status, "nothing-to-do")
|
|
172
|
+
self.assertEqual(
|
|
173
|
+
(repo / ".gitignore").read_text(encoding="utf-8"), original,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def test_missing_gitignore_is_created_with_only_the_block(self):
|
|
177
|
+
repo = make_repo(self.stack)
|
|
178
|
+
self.helper.apply(repo)
|
|
179
|
+
text = (repo / ".gitignore").read_text(encoding="utf-8")
|
|
180
|
+
self.assertTrue(text.startswith(self.helper.BLOCK_START))
|
|
181
|
+
|
|
182
|
+
def test_an_edited_marker_block_blocks_instead_of_rewriting(self):
|
|
183
|
+
repo = make_repo(self.stack)
|
|
184
|
+
self.helper.apply(repo)
|
|
185
|
+
text = (repo / ".gitignore").read_text(encoding="utf-8")
|
|
186
|
+
edited = text.replace("ANNAHMEN.md\n", "")
|
|
187
|
+
(repo / ".gitignore").write_text(edited, encoding="utf-8")
|
|
188
|
+
result = self.helper.apply(repo)
|
|
189
|
+
self.assertEqual(result.status, "blocked")
|
|
190
|
+
self.assertEqual(
|
|
191
|
+
(repo / ".gitignore").read_text(encoding="utf-8"), edited,
|
|
192
|
+
)
|
|
193
|
+
self.assertEqual(edited.count(self.helper.BLOCK_START), 1)
|
|
194
|
+
|
|
195
|
+
def test_cli_preview_writes_nothing_and_reports_json(self):
|
|
196
|
+
repo = make_repo(self.stack, "node_modules/\n")
|
|
197
|
+
before = (repo / ".gitignore").read_bytes()
|
|
198
|
+
result = subprocess.run(
|
|
199
|
+
[sys.executable, str(HELPER), "preview", "--repo", str(repo), "--json"],
|
|
200
|
+
capture_output=True, text=True,
|
|
201
|
+
)
|
|
202
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
203
|
+
payload = json.loads(result.stdout)
|
|
204
|
+
self.assertEqual(payload["status"], "append")
|
|
205
|
+
self.assertEqual((repo / ".gitignore").read_bytes(), before)
|
|
206
|
+
|
|
207
|
+
def test_cli_apply_exits_zero_and_is_idempotent(self):
|
|
208
|
+
repo = make_repo(self.stack, "node_modules/\n")
|
|
209
|
+
for _ in range(2):
|
|
210
|
+
result = subprocess.run(
|
|
211
|
+
[sys.executable, str(HELPER), "apply", "--repo", str(repo)],
|
|
212
|
+
capture_output=True, text=True,
|
|
213
|
+
)
|
|
214
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
215
|
+
text = (repo / ".gitignore").read_text(encoding="utf-8")
|
|
216
|
+
self.assertEqual(text.count(self.helper.BLOCK_START), 1)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class ReconciliationBoundaryTest(unittest.TestCase):
|
|
220
|
+
"""Only setup-workflow may reach the seeder; never init/update."""
|
|
221
|
+
|
|
222
|
+
def test_no_installer_command_invokes_the_seeder(self):
|
|
223
|
+
# bundle.mjs only DECLARES the file as shipped; every other installer
|
|
224
|
+
# module must not know the seeder exists.
|
|
225
|
+
declaration = REPO / "src/lib/bundle.mjs"
|
|
226
|
+
offenders = []
|
|
227
|
+
for path in sorted((REPO / "src").rglob("*.mjs")):
|
|
228
|
+
if path == declaration:
|
|
229
|
+
continue
|
|
230
|
+
if "ignore_seed" in path.read_text(encoding="utf-8"):
|
|
231
|
+
offenders.append(str(path.relative_to(REPO)))
|
|
232
|
+
self.assertEqual(offenders, [])
|
|
233
|
+
|
|
234
|
+
def test_the_seeder_ships_with_the_kit(self):
|
|
235
|
+
bundle = (REPO / "src/lib/bundle.mjs").read_text(encoding="utf-8")
|
|
236
|
+
self.assertIn("scripts/worktree-lifecycle/ignore_seed.py", bundle)
|
|
237
|
+
self.assertIn("scripts/worktree-lifecycle/plan-artifacts.json", bundle)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class SetupWorkflowOfferTest(unittest.TestCase):
|
|
241
|
+
"""The offer is an explicit, previewed, declinable setup step."""
|
|
242
|
+
|
|
243
|
+
def _skill(self, tree: Path) -> str:
|
|
244
|
+
return (tree / "setup-workflow/SKILL.md").read_text(encoding="utf-8")
|
|
245
|
+
|
|
246
|
+
def _seed(self, tree: Path) -> str:
|
|
247
|
+
return (tree / "setup-workflow/worktree-lifecycle.md").read_text(
|
|
248
|
+
encoding="utf-8",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def test_both_surfaces_document_the_previewed_offer(self):
|
|
252
|
+
for tree in SKILL_TREES:
|
|
253
|
+
body = self._skill(tree)
|
|
254
|
+
self.assertIn(
|
|
255
|
+
"python3 scripts/worktree-lifecycle/ignore_seed.py preview", body,
|
|
256
|
+
)
|
|
257
|
+
self.assertIn(
|
|
258
|
+
"python3 scripts/worktree-lifecycle/ignore_seed.py apply", body,
|
|
259
|
+
)
|
|
260
|
+
flat = re.sub(r"\s+", " ", body)
|
|
261
|
+
self.assertIn("Add the rules", flat)
|
|
262
|
+
self.assertIn("Not now", flat)
|
|
263
|
+
|
|
264
|
+
def test_the_seed_contract_carries_the_ignore_offer_matrix(self):
|
|
265
|
+
for tree in SKILL_TREES:
|
|
266
|
+
flat = re.sub(r"\s+", " ", self._seed(tree))
|
|
267
|
+
self.assertIn("ignore_seed.py", flat)
|
|
268
|
+
for row in ("approve", "decline", "already ignored", "re-run"):
|
|
269
|
+
self.assertIn(row, flat)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class ProseClaimCensusTest(unittest.TestCase):
|
|
273
|
+
"""No shipped skill asserts an ignore state the kit cannot guarantee."""
|
|
274
|
+
|
|
275
|
+
ARTIFACT = re.compile(
|
|
276
|
+
r"PLAN\.md|PLAN-REVIEW-LOG\.md|ANNAHMEN\.md|plan doc|assumptions log",
|
|
277
|
+
re.IGNORECASE,
|
|
278
|
+
)
|
|
279
|
+
# Unconditional assertions of the ignore state. Conditional or hedged
|
|
280
|
+
# wording ("a project may gitignore these files", "when `PLAN.md` is
|
|
281
|
+
# ignored") is deliberately allowed.
|
|
282
|
+
FORBIDDEN = (
|
|
283
|
+
re.compile(r"\(gitignored\b", re.IGNORECASE),
|
|
284
|
+
re.compile(r"\b(?:is|are|stays?|remains?)\s+gitignored\b", re.IGNORECASE),
|
|
285
|
+
re.compile(r"\bgitignored\s+(?:at|in|since)\b", re.IGNORECASE),
|
|
286
|
+
re.compile(r"\ba\s+gitignored\s+plan\s+doc\b", re.IGNORECASE),
|
|
287
|
+
re.compile(r"\bgitignored,\s*on-disk only\b", re.IGNORECASE),
|
|
288
|
+
)
|
|
289
|
+
WINDOW = 160
|
|
290
|
+
|
|
291
|
+
def _claim_offenders(self):
|
|
292
|
+
offenders = []
|
|
293
|
+
for tree in SKILL_TREES:
|
|
294
|
+
for path in sorted(tree.rglob("*.md")):
|
|
295
|
+
flat = re.sub(r"\s+", " ", path.read_text(encoding="utf-8"))
|
|
296
|
+
for pattern in self.FORBIDDEN:
|
|
297
|
+
for match in pattern.finditer(flat):
|
|
298
|
+
window = flat[
|
|
299
|
+
max(0, match.start() - self.WINDOW):
|
|
300
|
+
match.end() + self.WINDOW
|
|
301
|
+
]
|
|
302
|
+
if self.ARTIFACT.search(window):
|
|
303
|
+
offenders.append(
|
|
304
|
+
f"{path.relative_to(REPO)}: {match.group(0)}"
|
|
305
|
+
)
|
|
306
|
+
return sorted(set(offenders))
|
|
307
|
+
|
|
308
|
+
def test_no_skill_states_the_ignore_rule_as_an_installed_fact(self):
|
|
309
|
+
self.assertEqual(self._claim_offenders(), [])
|
|
310
|
+
|
|
311
|
+
def test_the_replacement_wording_names_who_can_make_it_true(self):
|
|
312
|
+
for name in ("grill-me", "grill-with-docs", "orchestrate-wave"):
|
|
313
|
+
for tree in SKILL_TREES:
|
|
314
|
+
body = (tree / name / "SKILL.md").read_text(encoding="utf-8")
|
|
315
|
+
flat = re.sub(r"\s+", " ", body)
|
|
316
|
+
self.assertIn("setup-workflow", flat, f"{tree}/{name}")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
unittest.main()
|
|
@@ -58,6 +58,24 @@ the core proves the target is unsafe.
|
|
|
58
58
|
| `enforce-worktree-discipline.py` | PreToolUse | routes issue-branch creation through the configured setup entry |
|
|
59
59
|
| `slice-handoff-hint.py` | UserPromptSubmit | names the configured setup entry for a defined slice |
|
|
60
60
|
|
|
61
|
+
## Planning-artifact ignore rules
|
|
62
|
+
|
|
63
|
+
`plan-artifacts.json` is the kit-owned declaration of the planning artefacts the
|
|
64
|
+
shipped skills write (`PLAN.md`, `PLAN-REVIEW-LOG.md`, `ANNAHMEN.md`).
|
|
65
|
+
`ignore_seed.py` turns that declaration into an offer, never an installation:
|
|
66
|
+
`preview` reports what an approval would append and writes nothing, and `apply`
|
|
67
|
+
appends exactly one marker block. `.gitignore` is a consumer file the kit does
|
|
68
|
+
not own, so only an explicit approved `/setup-workflow` step may run `apply`;
|
|
69
|
+
`init` and `update` reconciliation never reach it (ADR 0008).
|
|
70
|
+
|
|
71
|
+
The append never rewrites, reorders, or removes an existing line. A repository
|
|
72
|
+
that already ignores every artefact reports `nothing-to-do`, a re-run after an
|
|
73
|
+
approval is a byte-identical no-op, and a marker block the consumer has since
|
|
74
|
+
edited reports `blocked` rather than being repaired. An artefact already tracked
|
|
75
|
+
in git is named separately — an ignore rule cannot untrack it, and the helper
|
|
76
|
+
never runs `git rm`. Only after such an approval does the `scratchPatterns`
|
|
77
|
+
derivation have real ignored planning artefacts to read.
|
|
78
|
+
|
|
61
79
|
## Cleanup
|
|
62
80
|
|
|
63
81
|
`cleanup.py` previews by default. Removal refuses protected, tracked-dirty,
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Offer the planning-artifact ignore rules for a consumer `.gitignore`.
|
|
3
|
+
|
|
4
|
+
The shipped skills write `PLAN.md`, `PLAN-REVIEW-LOG.md`, and `ANNAHMEN.md`
|
|
5
|
+
into a session worktree, but `.gitignore` is a consumer file the kit does not
|
|
6
|
+
own: `init` and `update` never touch it. This helper is the one place that
|
|
7
|
+
closes that gap, and it closes it only when a user explicitly approves the
|
|
8
|
+
offer inside `/setup-workflow` (ADR 0008).
|
|
9
|
+
|
|
10
|
+
Contract:
|
|
11
|
+
|
|
12
|
+
- `preview` reads and reports; it never writes.
|
|
13
|
+
- `apply` appends one marker block and nothing else. It never rewrites,
|
|
14
|
+
reorders, or removes an existing line, so a second run is a byte-identical
|
|
15
|
+
no-op once the rules are in place.
|
|
16
|
+
- Rules a consumer already has — by any pattern, including a wildcard — are
|
|
17
|
+
reported as covered and never duplicated.
|
|
18
|
+
- A marker block that the consumer has since edited is theirs: the helper
|
|
19
|
+
blocks instead of repairing it.
|
|
20
|
+
- Nothing here runs during `init`/`update` reconciliation.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
python3 scripts/worktree-lifecycle/ignore_seed.py preview [--repo PATH] [--json]
|
|
24
|
+
python3 scripts/worktree-lifecycle/ignore_seed.py apply [--repo PATH] [--json]
|
|
25
|
+
|
|
26
|
+
Exit codes: 0 previewed/appended/nothing-to-do · 2 blocked · 1 error.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import json
|
|
33
|
+
import subprocess
|
|
34
|
+
import sys
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
MANIFEST = Path(__file__).resolve().parent / "plan-artifacts.json"
|
|
39
|
+
|
|
40
|
+
BLOCK_START = "# >>> agent-workflow-kit: planning-artifacts/v1 >>>"
|
|
41
|
+
BLOCK_END = "# <<< agent-workflow-kit: planning-artifacts/v1 <<<"
|
|
42
|
+
BLOCK_NOTE = (
|
|
43
|
+
"# Planning artifacts the agent workflow skills write into a session\n"
|
|
44
|
+
"# worktree. They are session scratch, never part of the change. Added by\n"
|
|
45
|
+
"# setup-workflow on request; this block is yours to edit or delete.\n"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class IgnoreSeedError(RuntimeError):
|
|
50
|
+
"""A safe, user-visible refusal."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class IgnorePlan:
|
|
55
|
+
"""What an approval would append — and nothing more."""
|
|
56
|
+
|
|
57
|
+
repo: Path
|
|
58
|
+
gitignore_exists: bool
|
|
59
|
+
already_ignored: tuple[str, ...]
|
|
60
|
+
pending: tuple[str, ...]
|
|
61
|
+
tracked: tuple[str, ...]
|
|
62
|
+
block: str | None
|
|
63
|
+
status: str
|
|
64
|
+
detail: str
|
|
65
|
+
|
|
66
|
+
def as_dict(self) -> dict:
|
|
67
|
+
return {
|
|
68
|
+
"status": self.status,
|
|
69
|
+
"detail": self.detail,
|
|
70
|
+
"gitignoreExists": self.gitignore_exists,
|
|
71
|
+
"alreadyIgnored": list(self.already_ignored),
|
|
72
|
+
"pending": list(self.pending),
|
|
73
|
+
"tracked": list(self.tracked),
|
|
74
|
+
"block": self.block,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_artifacts(manifest: Path = MANIFEST) -> tuple[str, ...]:
|
|
79
|
+
"""Return the kit-declared planning artifacts, in declaration order."""
|
|
80
|
+
try:
|
|
81
|
+
document = json.loads(manifest.read_text(encoding="utf-8"))
|
|
82
|
+
return tuple(entry["path"] for entry in document["artifacts"])
|
|
83
|
+
except (OSError, json.JSONDecodeError, KeyError, TypeError) as error:
|
|
84
|
+
raise IgnoreSeedError(f"cannot read planning-artifact manifest: {error}") from error
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _git(repo: Path, *args: str) -> subprocess.CompletedProcess:
|
|
88
|
+
try:
|
|
89
|
+
return subprocess.run(
|
|
90
|
+
["git", *args], cwd=repo, capture_output=True, text=True,
|
|
91
|
+
)
|
|
92
|
+
except OSError as error: # pragma: no cover - git absent
|
|
93
|
+
raise IgnoreSeedError(f"cannot run git: {error}") from error
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def is_ignored(repo: Path, path: str) -> bool:
|
|
97
|
+
"""True when any ignore rule already matches `path`.
|
|
98
|
+
|
|
99
|
+
`--no-index` asks the pattern question, not the index question, so an
|
|
100
|
+
artifact that a consumer once committed still reports its real rule state.
|
|
101
|
+
"""
|
|
102
|
+
return _git(repo, "check-ignore", "-q", "--no-index", "--", path).returncode == 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_tracked(repo: Path, path: str) -> bool:
|
|
106
|
+
"""True when the artifact is committed — an ignore rule cannot untrack it."""
|
|
107
|
+
result = _git(repo, "ls-files", "--error-unmatch", "--", path)
|
|
108
|
+
return result.returncode == 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def render_block(paths: tuple[str, ...]) -> str:
|
|
112
|
+
"""The exact text an approval appends."""
|
|
113
|
+
listed = "".join(f"{path}\n" for path in paths)
|
|
114
|
+
return f"{BLOCK_START}\n{BLOCK_NOTE}{listed}{BLOCK_END}\n"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _read_gitignore(repo: Path) -> tuple[bool, str]:
|
|
118
|
+
target = repo / ".gitignore"
|
|
119
|
+
try:
|
|
120
|
+
return True, target.read_text(encoding="utf-8")
|
|
121
|
+
except FileNotFoundError:
|
|
122
|
+
return False, ""
|
|
123
|
+
except OSError as error:
|
|
124
|
+
raise IgnoreSeedError(f"cannot read {target}: {error}") from error
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def plan(repo: Path, manifest: Path = MANIFEST) -> IgnorePlan:
|
|
128
|
+
"""Report what an approval would change. Reads only."""
|
|
129
|
+
repo = Path(repo)
|
|
130
|
+
if not (repo / ".git").exists():
|
|
131
|
+
raise IgnoreSeedError(f"not a git repository: {repo}")
|
|
132
|
+
artifacts = load_artifacts(manifest)
|
|
133
|
+
exists, text = _read_gitignore(repo)
|
|
134
|
+
covered = tuple(path for path in artifacts if is_ignored(repo, path))
|
|
135
|
+
pending = tuple(path for path in artifacts if path not in covered)
|
|
136
|
+
tracked = tuple(path for path in artifacts if is_tracked(repo, path))
|
|
137
|
+
has_block = BLOCK_START in text
|
|
138
|
+
|
|
139
|
+
if not pending:
|
|
140
|
+
return IgnorePlan(
|
|
141
|
+
repo=repo, gitignore_exists=exists, already_ignored=covered,
|
|
142
|
+
pending=(), tracked=tracked, block=None, status="nothing-to-do",
|
|
143
|
+
detail="every planning artifact is already ignored",
|
|
144
|
+
)
|
|
145
|
+
if has_block:
|
|
146
|
+
return IgnorePlan(
|
|
147
|
+
repo=repo, gitignore_exists=exists, already_ignored=covered,
|
|
148
|
+
pending=pending, tracked=tracked, block=None, status="blocked",
|
|
149
|
+
detail=(
|
|
150
|
+
"the marker block exists but no longer covers "
|
|
151
|
+
f"{', '.join(pending)} — that block is consumer-owned; "
|
|
152
|
+
"edit .gitignore yourself"
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
return IgnorePlan(
|
|
156
|
+
repo=repo, gitignore_exists=exists, already_ignored=covered,
|
|
157
|
+
pending=pending, tracked=tracked, block=render_block(pending),
|
|
158
|
+
status="append",
|
|
159
|
+
detail=f"would append {len(pending)} rule(s) in one marker block",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def apply(repo: Path, manifest: Path = MANIFEST) -> IgnorePlan:
|
|
164
|
+
"""Append the previewed block. Never rewrites an existing line."""
|
|
165
|
+
decision = plan(repo, manifest)
|
|
166
|
+
if decision.status != "append":
|
|
167
|
+
return decision
|
|
168
|
+
target = Path(repo) / ".gitignore"
|
|
169
|
+
_, text = _read_gitignore(Path(repo))
|
|
170
|
+
prefix = text
|
|
171
|
+
if prefix and not prefix.endswith("\n"):
|
|
172
|
+
prefix += "\n"
|
|
173
|
+
if prefix and not prefix.endswith("\n\n"):
|
|
174
|
+
prefix += "\n"
|
|
175
|
+
try:
|
|
176
|
+
target.write_text(prefix + decision.block, encoding="utf-8")
|
|
177
|
+
except OSError as error:
|
|
178
|
+
raise IgnoreSeedError(f"cannot write {target}: {error}") from error
|
|
179
|
+
return IgnorePlan(
|
|
180
|
+
repo=decision.repo, gitignore_exists=True,
|
|
181
|
+
already_ignored=decision.already_ignored, pending=decision.pending,
|
|
182
|
+
tracked=decision.tracked, block=decision.block, status="appended",
|
|
183
|
+
detail=f"appended {len(decision.pending)} rule(s) to .gitignore",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _report(decision: IgnorePlan, *, as_json: bool) -> None:
|
|
188
|
+
if as_json:
|
|
189
|
+
print(json.dumps(decision.as_dict(), indent=2))
|
|
190
|
+
return
|
|
191
|
+
print(f"{decision.status} — {decision.detail}")
|
|
192
|
+
if decision.already_ignored:
|
|
193
|
+
print(f" already ignored: {', '.join(decision.already_ignored)}")
|
|
194
|
+
if decision.tracked:
|
|
195
|
+
print(
|
|
196
|
+
" tracked in git (an ignore rule cannot untrack these): "
|
|
197
|
+
f"{', '.join(decision.tracked)}"
|
|
198
|
+
)
|
|
199
|
+
if decision.block:
|
|
200
|
+
label = "appended" if decision.status == "appended" else "would append"
|
|
201
|
+
print(f" {label}:")
|
|
202
|
+
for line in decision.block.rstrip("\n").split("\n"):
|
|
203
|
+
print(f" {line}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
207
|
+
parser = argparse.ArgumentParser(prog="ignore_seed.py")
|
|
208
|
+
parser.add_argument("command", choices=("preview", "apply"))
|
|
209
|
+
parser.add_argument("--repo", default=".")
|
|
210
|
+
parser.add_argument("--json", action="store_true")
|
|
211
|
+
return parser.parse_args(argv)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def main(argv: list[str]) -> int:
|
|
215
|
+
args = parse_args(argv)
|
|
216
|
+
try:
|
|
217
|
+
decision = plan(args.repo) if args.command == "preview" else apply(args.repo)
|
|
218
|
+
except IgnoreSeedError as error:
|
|
219
|
+
print(str(error), file=sys.stderr)
|
|
220
|
+
return 1
|
|
221
|
+
_report(decision, as_json=args.json)
|
|
222
|
+
return 2 if decision.status == "blocked" else 0
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"artifacts": [
|
|
4
|
+
{
|
|
5
|
+
"path": "PLAN.md",
|
|
6
|
+
"writtenBy": [
|
|
7
|
+
"grill-me",
|
|
8
|
+
"grill-with-docs",
|
|
9
|
+
"codex-review",
|
|
10
|
+
"grill-me-codex",
|
|
11
|
+
"grill-with-docs-codex",
|
|
12
|
+
"codex-build",
|
|
13
|
+
"orchestrate-wave"
|
|
14
|
+
],
|
|
15
|
+
"why": "the locked plan a planning session hands to the implementing session"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "PLAN-REVIEW-LOG.md",
|
|
19
|
+
"writtenBy": [
|
|
20
|
+
"codex-review",
|
|
21
|
+
"grill-me-codex",
|
|
22
|
+
"grill-with-docs-codex",
|
|
23
|
+
"codex-build"
|
|
24
|
+
],
|
|
25
|
+
"why": "the append-only cross-model review transcript of that plan"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "ANNAHMEN.md",
|
|
29
|
+
"writtenBy": [
|
|
30
|
+
"tdd",
|
|
31
|
+
"orchestrate-wave",
|
|
32
|
+
"wrapup"
|
|
33
|
+
],
|
|
34
|
+
"why": "the build-time assumption-drift log wrapup propagates before teardown"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
package/src/lib/bundle.mjs
CHANGED
|
@@ -147,6 +147,12 @@ export const HELPER_FILES = [
|
|
|
147
147
|
{ path: 'scripts/worktree-lifecycle/session.py', kind: 'script', mode: 0o755 },
|
|
148
148
|
{ path: 'scripts/worktree-lifecycle/capabilities.json', kind: 'doc', mode: 0o644 },
|
|
149
149
|
{ path: 'scripts/worktree-lifecycle/README.md', kind: 'doc', mode: 0o644 },
|
|
150
|
+
// Offered planning-artifact ignore rules for /setup-workflow Section A11.
|
|
151
|
+
// `.gitignore` is consumer-owned, so only an approved setup step may append
|
|
152
|
+
// the block — init/update reconciliation never reach this. plan-artifacts.json
|
|
153
|
+
// is the kit-owned declaration it reads. Invokable CLI → 0o755.
|
|
154
|
+
{ path: 'scripts/worktree-lifecycle/plan-artifacts.json', kind: 'doc', mode: 0o644 },
|
|
155
|
+
{ path: 'scripts/worktree-lifecycle/ignore_seed.py', kind: 'script', mode: 0o755 },
|
|
150
156
|
// Shared hook utility imported by the shipped hooks (drift-guard,
|
|
151
157
|
// sync-board-status). Library (imported, not run) → 0o644. MUST ship or those
|
|
152
158
|
// hooks ImportError on arrival.
|