mandrel 2.65.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 (82) hide show
  1. package/.agents/agents/acceptance-critic.md +7 -7
  2. package/.agents/agents/auditor.md +17 -18
  3. package/.agents/agents/plan-critic.md +5 -5
  4. package/.agents/agents/story-worker.md +5 -5
  5. package/.agents/docs/agentrc-reference.json +2 -1
  6. package/.agents/docs/configuration.md +2 -1
  7. package/.agents/docs/execution-reference.md +27 -5
  8. package/.agents/docs/workflows.md +4 -2
  9. package/.agents/instructions.md +12 -13
  10. package/.agents/rules/ci-remediation.md +3 -3
  11. package/.agents/rules/gherkin-standards.md +3 -2
  12. package/.agents/rules/git-conventions-reference.md +17 -8
  13. package/.agents/rules/git-conventions.md +10 -8
  14. package/.agents/rules/testing-standards.md +8 -7
  15. package/.agents/runtime-deps.json +1 -1
  16. package/.agents/schemas/agentrc.schema.json +6 -1
  17. package/.agents/scripts/boot-sweep.js +97 -9
  18. package/.agents/scripts/bootstrap.js +94 -89
  19. package/.agents/scripts/{git-cleanup.js → clean-git.js} +2 -2
  20. package/.agents/scripts/clean-temp.js +54 -0
  21. package/.agents/scripts/clean-worktrees.js +593 -0
  22. package/.agents/scripts/drain-pending-cleanup.js +5 -4
  23. package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
  24. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +78 -78
  25. package/.agents/scripts/lib/clean-temp.js +440 -0
  26. package/.agents/scripts/lib/cli/standard-args.js +60 -76
  27. package/.agents/scripts/lib/cli-args.js +26 -0
  28. package/.agents/scripts/lib/config/gates/shared.js +3 -3
  29. package/.agents/scripts/lib/config-settings-schema-delivery.js +11 -2
  30. package/.agents/scripts/lib/feedback-loop/graduate-steps.js +205 -0
  31. package/.agents/scripts/lib/feedback-loop/graduator-core.js +47 -782
  32. package/.agents/scripts/lib/feedback-loop/graduator-gh.js +449 -0
  33. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  34. package/.agents/scripts/lib/observability/close-telemetry.js +330 -0
  35. package/.agents/scripts/lib/observability/runtime-friction.js +2 -0
  36. package/.agents/scripts/lib/observability/signal-validator.js +17 -5
  37. package/.agents/scripts/lib/observability/source-classifier.js +3 -1
  38. package/.agents/scripts/lib/orchestration/code-review.js +22 -0
  39. package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
  40. package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
  41. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +149 -97
  42. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +23 -0
  43. package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
  44. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -0
  45. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +349 -263
  46. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
  47. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
  48. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +327 -314
  49. package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
  50. package/.agents/scripts/lib/signals/detectors/common.js +63 -51
  51. package/.agents/scripts/lib/single-story-sweep.js +2 -2
  52. package/.agents/scripts/lib/temp-removal.js +110 -0
  53. package/.agents/scripts/lib/temp-retention.js +122 -73
  54. package/.agents/scripts/lib/transpile.js +28 -3
  55. package/.agents/scripts/lib/worktree/canonical-path.js +34 -0
  56. package/.agents/scripts/lib/worktree/lifecycle/reap.js +15 -4
  57. package/.agents/scripts/single-story-close.js +10 -2
  58. package/.agents/scripts/single-story-confirm-merge.js +267 -238
  59. package/.agents/scripts/single-story-init.js +120 -17
  60. package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
  61. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -2
  62. package/.agents/workflows/audit-architecture.md +5 -4
  63. package/.agents/workflows/audit-documentation.md +5 -5
  64. package/.agents/workflows/audit-performance.md +10 -10
  65. package/.agents/workflows/{git-cleanup.md → clean-git.md} +10 -10
  66. package/.agents/workflows/clean-temp.md +67 -0
  67. package/.agents/workflows/clean-worktrees.md +63 -0
  68. package/.agents/workflows/git-deliver.md +1 -1
  69. package/.agents/workflows/helpers/acceptance-self-eval.md +11 -10
  70. package/.agents/workflows/helpers/audit-lens-core.md +30 -57
  71. package/.agents/workflows/helpers/deliver-digest.md +2 -2
  72. package/.agents/workflows/helpers/deliver-reference.md +3 -1
  73. package/.agents/workflows/helpers/deliver-story-reference.md +2 -2
  74. package/.agents/workflows/helpers/deliver-story.md +6 -1
  75. package/.agents/workflows/helpers/parallel-tooling.md +16 -18
  76. package/.agents/workflows/mandrel-deliver.md +1 -1
  77. package/.agents/workflows/mandrel-plan.md +6 -5
  78. package/docs/CHANGELOG.md +39 -0
  79. package/lib/cli/guarded-sync.js +87 -0
  80. package/lib/cli/sync-agents.js +9 -92
  81. package/lib/cli/sync-commands.js +9 -101
  82. package/package.json +2 -2
