@brunosps00/dev-workflow 2.1.0 → 2.3.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 (129) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +118 -0
  3. package/README.md +17 -6
  4. package/lib/constants.js +24 -20
  5. package/lib/init.js +34 -0
  6. package/lib/instruction-health.js +38 -0
  7. package/lib/post-update.js +2 -1
  8. package/lib/validate.js +3 -1
  9. package/package.json +1 -1
  10. package/scaffold/config/routing.json +61 -23
  11. package/scaffold/en/agent-instructions.md +31 -135
  12. package/scaffold/en/commands/dw-analyze-project.md +2 -0
  13. package/scaffold/en/commands/dw-autopilot.md +22 -243
  14. package/scaffold/en/commands/dw-bugfix.md +9 -35
  15. package/scaffold/en/commands/dw-claude-run.md +29 -45
  16. package/scaffold/en/commands/dw-codex-run.md +30 -45
  17. package/scaffold/en/commands/dw-context-budget.md +6 -14
  18. package/scaffold/en/commands/dw-copilot-run.md +29 -41
  19. package/scaffold/en/commands/dw-generate-pr.md +2 -2
  20. package/scaffold/en/commands/dw-goal.md +11 -5
  21. package/scaffold/en/commands/dw-harness-audit.md +2 -0
  22. package/scaffold/en/commands/dw-help.md +8 -4
  23. package/scaffold/en/commands/dw-pause.md +1 -0
  24. package/scaffold/en/commands/dw-plan.md +14 -7
  25. package/scaffold/en/commands/dw-qa.md +2 -0
  26. package/scaffold/en/commands/dw-report.md +169 -0
  27. package/scaffold/en/commands/dw-resume.md +5 -11
  28. package/scaffold/en/commands/dw-review.md +4 -3
  29. package/scaffold/en/commands/dw-run.md +19 -165
  30. package/scaffold/en/commands/dw-skill-health.md +7 -0
  31. package/scaffold/en/commands/dw-update.md +18 -4
  32. package/scaffold/en/commands/dw-worktree.md +100 -0
  33. package/scaffold/en/references/command-routing.md +53 -0
  34. package/scaffold/en/references/execution-contract.md +55 -0
  35. package/scaffold/en/templates/frontend-quality-template.md +43 -0
  36. package/scaffold/en/templates/task-template.md +20 -40
  37. package/scaffold/en/templates/tasks-template.md +9 -6
  38. package/scaffold/en/templates/techspec-template.md +4 -0
  39. package/scaffold/pt-br/agent-instructions.md +31 -135
  40. package/scaffold/pt-br/commands/dw-analyze-project.md +2 -0
  41. package/scaffold/pt-br/commands/dw-autopilot.md +22 -243
  42. package/scaffold/pt-br/commands/dw-bugfix.md +9 -35
  43. package/scaffold/pt-br/commands/dw-claude-run.md +29 -46
  44. package/scaffold/pt-br/commands/dw-codex-run.md +30 -45
  45. package/scaffold/pt-br/commands/dw-context-budget.md +6 -14
  46. package/scaffold/pt-br/commands/dw-copilot-run.md +29 -43
  47. package/scaffold/pt-br/commands/dw-generate-pr.md +2 -2
  48. package/scaffold/pt-br/commands/dw-goal.md +11 -5
  49. package/scaffold/pt-br/commands/dw-harness-audit.md +2 -0
  50. package/scaffold/pt-br/commands/dw-help.md +8 -4
  51. package/scaffold/pt-br/commands/dw-pause.md +1 -0
  52. package/scaffold/pt-br/commands/dw-plan.md +14 -7
  53. package/scaffold/pt-br/commands/dw-qa.md +2 -0
  54. package/scaffold/pt-br/commands/dw-report.md +169 -0
  55. package/scaffold/pt-br/commands/dw-resume.md +7 -11
  56. package/scaffold/pt-br/commands/dw-review.md +4 -3
  57. package/scaffold/pt-br/commands/dw-run.md +19 -165
  58. package/scaffold/pt-br/commands/dw-skill-health.md +7 -0
  59. package/scaffold/pt-br/commands/dw-update.md +18 -4
  60. package/scaffold/pt-br/commands/dw-worktree.md +100 -0
  61. package/scaffold/pt-br/references/command-routing.md +53 -0
  62. package/scaffold/pt-br/references/execution-contract.md +55 -0
  63. package/scaffold/pt-br/templates/frontend-quality-template.md +43 -0
  64. package/scaffold/pt-br/templates/task-template.md +20 -40
  65. package/scaffold/pt-br/templates/tasks-template.md +9 -6
  66. package/scaffold/pt-br/templates/techspec-template.md +4 -0
  67. package/scaffold/scripts/hooks/git-guardrails.mjs +1 -0
  68. package/scaffold/scripts/lib/workflow-contract.mjs +101 -0
  69. package/scaffold/scripts/lib/worktree-gc.mjs +460 -0
  70. package/scaffold/skill-registry.json +23 -23
  71. package/scaffold/skills/dw-cli-run/SKILL.md +33 -208
  72. package/scaffold/skills/dw-cli-run/references/dispatch-tuning.md +14 -72
  73. package/scaffold/skills/dw-codebase-intel/SKILL.md +5 -39
  74. package/scaffold/skills/dw-codebase-intel/agents/intel-updater.md +1 -1
  75. package/scaffold/skills/dw-codebase-intel/references/agents-detail.md +7 -0
  76. package/scaffold/skills/dw-codebase-intel/references/files-in-dw-intel-detail.md +16 -0
  77. package/scaffold/skills/dw-codebase-intel/references/how-dw-intel-reads-this-detail.md +12 -0
  78. package/scaffold/skills/dw-codebase-intel/references/how-to-compose-the-typical-flow-detail.md +11 -0
  79. package/scaffold/skills/dw-council/SKILL.md +2 -17
  80. package/scaffold/skills/dw-council/references/integration-with-other-dw-commands-detail.md +7 -0
  81. package/scaffold/skills/dw-council/references/phase-2-tensions-and-rebuttals-detail.md +15 -0
  82. package/scaffold/skills/dw-domain-modeling/SKILL.md +1 -1
  83. package/scaffold/skills/dw-execute-phase/SKILL.md +12 -32
  84. package/scaffold/skills/dw-execute-phase/agents/executor.md +15 -252
  85. package/scaffold/skills/dw-execute-phase/agents/plan-checker.md +4 -207
  86. package/scaffold/skills/dw-execute-phase/references/atomic-commits.md +8 -18
  87. package/scaffold/skills/dw-execute-phase/references/de-sloppify-pass-optional-cleanup-task-detail.md +12 -0
  88. package/scaffold/skills/dw-execute-phase/references/deviation-rules-during-execution-detail.md +8 -0
  89. package/scaffold/skills/dw-execute-phase/references/how-the-two-agents-compose-detail.md +7 -0
  90. package/scaffold/skills/dw-execute-phase/references/plan-verification.md +9 -155
  91. package/scaffold/skills/dw-execute-phase/references/wave-coordination.md +6 -97
  92. package/scaffold/skills/dw-git-discipline/SKILL.md +2 -2
  93. package/scaffold/skills/dw-grilling/SKILL.md +1 -1
  94. package/scaffold/skills/dw-incident-response/SKILL.md +1 -64
  95. package/scaffold/skills/dw-incident-response/references/the-five-phases-detail.md +66 -0
  96. package/scaffold/skills/dw-llm-eval/SKILL.md +5 -53
  97. package/scaffold/skills/dw-llm-eval/references/agent-tool-use-evaluation-detail.md +23 -0
  98. package/scaffold/skills/dw-llm-eval/references/anti-patterns-will-block-in-dw-review-code-only-detail.md +9 -0
  99. package/scaffold/skills/dw-llm-eval/references/consistency-run-to-run-stability-detail.md +10 -0
  100. package/scaffold/skills/dw-llm-eval/references/integration-with-dev-workflow-commands-detail.md +6 -0
  101. package/scaffold/skills/dw-llm-eval/references/the-oracle-ladder-detail.md +15 -0
  102. package/scaffold/skills/dw-memory/SKILL.md +4 -50
  103. package/scaffold/skills/dw-memory/references/context-budget.md +8 -56
  104. package/scaffold/skills/dw-memory/references/promotion-decision-test-detail.md +30 -0
  105. package/scaffold/skills/dw-minimalism/SKILL.md +4 -5
  106. package/scaffold/skills/dw-review-rigor/SKILL.md +1 -54
  107. package/scaffold/skills/dw-review-rigor/references/the-five-rules-detail.md +56 -0
  108. package/scaffold/skills/dw-simplification/SKILL.md +2 -55
  109. package/scaffold/skills/dw-simplification/references/pattern-recognition-detail.md +11 -0
  110. package/scaffold/skills/dw-simplification/references/the-five-rules-detail.md +48 -0
  111. package/scaffold/skills/dw-source-grounding/SKILL.md +1 -56
  112. package/scaffold/skills/dw-source-grounding/references/the-protocol-detect-fetch-implement-cite-detail.md +58 -0
  113. package/scaffold/skills/dw-testing-discipline/SKILL.md +25 -177
  114. package/scaffold/skills/dw-testing-discipline/references/agent-guardrails.md +9 -167
  115. package/scaffold/skills/dw-testing-discipline/references/core-rules.md +3 -3
  116. package/scaffold/skills/dw-testing-discipline/references/mutation-testing.md +19 -0
  117. package/scaffold/skills/dw-testing-discipline/references/patterns.md +3 -12
  118. package/scaffold/skills/dw-ui-discipline/SKILL.md +2 -59
  119. package/scaffold/skills/dw-ui-discipline/references/frontend-engineering.md +46 -0
  120. package/scaffold/skills/dw-ui-discipline/references/the-four-grounding-questions-detail.md +61 -0
  121. package/scaffold/skills/dw-verify/SKILL.md +26 -148
  122. package/scaffold/skills/dw-verify/references/verification-discovery-and-attribution.md +2 -2
  123. package/scaffold/skills/humanizer/SKILL.md +2 -1
  124. package/scaffold/skills/security-review/SKILL.md +3 -183
  125. package/scaffold/skills/security-review/references/do-not-flag-detail.md +49 -0
  126. package/scaffold/skills/security-review/references/quick-patterns-reference-detail.md +56 -0
  127. package/scaffold/skills/security-review/references/review-process-detail.md +84 -0
  128. package/scaffold/skills/vercel-react-best-practices/SKILL.md +1 -90
  129. package/scaffold/skills/vercel-react-best-practices/references/quick-reference-detail.md +92 -0
