@ikon85/agent-workflow-kit 0.43.0 → 0.44.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.
Files changed (126) hide show
  1. package/.agents/skills/grill-me/SKILL.md +3 -1
  2. package/.agents/skills/grill-with-docs/SKILL.md +3 -1
  3. package/.agents/skills/kit-update/SKILL.md +9 -11
  4. package/.agents/skills/orchestrate-wave/SKILL.md +8 -8
  5. package/.agents/skills/setup-workflow/SKILL.md +20 -18
  6. package/.agents/skills/setup-workflow/board-sync.md +4 -1
  7. package/.agents/skills/setup-workflow/workflow-advisories.md +3 -2
  8. package/.agents/skills/setup-workflow/worktree-lifecycle.md +36 -31
  9. package/.agents/skills/to-issues/SKILL.md +43 -0
  10. package/.agents/skills/wrapup/SKILL.md +98 -54
  11. package/.claude/skills/grill-me/SKILL.md +3 -1
  12. package/.claude/skills/grill-me-codex/SKILL.md +3 -1
  13. package/.claude/skills/grill-with-docs/SKILL.md +3 -1
  14. package/.claude/skills/grill-with-docs-codex/SKILL.md +3 -1
  15. package/.claude/skills/kit-update/SKILL.md +9 -11
  16. package/.claude/skills/orchestrate-wave/SKILL.md +8 -8
  17. package/.claude/skills/setup-workflow/SKILL.md +20 -18
  18. package/.claude/skills/setup-workflow/board-sync.md +4 -1
  19. package/.claude/skills/setup-workflow/workflow-advisories.md +3 -2
  20. package/.claude/skills/setup-workflow/worktree-lifecycle.md +36 -31
  21. package/.claude/skills/to-issues/SKILL.md +43 -0
  22. package/.claude/skills/wrapup/SKILL.md +98 -54
  23. package/README.md +249 -7
  24. package/agent-workflow-kit.package.json +42 -42
  25. package/package.json +16 -12
  26. package/scripts/marker_lib.py +3 -0
  27. package/scripts/profile_globs.py +26 -45
  28. package/scripts/release-delta-guard.mjs +26 -9
  29. package/scripts/worktree-lifecycle/README.md +57 -160
  30. package/scripts/worktree-lifecycle/capabilities.json +2 -3
  31. package/scripts/worktree-lifecycle/classify.py +480 -0
  32. package/scripts/worktree-lifecycle/cleanup.py +9 -29
  33. package/scripts/worktree-lifecycle/core.py +68 -1429
  34. package/scripts/worktree-lifecycle/ignore_seed.py +1 -1
  35. package/scripts/worktree-lifecycle/profile.py +46 -29
  36. package/scripts/worktree-lifecycle/setup.py +7 -19
  37. package/scripts/wrapup-land.py +1292 -545
  38. package/src/consumer-migrations.json +1 -16
  39. package/src/lib/bundle.mjs +31 -1
  40. package/docs/adr/0001-consumer-divergence-policy.md +0 -53
  41. package/docs/adr/0002-capability-gated-orchestration.md +0 -70
  42. package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +0 -63
  43. package/docs/adr/0004-release-intent-is-a-version-tag.md +0 -64
  44. package/docs/adr/0005-to-issues-is-the-planning-facade.md +0 -42
  45. package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +0 -95
  46. package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +0 -88
  47. package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +0 -84
  48. package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +0 -101
  49. package/docs/adr/0010-model-roster-replaces-the-optimization-dial.md +0 -87
  50. package/docs/agents/board-sync.md +0 -74
  51. package/docs/agents/code-review.md +0 -14
  52. package/docs/agents/workflow-capabilities.json +0 -28
  53. package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
  54. package/docs/research/benchlm-routing-source.md +0 -198
  55. package/docs/research/consumer-owned-protocol-files.md +0 -238
  56. package/docs/research/frontend-agent-benchmarks.md +0 -282
  57. package/docs/research/model-effort-routing-benchmarks.md +0 -261
  58. package/docs/research/provider-neutral-agent-routing.md +0 -207
  59. package/docs/research/wave-152-consumer-acceptance.md +0 -98
  60. package/docs/research/wave-43-script-hook-census.md +0 -128
  61. package/scripts/build-kit.mjs +0 -117
  62. package/scripts/build-kit.test.mjs +0 -207
  63. package/scripts/census/census.test.mjs +0 -252
  64. package/scripts/census/state.test.mjs +0 -15
  65. package/scripts/census/transaction.test.mjs +0 -103
  66. package/scripts/census-contract.test.mjs +0 -227
  67. package/scripts/check-kit-staleness.mjs +0 -45
  68. package/scripts/check-kit-staleness.test.mjs +0 -19
  69. package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -165
  70. package/scripts/codex-exec.test.mjs +0 -871
  71. package/scripts/grill-census-wiring-guard.mjs +0 -87
  72. package/scripts/grill-census-wiring-guard.test.mjs +0 -107
  73. package/scripts/kit-release.test.mjs +0 -273
  74. package/scripts/kit-update-pr.test.mjs +0 -151
  75. package/scripts/lib/audit-refs.mjs +0 -146
  76. package/scripts/lib/scrub.mjs +0 -113
  77. package/scripts/lib/scrub.test.mjs +0 -187
  78. package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
  79. package/scripts/portability_profile_scan.py +0 -53
  80. package/scripts/release-delta-guard.test.mjs +0 -174
  81. package/scripts/release-parity.test.mjs +0 -90
  82. package/scripts/release-state.test.mjs +0 -360
  83. package/scripts/test_anchor_table.py +0 -69
  84. package/scripts/test_board_bootstrap.py +0 -348
  85. package/scripts/test_board_sync.py +0 -208
  86. package/scripts/test_board_sync_create_idempotency.py +0 -117
  87. package/scripts/test_board_sync_wave_title.py +0 -120
  88. package/scripts/test_census_backstop.py +0 -712
  89. package/scripts/test_census_forward_contract.py +0 -91
  90. package/scripts/test_census_update_contract.test.mjs +0 -454
  91. package/scripts/test_codex_adapter_sync_contract.py +0 -169
  92. package/scripts/test_dist_kit_smoke.py +0 -220
  93. package/scripts/test_drift_guard_diagnostics.py +0 -295
  94. package/scripts/test_issue_claim_contract.py +0 -169
  95. package/scripts/test_kit_docs_language_census.py +0 -178
  96. package/scripts/test_marker_lib.py +0 -162
  97. package/scripts/test_orchestrate_wave_contract.py +0 -381
  98. package/scripts/test_pr_body_check.py +0 -264
  99. package/scripts/test_profile_globs.py +0 -280
  100. package/scripts/test_program_planning_contract.py +0 -133
  101. package/scripts/test_release_authorization_contract.py +0 -101
  102. package/scripts/test_render_anchor.py +0 -267
  103. package/scripts/test_retro_wrapup_contract.py +0 -153
  104. package/scripts/test_skill_code_review_seed.py +0 -89
  105. package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
  106. package/scripts/test_skill_frontmatter_lint.py +0 -234
  107. package/scripts/test_skill_gh_lint.py +0 -124
  108. package/scripts/test_skill_language_census.py +0 -254
  109. package/scripts/test_skill_optional_readiness.py +0 -171
  110. package/scripts/test_skill_portability_lint.py +0 -866
  111. package/scripts/test_skill_precommit_template.py +0 -30
  112. package/scripts/test_skill_publish_audit.py +0 -358
  113. package/scripts/test_skill_readiness_contract.py +0 -264
  114. package/scripts/test_skill_readiness_preflight.py +0 -180
  115. package/scripts/test_skill_required_readiness.py +0 -233
  116. package/scripts/test_skill_selfcontainment_lint.py +0 -255
  117. package/scripts/test_skill_setup_workflow_seeds.py +0 -691
  118. package/scripts/test_skill_stale_name_lint.py +0 -126
  119. package/scripts/test_skill_surface_refs.py +0 -300
  120. package/scripts/test_skill_trailing_artifact_lint.py +0 -71
  121. package/scripts/test_tdd_contract.py +0 -78
  122. package/scripts/test_worktree_ignore_seed.py +0 -320
  123. package/scripts/test_worktree_setup_base_guard.py +0 -140
  124. package/scripts/test_worktree_wrapup_contract.py +0 -1737
  125. package/scripts/test_wrapup_land.py +0 -428
  126. package/scripts/worktree-lifecycle/session.py +0 -1857
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env python3
2
2
  """One repository-relative glob dialect for every consumer workflow profile.
3
3
 
4
- Workflow Advisories and Worktree Lifecycle both select repository-relative
5
- paths from consumer-owned profile globs. They share this single matcher so the
6
- same pattern can never select one set of paths for an advisory and a different
7
- set for a cleanup decision.
4
+ Workflow Advisories selects repository-relative paths from consumer-owned
5
+ profile globs, and this is the single matcher it uses, so a pattern can never
6
+ select one set of paths on one advisory surface and a different set on another.
7
+ No Worktree Lifecycle decision reads a glob at all: deletion policy has exactly
8
+ one configuration surface, the ignore mechanism, so no consumer pattern here
9
+ carries deletion authority.
8
10
 
9
11
  Dialect
10
12
  -------
@@ -24,9 +26,9 @@ Run this module to review an installed profile before trusting it:
24
26
  python3 scripts/profile_globs.py [docs/agents/workflow-capabilities.json]
25
27
 
26
28
  It names every pattern whose match set narrows or widens against the legacy
27
- matcher of its own capability, marks the keys that carry deletion authority,
28
- and never edits the profile. Exit code 0 means nothing changed meaning, 1 means
29
- at least one pattern needs review, 2 means the profile could not be read.
29
+ matcher and never edits the profile. Exit code 0 means nothing changed meaning,
30
+ 1 means at least one pattern needs review, 2 means the profile could not be
31
+ read.
30
32
  """
