mandrel 2.66.0 → 2.67.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 (33) hide show
  1. package/.agents/agents/acceptance-critic.md +2 -2
  2. package/.agents/docs/agentrc-reference.json +2 -1
  3. package/.agents/docs/configuration.md +2 -1
  4. package/.agents/docs/workflows.md +4 -2
  5. package/.agents/instructions.md +2 -1
  6. package/.agents/rules/git-conventions-reference.md +5 -5
  7. package/.agents/rules/git-conventions.md +1 -1
  8. package/.agents/schemas/agentrc.schema.json +6 -1
  9. package/.agents/scripts/boot-sweep.js +97 -9
  10. package/.agents/scripts/{git-cleanup.js → clean-git.js} +2 -2
  11. package/.agents/scripts/clean-temp.js +54 -0
  12. package/.agents/scripts/clean-worktrees.js +593 -0
  13. package/.agents/scripts/drain-pending-cleanup.js +5 -4
  14. package/.agents/scripts/lib/clean-temp.js +440 -0
  15. package/.agents/scripts/lib/config-settings-schema-delivery.js +11 -2
  16. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  17. package/.agents/scripts/lib/observability/source-classifier.js +3 -1
  18. package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
  19. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +149 -97
  20. package/.agents/scripts/lib/single-story-sweep.js +2 -2
  21. package/.agents/scripts/lib/temp-removal.js +110 -0
  22. package/.agents/scripts/lib/temp-retention.js +122 -73
  23. package/.agents/scripts/lib/worktree/canonical-path.js +34 -0
  24. package/.agents/scripts/lib/worktree/lifecycle/reap.js +15 -4
  25. package/.agents/scripts/single-story-init.js +120 -17
  26. package/.agents/workflows/{git-cleanup.md → clean-git.md} +10 -10
  27. package/.agents/workflows/clean-temp.md +67 -0
  28. package/.agents/workflows/clean-worktrees.md +63 -0
  29. package/.agents/workflows/git-deliver.md +1 -1
  30. package/.agents/workflows/helpers/acceptance-self-eval.md +3 -2
  31. package/.agents/workflows/helpers/deliver-story-reference.md +2 -2
  32. package/docs/CHANGELOG.md +13 -0
  33. package/package.json +1 -1
@@ -98,8 +98,8 @@ For each acceptance item:
98
98
 
99
99
  ## Verdict schema (MUST)
100
100
 
101
- Write **one** verdict file under `temp/` (e.g.
102
- `temp/acceptance-verdict-<storyId>-r<round>.json`) conforming to
101
+ Write **one** verdict file under `temp/scratch/story-<storyId>/` (e.g.
102
+ `temp/scratch/story-<storyId>/acceptance-verdict-r<round>.json`) conforming to
103
103
  [`acceptance-eval-verdict.schema.json`](../schemas/acceptance-eval-verdict.schema.json):
104
104
  one `criteria[]` record per acceptance item, in acceptance-array order, with
105
105
  `index` being the criterion's position in that array.
@@ -87,7 +87,8 @@
87
87
  "orchestrationLogs": true,
88
88
  "validationEvidence": true,
89
89
  "auditResults": true,
90
- "planDirs": true
90
+ "planDirs": true,
91
+ "scratch": true
91
92
  }
92
93
  },