@@ -0,0 +1,460 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dev-workflow worktree GC — deterministic lifecycle for delegation worktrees.
4
+ *
5
+ * The dw-*-run adapters create one git worktree per delegated task next to the
6
+ * main checkout (`../<project>-<slug>`). Left alone, merged worktrees pile up
7
+ * with their node_modules/build output (tens of GB). This script is the hard
8
+ * enforcement behind `/dw-worktree`: it lists every secondary worktree with a
9
+ * verdict, removes ONLY the ones proven safe (merged into an integration branch,
10
+ * clean, not in use, not locked), and never uses --force.
11
+ *
12
+ * node .dw/scripts/lib/worktree-gc.mjs list [--base <branch>] [--json] [--strict]
13
+ * node .dw/scripts/lib/worktree-gc.mjs clean [--base <branch>] [--apply] [--older-than <days>] [--keep-branches]
14
+ * node .dw/scripts/lib/worktree-gc.mjs create <slug> [--branch <name>] [--base <branch>] [--no-prep]
15
+ * node .dw/scripts/lib/worktree-gc.mjs merge <slug|path> [--base <branch>] [--keep-branches]
16
+ * node .dw/scripts/lib/worktree-gc.mjs prune
17
+ *
18
+ * Verdicts: REMOVABLE · KEEP:unmerged · KEEP:dirty · KEEP:in-use · KEEP:locked ·
19
+ * KEEP:recent (only with --older-than) · PRUNABLE (directory gone → `git worktree prune`).
20
+ *
21
+ * `clean` is a DRY-RUN unless --apply is passed. `merge` is the safe order from the
22
+ * main checkout: ff-only merge → worktree remove → branch delete → prune. Both refuse
23
+ * anything that is not proven safe and say why. Exit codes: 0 ok · 1 usage/abort ·
24
+ * 2 prep failed (worktree created but the delegate would run blind) · 3 `list --strict`
25
+ * found REMOVABLE leftovers.
26
+ */
27
+
28
+ import { spawnSync } from 'node:child_process';
29
+ import { existsSync, readdirSync, readFileSync, readlinkSync, statfsSync } from 'node:fs';
30
+ import { basename, dirname, join, resolve } from 'node:path';
31
+
32
+ const INTEGRATION_DEFAULTS = ['develop', 'main', 'master'];
33
+
34
+ function git(cwd, args, { allowFail = false } = {}) {
35
+ const r = spawnSync('git', args, { cwd, encoding: 'utf8' });
36
+ const out = (r.stdout || '').trim();
37
+ const err = (r.stderr || '').trim();
38
+ if (r.status !== 0 && !allowFail) {
39
+ throw new Error(`git ${args.join(' ')} failed in ${cwd}: ${err || out}`);
40
+ }
41
+ return { ok: r.status === 0, out, err };
42
+ }
43
+
44
+ function parseArgs(argv) {
45
+ const args = { _: [], flags: {} };
46
+ for (let i = 0; i < argv.length; i++) {
47
+ const a = argv[i];
48
+ if (a.startsWith('--')) {
49
+ const key = a.slice(2);
50
+ const next = argv[i + 1];
51
+ if (['base', 'branch', 'older-than'].includes(key)) {
52
+ if (next === undefined || next.startsWith('--')) throw new Error(`--${key} needs a value`);
53
+ args.flags[key] = next;
54
+ i++;
55
+ } else {
56
+ args.flags[key] = true;
57
+ }
58
+ } else {
59
+ args._.push(a);
60
+ }
61
+ }
62
+ return args;
63
+ }
64
+
65
+ function mainCheckout(cwd) {
66
+ const { out } = git(cwd, ['worktree', 'list', '--porcelain']);
67
+ const first = out.split('\n').find((l) => l.startsWith('worktree '));
68
+ if (!first) throw new Error('not inside a git repository');
69
+ return first.slice('worktree '.length);
70
+ }
71
+
72
+ function parseWorktrees(main) {
73
+ const { out } = git(main, ['worktree', 'list', '--porcelain']);
74
+ const entries = [];
75
+ let cur = null;
76
+ for (const line of out.split('\n')) {
77
+ if (line.startsWith('worktree ')) {
78
+ cur = { path: line.slice(9), head: null, branch: null, detached: false, bare: false, locked: null, prunable: null };
79
+ entries.push(cur);
80
+ } else if (!cur || !line.trim()) {
81
+ continue;
82
+ } else if (line.startsWith('HEAD ')) cur.head = line.slice(5);
83
+ else if (line.startsWith('branch ')) cur.branch = line.slice(7).replace(/^refs\/heads\//, '');
84
+ else if (line === 'detached') cur.detached = true;
85
+ else if (line === 'bare') cur.bare = true;
86
+ else if (line.startsWith('locked')) cur.locked = line.slice(6).trim() || 'locked';
87
+ else if (line.startsWith('prunable')) cur.prunable = line.slice(8).trim() || 'prunable';
88
+ }
89
+ return entries;
90
+ }
91
+
92
+ function readConfig(main) {
93
+ try {
94
+ const cfg = JSON.parse(readFileSync(join(main, '.dw', 'config.json'), 'utf8'));
95
+ return cfg && typeof cfg === 'object' ? cfg.worktree || {} : {};
96
+ } catch {
97
+ return {};
98
+ }
99
+ }
100
+
101
+ function branchExists(main, name) {
102
+ return git(main, ['show-ref', '--verify', '--quiet', `refs/heads/${name}`], { allowFail: true }).ok;
103
+ }
104
+
105
+ function integrationBranches(main, baseFlag) {
106
+ const cfg = readConfig(main);
107
+ const originHead = git(main, ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], { allowFail: true });
108
+ const candidates = [
109
+ baseFlag,
110
+ process.env.DW_WORKTREE_BASE,
111
+ cfg.base,
112
+ originHead.ok ? originHead.out.replace(/^origin\//, '') : null,
113
+ ...INTEGRATION_DEFAULTS,
114
+ ].filter(Boolean);
115
+ const seen = new Set();
116
+ const result = [];
117
+ for (const c of candidates) {
118
+ if (seen.has(c)) continue;
119
+ seen.add(c);
120
+ if (branchExists(main, c)) result.push(c);
121
+ }
122
+ if (baseFlag && !result.includes(baseFlag)) throw new Error(`--base ${baseFlag}: no such local branch`);
123
+ if (result.length === 0) throw new Error('no integration branch found (tried --base, DW_WORKTREE_BASE, .dw/config.json worktree.base, origin/HEAD, develop, main, master)');
124
+ return result;
125
+ }
126
+
127
+ function processesIn(path) {
128
+ const found = [];
129
+ if (process.platform !== 'linux') return found;
130
+ let pids = [];
131
+ try {
132
+ pids = readdirSync('/proc').filter((d) => /^\d+$/.test(d));
133
+ } catch {
134
+ return found;
135
+ }
136
+ for (const pid of pids) {
137
+ if (Number(pid) === process.pid) continue;
138
+ try {
139
+ const cwd = readlinkSync(`/proc/${pid}/cwd`);
140
+ if (cwd === path || cwd.startsWith(path + '/')) {
141
+ let cmd = '';
142
+ try {
143
+ cmd = readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ').trim().slice(0, 80);
144
+ } catch {
145
+ cmd = '?';
146
+ }
147
+ found.push({ pid: Number(pid), cmd });
148
+ }
149
+ } catch {
150
+ // process vanished or not ours — ignore
151
+ }
152
+ }
153
+ return found;
154
+ }
155
+
156
+ function sizeKb(path) {
157
+ const r = spawnSync('du', ['-sk', path], { encoding: 'utf8' });
158
+ if (r.status !== 0) return null;
159
+ const n = parseInt(r.stdout.split('\t')[0], 10);
160
+ return Number.isFinite(n) ? n : null;
161
+ }
162
+
163
+ function humanKb(kb) {
164
+ if (kb === null || kb === undefined) return '-';
165
+ if (kb >= 1024 * 1024) return `${(kb / 1024 / 1024).toFixed(1)}G`;
166
+ if (kb >= 1024) return `${Math.round(kb / 1024)}M`;
167
+ return `${kb}K`;
168
+ }
169
+
170
+ function inspect(main, { base, olderThan, withSize = true } = {}) {
171
+ const integration = integrationBranches(main, base);
172
+ const primary = integration[0];
173
+ const entries = parseWorktrees(main).filter((e) => e.path !== main && !e.bare);
174
+ const now = Math.floor(Date.now() / 1000);
175
+ return {
176
+ main,
177
+ integration,
178
+ worktrees: entries.map((e) => {
179
+ const exists = existsSync(e.path);
180
+ const mergedInto = e.head
181
+ ? integration.filter((b) => git(main, ['merge-base', '--is-ancestor', e.head, b], { allowFail: true }).ok)
182
+ : [];
183
+ const ahead = e.head ? parseInt(git(main, ['rev-list', '--count', `${primary}..${e.head}`], { allowFail: true }).out || '0', 10) : 0;
184
+ const dirty = exists ? git(e.path, ['status', '--porcelain'], { allowFail: true }).out.split('\n').filter(Boolean).length : null;
185
+ const lastCommit = e.head ? parseInt(git(main, ['log', '-1', '--format=%ct', e.head], { allowFail: true }).out || '0', 10) : 0;
186
+ const ageDays = lastCommit ? Math.floor((now - lastCommit) / 86400) : null;
187
+ const procs = exists ? processesIn(e.path) : [];
188
+ const size = exists && withSize ? sizeKb(e.path) : null;
189
+
190
+ let verdict;
191
+ let reason;
192
+ if (!exists || e.prunable) {
193
+ verdict = 'PRUNABLE';
194
+ reason = e.prunable || 'directory missing';
195
+ } else if (e.locked) {
196
+ verdict = 'KEEP:locked';
197
+ reason = e.locked;
198
+ } else if (procs.length) {
199
+ verdict = 'KEEP:in-use';
200
+ reason = procs.map((p) => `${p.pid} ${p.cmd}`).join('; ');
201
+ } else if (dirty > 0) {
202
+ verdict = 'KEEP:dirty';
203
+ reason = `${dirty} uncommitted file(s)`;
204
+ } else if (mergedInto.length === 0) {
205
+ verdict = 'KEEP:unmerged';
206
+ reason = `${ahead} commit(s) ahead of ${primary}, merged into none of [${integration.join(', ')}]`;
207
+ } else if (olderThan !== undefined && ageDays !== null && ageDays < olderThan) {
208
+ verdict = 'KEEP:recent';
209
+ reason = `merged but last commit ${ageDays}d ago (< ${olderThan}d)`;
210
+ } else {
211
+ verdict = 'REMOVABLE';
212
+ reason = `merged into ${mergedInto.join(', ')}; clean; no process inside`;
213
+ }
214
+ return {
215
+ name: basename(e.path),
216
+ path: e.path,
217
+ branch: e.branch,
218
+ detached: e.detached,
219
+ head: e.head,
220
+ mergedInto,
221
+ ahead,
222
+ dirty,
223
+ ageDays,
224
+ sizeKb: size,
225
+ procs,
226
+ locked: e.locked,
227
+ verdict,
228
+ reason,
229
+ };
230
+ }),
231
+ };
232
+ }
233
+
234
+ function printTable(report) {
235
+ const rows = report.worktrees;
236
+ console.log(`main checkout: ${report.main}`);
237
+ console.log(`integration branches: ${report.integration.join(', ')} (primary: ${report.integration[0]})`);
238
+ if (!rows.length) {
239
+ console.log('no secondary worktrees.');
240
+ return;
241
+ }
242
+ const w = Math.max(...rows.map((r) => r.name.length), 8);
243
+ const b = Math.max(...rows.map((r) => (r.branch || '(detached)').length), 6);
244
+ console.log(`${'worktree'.padEnd(w)} ${'branch'.padEnd(b)} ${'size'.padStart(6)} age dirty verdict reason`);
245
+ for (const r of rows) {
246
+ console.log(
247
+ `${r.name.padEnd(w)} ${(r.branch || '(detached)').padEnd(b)} ${humanKb(r.sizeKb).padStart(6)} ${String(r.ageDays ?? '-').padStart(3)}d ${String(r.dirty ?? '-').padStart(5)} ${r.verdict.padEnd(15)} ${r.reason}`
248
+ );
249
+ }
250
+ const removable = rows.filter((r) => r.verdict === 'REMOVABLE');
251
+ const freed = removable.reduce((s, r) => s + (r.sizeKb || 0), 0);
252
+ console.log(`\n${rows.length} worktree(s): ${removable.length} REMOVABLE (~${humanKb(freed)} apparent), ${rows.filter((r) => r.verdict === 'PRUNABLE').length} PRUNABLE, ${rows.length - removable.length - rows.filter((r) => r.verdict === 'PRUNABLE').length} KEEP`);
253
+ }
254
+
255
+ function deleteBranch(main, r, keepBranches) {
256
+ if (!r.branch || keepBranches) return 'kept';
257
+ // A branch checked out in another worktree cannot be deleted.
258
+ const stillUsed = parseWorktrees(main).some((e) => e.branch === r.branch);
259
+ if (stillUsed) return 'kept (checked out elsewhere)';
260
+ const soft = git(main, ['branch', '-d', r.branch], { allowFail: true });
261
+ if (soft.ok) return 'deleted';
262
+ // `-d` only trusts HEAD/upstream. We already proved the branch head is an ancestor
263
+ // of an integration branch, so the commits are preserved — delete with that evidence.
264
+ if (r.mergedInto.length && r.head && git(main, ['merge-base', '--is-ancestor', r.head, r.mergedInto[0]], { allowFail: true }).ok) {
265
+ const hard = git(main, ['branch', '-D', r.branch], { allowFail: true });
266
+ return hard.ok ? `deleted (verified merged into ${r.mergedInto[0]})` : `kept (${hard.err})`;
267
+ }
268
+ return `kept (${soft.err})`;
269
+ }
270
+
271
+ function removeWorktree(main, r, keepBranches) {
272
+ // Never --force: a dirty or locked worktree is a KEEP verdict and never reaches here.
273
+ const rm = git(main, ['worktree', 'remove', r.path], { allowFail: true });
274
+ if (!rm.ok) return { removed: false, detail: rm.err };
275
+ const branch = deleteBranch(main, r, keepBranches);
276
+ return { removed: true, detail: `branch ${r.branch || '(detached)'}: ${branch}` };
277
+ }
278
+
279
+ function diskFree(path) {
280
+ try {
281
+ const s = statfsSync(path);
282
+ const free = (Number(s.bavail) * Number(s.bsize)) / 1024 / 1024 / 1024;
283
+ return `${free.toFixed(1)}G free on the filesystem of ${path}`;
284
+ } catch {
285
+ return null;
286
+ }
287
+ }
288
+
289
+ function cmdList(main, flags) {
290
+ const report = inspect(main, { base: flags.base });
291
+ if (flags.json) console.log(JSON.stringify(report, null, 2));
292
+ else printTable(report);
293
+ if (flags.strict && report.worktrees.some((r) => r.verdict === 'REMOVABLE' || r.verdict === 'PRUNABLE')) process.exit(3);
294
+ }
295
+
296
+ function cmdClean(main, flags) {
297
+ const olderThan = flags['older-than'] !== undefined ? parseInt(flags['older-than'], 10) : undefined;
298
+ if (olderThan !== undefined && !Number.isFinite(olderThan)) throw new Error('--older-than needs a number of days');
299
+ const report = inspect(main, { base: flags.base, olderThan });
300
+ printTable(report);
301
+ const targets = report.worktrees.filter((r) => r.verdict === 'REMOVABLE');
302
+ const prunable = report.worktrees.filter((r) => r.verdict === 'PRUNABLE');
303
+ if (!flags.apply) {
304
+ console.log(`\nDRY-RUN — nothing removed. Re-run with --apply to remove the ${targets.length} REMOVABLE worktree(s)${prunable.length ? ` and prune ${prunable.length} stale entry(ies)` : ''}.`);
305
+ return;
306
+ }
307
+ let freed = 0;
308
+ console.log('');
309
+ for (const r of targets) {
310
+ const res = removeWorktree(main, r, flags['keep-branches']);
311
+ if (res.removed) {
312
+ freed += r.sizeKb || 0;
313
+ console.log(`removed ${r.path} (${humanKb(r.sizeKb)}; ${res.detail})`);
314
+ } else {
315
+ console.log(`FAILED ${r.path} ${res.detail}`);
316
+ }
317
+ }
318
+ const pruned = git(main, ['worktree', 'prune', '-v'], { allowFail: true });
319
+ if (pruned.out) console.log(pruned.out);
320
+ const kept = report.worktrees.filter((r) => r.verdict.startsWith('KEEP'));
321
+ console.log(`\nremoved ${targets.length} worktree(s), ~${humanKb(freed)} apparent; kept ${kept.length}${kept.length ? ': ' + kept.map((r) => `${r.name} (${r.verdict})`).join(', ') : ''}`);
322
+ const free = diskFree(dirname(main));
323
+ if (free) console.log(free);
324
+ }
325
+
326
+ function detectPrep(worktreePath) {
327
+ const has = (f) => existsSync(join(worktreePath, f));
328
+ const cmds = [];
329
+ if (has('pnpm-lock.yaml')) cmds.push('pnpm install --frozen-lockfile --prefer-offline');
330
+ else if (has('package-lock.json')) cmds.push('npm ci --prefer-offline');
331
+ else if (has('yarn.lock')) cmds.push('yarn install --frozen-lockfile');
332
+ else if (has('bun.lockb') || has('bun.lock')) cmds.push('bun install --frozen-lockfile');
333
+ if (has('uv.lock')) cmds.push('uv sync --frozen');
334
+ if (cmds.length && has('package.json')) {
335
+ try {
336
+ const pkg = JSON.parse(readFileSync(join(worktreePath, 'package.json'), 'utf8'));
337
+ if (pkg.scripts && pkg.scripts['build:packages']) {
338
+ const runner = has('pnpm-lock.yaml') ? 'pnpm' : has('yarn.lock') ? 'yarn' : has('bun.lockb') || has('bun.lock') ? 'bun run' : 'npm run';
339
+ cmds.push(`${runner} build:packages`);
340
+ }
341
+ } catch {
342
+ // unreadable package.json — skip the build step
343
+ }
344
+ }
345
+ return cmds;
346
+ }
347
+
348
+ function cmdCreate(main, args) {
349
+ const slug = args._[1];
350
+ if (!slug || !/^[A-Za-z0-9._-]+$/.test(slug)) throw new Error('create needs a <slug> matching [A-Za-z0-9._-]+');
351
+ const integration = integrationBranches(main, args.flags.base);
352
+ const base = integration[0];
353
+ const branch = args.flags.branch || `feat/${slug}`;
354
+ const path = join(dirname(main), `${basename(main)}-${slug}`);
355
+ if (existsSync(path)) throw new Error(`${path} already exists — reuse it or pick another slug`);
356
+ if (branchExists(main, branch)) {
357
+ git(main, ['worktree', 'add', path, branch]);
358
+ } else {
359
+ git(main, ['worktree', 'add', '-b', branch, path, base]);
360
+ }
361
+ console.log(`created ${path} on ${branch} (from ${base})`);
362
+ const cfg = readConfig(main);
363
+ const prep = args.flags['no-prep'] ? [] : Array.isArray(cfg.prep) ? cfg.prep : detectPrep(path);
364
+ for (const cmd of prep) {
365
+ console.log(`prep: ${cmd}`);
366
+ const r = spawnSync(cmd, { cwd: path, shell: true, stdio: 'inherit' });
367
+ if (r.status !== 0) {
368
+ console.error(`PREP FAILED (${cmd}) — the worktree exists but a delegate would run blind. Fix it before dispatching.`);
369
+ process.exit(2);
370
+ }
371
+ }
372
+ if (!prep.length) console.log('prep: none (no lockfile detected; set .dw/config.json worktree.prep to override)');
373
+ console.log(`WORKTREE=${path}`);
374
+ }
375
+
376
+ function resolveTarget(main, ref) {
377
+ if (!ref) throw new Error('merge needs a <slug|path>');
378
+ const entries = parseWorktrees(main).filter((e) => e.path !== main);
379
+ const abs = resolve(ref);
380
+ return (
381
+ entries.find((e) => e.path === abs) ||
382
+ entries.find((e) => basename(e.path) === ref) ||
383
+ entries.find((e) => basename(e.path) === `${basename(main)}-${ref}`) ||
384
+ entries.find((e) => e.branch === ref)
385
+ );
386
+ }
387
+
388
+ function cmdMerge(main, args) {
389
+ const target = resolveTarget(main, args._[1]);
390
+ if (!target) throw new Error(`no worktree matches "${args._[1]}" (try the folder name, slug, path, or branch)`);
391
+ if (!target.branch) throw new Error(`${target.path} is detached — nothing to merge`);
392
+ const integration = integrationBranches(main, args.flags.base);
393
+ const base = integration[0];
394
+ const mainBranch = git(main, ['rev-parse', '--abbrev-ref', 'HEAD']).out;
395
+ if (mainBranch !== base) {
396
+ throw new Error(`main checkout is on ${mainBranch}, not ${base}. Switch it yourself (git -C ${main} checkout ${base}) or pass --base ${mainBranch}; this tool never changes the owner's active branch.`);
397
+ }
398
+ if (git(main, ['status', '--porcelain']).out) throw new Error(`main checkout ${main} has uncommitted changes — commit or stash before merging`);
399
+ const dirty = git(target.path, ['status', '--porcelain']).out;
400
+ if (dirty) throw new Error(`${target.path} has uncommitted changes:\n${dirty}\nCommit them in the worktree first (nothing is merged or removed).`);
401
+ const procs = processesIn(target.path);
402
+ if (procs.length) throw new Error(`${target.path} is in use by: ${procs.map((p) => `${p.pid} ${p.cmd}`).join('; ')} — stop them first`);
403
+
404
+ const merge = git(main, ['merge', '--ff-only', target.branch], { allowFail: true });
405
+ if (!merge.ok) {
406
+ throw new Error(`ff-only merge of ${target.branch} into ${base} refused: ${merge.err || merge.out}\nRebase inside the worktree (git -C ${target.path} rebase ${base}), re-run the gate, then merge again. Nothing was removed.`);
407
+ }
408
+ console.log(`merged ${target.branch} into ${base} (ff-only): ${git(main, ['rev-parse', '--short', 'HEAD']).out}`);
409
+ const row = { path: target.path, branch: target.branch, head: target.head, mergedInto: [base], sizeKb: sizeKb(target.path) };
410
+ const res = removeWorktree(main, row, args.flags['keep-branches']);
411
+ if (!res.removed) throw new Error(`merged, but could not remove ${target.path}: ${res.detail}`);
412
+ git(main, ['worktree', 'prune'], { allowFail: true });
413
+ console.log(`removed ${target.path} (${humanKb(row.sizeKb)}; ${res.detail}); worktrees pruned`);
414
+ const free = diskFree(dirname(main));
415
+ if (free) console.log(free);
416
+ }
417
+
418
+ function usage() {
419
+ console.log(`usage:
420
+ worktree-gc.mjs list [--base <branch>] [--json] [--strict]
421
+ worktree-gc.mjs clean [--base <branch>] [--apply] [--older-than <days>] [--keep-branches]
422
+ worktree-gc.mjs create <slug> [--branch <name>] [--base <branch>] [--no-prep]
423
+ worktree-gc.mjs merge <slug|path> [--base <branch>] [--keep-branches]
424
+ worktree-gc.mjs prune`);
425
+ }
426
+
427
+ function main() {
428
+ const args = parseArgs(process.argv.slice(2));
429
+ const cmd = args._[0];
430
+ if (!cmd || args.flags.help) {
431
+ usage();
432
+ process.exit(cmd ? 0 : 1);
433
+ }
434
+ const root = mainCheckout(process.cwd());
435
+ switch (cmd) {
436
+ case 'list':
437
+ return cmdList(root, args.flags);
438
+ case 'clean':
439
+ return cmdClean(root, args.flags);
440
+ case 'create':
441
+ return cmdCreate(root, args);
442
+ case 'merge':
443
+ return cmdMerge(root, args);
444
+ case 'prune': {
445
+ const r = git(root, ['worktree', 'prune', '-v'], { allowFail: true });
446
+ console.log(r.out || 'nothing to prune');
447
+ return;
448
+ }
449
+ default:
450
+ usage();
451
+ process.exit(1);
452
+ }
453
+ }
454
+
455
+ try {
456
+ main();
457
+ } catch (err) {
458
+ console.error(`worktree-gc: ${err.message}`);
459
+ process.exit(1);
460
+ }
@@ -9,7 +9,7 @@
9
9
  "trigger": "API QA, endpoint testing, HTTP recipe generation",
10
10
  "expected_output": "A selected API testing recipe and evidence path for the active stack.",
11
11
  "load_policy": "lazy-references",
12
- "context_limit": 12000,
12
+ "context_limit": 8000,
13
13
  "invocation": "explicit",
14
14
  "exportable": true,
15
15
  "bundled": true
@@ -22,7 +22,7 @@
22
22
  "trigger": "Docker Compose service selection or local infrastructure setup",
23
23
  "expected_output": "A selected service recipe and composition guidance.",
24
24
  "load_policy": "lazy-references",
25
- "context_limit": 12000,
25
+ "context_limit": 8000,
26
26
  "invocation": "explicit",
27
27
  "exportable": true,
28
28
  "bundled": true
@@ -32,10 +32,10 @@
32
32
  "kind": "protocol",
33
33
  "tier": "core",
34
34
  "owner": "dw-codex-run,dw-claude-run,dw-copilot-run",
35
- "trigger": "Fire an AI CLI (Codex/Claude/Copilot) in a dedicated git worktree to implement a prepared prompt; resumable per-task session; STOP for the gate",
36
- "expected_output": "Audit-logged run with a 0-10 dual evaluation, a resumable session-id sidecar, and a STOP-for-gate Structured Return.",
35
+ "trigger": "Execute an approved task through a supported external CLI adapter",
36
+ "expected_output": "Reviewed task handoff with durable session, acceptance evidence and next task or blocker.",
37
37
  "load_policy": "lazy-references",
38
- "context_limit": 19000,
38
+ "context_limit": 8000,
39
39
  "bundled": true
40
40
  },