31
33
 
32
34
  from __future__ import annotations
@@ -94,7 +96,6 @@ class PatternMigration:
94
96
  class ProfileGlobFinding:
95
97
  location: str
96
98
  pattern: str
97
- deletion_authority: bool
98
99
  migration: PatternMigration
99
100
 
100
101
 
@@ -232,52 +233,34 @@ def _surface_globs(advisories: dict, section: str) -> list[tuple[str, str]]:
232
233
  ]
233
234
 
234
235
 
235
- def _profile_globs(document: object) -> list[tuple[str, str, bool]]:
236
+ def _profile_globs(document: object) -> list[tuple[str, str]]:
236
237
  """List every shipped consumer-profile glob call site's configured patterns."""
237
238
  if not isinstance(document, dict):
238
239
  return []
239
- globs: list[tuple[str, str, bool]] = []
240
- for index, pattern in _string_entries(
241
- document.get("worktreeLifecycle"), "scratchPatterns",
242
- ):
243
- globs.append((f"worktreeLifecycle.scratchPatterns[{index}]", pattern, True))
244
- for index, pattern in _string_entries(
245
- document.get("wrapup"), "landingGeneratedArtifactPatterns",
246
- ):
247
- globs.append(
248
- (f"wrapup.landingGeneratedArtifactPatterns[{index}]", pattern, True),
249
- )
250
240
  advisories = document.get("workflowAdvisories")
