@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.
@@ -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 (gitignored since, doesn't travel over git — 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.)
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 (gitignored since, doesn't travel over git — 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.)
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 gitignored plan doc
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
- log, gitignored at worktree-root) and propagate each build-time assumption marker
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 (gitignored since, doesn't travel over git — 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.)
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 (gitignored since, doesn't travel over git — 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.)
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 gitignored plan doc
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
- log, gitignored at worktree-root) and propagate each build-time assumption marker
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.39.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": "e106dbb6f62415cc5eef7d923d864a30e75e108a079d12c0f1624ef7d03d6f6d",
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": "5616f15e6660270f9f84fba245caa647c47d8af8dc9e526045bb503e7f1f5863",
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": "02862cd5e7e07a5f06dad89792fcebd0a6c17cf2216a27c7e0f724026fdda53a",
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": "4ef2a06e17e08f0fc0c5cc09e4414af67a186a73ca7634e8c378aab5b5638274",
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": "bf68e0a80b212159f6838bc2bcd3e85714a70cceeff55ed019f87246175182e7",
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": "ec8d9f299db153a0412b7ef1d01832dc6a765f1819713ca45dc64294541cf9d4",
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": "a17521a8d72eb19316c6adb21837e790c9839834ff441abf3ff44d8e1f63809a",
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": "02862cd5e7e07a5f06dad89792fcebd0a6c17cf2216a27c7e0f724026fdda53a",
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": "4ef2a06e17e08f0fc0c5cc09e4414af67a186a73ca7634e8c378aab5b5638274",
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": "bf68e0a80b212159f6838bc2bcd3e85714a70cceeff55ed019f87246175182e7",
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": "91cfa8ebb74872ccec1160c0a7b28edcfacea3217900805034dca0f0c8f39c05",
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.39.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
+ }
@@ -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.