41
41
  {
@@ -46,7 +46,7 @@
46
46
  "trigger": "Codebase indexing or codebase intelligence query",
47
47
  "expected_output": ".dw/intel index files or a cited answer from the index.",
48
48
  "load_policy": "lazy-references",
49
- "context_limit": 12000,
49
+ "context_limit": 8000,
50
50
  "bundled": true
51
51
  },
52
52
  {
@@ -57,7 +57,7 @@
57
57
  "trigger": "Council mode or high-impact architectural debate",
58
58
  "expected_output": "Advisor positions, tension synthesis, and decision recommendation.",
59
59
  "load_policy": "lazy-references",
60
- "context_limit": 12000,
60
+ "context_limit": 8000,
61
61
  "bundled": true
62
62
  },
63
63
  {
@@ -68,7 +68,7 @@
68
68
  "trigger": "Native Grill domain vocabulary: challenge fuzzy/overloaded terms, cross-check against code, persist a canonical glossary to .dw/domain",
69
69
  "expected_output": "Canonical terms with discouraged synonyms persisted lazily to .dw/domain (single or multi-context), or fuzzy-term findings.",
70
70
  "load_policy": "lazy-references",
71
- "context_limit": 12000,
71
+ "context_limit": 8000,
72
72
  "exportable": false,
73
73
  "bundled": true
74
74
  },
