@ikon85/agent-workflow-kit 0.42.1 → 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 (133) 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 +268 -7
  24. package/agent-workflow-kit.package.json +42 -42
  25. package/package.json +16 -2
  26. package/scripts/marker_lib.py +3 -0
  27. package/scripts/profile_globs.py +26 -45
  28. package/scripts/release-delta-guard.mjs +29 -4
  29. package/scripts/worktree-lifecycle/README.md +57 -154
  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 -1289
  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/domain.md +0 -7
  53. package/docs/agents/issue-tracker.md +0 -23
  54. package/docs/agents/skills/local-ci.md +0 -90
  55. package/docs/agents/skills/orchestrate-wave.md +0 -166
  56. package/docs/agents/skills/spec-self-critique.md +0 -18
  57. package/docs/agents/triage-labels.md +0 -10
  58. package/docs/agents/workflow-capabilities.json +0 -28
  59. package/docs/conventions/spec-completeness.md +0 -23
  60. package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
  61. package/docs/research/benchlm-routing-source.md +0 -198
  62. package/docs/research/consumer-owned-protocol-files.md +0 -238
  63. package/docs/research/frontend-agent-benchmarks.md +0 -282
  64. package/docs/research/model-effort-routing-benchmarks.md +0 -261
  65. package/docs/research/provider-neutral-agent-routing.md +0 -207
  66. package/docs/research/wave-152-consumer-acceptance.md +0 -98
  67. package/docs/research/wave-43-script-hook-census.md +0 -128
  68. package/scripts/build-kit.mjs +0 -117
  69. package/scripts/build-kit.test.mjs +0 -204
  70. package/scripts/census/census.test.mjs +0 -252
  71. package/scripts/census/state.test.mjs +0 -15
  72. package/scripts/census/transaction.test.mjs +0 -103
  73. package/scripts/census-contract.test.mjs +0 -227
  74. package/scripts/check-kit-staleness.mjs +0 -45
  75. package/scripts/check-kit-staleness.test.mjs +0 -19
  76. package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -152
  77. package/scripts/codex-exec.test.mjs +0 -871
  78. package/scripts/grill-census-wiring-guard.mjs +0 -87
  79. package/scripts/grill-census-wiring-guard.test.mjs +0 -107
  80. package/scripts/kit-release.test.mjs +0 -273
  81. package/scripts/kit-update-pr.test.mjs +0 -151
  82. package/scripts/lib/audit-refs.mjs +0 -146
  83. package/scripts/lib/scrub.mjs +0 -113
  84. package/scripts/lib/scrub.test.mjs +0 -187
  85. package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
  86. package/scripts/portability_profile_scan.py +0 -53
  87. package/scripts/release-delta-guard.test.mjs +0 -151
  88. package/scripts/release-parity.test.mjs +0 -90
  89. package/scripts/release-state.test.mjs +0 -360
  90. package/scripts/test_anchor_table.py +0 -69
  91. package/scripts/test_board_bootstrap.py +0 -348
  92. package/scripts/test_board_sync.py +0 -208
  93. package/scripts/test_board_sync_create_idempotency.py +0 -117
  94. package/scripts/test_board_sync_wave_title.py +0 -120
  95. package/scripts/test_census_backstop.py +0 -712
  96. package/scripts/test_census_forward_contract.py +0 -91
  97. package/scripts/test_census_update_contract.test.mjs +0 -454
  98. package/scripts/test_codex_adapter_sync_contract.py +0 -169
  99. package/scripts/test_dist_kit_smoke.py +0 -220
  100. package/scripts/test_drift_guard_diagnostics.py +0 -295
  101. package/scripts/test_issue_claim_contract.py +0 -169
  102. package/scripts/test_kit_docs_language_census.py +0 -178
  103. package/scripts/test_marker_lib.py +0 -162
  104. package/scripts/test_orchestrate_wave_contract.py +0 -381
  105. package/scripts/test_pr_body_check.py +0 -264
  106. package/scripts/test_profile_globs.py +0 -280
  107. package/scripts/test_program_planning_contract.py +0 -133
  108. package/scripts/test_release_authorization_contract.py +0 -101
  109. package/scripts/test_render_anchor.py +0 -267
  110. package/scripts/test_retro_wrapup_contract.py +0 -153
  111. package/scripts/test_skill_code_review_seed.py +0 -89
  112. package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
  113. package/scripts/test_skill_frontmatter_lint.py +0 -234
  114. package/scripts/test_skill_gh_lint.py +0 -124
  115. package/scripts/test_skill_language_census.py +0 -254
  116. package/scripts/test_skill_optional_readiness.py +0 -171
  117. package/scripts/test_skill_portability_lint.py +0 -866
  118. package/scripts/test_skill_precommit_template.py +0 -30
  119. package/scripts/test_skill_publish_audit.py +0 -358
  120. package/scripts/test_skill_readiness_contract.py +0 -264
  121. package/scripts/test_skill_readiness_preflight.py +0 -180
  122. package/scripts/test_skill_required_readiness.py +0 -233
  123. package/scripts/test_skill_selfcontainment_lint.py +0 -255
  124. package/scripts/test_skill_setup_workflow_seeds.py +0 -691
  125. package/scripts/test_skill_stale_name_lint.py +0 -126
  126. package/scripts/test_skill_surface_refs.py +0 -300
  127. package/scripts/test_skill_trailing_artifact_lint.py +0 -71
  128. package/scripts/test_tdd_contract.py +0 -78
  129. package/scripts/test_worktree_ignore_seed.py +0 -320
  130. package/scripts/test_worktree_setup_base_guard.py +0 -140
  131. package/scripts/test_worktree_wrapup_contract.py +0 -1648
  132. package/scripts/test_wrapup_land.py +0 -428
  133. 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,6 +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, isPublishExcluded } from '../src/lib/bundle.mjs';