251
- if isinstance(advisories, dict):
252
- for index, pattern in _string_entries(advisories.get("baseline"), "sourceGlobs"):
253
- globs.append(
254
- (f"workflowAdvisories.baseline.sourceGlobs[{index}]", pattern, False),
255
- )
256
- for section in ("preRefactor", "stopChecks"):
257
- globs.extend(
258
- (location, pattern, False)
259
- for location, pattern in _surface_globs(advisories, section)
260
- )
241
+ if not isinstance(advisories, dict):
242
+ return []
243
+ globs: list[tuple[str, str]] = [
244
+ (f"workflowAdvisories.baseline.sourceGlobs[{index}]", pattern)
245
+ for index, pattern in _string_entries(advisories.get("baseline"), "sourceGlobs")
246
+ ]
247
+ for section in ("preRefactor", "stopChecks"):
248
+ globs.extend(_surface_globs(advisories, section))
261
249
  return globs
262
250
 
263
251
 
264
252
  def scan_profile(document: object) -> tuple[ProfileGlobFinding, ...]:
265
253
  """Classify every consumer-profile glob against its own legacy matcher.
266
254
 
267
- Worktree Lifecycle globs were always matched case-sensitively, while
268
- Workflow Advisories globs went through the case-normalizing `fnmatch`, so
269
- only the advisory keys can narrow on a case-insensitive host.
255
+ Every remaining glob key is a Workflow Advisories key, and those went
256
+ through the case-normalizing `fnmatch`, so all of them can narrow on a
257
+ case-insensitive host.
270
258
  """