@@ -80,7 +80,7 @@
80
80
  "trigger": "Native Grill decision-tree interview: sharpen a plan/PRD to shared understanding, exactly one decision per turn with a recommended answer",
81
81
  "expected_output": "A dependency-ordered decision tree, resolved decisions with recommendations, and an explicit shared-understanding gate.",
82
82
  "load_policy": "lazy-references",
83
- "context_limit": 12000,
83
+ "context_limit": 8000,
84
84
  "exportable": false,
85
85
  "bundled": true
86
86
  },
@@ -92,7 +92,7 @@
92
92
  "trigger": "Bugfix, QA fix loop, or non-reproducible failure",
93
93
  "expected_output": "Six-step triage, root cause, guard, and verification path.",
94
94
  "load_policy": "lazy-references",
95
- "context_limit": 12000,
95
+ "context_limit": 8000,
96
96
  "bundled": true
97
97
  },
98
98
  {
@@ -103,7 +103,7 @@
103
103
  "trigger": "Task execution, plan-checking, waves, checkpoint recovery",
104
104
  "expected_output": "Plan-check verdict, wave execution report, or checkpoint summary.",
105
105
  "load_policy": "lazy-references",
106
- "context_limit": 12000,
106
+ "context_limit": 8000,
107
107
  "bundled": true
108
108
  },