@@ -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
  /**
@@ -716,6 +716,13 @@ async function detectCreation(answers, skipGithub) {
716
716
  return creation;
717
717
  }
718
718
 
719
+ function projectNameFor(pn, projects) {
720
+ const match = projects.find((p) => p.value === pn);
721
+ if (!match) return '(unknown)';
722
+ const m = /^(.*)\s+\(#\d+\)$/.exec(match.label);
723
+ return m ? m[1] : match.label;
724
+ }
725
+
719
726
  /**
720
727
  * `{ name, number }` for the summary; the picker stores only the number, so
721
728
  * an existing project's name is looked up.
@@ -723,20 +730,78 @@ async function detectCreation(answers, skipGithub) {
723
730
  function resolveProjectDisplay(answers, skipGithub, projectsList) {
724
731
  const pn = answers.projectNumber;
725
732
  if (!pn) return { name: '(skip)', number: '(skip)' };
726
- if (/^\d+$/.test(pn)) {
727
- let name = '(unknown)';
728
- if (!skipGithub) {
729
- const projects =
730
- projectsList ?? safeList(() => listProjects({ owner: answers.owner }));
731
- const match = projects.find((p) => p.value === pn);
732
- if (match) {
733
- const m = /^(.*)\s+\(#\d+\)$/.exec(match.label);
734
- name = m ? m[1] : match.label;
735
- }
733
+ if (!/^\d+$/.test(pn)) return { name: pn, number: '(new)' };
734
+ if (skipGithub) return { name: '(unknown)', number: pn };
735
+ const projects =
736
+ projectsList ?? safeList(() => listProjects({ owner: answers.owner }));
737
+ return { name: projectNameFor(pn, projects), number: pn };
738
+ }
739
+
740
+ /** Opt-ins default off; dry-run resolves them without prompting. */
741
+ const OPT_INS = Object.freeze([
742
+ {
743
+ key: 'withProjectBoard',
744
+ flag: 'with-project-board',
745
+ prompt: 'Set up project board fields (Status, custom)?',
746
+ },
747
+ {
748
+ key: 'withIssueForms',
749
+ flag: 'with-issue-forms',
750
+ prompt: 'Generate GitHub Issue Form templates?',
751
+ },
752
+ {
753
+ key: 'withQuality',
754
+ flag: 'with-quality',
755
+ prompt:
756
+ 'Install local quality gates (pre-commit hook + quality:preview/watch scripts)?',
757
+ },
758
+ ]);
759
+
760
+ async function resolveOptIns(state) {
761
+ const optIns = {};
762
+ for (const { key, flag, prompt } of OPT_INS) {
763
+ let on = Boolean(state.flags[flag]);
764
+ if (!state.flags['dry-run'] && !on) {
765
+ on = await confirmYesNo(prompt, state.interactive, false);
736
766
  }
737
- return { name, number: pn };
767
+ optIns[key] = on;
738
768
  }
739
- return { name: pn, number: '(new)' };
769
+ return optIns;
770
+ }
771
+
772
+ async function approveCreation(state, creation) {
773
+ if (state.flags['dry-run'] || !(creation.newRepo || creation.newProject)) {
774
+ return true;
775
+ }
776
+ return confirmYesNo(
777
+ 'Create the new GitHub repo/project listed above?',
778
+ state.interactive,
779
+ );
780
+ }
781
+
782
+ async function confirmSummary(state, answers, creation, projectsList) {
783
+ const skipGithub = Boolean(state.flags['skip-github']);
784
+ const project = resolveProjectDisplay(answers, skipGithub, projectsList);
785
+ Logger.info(
786
+ renderAnswerSummary(
787
+ answers,
788
+ creation,
789
+ project,
790
+ state.gitInitialized,
791
+ resolveRepoVisibility(state.flags),
792
+ ),
793
+ );
794
+ return confirmYesNo('Is this correct?', state.interactive);
795
+ }
796
+
797
+ /** Owner repo/project lists, fetched once for pickers and summary. */
798
+ function fetchPickerLists(state, skipGithub) {
799
+ const owner = resolveOwnerForPicker(state.defaults, state.flags);
800
+ if (skipGithub || !owner) return { reposList: [], projectsList: [] };
801
+ return {
802
+ reposList: safeList(() => listRepos({ owner }).map(bareRepoName)),
803
+ projectsList: safeList(() => listProjects({ owner })),
804
+ };
740
805
  }