7
8
  import { sha256 } from '../src/lib/hash.mjs';
8
9
  import { buildKit } from './build-kit.mjs';
9
10
 
@@ -30,9 +31,21 @@ function mergeDeltas(...deltas) {
30
31
  ]));
31
32
  }
32
33
 
33
- export function recommendBump(delta) {
34
- if (delta.removed.length) return 'major';
35
- if (delta.added.length) return 'minor';
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 } = {}) {
45
+ const minorRemovalSet = new Set(minorRemovals);
46
+ const breaking = delta.removed.some((path) => !minorRemovalSet.has(path));
47
+ if (breaking) return initialDevelopment ? 'minor' : 'major';
48
+ if (delta.added.length || delta.removed.length) return 'minor';
36
49
  return delta.changed.length ? 'patch' : null;
37
50
  }
38
51
 
@@ -79,7 +92,19 @@ export function assessRelease(input) {
79
92
  + `tag and publish it before preparing ${input.currentVersion}`,
80
93
  );
81
94
  }
82
- const recommendedBump = recommendBump(delta);
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),
103
+ );
104
+ const recommendedBump = recommendBump(delta, {
105
+ minorRemovals: payloadOnlyRemovals,
106
+ initialDevelopment: Number(input.baseVersion.split('.')[0]) === 0,
107
+ });
83
108
  const actual = bumpKind(input.baseVersion, input.currentVersion);
84
109
  const rank = { patch: 1, minor: 2, major: 3 };
85
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,151 +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 regular scratch
90
- files from that inventory, and uses ordinary `git worktree remove`. It never
91
- bypasses Git's final concurrent-change check with force removal.
92
-
93
- The generic setup route atomically records its ignored and complete
94
- untracked-file inventories in the linked worktree's Git metadata after its
95
- configured setup steps. The record is bound to the worktree path, branch, root
96
- device/inode, and setup HEAD, and carries a canonical digest. Existing
97
- worktrees can receive a conservative baseline only when they are the exact
98
- registered no-follow directory on an attached branch with a clean tracked
99
- worktree and index. Setup reuse defers that backfill, without blocking the
100
- existing worktree, while tracked work is dirty or landing-generated blockers
101
- are present. Landing classifies those blockers before writing any baseline;
102
- after they are moved, every remaining current ignored and untracked path is
103
- recorded as pre-existing and therefore protected. A corrupt baseline or an
104
- active landing attempt is never overwritten. The claim-bound session route
105
- below captures its stricter baseline before project setup so a failed setup has
106
- an exact recovery boundary.
107
-
108
- Before merge, the committed worktree profile may only nominate exact,
109
- identity-bound landing candidates; it authorizes no deletion. After merge and
110
- `fetch origin/main`, cleanup reloads the profile directly from canonical
111
- `origin/main`, requires its scratch and generator policies to equal the
112
- worktree candidate and the policy digest frozen at attempt start, and only then
113
- authorizes mutation. Every supplied generator-evidence path is independently
114
- checked against that canonical generator policy. A missing policy is distinct
115
- from an explicit empty policy. An unmerged or transient branch policy therefore
116
- cannot grant itself broader cleanup authority.
117
-
118
- Canonical policy that drifts between attempt start and post-merge cleanup keeps
119
- that refusal, and the refusal names its supported recovery. Recovery re-derives
120
- authority from the merged canonical policy alone: it never consults the stale
121
- worktree candidate, never re-scans for new candidates, requires the branch to
122
- already be an ancestor of canonical main, and requires each frozen identity to
123
- be both named by canonical policy and unchanged on disk. Evidence outside
124
- canonical policy, a changed identity, and pre-existing or foreign state stop the
125
- recovery instead of being deleted, and a repeated run after a completed teardown
126
- is a no-op.
127
-
128
- The landing adapter may carry exact scratch evidence only for current ignored
129
- files that match the consumer-owned
130
- `wrapup.landingGeneratedArtifactPatterns` profile and were absent from that
131
- creation baseline. Missing, changed, or incoherent provenance stops landing
132
- cleanup. Initial/profile-matched files, unmatched files, symlinks, and writes
133
- after the landing evidence snapshot remain cleanup stops; deletion still uses
134
- the same descriptor-bound regular-file primitive and a second inventory check.
135
- Mutable session logs belong in explicit `scratchPatterns`, not in the landing
136
- generator allowlist: their identity is frozen at final cleanup assessment, so
137
- normal logging before teardown remains live while a later append or replacement
138
- still stops deletion.
139
- Landing-start blockers are classified before a journal is written, so moving a
140
- protected blocker permits a clean next attempt. An explicit relinquish archives
141
- either a started or frozen attempt, including drifted evidence, without
142
- deleting or claiming any file; the next preflight treats every current file as
143
- pre-existing. Exact unchanged frozen evidence remains directly resumable. The
144
- attempt journal name is classified without following a symlink, so a symlinked
145
- or dangling journal entry stops instead of being read or replaced, and each
146
- archived receipt is filed under a contract-version-neutral stem plus its own
147
- recorded contract version. A journal written under a superseded contract is
148
- classified as legacy rather than corrupt when it still satisfies its own
149
- recorded contract; that refusal names the archive route explicitly, while a
150
- 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.
151
111
 