93
94
  "deliverRunner": {
@@ -139,13 +139,14 @@ Everything `/mandrel-deliver` and `single-story-close` consume: worktree isolati
139
139
  | `execution.fullSuiteLock` | No | `boolean` | `true` | Serialize full-suite spawns (`npm test` / `npm run test:coverage`) behind a host-level advisory lock, so two concurrent deliveries on one checkout do not run two suites against the same cores. Best-effort: a wait that expires spawns anyway, so the lock can never fail a delivery. Set false — or export `MANDREL_FULL_SUITE_LOCK=0` for one invocation — to disable. |
140
140
  | `docsFreshness` | No | `object` | — | Documentation-freshness scope: the files a change of consequence is expected to touch. Read by the audit-documentation lens to seed its target set; no delivery gate enforces it. |
141
141
  | `docsFreshness.paths` | No | `array<string>` | `["README.md"]` | Repo-relative documentation paths the audit-documentation lens adds to its target set. |
142
- | `tempRetention` | No | `object` | — | Story #4794. Auto-purge of spent temp artifacts once their Story lands. Classification is an allowlist: only the declared classes below are ever deleted, so operator scratch files under tempRoot are reported with their size and left alone. signals.ndjson is never purged by any path. |
142
+ | `tempRetention` | No | `object` | — | Story #4794. Auto-purge of spent temp artifacts once their Story lands. Classification is an allowlist: only the declared classes below are ever deleted, so unrecognized files under tempRoot are reported with their size and left alone (`/clean-temp` is the operator path for them). signals.ndjson is never purged by any path. |
143
143
  | `tempRetention.enabled` | No | `boolean` | `true` | Master switch. Default true — reclaiming a landed Story's gate transcripts and validation evidence is the behaviour, and this knob turns it off. When false every purge path is a reported no-op. |
144
144
  | `tempRetention.classes` | No | `object` | — | Per-class opt-out. Each defaults to true; set one false to keep that family while the rest are purged. |
145
145
  | `tempRetention.classes.orchestrationLogs` | No | `boolean` | `true` | <tempRoot>/orchestration/*.log — close gate transcripts and terse-result detail dumps. |
146
146
  | `tempRetention.classes.validationEvidence` | No | `boolean` | `true` | Per-Story validation-evidence.json, lifecycle.ndjson, and manifest.md under the standalone and per-run story trees. |
147
147
  | `tempRetention.classes.auditResults` | No | `boolean` | `true` | <tempRoot>/audits/ — audit lens reports. |
148
148
  | `tempRetention.classes.planDirs` | No | `boolean` | `true` | <tempRoot>/plan-<slug>/ — abandoned plan authoring dirs. Age-floored only; the current run is always excluded. |
149
+ | `tempRetention.classes.scratch` | No | `boolean` | `true` | <tempRoot>/scratch/ — agent-authored scratch. `scratch/story-<id>/` is purged when that Story lands; any other `scratch/` entry is age-floored. |
149
150
  | `deliverRunner` | No | `object` | — | Bounded-concurrency knob for the /mandrel-deliver fan-out. |
150
151
  | `deliverRunner.concurrencyCap` | No | `integer` | `3` | Maximum ready Stories dispatched by /mandrel-deliver at once. Default 3. Moderate by design — keeps host-quota consumption predictable while allowing a small ready-set fan-out. Set 1 for strictly sequential delivery; raise further on hosts with adequate parallel-agent quota. See deliver.md for the sequencing model and throughput tradeoff. |
151
152
  | `deliverRunner.footprintGuard` | No | `"enforce"` \| `"advisory"` | `"enforce"` | How a file-footprint collision affects dispatch. 'enforce' (default, and the behaviour to keep unless you have a reason) withholds a Story whose footprint races a peer admitted this beat or one still in flight — the guard encodes delivery-time-only knowledge (open implementation windows, foreign leases, ground that moved since planning) that no depends_on edge can carry. 'advisory' still DETECTS every collision and reports each would-be withhold in the tick envelope, but lets dispatch follow the declared depends_on edges alone — a deliberate throughput trade for a run whose ordering is fully declared. See stories-wave-tick.js and helpers/deliver-reference.md. |
@@ -32,7 +32,7 @@ by `node .agents/scripts/generate-workflows-doc.js`; `npm run docs:check`
32
32
  fails when it drifts from the on-disk workflow set. To change a command’s
33
33
  description, edit the workflow file’s front-matter and regenerate.
34
34
 
35
- ## Commands (29)
35
+ ## Commands (31)
36
36
 
37
37
  | Command | Description |
38
38
  | --- | --- |
@@ -55,7 +55,9 @@ description, edit the workflow file’s front-matter and regenerate.
55
55
  | `/audit-sre` | "Audit production-readiness for a release candidate: SLOs, observability, runbooks, error budgets, and rollback paths." |
56
56
  | `/audit-to-stories` | Convert findings produced by the audit-\* workflows into actionable GitHub Stories. Reads temp/audits/audit-\*-results.md, groups findings cross-audit, deduplicates against existing Issues by fingerprint, and either chains into /mandrel-plan --seed-file or opens standalone Stories. |
57
57
  | `/audit-ux-ui` | Audit UX/UI consistency and design system adherence |
58
- | `/git-cleanup` | Tidy the local checkout in four phases: fast-forward `main`, prune stale remote-tracking refs, sweep merged branches (squash-aware), and triage `git stash` entries — each step gated by operator confirmation. |
58
+ | `/clean-git` | Tidy the local checkout in four phases: fast-forward `main`, prune stale remote-tracking refs, sweep merged branches (squash-aware), and triage `git stash` entries — each step gated by operator confirmation. |
59
+ | `/clean-temp` | Clear the temp-tree backlog the land-time purge cannot attribute: sort every top-level entry under the project's tempRoot into framework, closed-issue, aged and kept buckets, preview by default, and delete only confirmed buckets. |
60
+ | `/clean-worktrees` | Reclaim disk from dead worktrees: list every worktree of this project as a removal candidate (closed Story, merged branch, orphaned directory, detached HEAD) or as kept with a reason, then remove candidates only on `--execute`. |
59
61
  | `/git-deliver` | Single ad-hoc delivery command for working-tree changes. Detects the git setup and escalates to the right terminal step — commit only, commit + push, or commit + push + open a PR with native auto-merge — picking the default from observable state and letting flags pin any level explicitly. Replaces the retired git-commit-all, git-push, and git-pr-all trio. |
60
62
  | `/mandrel-deliver` | Unified delivery entry point. Takes Story ids or a plain-language prompt, derives which path the work belongs on, and lands it via the single deliver-story engine — story-<id> → PR → main. |
61
63
  | `/mandrel-plan` | Unified planning entry point. Interrogate → author → persist. Emits one Story by default; splits into N>1 only under the default-single split policy. |
@@ -181,7 +181,8 @@ never delivered): `/mandrel-plan` offers one above 2 Stories and
181
181
 
182
182
  All temporary files, scratch scripts, and intermediate outputs MUST
183
183
  live in the gitignored workspace-root `/temp/` directory — do NOT commit
184
- anything under it.
184
+ anything under it. Put ad-hoc scratch in `temp/scratch/story-<id>/` (or
185
+ `temp/scratch/` with no Story), the layout the temp purge reaps.
185
186
 
186
187
  ---
187
188
 
@@ -96,13 +96,13 @@ signature is worth naming:
96
96
 
97
97
  **Invariant (stated in the core): the delivering flow owns tidying the local
98
98
  checkout — reaping its own merged refs and fast-forwarding the base branch.
99
- `/git-cleanup` is a recovery tool, not a routine chore.** The outcome every
99
+ `/clean-git` is a recovery tool, not a routine chore.** The outcome every
100
100
  delivering flow (`/mandrel-deliver`, `/git-deliver`) guarantees, with the mechanics
101
- owned by `boot-sweep.js` / `git-cleanup.js`:
101
+ owned by `boot-sweep.js` / `clean-git.js`:
102
102
 
103
103
  - **`main` is fast-forwarded** by the flow itself in its cleanup phase, so the
104
104
  next init seeds from a current base. No workflow ends by telling the operator
105
- to run `/git-cleanup` to catch up.
105
+ to run `/clean-git` to catch up.
106
106
  - **Merged local refs are reaped** at the next workflow boot's protected sweep
107
107
  (`boot-sweep.js`) — every local branch whose PR is already merged, skipping
108
108
  any candidate with unpushed work, a dirty worktree, or a still-open parent
@@ -112,9 +112,9 @@ owned by `boot-sweep.js` / `git-cleanup.js`:
112
112
  weaker content-equivalence signal (`detectedBy: 'content-merged'` — content
113
113
  already landed in the base by another route, with no merged PR or git
114
114
  ancestry of its own) is **never** reaped by the boot sweep; it is surfaced
115
- under `contentMerged` for the operator to send to `/git-cleanup` for a
115
+ under `contentMerged` for the operator to send to `/clean-git` for a
116
116
  confirmed, eyeballed reap.
117
- - **`/git-cleanup` is recovery, not routine.** Run it by hand only for a state
117
+ - **`/clean-git` is recovery, not routine.** Run it by hand only for a state
118
118
  the automated hygiene does not cover — triaging stashes, reaping across
119
119
  non-standard namespaces, or `--remote` pruning after a force-push. Reaching
120
120
  for it after every routine delivery signals the owning flow's hygiene step
@@ -56,7 +56,7 @@ subject referencing the Story via `(refs #<storyId>)` — see
56
56
  **The delivering flow owns tidying the local checkout** — it
57
57
  fast-forwards the base branch itself and reaps its own merged refs on
58
58
  the next workflow boot (the `boot-sweep.js` protected sweep).
59
- `/git-cleanup` is a recovery tool, not a routine chore — never end a
59
+ `/clean-git` is a recovery tool, not a routine chore — never end a
60
60
  workflow by telling the operator to run it. Scope rules and the
61
61
  shared-checkout contention guard:
62
62
  [`git-conventions-reference.md` § Local checkout hygiene](git-conventions-reference.md).
@@ -386,7 +386,7 @@
386
386
  },
387
387
  "tempRetention": {
388
388
  "type": "object",
389
- "description": "Story #4794. Auto-purge of spent temp artifacts once their Story lands. Classification is an allowlist: only the declared classes below are ever deleted, so operator scratch files under tempRoot are reported with their size and left alone. signals.ndjson is never purged by any path.",
389
+ "description": "Story #4794. Auto-purge of spent temp artifacts once their Story lands. Classification is an allowlist: only the declared classes below are ever deleted, so unrecognized files under tempRoot are reported with their size and left alone (`/clean-temp` is the operator path for them). signals.ndjson is never purged by any path.",
390
390
  "properties": {
391
391
  "enabled": {
392
392
  "type": "boolean",
@@ -416,6 +416,11 @@
416
416
  "type": "boolean",
417
417
  "description": "<tempRoot>/plan-<slug>/ — abandoned plan authoring dirs. Age-floored only; the current run is always excluded.",
418
418
  "default": true
419
+ },
420
+ "scratch": {
421
+ "type": "boolean",
422
+ "description": "<tempRoot>/scratch/ — agent-authored scratch. `scratch/story-<id>/` is purged when that Story lands; any other `scratch/` entry is age-floored.",
423
+ "default": true
419
424
  }
420
425
  },
421
426
  "additionalProperties": false
@@ -3,13 +3,18 @@
3
3
 
4
4
  /**
5
5
  * boot-sweep.js — non-interactive *protected* merged-branch sweep over
6
- * `sweepMergedBranches` (flags: see HELP). Unlike `git-cleanup --branches` it
6
+ * `sweepMergedBranches` (flags: see HELP). Unlike `clean-git --branches` it
7
7
  * always skips a branch with unpushed work, a dirty worktree or an open
8
8
  * parent Story. Best-effort: failures land in the envelope, exit is always 0.
9
9
  *
10
+ * After the branch sweep it runs the closed-Story worktree sweep
11
+ * (`sweepStaleStoryWorktrees`) under the same lock: `.worktrees/story-<id>`
12
+ * trees whose Story is closed or `agent::done` are removed; an open Story's
13
+ * tree and the tree this process runs from are never touched.
14
+ *
10
15
  * `content-merged` branches (merge-tree equivalence — no merge check ever
11
16
  * validated their exact diff) are never reaped here, only reported for
12
- * `/git-cleanup`.
17
+ * `/clean-git`.
13
18
  */
14
19
 
15
20
  import path from 'node:path';
@@ -18,9 +23,13 @@ import { parseArgs } from 'node:util';
18
23
  import { runAsCli } from './lib/cli-utils.js';
19
24
  import { PROJECT_ROOT, resolveConfig } from './lib/config-resolver.js';
20
25
  import { Logger } from './lib/Logger.js';
26
+ import { sweepStaleStoryWorktrees } from './lib/orchestration/plan-runner/worktree-sweep.js';
21
27
  import { createProvider } from './lib/provider-factory.js';
22
28
  import { buildProtectionCtx } from './lib/single-story-sweep/protection-ctx.js';
23
- import { resolveSweepLockPath } from './lib/single-story-sweep/sweep-lock.js';
29
+ import {
30
+ acquireSweepLock,
31
+ resolveSweepLockPath,
32
+ } from './lib/single-story-sweep/sweep-lock.js';
24
33
  import { sweepMergedBranches } from './lib/single-story-sweep.js';
25
34
  import { sweepTempRetention } from './lib/temp-retention.js';
26
35
 
@@ -46,10 +55,13 @@ Runs the protected merged-branch boot sweep non-interactively: reaps every
46
55
  local branch whose PR is MERGED and whose HEAD matches the merged headRefOid,
47
56
  skipping any candidate the protection partition flags (unpushed work, dirty
48
57
  worktree, still-open parent Story), then fast-forwards the base branch.
58
+ Then removes every .worktrees/story-<id> tree whose Story is closed or
59
+ agent::done (never an open Story's, never the tree this process runs from);
60
+ the outcome lands under "worktreeSweep".
49
61
  Branches detected only via the weaker content-equivalence signal
50
62
  (detectedBy: 'content-merged') are never reaped here — they are reported
51
63
  under "contentMerged" (and a routing hint in the summary line) for the
52
- operator to send to /git-cleanup.
64
+ operator to send to /clean-git.
53
65
 
54
66
  Options:
55
67
  --include <glob> Branch glob to sweep (repeatable). Default: story-*
@@ -60,6 +72,63 @@ Options:
60
72
  --json Emit the result envelope as JSON.
61
73
  `;
62
74
 
75
+ /**
76
+ * The closed-Story worktree sweep under the shared sweep lock; never throws.
77
+ * A contended lock skips it — the holder's next boot picks the trees up.
78
+ * Shared with `single-story-init.js`, whose boot never reaches
79
+ * {@link runBootSweep}: one sweep, one lock, two callers. `keepPaths` joins
80
+ * the sweep's running-tree guard — init passes the tree it is about to
81
+ * work in, so the Story being initialized is never removed.
82
+ *
83
+ * @param {{
84
+ * root: string,
85
+ * provider: object,
86
+ * lockPath: string,
87
+ * lockTimeoutMs: number,
88
+ * sweepFn?: Function,
89
+ * acquireLockFn?: Function,
90
+ * logger: object,
91
+ * logTag?: string,
92
+ * keepPaths?: string[],
93
+ * }} args
94
+ * @returns {Promise<object>} `{ ok, reaped, skipped, reason?, error? }`.
95
+ */
96
+ export async function runWorktreeSweep({
97
+ root,
98
+ provider,
99
+ lockPath,
100
+ lockTimeoutMs,
101
+ sweepFn = sweepStaleStoryWorktrees,
102
+ acquireLockFn = acquireSweepLock,
103
+ logger,
104
+ logTag = '[boot-sweep]',
105
+ keepPaths = [],
106
+ }) {
107
+ const lock = acquireLockFn({ lockPath, timeoutMs: lockTimeoutMs });
108
+ if (!lock.acquired) {
109
+ return { ok: true, reason: `lock-${lock.reason}`, reaped: [], skipped: [] };
110
+ }
111
+ try {
112
+ const result = await sweepFn({
113
+ provider,
114
+ repoRoot: root,
115
+ runningPaths: keepPaths,
116
+ logger: {
117
+ info: (m) => logger.info?.(`${logTag} ${m}`),
118
+ warn: (m) => logger.warn?.(`${logTag} ${m}`),
119
+ error: (m) => logger.warn?.(`${logTag} ${m}`),
120
+ },
121
+ });
122
+ return { ok: true, ...result };
123
+ } catch (err) {
124
+ const msg = err?.message ?? String(err);
125
+ logger.warn?.(`${logTag} worktree sweep threw (host continues): ${msg}`);
126
+ return { ok: false, error: msg, reaped: [], skipped: [] };
127
+ } finally {
128
+ lock.release();
129
+ }
130
+ }
131
+
63
132
  /**
64
133
  * Run the protected boot sweep; never throws.
65
134
  *
@@ -73,11 +142,13 @@ Options:
73
142
  * injectedConfig?: object,
74
143
  * injectedProvider?: object,
75
144
  * injectedSweep?: Function,
145
+ * worktreeSweepFn?: Function,
146
+ * acquireLockFn?: Function,
76
147
  * purgeFn?: Function,
77
148
  * logger?: { info?: Function, warn?: Function },
78
149
  * }} [args]
79
150
  * @returns {Promise<object>} the {@link sweepMergedBranches} envelope plus
80
- * `tempPurge`.
151
+ * `worktreeSweep` and `tempPurge`.
81
152
  */
82
153
  export async function runBootSweep({
83
154
  cwd,
@@ -89,6 +160,8 @@ export async function runBootSweep({
89
160
  injectedConfig,
90
161
  injectedProvider,
91
162
  injectedSweep,
163
+ worktreeSweepFn = sweepStaleStoryWorktrees,
164
+ acquireLockFn = acquireSweepLock,
92
165
  purgeFn = sweepTempRetention,
93
166
  logger = Logger,
94
167
  } = {}) {
@@ -130,6 +203,16 @@ export async function runBootSweep({
130
203
  lockTimeoutMs,
131
204
  });
132
205
 
206
+ const worktreeSweep = await runWorktreeSweep({
207
+ root,
208
+ provider,
209
+ lockPath,
210
+ lockTimeoutMs,
211
+ sweepFn: worktreeSweepFn,
212
+ acquireLockFn,
213
+ logger,
214
+ });
215
+
133
216
  // Temp-retention catch-up: reaped branches are confirmed merges, so their
134
217
  // artifacts are spent; the age floor collects the rest.
135
218
  const purge = await purgeFn({
@@ -138,7 +221,7 @@ export async function runBootSweep({
138
221
  label: 'boot-sweep',
139
222
  logger,
140
223
  });
141
- return { ...result, tempPurge: purge };
224
+ return { ...result, worktreeSweep, tempPurge: purge };
142
225
  } catch (err) {
143
226
  const msg = err?.message ?? String(err);
144
227
  logger.warn?.(`[boot-sweep] sweep threw (host continues): ${msg}`);
@@ -157,7 +240,7 @@ export async function runBootSweep({
157
240
  }
158
241
 
159
242
  /**
160
- * One-line summary; a nonzero `contentMerged` count adds a `/git-cleanup` hint.
243
+ * One-line summary; a nonzero `contentMerged` count adds a `/clean-git` hint.
161
244
  *
162
245
  * @param {{ localDeleted: number, remoteDeleted: number, protected?: Array, contentMerged?: Array }} result
163
246
  * @returns {string}
@@ -165,11 +248,16 @@ export async function runBootSweep({
165
248
  export function buildSummaryLine(result) {
166
249
  const protectedCount = result.protected?.length ?? 0;
167
250
  const contentMergedCount = result.contentMerged?.length ?? 0;
251
+ const worktreesReaped = result.worktreeSweep?.reaped?.length ?? 0;
252
+ const worktreeSuffix =
253
+ worktreesReaped > 0
254
+ ? `; removed ${worktreesReaped} closed-Story worktree(s)`
255
+ : '';
168
256
  const contentMergedSuffix =
169
257
  contentMergedCount > 0
170
- ? `; ${contentMergedCount} content-merged branch(es) left for /git-cleanup`
258
+ ? `; ${contentMergedCount} content-merged branch(es) left for /clean-git`
171
259
  : '';
172
- return `[boot-sweep] reaped ${result.localDeleted} local + ${result.remoteDeleted} remote; protected ${protectedCount}${contentMergedSuffix}.`;
260
+ return `[boot-sweep] reaped ${result.localDeleted} local + ${result.remoteDeleted} remote; protected ${protectedCount}${worktreeSuffix}${contentMergedSuffix}.`;
173
261
  }
174
262
 
175
263
  /**
@@ -100,10 +100,10 @@ async function main() {
100
100
  }
101
101
 
102
102
  runAsCli(import.meta.url, main, {
103
- source: 'git-cleanup',
103
+ source: 'clean-git',
104
104
  usage: {
105
105
  invocation:
106
- 'node .agents/scripts/git-cleanup.js [--execute] [--yes] [--json] [phase flags] [filters]',
106
+ 'node .agents/scripts/clean-git.js [--execute] [--yes] [--json] [phase flags] [filters]',
107
107
  summary:
108
108
  'Tidy the local checkout in four phases — fast-forward the base branch, prune stale remote refs, reap merged branches, triage stashes. Dry-run unless --execute.',
109
109
  flags: [
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ /* node:coverage ignore file -- thin CLI shell; the engine is lib/clean-temp.js */
3
+
4
+ /**
5
+ * `/clean-temp` — preview (default) or delete spent entries under the
6
+ * project's tempRoot. Thin shell over `lib/clean-temp.js`.
7
+ *
8
+ * Exit codes: 0 clean or dry-run, 1 refused (tempRoot outside the project
9
+ * root) or a deletion failed.
10
+ */
11
+
12
+ import { runCleanTemp } from './lib/clean-temp.js';
13
+ import { runAsCli } from './lib/cli-utils.js';
14
+ import { resolveConfig } from './lib/config-resolver.js';
15
+ import { promptYesNo } from './lib/orchestration/git-cleanup/phases/prompts.js';
16
+ import { createProvider } from './lib/provider-factory.js';
17
+
18
+ /* node:coverage ignore next */
19
+ async function main() {
20
+ const { exitCode } = await runCleanTemp({
21
+ argv: process.argv.slice(2),
22
+ cwd: process.cwd(),
23
+ loadConfig: (projectRoot) => resolveConfig({ cwd: projectRoot }),
24
+ getProvider: createProvider,
25
+ confirm: promptYesNo,
26
+ write: (text) => process.stdout.write(text),
27
+ writeErr: (text) => process.stderr.write(text),
28
+ });
29
+ return exitCode;
30
+ }
31
+
32
+ runAsCli(import.meta.url, main, {
33
+ source: 'clean-temp',
34
+ propagateExitCode: true,
35
+ usage: {
36
+ invocation:
37
+ 'node .agents/scripts/clean-temp.js [--execute] [--yes] [--json] [--cwd <path>]',
38
+ summary:
39
+ "Sort every top-level entry under the project's tempRoot into framework / closed-issue / aged / kept buckets. Dry-run unless --execute.",
40
+ flags: [
41
+ ['--execute', 'Delete the confirmed buckets (default is a dry run).'],
42
+ [
43
+ '--yes',
44
+ 'Skip the per-bucket prompts; deletes framework and closed-issue only — aged is never deleted unattended.',
45
+ ],
46
+ ['--json', 'Emit the result envelope as JSON.'],
47
+ ['--cwd <path>', 'Project directory (default: process cwd).'],
48
+ ],
49
+ notes: [
50
+ 'Refuses (exit 1) when the resolved tempRoot is not inside the project root.',
51
+ 'qa/, cache/, *.lock and signals.ndjson (at any depth) are never deleted.',
52
+ ],
53
+ },
54
+ });