741
806
 
742
807
  /**
@@ -745,22 +810,16 @@ function resolveProjectDisplay(answers, skipGithub, projectsList) {
745
810
  */
746
811
  export async function collectAndConfirm(state) {
747
812
  const skipGithub = Boolean(state.flags['skip-github']);
748
- const owner = resolveOwnerForPicker(state.defaults, state.flags);
749
- // Fetched once for pickers and summary, so the name never needs a second call.
750
- const reposList =
751
- !skipGithub && owner
752
- ? safeList(() => listRepos({ owner }).map(bareRepoName))
753
- : [];
754
- const projectsList =
755
- !skipGithub && owner ? safeList(() => listProjects({ owner })) : [];
756
-
813
+ const lists = fetchPickerLists(state, skipGithub);
757
814
  let silentAccept = state.silentAccept;
758
815
  for (;;) {
759
816
  const { answers, missing } = await collectAnswers({
760
- questions: buildQuestions(state.defaults, state.flags, process.env, {
761
- reposList,
762
- projectsList,
763
- }),
817
+ questions: buildQuestions(
818
+ state.defaults,
819
+ state.flags,
820
+ process.env,
821
+ lists,
822
+ ),
764
823
  flags: state.flags,
765
824
  interactive: state.interactive,
766
825
  assumeYes: state.assumeYes,
@@ -775,78 +834,24 @@ export async function collectAndConfirm(state) {
775
834
  );
776
835
  return { ok: false, exit: 1 };
777
836
  }
778
- if (!answers.operatorHandle) answers.operatorHandle = answers.owner;
779
- answers.operatorHandle = normalizeHandleAnswer(answers.operatorHandle);
837
+ answers.operatorHandle = normalizeHandleAnswer(
838
+ answers.operatorHandle || answers.owner,
839
+ );
780
840
 
781
841
  const creation = await detectCreation(answers, skipGithub);
782
- const project = resolveProjectDisplay(answers, skipGithub, projectsList);
783
- Logger.info(
784
- renderAnswerSummary(
785
- answers,
786
- creation,
787
- project,
788
- state.gitInitialized,
789
- resolveRepoVisibility(state.flags),
790
- ),
791
- );
792
- const correct = await confirmYesNo('Is this correct?', state.interactive);
793
- if (!correct) {
842
+ if (!(await confirmSummary(state, answers, creation, lists.projectsList))) {
794
843
  Logger.info('[Bootstrap] Okay — let’s try again.');
795
844
  silentAccept = [];
796
845
  continue;
797
846
  }
798
-
799
- if (!state.flags['dry-run'] && (creation.newRepo || creation.newProject)) {
800
- const approved = await confirmYesNo(
801
- 'Create the new GitHub repo/project listed above?',
802
- state.interactive,
803
- );
804
- if (!approved) {
805
- Logger.error(
806
- '[Bootstrap] Creation declined — cannot continue without the repo/project. Exiting.',
807
- );
808
- return { ok: false, exit: 1 };
809
- }
810
- }
811
-
812
- // Opt-ins default off; dry-run resolves them without prompting.
813
- let withProjectBoard = Boolean(state.flags['with-project-board']);
814
- if (!state.flags['dry-run'] && !withProjectBoard) {
815
- withProjectBoard = await confirmYesNo(
816
- 'Set up project board fields (Status, custom)?',
817
- state.interactive,
818
- false,
819
- );
820
- }
821
-
822
- let withIssueForms = Boolean(state.flags['with-issue-forms']);
823
- if (!state.flags['dry-run'] && !withIssueForms) {
824
- withIssueForms = await confirmYesNo(
825
- 'Generate GitHub Issue Form templates?',
826
- state.interactive,
827
- false,
828
- );
829
- }
830
-
831
- let withQuality = Boolean(state.flags['with-quality']);
832
- if (!state.flags['dry-run'] && !withQuality) {
833
- withQuality = await confirmYesNo(
834
- 'Install local quality gates (pre-commit hook + quality:preview/watch scripts)?',
835
- state.interactive,
836
- false,
847
+ if (!(await approveCreation(state, creation))) {
848
+ Logger.error(
849
+ '[Bootstrap] Creation declined — cannot continue without the repo/project. Exiting.',
837
850
  );
851
+ return { ok: false, exit: 1 };
838
852
  }
839
-
840
- return {
841
- ok: true,
842
- payload: {
843
- answers,
844
- creation,
845
- withProjectBoard,
846
- withIssueForms,
847
- withQuality,
848
- },
849
- };
853
+ const optIns = await resolveOptIns(state);
854
+ return { ok: true, payload: { answers, creation, ...optIns } };
850
855
  }
851
856
  }
852
857
 
@@ -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
+ });