109
109
  {
@@ -114,7 +114,7 @@
114
114
  "trigger": "Commit creation, branch hygiene, PR generation",
115
115
  "expected_output": "Atomic commit plan and git hygiene checks.",
116
116
  "load_policy": "lazy-references",
117
- "context_limit": 12000,
117
+ "context_limit": 8000,
118
118
  "bundled": true
119
119
  },
120
120
  {
@@ -125,7 +125,7 @@
125
125
  "trigger": "Production critical bug, outage, incident, SEV workflow",
126
126
  "expected_output": "Incident triage, communication, resolution, and postmortem artifacts.",
127
127
  "load_policy": "lazy-references",
128
- "context_limit": 12000,
128
+ "context_limit": 8000,
129
129
  "bundled": true
130
130
  },
131
131
  {
@@ -136,7 +136,7 @@
136
136
  "trigger": "AI, LLM, RAG, classifier, agent, or tool-use feature",
137
137
  "expected_output": "Eval strategy, oracle choice, dataset requirements, and result checks.",
138
138
  "load_policy": "lazy-references",
139
- "context_limit": 12000,
139
+ "context_limit": 8000,
140
140
  "bundled": true
141
141
  },
142
142
  {
@@ -147,7 +147,7 @@
147
147
  "trigger": "Task execution memory, checkpointing, compaction",
148
148
  "expected_output": "Updated workflow memory and task-local memory guidance.",
149
149
  "load_policy": "lazy-references",
150
- "context_limit": 12000,
150
+ "context_limit": 8000,
151
151
  "bundled": true
152
152
  },