271
259
  return tuple(
272
260
  ProfileGlobFinding(
273
- location,
274
- pattern,
275
- deletion_authority,
276
- classify_pattern(
277
- pattern, case_insensitive_legacy=not deletion_authority,
278
- ),
261
+ location, pattern, classify_pattern(pattern, case_insensitive_legacy=True),
279
262
  )
280
- for location, pattern, deletion_authority in _profile_globs(document)
263
+ for location, pattern in _profile_globs(document)
281
264
  )
282
265
 
283
266
 
@@ -290,9 +273,8 @@ def render_report(findings: tuple[ProfileGlobFinding, ...], source: str) -> str:
290
273
  f"{len(changed)} change meaning.",
291
274
  ]
292
275
  for finding in changed:
293
- authority = " [deletion authority]" if finding.deletion_authority else ""
294
276
  lines.append("")
295
- lines.append(f"{finding.location}{authority}: {finding.pattern}")
277
+ lines.append(f"{finding.location}: {finding.pattern}")
296
278
  for effect in finding.migration.effects:
297
279
  witness = finding.migration.witnesses[effect]
298
280
  verb = "now also matches" if effect == WIDENS else "no longer matches"
@@ -301,8 +283,8 @@ def render_report(findings: tuple[ProfileGlobFinding, ...], source: str) -> str:
301
283
  lines.append("")
302
284
  lines.append(
303
285
  "Review each line above and rewrite the pattern yourself. A widened "
304
- "deletion-authority pattern expands what cleanup may remove; this "
305
- "check never edits the profile and never migrates a pattern for you.",
286
+ "pattern expands the surface an advisory selects; this check never "
287
+ "edits the profile and never migrates a pattern for you.",
306
288
  )
307
289
  return "\n".join(lines) + "\n"
308
290
 
@@ -331,7 +313,6 @@ def main(argv: list[str]) -> int:
331
313
  {
332
314
  "location": finding.location,
333
315
  "pattern": finding.pattern,
334
- "deletionAuthority": finding.deletion_authority,
335
316
  "effects": list(finding.migration.effects),
336
317
  "witnesses": finding.migration.witnesses,
337
318
  }
@@ -4,7 +4,7 @@ import { mkdir, mkdtemp, readFile, readdir, rm } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
- import { STUB_TARGETS } from '../src/lib/bundle.mjs';
7
+ import { STUB_TARGETS, isPublishExcluded } from '../src/lib/bundle.mjs';
8
8
  import { sha256 } from '../src/lib/hash.mjs';
9
9
  import { buildKit } from './build-kit.mjs';
10
10
 
@@ -31,9 +31,20 @@ function mergeDeltas(...deltas) {
31
31
  ]));
32
32
  }
33
33
 
34
- export function recommendBump(delta, { minorRemovals = [] } = {}) {
34
+ /**
35
+ * Recommend the smallest bump the delta justifies.
36
+ *
37
+ * `initialDevelopment` is Semver's major-version-zero rule: while the package
38
+ * is still `0.y.z` there is no stable public contract to break, so a removal
39
+ * that would demand a major on a released package demands a minor here. The
40
+ * protection that matters at 0.x survives either way — a removal can never be
41
+ * a patch. Callers pass the BASE version's major, so the recommendation cannot
42
+ * be softened by choosing the target version.
43
+ */
44
+ export function recommendBump(delta, { minorRemovals = [], initialDevelopment = false } = {}) {
35
45
  const minorRemovalSet = new Set(minorRemovals);
36
- if (delta.removed.some((path) => !minorRemovalSet.has(path))) return 'major';
46
+ const breaking = delta.removed.some((path) => !minorRemovalSet.has(path));
47
+ if (breaking) return initialDevelopment ? 'minor' : 'major';
37
48
  if (delta.added.length || delta.removed.length) return 'minor';
38
49
  return delta.changed.length ? 'patch' : null;
39
50
  }
@@ -81,13 +92,19 @@ export function assessRelease(input) {
81
92
  + `tag and publish it before preparing ${input.currentVersion}`,
82
93
  );
83
94
  }