152
112
  `cleanup.py sweep` is the read-only inventory entrypoint. It accounts once for
153
113
  every linked worktree and local branch, reports issue/PR/merge/age/removal
154
114
  facts, and counts merged remote branches separately. It never removes a
155
115
  worktree or branch.
156
116
 
157
- ## Session-owned teardown
158
-
159
- `session.py` is the narrower orchestration route. It binds one receipt in the
160
- Git common directory to the active `wave-active/<anchor>` annotated claim:
161
-
162
- ```sh
163
- python3 scripts/worktree-lifecycle/session.py begin --anchor <n> --owner <run> --base <wave-head>
164
- python3 scripts/worktree-lifecycle/session.py create --anchor <n> --owner <run> --base <current-wave-head> <issue> <slug> <type>
165
- python3 scripts/worktree-lifecycle/session.py recover --anchor <n> --owner <run> --branch <exact-branch>
166
- python3 scripts/worktree-lifecycle/session.py seal --anchor <n> --owner <run>
167
- python3 scripts/worktree-lifecycle/session.py inspect --anchor <n> --owner <run> --main origin/main
168
- python3 scripts/worktree-lifecycle/session.py teardown --anchor <n> --owner <run> --main origin/main
169
- ```
170
-
171
- Only `create` can add an inventory row, and it refuses any branch or path that
172
- already existed. It journals a provisional exact row before `git worktree add`,
173
- then captures the shared artifact baseline before project setup and promotes
174
- the row only after setup succeeds. Session inspection
175
- therefore accepts generated scratch only when the exact path matches the
176
- profile and is absent from the creation baseline; missing, changed, or
177
- incoherent provenance stops. `seal` records the final exact branch OIDs.
178
- Inspection reports ancestry, one-to-one stable patch equivalence, unique
179
- content, and ambiguity separately. Empty commits, non-ancestry merge commits,
180
- duplicate patch IDs, open or unreadable PR evidence, dirt, protected names,
181
- stale registrations, recreated removed targets, and identity drift stop the
182
- whole teardown before mutation.
183
-
184
- If setup fails, the provisional row becomes `recovery-pending`. Automatic and
185
- explicit `recover` use the same bounded route: active claim and receipt,
186
- protected name, PR state, exact ref OID, registration/root identity, and the
187
- creation baseline are revalidated. A branch-only remainder is compare-deleted
188
- only at its provisional OID. A registered worktree whose add succeeded before
189
- root journaling is adopted only when its Git registration/backlink, branch,
190
- OID, and exact-clean inventory all match.
191
-
192
- For failed project setup, exact index/worktree diff hashes and paths are frozen
193
- without storing diff content. Exact regular-file or no-follow symlink identity
194
- is frozen for every setup-created untracked path. Recovery revalidates that
195
- evidence, restores only the frozen tracked paths to the creation OID, and
196
- unlinks only matching untracked identities; later foreign files,
197
- modifications, or replacements stop without losing receipt ownership. If
198
- identity capture cannot finish, the receipt retains only a bounded failure
199
- class and can retry from the unchanged inventory or an exact-clean worktree.
200
- Raw command, exception, stdout, and stderr text is never retained or archived.
201
- Recovery uses ordinary worktree removal and compare-deletes the exact ref.
202
-
203
- Teardown re-runs that complete assessment, archives every recovery OID in the
204
- receipt before its first mutation, and revalidates canonical main, the active
205
- claim and receipt, profile protection, PR state, branch OID, worktree
206
- registration/root identity, dirt, and the exact scratch inventory immediately
207
- before each target mutation. After ordinary worktree removal it rechecks the
208
- ref-side gates, then compare-deletes only the recorded local refs with
209
- `git update-ref -d <ref> <expected-oid>`. A concurrent ref move therefore
210
- survives, while a partial run keeps its recovery OIDs and can resume. Generic
211
- cleanup and sweep never consume this ownership route and never infer authority
212
- over a foreign branch.
213
-
214
117
  Claude hook wiring and any Codex adaptation consume this same profile and core.
215
118
  An adapter may change only the surface event envelope; it must preserve the
216
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
  }