153
153
  {
@@ -171,7 +171,7 @@
171
171
  "trigger": "Code review, implementation review, or review consolidation",
172
172
  "expected_output": "Deduplicated findings ordered by defensible severity.",
173
173
  "load_policy": "always-small",
174
- "context_limit": 12000,
174
+ "context_limit": 8000,
175
175
  "bundled": true
176
176
  },
177
177
  {
@@ -205,7 +205,7 @@
205
205
  "trigger": "Refactor analysis, code smells, complexity, or behavior-preserving simplification",
206
206
  "expected_output": "Simplification target, risk, behavior-preserving checks, and recommendation.",
207
207
  "load_policy": "lazy-references",
208
- "context_limit": 12000,
208
+ "context_limit": 8000,
209
209
  "exportable": true,
210
210
  "bundled": true
211
211
  },
@@ -217,7 +217,7 @@
217
217
  "trigger": "Library/framework/API decision, technical research, dependency audit",
218
218
  "expected_output": "Cited source-grounded decision with version and retrieval date.",
219
219
  "load_policy": "lazy-references",
220
- "context_limit": 12000,
220
+ "context_limit": 8000,
221
221
  "bundled": true
222
222
  },
223
223
  {
@@ -228,7 +228,7 @@
228
228
  "trigger": "Writing, reviewing, or debugging tests",
229
229
  "expected_output": "Test placement, invariant, anti-pattern checks, and verification guidance.",
230
230
  "load_policy": "lazy-references",
231
- "context_limit": 12000,
231
+ "context_limit": 8000,
232
232
  "bundled": true
233
233
  },