84
- // Project-layer stubs are seed inputs, never installed Consumer files.
85
- // Removing an accidentally packed copy is additive hygiene, unless the same
86
- // path also disappeared from the actual Consumer bundle.
87
- const seedOnlyPackageRemovals = packageDelta.removed.filter(
88
- (path) => STUB_TARGETS.includes(path) && !bundleDelta.removed.includes(path),
95
+ // Project-layer stubs are seed inputs, never installed Consumer files, and
96
+ // publish-excluded sources (tests, build tooling, maintainer docs) are never
97
+ // installed either. Removing an accidentally packed copy of either is
98
+ // additive hygiene, unless the same path also disappeared from the actual
99
+ // Consumer bundle.
100
+ const payloadOnlyRemovals = packageDelta.removed.filter(
101
+ (path) => (STUB_TARGETS.includes(path) || isPublishExcluded(path))
102
+ && !bundleDelta.removed.includes(path),
89
103
  );
90
- const recommendedBump = recommendBump(delta, { minorRemovals: seedOnlyPackageRemovals });
104
+ const recommendedBump = recommendBump(delta, {
105
+ minorRemovals: payloadOnlyRemovals,
106
+ initialDevelopment: Number(input.baseVersion.split('.')[0]) === 0,
107
+ });
91
108
  const actual = bumpKind(input.baseVersion, input.currentVersion);
92
109
  const rank = { patch: 1, minor: 2, major: 3 };
93
110
  if (hasDelta(delta) && input.currentVersion !== input.baseVersion && !actual) {
@@ -13,35 +13,32 @@ they do not carry a second branch regex, worktree traversal, or failure policy.
13
13
  - `enabled`: explicit activation gate.
14
14
  - `worktreeRoot`, `branchTemplate`, `pathTemplate`, and `branchRegex`: consumer
15
15
  naming and location policy.
16
+ - `contentBranchTemplate` (default `{type}/{slug}`): the issue-less branch a
17
+ session cuts when its output is durable content. It renders `{type}` and
18
+ `{slug}` only — a durable-content session has no issue number — and refuses
19
+ an `{issue}` placeholder instead of inventing one.
16
20
  - `mainBranches` and `protectedBranches`: branches guarded in the main checkout.
17
21
  - `setupEntry` and ordered `setupSteps`: the portable setup command and project
18
22
  setup sequence.
19
23
  - `riskyCommandPatterns`: commands that must target the active linked worktree.
20
- - `scratchPatterns`: consumer-owned glob patterns for untracked disposable
21
- planning artefacts. No filename is assumed by Core.
22
- - `wrapup.landingGeneratedArtifactPatterns`: an explicitly reviewed
23
- consumer-owned allowlist for outputs created by the landing commands. It is
24
- deletion authority, so setup never derives it from `.gitignore` alone and
25
- update never installs a universal default.
26
-
27
- Profile globs use one repository-relative POSIX dialect, implemented once in
28
- `scripts/profile_globs.py` and loaded by this core and by
29
- `scripts/workflow-advisories/core.py`. `*` and `?` stay inside one path
30
- segment; `[seq]`/`[!seq]` are per-segment character classes; `**` as a whole
31
- segment matches zero or more segments, so a leading `**/` also matches the
32
- repository root and `dir/**` also matches `dir` itself; matching is always
33
- case-sensitive on every host filesystem; and a pattern must match the whole
34
- path. For example, `**/__pycache__/**` matches both root and nested caches,
35
- while `dist-kit/*` does not match `dist-kit/a/b`.
36
-
37
- Because both capabilities load the same matcher, a pattern can never select one
38
- set of paths for an advisory and a different set for a deletion decision. Run
24
+
25
+ The profile carries **structural facts only**. It declares no pattern list,
26
+ because deletion policy has exactly one configuration surface: the ignore
27
+ mechanism. Making a file deletable at teardown means ignoring it.
28
+ Keys this loader does not know are ignored in silence, so a profile written for
29
+ an older kit keeps working without warning noise.
30
+
31
+ Profile globs elsewhere in the kit (Workflow Advisories) use one
32
+ repository-relative POSIX dialect, implemented once in
33
+ `scripts/profile_globs.py`. `*` and `?` stay inside one path segment;
34
+ `[seq]`/`[!seq]` are per-segment character classes; `**` as a whole segment
35
+ matches zero or more segments, so a leading `**/` also matches the repository
36
+ root and `dir/**` also matches `dir` itself; matching is always case-sensitive
37
+ on every host filesystem; and a pattern must match the whole path. Run
39
38
  `python3 scripts/profile_globs.py <profile.json>` to review an installed
40
- profile: it names every pattern whose match set narrows or widens against that
41
- key's legacy matcher, prints the witness path proving it, marks the keys that
42
- carry deletion authority, and exits 1 when anything needs review. It reads the
43
- profile and never rewrites a pattern, so a migration cannot silently expand
44
- cleanup authority.
39
+ profile. No Worktree Lifecycle decision reads a glob: the ignore mechanism is
40
+ the single deletion-policy surface, so a pattern can no longer widen cleanup
41
+ authority at all.
45
42
 
46
43
  Unknown or malformed events fail open without changing repository state.
47
44
  Security-sensitive, profile-matched edits and commands fail closed only when
@@ -66,157 +63,57 @@ shipped skills write (`PLAN.md`, `PLAN-REVIEW-LOG.md`, `ANNAHMEN.md`).
66
63
  `preview` reports what an approval would append and writes nothing, and `apply`
67
64
  appends exactly one marker block. `.gitignore` is a consumer file the kit does
68
65
  not own, so only an explicit approved `/setup-workflow` step may run `apply`;
69
- `init` and `update` reconciliation never reach it (ADR 0008).
66
+ `init` and `update` reconciliation never reach it.
70
67
 
71
68
  The append never rewrites, reorders, or removes an existing line. A repository
72
69
  that already ignores every artefact reports `nothing-to-do`, a re-run after an
73
70
  approval is a byte-identical no-op, and a marker block the consumer has since
74
71
  edited reports `blocked` rather than being repaired. An artefact already tracked
75
72
  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.
73
+ never runs `git rm`. Approving that offer is also what makes the artefacts
74
+ deletable at teardown: `.gitignore` is the one deletion-policy surface.
78
75
 
79
76
  ## Cleanup
80
77
 
81
- `cleanup.py` previews by default. Removal refuses protected, tracked-dirty,
82
- non-scratch-dirty, open-PR, or unmerged worktrees. Profile-declared untracked
83
- scratch is named in the report but does not block removal. The assessment reads
84
- `ANNAHMEN.md` before removal and returns its contents for propagation.
85
- `wrapup-land.py` invokes this same assessment after a merge and before killing
86
- processes or removing the worktree.
87
-
88
- Explicit removal re-collects facts immediately before mutation, requires the
89
- same removable inventory, deletes only the exact contained scratch entries
90
- from that inventory, and uses ordinary `git worktree remove`. Regular-file
91
- identity stays byte-bound. A profile-authorized symlink is eligible only when
92
- its relative target resolves to an existing entry inside the assessed
93
- worktree; cleanup freezes both the link and resolved target identities, then
94
- unlinks the link without following it. Absolute, escaping, dangling, changed,
95
- unmatched, or late symlinks stop cleanup. It never bypasses Git's final
96
- concurrent-change check with force removal.
97
-
98
- The generic setup route atomically records its ignored and complete
99
- untracked-file inventories in the linked worktree's Git metadata after its
100
- configured setup steps. The record is bound to the worktree path, branch, root
101
- device/inode, and setup HEAD, and carries a canonical digest. Existing
102
- worktrees can receive a conservative baseline only when they are the exact
103
- registered no-follow directory on an attached branch with a clean tracked
104
- worktree and index. Setup reuse defers that backfill, without blocking the
105
- existing worktree, while tracked work is dirty or landing-generated blockers
106
- are present. Landing classifies those blockers before writing any baseline;
107
- after they are moved, every remaining current ignored and untracked path is
108
- recorded as pre-existing and therefore protected. A corrupt baseline or an
109
- active landing attempt is never overwritten. The claim-bound session route
110
- below captures its stricter baseline before project setup so a failed setup has
111
- an exact recovery boundary.
112
-
113
- Before merge, the committed worktree profile may only nominate exact,
114
- identity-bound landing candidates; it authorizes no deletion. After merge and
115
- `fetch origin/main`, cleanup reloads the profile directly from canonical
116
- `origin/main`, requires its scratch and generator policies to equal the
117
- worktree candidate and the policy digest frozen at attempt start, and only then
118
- authorizes mutation. Every supplied generator-evidence path is independently
119
- checked against that canonical generator policy. A missing policy is distinct
120
- from an explicit empty policy. An unmerged or transient branch policy therefore
121
- cannot grant itself broader cleanup authority.
122
-
123
- Canonical policy that drifts between attempt start and post-merge cleanup keeps
124
- that refusal, and the refusal names its supported recovery. Recovery re-derives
125
- authority from the merged canonical policy alone: it never consults the stale
126
- worktree candidate, never re-scans for new candidates, requires the branch to
127
- already be an ancestor of canonical main, and requires each frozen identity to
128
- be both named by canonical policy and unchanged on disk. Evidence outside
129
- canonical policy, a changed identity, and pre-existing or foreign state stop the
130
- recovery instead of being deleted, and a repeated run after a completed teardown
131
- is a no-op.
132
-
133
- The landing adapter may carry exact scratch evidence only for current ignored
134
- files that match the consumer-owned
135
- `wrapup.landingGeneratedArtifactPatterns` profile and were absent from that
136
- creation baseline. Missing, changed, or incoherent provenance stops landing
137
- cleanup. Initial/profile-matched files, unmatched files, landing-generated
138
- symlinks, and writes after the landing evidence snapshot remain cleanup stops;
139
- generator evidence and regular-file removal stay descriptor-bound and require
140
- a second inventory check.
141
- Mutable session logs belong in explicit `scratchPatterns`, not in the landing
142
- generator allowlist: their identity is frozen at final cleanup assessment, so
143
- normal logging before teardown remains live while a later append or replacement
144
- still stops deletion.
145
- Landing-start blockers are classified before a journal is written, so moving a
146
- protected blocker permits a clean next attempt. An explicit relinquish archives
147
- either a started or frozen attempt, including drifted evidence, without
148
- deleting or claiming any file; the next preflight treats every current file as
149
- pre-existing. Exact unchanged frozen evidence remains directly resumable. The
150
- attempt journal name is classified without following a symlink, so a symlinked
151
- or dangling journal entry stops instead of being read or replaced, and each
152
- archived receipt is filed under a contract-version-neutral stem plus its own
153
- recorded contract version. A journal written under a superseded contract is
154
- classified as legacy rather than corrupt when it still satisfies its own
155
- recorded contract; that refusal names the archive route explicitly, while a
156
- journal that fails its own contract is still reported as incoherent evidence.
78
+ `classify.py` is the teardown authority. It reads the worktree's current state
79
+ at the moment of action and nothing else: a tracked change or an unmerged path
80
+ blocks, an untracked non-ignored file blocks with a bounded report (count plus
81
+ top directories, never a path dump), and an ignored entry is Scratch and
82
+ deletable. The single hardcoded exception is `.env*` by basename glob, which is
83
+ deletable only when it is byte-identical to its counterpart at the same
84
+ relative path in the main checkout, both opened no-follow. An ignored symlink
85
+ is deletable only when its target resolves inside the assessed worktree; the
86
+ link itself is unlinked and never followed.
87
+
88
+ `cleanup.py` previews by default. Removal additionally refuses an unregistered
89
+ path, a detached branch, a protected branch or the main checkout, an open PR,
90
+ and an unmerged branch. The assessment reads `ANNAHMEN.md` before removal and
91
+ returns its contents for propagation.
92
+
93
+ Explicit removal re-collects facts immediately before mutation and requires the
94
+ same removable inventory. The deletion walk re-opens the assessed root
95
+ no-follow, re-checks its device/inode, and re-checks every entry's kind — and a
96
+ symlink's target immediately before unlinking it. It uses ordinary `git
97
+ worktree remove` and never bypasses Git's final concurrent-change check with
98
+ force removal.
99
+
100
+ `wrapup-land.py` runs the same assessment after a merge: quiesce the worktree's
101
+ own declared `.dev-ports` listeners, classify, delete the Scratch, remove the
102
+ worktree. Teardown always runs a direct `/wrapup` invocation is its
103
+ authorization, including for a worktree an external tool created under a
104
+ foreign name and path. There is no persisted attempt state and no recovery
105
+ flag: an interrupted landing is resumed by re-running it, because every step
106
+ verifies present state and skips what is already done.
107
+
108
+ Two residual risks are accepted deliberately — between assessment and deletion
109
+ a file could in principle be replaced, and a valuable file a consumer keeps
110
+ gitignored outside `.env*` is deletable at teardown.
157
111
 
158
112
  `cleanup.py sweep` is the read-only inventory entrypoint. It accounts once for
159
113
  every linked worktree and local branch, reports issue/PR/merge/age/removal
160
114
  facts, and counts merged remote branches separately. It never removes a
161
115
  worktree or branch.
162
116
 
163
- ## Session-owned teardown
164
-
165
- `session.py` is the narrower orchestration route. It binds one receipt in the
166
- Git common directory to the active `wave-active/<anchor>` annotated claim:
167
-
168
- ```sh
169
- python3 scripts/worktree-lifecycle/session.py begin --anchor <n> --owner <run> --base <wave-head>
170
- python3 scripts/worktree-lifecycle/session.py create --anchor <n> --owner <run> --base <current-wave-head> <issue> <slug> <type>
171
- python3 scripts/worktree-lifecycle/session.py recover --anchor <n> --owner <run> --branch <exact-branch>
172
- python3 scripts/worktree-lifecycle/session.py seal --anchor <n> --owner <run>
173
- python3 scripts/worktree-lifecycle/session.py inspect --anchor <n> --owner <run> --main origin/main
174
- python3 scripts/worktree-lifecycle/session.py teardown --anchor <n> --owner <run> --main origin/main
175
- ```
176
-
177
- Only `create` can add an inventory row, and it refuses any branch or path that
178
- already existed. It journals a provisional exact row before `git worktree add`,
179
- then captures the shared artifact baseline before project setup and promotes
180
- the row only after setup succeeds. Session inspection
181
- therefore accepts generated scratch only when the exact path matches the
182
- profile and is absent from the creation baseline; missing, changed, or
183
- incoherent provenance stops. `seal` records the final exact branch OIDs.
184
- Inspection reports ancestry, one-to-one stable patch equivalence, unique
185
- content, and ambiguity separately. Empty commits, non-ancestry merge commits,
186
- duplicate patch IDs, open or unreadable PR evidence, dirt, protected names,
187
- stale registrations, recreated removed targets, and identity drift stop the
188
- whole teardown before mutation.
189
-
190
- If setup fails, the provisional row becomes `recovery-pending`. Automatic and
191
- explicit `recover` use the same bounded route: active claim and receipt,
192
- protected name, PR state, exact ref OID, registration/root identity, and the
193
- creation baseline are revalidated. A branch-only remainder is compare-deleted
194
- only at its provisional OID. A registered worktree whose add succeeded before
195
- root journaling is adopted only when its Git registration/backlink, branch,
196
- OID, and exact-clean inventory all match.
197
-
198
- For failed project setup, exact index/worktree diff hashes and paths are frozen
199
- without storing diff content. Exact regular-file or no-follow symlink identity
200
- is frozen for every setup-created untracked path. Recovery revalidates that
201
- evidence, restores only the frozen tracked paths to the creation OID, and
202
- unlinks only matching untracked identities; later foreign files,
203
- modifications, or replacements stop without losing receipt ownership. If
204
- identity capture cannot finish, the receipt retains only a bounded failure
205
- class and can retry from the unchanged inventory or an exact-clean worktree.
206
- Raw command, exception, stdout, and stderr text is never retained or archived.
207
- Recovery uses ordinary worktree removal and compare-deletes the exact ref.
208
-
209
- Teardown re-runs that complete assessment, archives every recovery OID in the
210
- receipt before its first mutation, and revalidates canonical main, the active
211
- claim and receipt, profile protection, PR state, branch OID, worktree
212
- registration/root identity, dirt, and the exact scratch inventory immediately
213
- before each target mutation. After ordinary worktree removal it rechecks the
214
- ref-side gates, then compare-deletes only the recorded local refs with
215
- `git update-ref -d <ref> <expected-oid>`. A concurrent ref move therefore
216
- survives, while a partial run keeps its recovery OIDs and can resume. Generic
217
- cleanup and sweep never consume this ownership route and never infer authority
218
- over a foreign branch.
219
-
220
117
  Claude hook wiring and any Codex adaptation consume this same profile and core.
221
118
  An adapter may change only the surface event envelope; it must preserve the
222
119
  core verdict and message.
@@ -39,13 +39,12 @@
39
39
  {
40
40
  "historicalPath": "scripts/cleanup-worktrees.sh",
41
41
  "artifact": "scripts/worktree-lifecycle/cleanup.py",
42
- "supportingArtifacts": ["scripts/worktree-lifecycle/session.py"],
42
+ "supportingArtifacts": ["scripts/worktree-lifecycle/classify.py"],
43
43
  "primitives": [
44
44
  "profile",
45
45
  "git-facts",
46
46
  "cleanup-safety",
47
- "claim-bound-session-receipt",
48
- "patch-equivalence",
47
+ "stateless-classification",
49
48
  "compare-and-delete"
50
49
  ]
51
50
  }