234
234
  {
@@ -239,7 +239,7 @@
239
239
  "trigger": "UI design, redesign, visual QA, accessibility, or frontend polish",
240
240
  "expected_output": "Grounding answers, state matrix, accessibility checks, and visual risks.",
241
241
  "load_policy": "lazy-references",
242
- "context_limit": 12000,
242
+ "context_limit": 8000,
243
243
  "bundled": true
244
244
  },
245
245
  {
@@ -250,7 +250,7 @@
250
250
  "trigger": "Completion claim, commit, PR generation, or fixed bug status",
251
251
  "expected_output": "Fresh verification report with cited command evidence.",
252
252
  "load_policy": "always-small",
253
- "context_limit": 12000,
253
+ "context_limit": 8000,
254
254
  "exportable": true,
255
255
  "bundled": true
256
256
  },
@@ -288,7 +288,7 @@
288
288
  "trigger": "Security review, sensitive code, OWASP checks, auth/payment/input surfaces",
289
289
  "expected_output": "High-confidence security findings or explicit pass.",
290
290
  "load_policy": "lazy-references",
291
- "context_limit": 14000,
291
+ "context_limit": 8000,
292
292
  "exportable": true,
293
293
  "bundled": true
294
294
  },
@@ -300,7 +300,7 @@
300
300
  "trigger": "React/Next.js performance, hydration, data fetching, bundle or render issues",
301
301
  "expected_output": "Selected performance rule references and metric-driven recommendations.",
302
302
  "load_policy": "lazy-references",
303
- "context_limit": 12000,
303
+ "context_limit": 8000,
304
304
  "invocation": "explicit",
305
305
  "exportable": true,
306
306
  "bundled": true