@undeemed/get-shit-done-codex 1.24.1 β†’ 1.24.2

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.
package/README.md CHANGED
@@ -268,9 +268,9 @@ All agents use **gpt-5.3-codex** with role-based thinking levels:
268
268
 
269
269
  | Profile | Planner/Debugger | Executor/Verifier | Researchers/Mapper |
270
270
  | ---------- | ---------------- | ----------------- | ------------------ |
271
- | `quality` | 🟒 high | 🟑 medium | 🟑 medium/πŸ”΅ low |
272
- | `balanced` | 🟒 high | 🟑 medium | πŸ”΅ low |
273
- | `budget` | 🟑 medium | πŸ”΅ low | πŸ”΅ low |
271
+ | `quality` | πŸ”΄ xhigh | 🟒 high | 🟒 high/🟑 medium |
272
+ | `balanced` | πŸ”΄ xhigh | 🟒 high | 🟑 medium |
273
+ | `budget` | 🟒 high | 🟑 medium | 🟑 medium |
274
274
 
275
275
  Switch profiles: `$gsd-set-profile budget`
276
276
 
@@ -311,6 +311,13 @@ npx @undeemed/get-shit-done-codex@latest
311
311
  - In-Codex update checks are available via `$gsd-update`.
312
312
  - For release notifications outside the CLI, enable GitHub release watching on this repo.
313
313
 
314
+ **`gsd-tools` scripting tips**
315
+
316
+ - Use `--cwd <path>` (or `--cwd=<path>`) to target a specific project directory from automation scripts.
317
+ - Use `state json` for machine-readable state; use `state-snapshot` for structured markdown-field extraction.
318
+ - Use `requirements mark-complete REQ-01 REQ-02` to update both requirement checkboxes and traceability rows.
319
+ - If `commit` appears to skip unexpectedly, check `.planning/config.json` for `commit_docs: false` and also check user defaults in `~/.gsd/defaults.json`.
320
+
314
321
  ## More Documentation
315
322
 
316
323
  For deeper guides, detailed workflows, and comprehensive documentation, see the [original get-shit-done README](https://github.com/taches/get-shit-done/blob/main/README.md).
@@ -122,25 +122,35 @@
122
122
  const fs = require('fs');
123
123
  const path = require('path');
124
124
  const { execSync } = require('child_process');
125
+ const stateLib = require('./lib/state.cjs');
126
+ const phaseLib = require('./lib/phase.cjs');
127
+ const milestoneLib = require('./lib/milestone.cjs');
128
+ const initLib = require('./lib/init.cjs');
129
+ const commandsLib = require('./lib/commands.cjs');
130
+ const configLib = require('./lib/config.cjs');
131
+ const roadmapLib = require('./lib/roadmap.cjs');
132
+ const templateLib = require('./lib/template.cjs');
133
+ const frontmatterLib = require('./lib/frontmatter.cjs');
134
+ const verifyLib = require('./lib/verify.cjs');
125
135
 
126
136
  // ─── Model Profile Table ─────────────────────────────────────────────────────
127
137
 
128
138
  const MODEL_PROFILES = {
129
- // quality balanced budget
130
- 'gsd-planner': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
131
- 'gsd-roadmapper': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
132
- 'gsd-executor': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
133
- 'gsd-phase-researcher': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'low' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
134
- 'gsd-project-researcher': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'low' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
135
- 'gsd-research-synthesizer': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'low' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
136
- 'gsd-debugger': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
137
- 'gsd-codebase-mapper': { quality: { m: 'gpt-5.3-codex', t: 'low' }, balanced: { m: 'gpt-5.3-codex', t: 'low' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
138
- 'gsd-verifier': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
139
- 'gsd-plan-checker': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'low' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
140
- 'gsd-integration-checker': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'low' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
139
+ // quality balanced budget
140
+ 'gsd-planner': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'xhigh' }, budget: { m: 'gpt-5.3-codex', t: 'high' } },
141
+ 'gsd-roadmapper': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
142
+ 'gsd-executor': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
143
+ 'gsd-phase-researcher': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
144
+ 'gsd-project-researcher': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
145
+ 'gsd-research-synthesizer': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
146
+ 'gsd-debugger': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'xhigh' }, budget: { m: 'gpt-5.3-codex', t: 'high' } },
147
+ 'gsd-codebase-mapper': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
148
+ 'gsd-verifier': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
149
+ 'gsd-plan-checker': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
150
+ 'gsd-integration-checker': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
141
151
  };
142
152
 
143
- const DEFAULT_ENTRY = { m: 'gpt-5.3-codex', t: 'medium' };
153
+ const DEFAULT_ENTRY = { m: 'gpt-5.3-codex', t: 'high' };
144
154
 
145
155
  // ─── Helpers ──────────────────────────────────────────────────────────────────
146
156
 
@@ -247,12 +257,80 @@ function execGit(cwd, args) {
247
257
  }
248
258
 
249
259
  function normalizePhaseName(phase) {
250
- const match = phase.match(/^(\d+(?:\.\d+)?)/);
260
+ const match = String(phase).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
251
261
  if (!match) return phase;
252
- const num = match[1];
253
- const parts = num.split('.');
254
- const padded = parts[0].padStart(2, '0');
255
- return parts.length > 1 ? `${padded}.${parts[1]}` : padded;
262
+ const padded = match[1].padStart(2, '0');
263
+ const letter = match[2] ? match[2].toUpperCase() : '';
264
+ const decimal = match[3] || '';
265
+ return padded + letter + decimal;
266
+ }
267
+
268
+ function escapeRegex(value) {
269
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
270
+ }
271
+
272
+ function comparePhaseNum(a, b) {
273
+ const pa = String(a).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
274
+ const pb = String(b).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
275
+ if (!pa || !pb) return String(a).localeCompare(String(b));
276
+
277
+ const intDiff = parseInt(pa[1], 10) - parseInt(pb[1], 10);
278
+ if (intDiff !== 0) return intDiff;
279
+
280
+ const la = (pa[2] || '').toUpperCase();
281
+ const lb = (pb[2] || '').toUpperCase();
282
+ if (la !== lb) {
283
+ if (!la) return -1;
284
+ if (!lb) return 1;
285
+ return la < lb ? -1 : 1;
286
+ }
287
+
288
+ const aDecParts = pa[3] ? pa[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
289
+ const bDecParts = pb[3] ? pb[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
290
+ const maxLen = Math.max(aDecParts.length, bDecParts.length);
291
+
292
+ if (aDecParts.length === 0 && bDecParts.length > 0) return -1;
293
+ if (bDecParts.length === 0 && aDecParts.length > 0) return 1;
294
+
295
+ for (let i = 0; i < maxLen; i++) {
296
+ const av = Number.isFinite(aDecParts[i]) ? aDecParts[i] : 0;
297
+ const bv = Number.isFinite(bDecParts[i]) ? bDecParts[i] : 0;
298
+ if (av !== bv) return av - bv;
299
+ }
300
+
301
+ return 0;
302
+ }
303
+
304
+ function getMilestonePhaseFilter(cwd) {
305
+ const milestonePhaseNums = new Set();
306
+
307
+ try {
308
+ const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
309
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
310
+ let m;
311
+ while ((m = phasePattern.exec(roadmap)) !== null) {
312
+ milestonePhaseNums.add(m[1]);
313
+ }
314
+ } catch {}
315
+
316
+ if (milestonePhaseNums.size === 0) {
317
+ const passAll = () => true;
318
+ passAll.phaseCount = 0;
319
+ return passAll;
320
+ }
321
+
322
+ const normalized = new Set(
323
+ [...milestonePhaseNums].map(n => (n.replace(/^0+/, '') || '0').toLowerCase())
324
+ );
325
+
326
+ function isDirInMilestone(dirName) {
327
+ const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
328
+ if (!m) return false;
329
+ return normalized.has(m[1].toLowerCase());
330
+ }
331
+
332
+ isDirInMilestone.phaseCount = milestonePhaseNums.size;
333
+ return isDirInMilestone;
256
334
  }
257
335
 
258
336
  function extractFrontmatter(content) {
@@ -859,12 +937,8 @@ function cmdPhasesList(cwd, options, raw) {
859
937
  }
860
938
  }
861
939
 
862
- // Sort numerically (handles decimals: 01, 02, 02.1, 02.2, 03)
863
- dirs.sort((a, b) => {
864
- const aNum = parseFloat(a.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
865
- const bNum = parseFloat(b.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
866
- return aNum - bNum;
867
- });
940
+ // Sort by canonical phase ordering (integers, decimals, letter suffixes)
941
+ dirs.sort((a, b) => comparePhaseNum(a, b));
868
942
 
869
943
  // If filtering by phase number
870
944
  if (phase) {
@@ -899,7 +973,7 @@ function cmdPhasesList(cwd, options, raw) {
899
973
  const result = {
900
974
  files,
901
975
  count: files.length,
902
- phase_dir: phase ? dirs[0].replace(/^\d+(?:\.\d+)?-?/, '') : null,
976
+ phase_dir: phase ? dirs[0].replace(/^\d+[A-Za-z]?(?:\.\d+)*-?/, '') : null,
903
977
  };
904
978
  output(result, raw, files.join('\n'));
905
979
  return;
@@ -1863,7 +1937,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
1863
1937
  let phaseDirName = null;
1864
1938
  try {
1865
1939
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
1866
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
1940
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
1867
1941
  const match = dirs.find(d => d.startsWith(normalized));
1868
1942
  if (match) {
1869
1943
  phaseDir = path.join(phasesDir, match);
@@ -1899,9 +1973,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
1899
1973
  const content = fs.readFileSync(planPath, 'utf-8');
1900
1974
  const fm = extractFrontmatter(content);
1901
1975
 
1902
- // Count tasks (## Task N patterns)
1903
- const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
1904
- const taskCount = taskMatches.length;
1976
+ // Count tasks from canonical XML first, fallback to markdown task headings.
1977
+ const xmlTaskMatches = content.match(/<task\b[^>]*>/gi) || [];
1978
+ const markdownTaskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
1979
+ const taskCount = xmlTaskMatches.length > 0 ? xmlTaskMatches.length : markdownTaskMatches.length;
1905
1980
 
1906
1981
  // Parse wave as integer
1907
1982
  const wave = parseInt(fm.wave, 10) || 1;
@@ -1916,10 +1991,24 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
1916
1991
  hasCheckpoints = true;
1917
1992
  }
1918
1993
 
1919
- // Parse files-modified
1994
+ // Parse files-modified/files_modified (both accepted).
1920
1995
  let filesModified = [];
1921
- if (fm['files-modified']) {
1922
- filesModified = Array.isArray(fm['files-modified']) ? fm['files-modified'] : [fm['files-modified']];
1996
+ const filesModifiedField = fm.files_modified ?? fm['files-modified'];
1997
+ if (filesModifiedField) {
1998
+ filesModified = Array.isArray(filesModifiedField) ? filesModifiedField : [filesModifiedField];
1999
+ }
2000
+
2001
+ // Objective: canonical <objective> block first line overrides frontmatter objective.
2002
+ let objective = fm.objective || null;
2003
+ const objectiveTagMatch = content.match(/<objective>\s*([\s\S]*?)<\/objective>/i);
2004
+ if (objectiveTagMatch) {
2005
+ const firstLine = objectiveTagMatch[1]
2006
+ .split('\n')
2007
+ .map(line => line.trim())
2008
+ .find(Boolean);
2009
+ if (firstLine) {
2010
+ objective = firstLine;
2011
+ }
1923
2012
  }
1924
2013
 
1925
2014
  const hasSummary = completedPlanIds.has(planId);
@@ -1931,7 +2020,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
1931
2020
  id: planId,
1932
2021
  wave,
1933
2022
  autonomous,
1934
- objective: fm.objective || null,
2023
+ objective,
1935
2024
  files_modified: filesModified,
1936
2025
  task_count: taskCount,
1937
2026
  has_summary: hasSummary,
@@ -3158,16 +3247,16 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3158
3247
  // Update ROADMAP.md: mark phase complete
3159
3248
  if (fs.existsSync(roadmapPath)) {
3160
3249
  let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
3250
+ const phaseEscaped = escapeRegex(phaseNum);
3161
3251
 
3162
3252
  // Checkbox: - [ ] Phase N: β†’ - [x] Phase N: (...completed DATE)
3163
3253
  const checkboxPattern = new RegExp(
3164
- `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseNum.replace('.', '\\.')}[:\\s][^\\n]*)`,
3254
+ `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
3165
3255
  'i'
3166
3256
  );
3167
3257
  roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
3168
3258
 
3169
3259
  // Progress table: update Status to Complete, add date
3170
- const phaseEscaped = phaseNum.replace('.', '\\.');
3171
3260
  const tablePattern = new RegExp(
3172
3261
  `(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
3173
3262
  'i'
@@ -3192,24 +3281,26 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3192
3281
  // Update REQUIREMENTS.md traceability for this phase's requirements
3193
3282
  const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
3194
3283
  if (fs.existsSync(reqPath)) {
3195
- // Extract Requirements line from roadmap for this phase
3196
- const reqMatch = roadmapContent.match(
3197
- new RegExp(`Phase\\s+${phaseNum.replace('.', '\\.')}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
3284
+ // Extract only this phase's section to avoid leaking into later phases.
3285
+ const sectionMatch = roadmapContent.match(
3286
+ new RegExp(`#{2,4}\\s*Phase\\s+${phaseEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s*Phase\\s+\\d|$)`, 'i')
3198
3287
  );
3199
3288
 
3200
- if (reqMatch) {
3201
- const reqIds = reqMatch[1].split(/[,\s]+/).map(r => r.trim()).filter(Boolean);
3289
+ if (sectionMatch) {
3290
+ const reqMatch = sectionMatch[0].match(/\*\*Requirements:\*\*\s*([^\n]+)/i);
3291
+ const reqIds = reqMatch ? (reqMatch[1].match(/\b[A-Z][A-Z0-9_-]*-\d+\b/g) || []) : [];
3202
3292
  let reqContent = fs.readFileSync(reqPath, 'utf-8');
3203
3293
 
3204
3294
  for (const reqId of reqIds) {
3295
+ const escapedReqId = escapeRegex(reqId);
3205
3296
  // Update checkbox: - [ ] **REQ-ID** β†’ - [x] **REQ-ID**
3206
3297
  reqContent = reqContent.replace(
3207
- new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqId}\\*\\*)`, 'gi'),
3298
+ new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${escapedReqId}\\*\\*)`, 'gi'),
3208
3299
  '$1x$2'
3209
3300
  );
3210
3301
  // Update traceability table: | REQ-ID | Phase N | Pending | β†’ | REQ-ID | Phase N | Complete |
3211
3302
  reqContent = reqContent.replace(
3212
- new RegExp(`(\\|\\s*${reqId}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
3303
+ new RegExp(`(\\|\\s*${escapedReqId}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
3213
3304
  '$1 Complete $2'
3214
3305
  );
3215
3306
  }
@@ -3225,16 +3316,20 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
3225
3316
  let isLastPhase = true;
3226
3317
 
3227
3318
  try {
3319
+ const isMilestonePhase = getMilestonePhaseFilter(cwd);
3228
3320
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
3229
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
3230
- const currentFloat = parseFloat(phaseNum);
3321
+ const dirs = entries
3322
+ .filter(e => e.isDirectory())
3323
+ .map(e => e.name)
3324
+ .filter(d => isMilestonePhase(d))
3325
+ .sort((a, b) => comparePhaseNum(a, b));
3326
+ const currentPhase = normalizePhaseName(phaseNum);
3231
3327
 
3232
3328
  // Find the next phase directory after current
3233
3329
  for (const dir of dirs) {
3234
- const dm = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/);
3330
+ const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
3235
3331
  if (dm) {
3236
- const dirFloat = parseFloat(dm[1]);
3237
- if (dirFloat > currentFloat) {
3332
+ if (comparePhaseNum(dm[1], currentPhase) > 0) {
3238
3333
  nextPhaseNum = dm[1];
3239
3334
  nextPhaseName = dm[2] || null;
3240
3335
  isLastPhase = false;
@@ -3328,10 +3423,15 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
3328
3423
  let totalPlans = 0;
3329
3424
  let totalTasks = 0;
3330
3425
  const accomplishments = [];
3426
+ const isMilestonePhase = getMilestonePhaseFilter(cwd);
3331
3427
 
3332
3428
  try {
3333
3429
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
3334
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
3430
+ const dirs = entries
3431
+ .filter(e => e.isDirectory())
3432
+ .map(e => e.name)
3433
+ .filter(d => isMilestonePhase(d))
3434
+ .sort((a, b) => comparePhaseNum(a, b));
3335
3435
 
3336
3436
  for (const dir of dirs) {
3337
3437
  phaseCount++;
@@ -3375,13 +3475,16 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
3375
3475
  fs.renameSync(auditFile, path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`));
3376
3476
  }
3377
3477
 
3378
- // Create/append MILESTONES.md entry
3478
+ // Create/prepend MILESTONES.md entry (reverse chronological order)
3379
3479
  const accomplishmentsList = accomplishments.map(a => `- ${a}`).join('\n');
3380
3480
  const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n---\n\n`;
3381
3481
 
3382
3482
  if (fs.existsSync(milestonesPath)) {
3383
3483
  const existing = fs.readFileSync(milestonesPath, 'utf-8');
3384
- fs.writeFileSync(milestonesPath, existing + '\n' + milestoneEntry, 'utf-8');
3484
+ const existingBody = existing
3485
+ .replace(/^#\s*Milestones[^\n]*\n*/i, '')
3486
+ .trimStart();
3487
+ fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}${existingBody}`, 'utf-8');
3385
3488
  } else {
3386
3489
  fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
3387
3490
  }
@@ -3412,7 +3515,11 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
3412
3515
  fs.mkdirSync(phaseArchiveDir, { recursive: true });
3413
3516
 
3414
3517
  const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
3415
- const phaseDirNames = phaseEntries.filter(e => e.isDirectory()).map(e => e.name);
3518
+ const phaseDirNames = phaseEntries
3519
+ .filter(e => e.isDirectory())
3520
+ .map(e => e.name)
3521
+ .filter(d => isMilestonePhase(d))
3522
+ .sort((a, b) => comparePhaseNum(a, b));
3416
3523
  for (const dir of phaseDirNames) {
3417
3524
  fs.renameSync(path.join(phasesDir, dir), path.join(phaseArchiveDir, dir));
3418
3525
  }
@@ -3977,15 +4084,15 @@ function resolveModelInternal(cwd, agentType) {
3977
4084
  if (override) {
3978
4085
  // Override can be a string (legacy) or { m, t } object
3979
4086
  if (typeof override === 'string') {
3980
- return { model: 'inherit', thinking: override === 'high' || override === 'medium' || override === 'low' ? override : 'medium' };
4087
+ return { model: 'inherit', thinking: override === 'xhigh' || override === 'high' || override === 'medium' || override === 'low' ? override : 'high' };
3981
4088
  }
3982
- return { model: 'inherit', thinking: override.t || 'medium' };
4089
+ return { model: 'inherit', thinking: override.t || 'high' };
3983
4090
  }
3984
4091
 
3985
4092
  // Fall back to profile lookup
3986
4093
  const profile = config.model_profile || 'balanced';
3987
4094
  const agentModels = MODEL_PROFILES[agentType];
3988
- if (!agentModels) return { model: 'inherit', thinking: 'medium' };
4095
+ if (!agentModels) return { model: 'inherit', thinking: 'high' };
3989
4096
  const entry = agentModels[profile] || agentModels['balanced'] || DEFAULT_ENTRY;
3990
4097
  return { model: 'inherit', thinking: entry.t };
3991
4098
  }
@@ -4844,12 +4951,54 @@ function cmdInitProgress(cwd, includes, raw) {
4844
4951
 
4845
4952
  async function main() {
4846
4953
  const args = process.argv.slice(2);
4847
- const rawIndex = args.indexOf('--raw');
4848
- const raw = rawIndex !== -1;
4849
- if (rawIndex !== -1) args.splice(rawIndex, 1);
4954
+ let raw = false;
4955
+ let cwd = process.cwd();
4956
+
4957
+ const setCwd = (value) => {
4958
+ if (!value) {
4959
+ error('Missing value for --cwd');
4960
+ }
4961
+ const resolved = path.resolve(process.cwd(), value);
4962
+ try {
4963
+ const stat = fs.statSync(resolved);
4964
+ if (!stat.isDirectory()) {
4965
+ throw new Error('not-a-dir');
4966
+ }
4967
+ } catch {
4968
+ error(`Invalid --cwd: ${value}`);
4969
+ }
4970
+ cwd = resolved;
4971
+ };
4972
+
4973
+ for (let i = 0; i < args.length;) {
4974
+ const arg = args[i];
4975
+ if (arg === '--raw') {
4976
+ raw = true;
4977
+ args.splice(i, 1);
4978
+ continue;
4979
+ }
4980
+ if (arg === '--cwd') {
4981
+ const value = args[i + 1];
4982
+ if (!value || value.startsWith('--')) {
4983
+ error('Missing value for --cwd');
4984
+ }
4985
+ setCwd(value);
4986
+ args.splice(i, 2);
4987
+ continue;
4988
+ }
4989
+ if (arg.startsWith('--cwd=')) {
4990
+ const value = arg.slice('--cwd='.length);
4991
+ if (!value) {
4992
+ error('Missing value for --cwd');
4993
+ }
4994
+ setCwd(value);
4995
+ args.splice(i, 1);
4996
+ continue;
4997
+ }
4998
+ i++;
4999
+ }
4850
5000
 
4851
5001
  const command = args[0];
4852
- const cwd = process.cwd();
4853
5002
 
4854
5003
  if (!command) {
4855
5004
  error('Usage: gsd-tools <command> [args] [--raw]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init');
@@ -4859,9 +5008,9 @@ async function main() {
4859
5008
  case 'state': {
4860
5009
  const subcommand = args[1];
4861
5010
  if (subcommand === 'update') {
4862
- cmdStateUpdate(cwd, args[2], args[3]);
5011
+ stateLib.cmdStateUpdate(cwd, args[2], args[3]);
4863
5012
  } else if (subcommand === 'get') {
4864
- cmdStateGet(cwd, args[2], raw);
5013
+ stateLib.cmdStateGet(cwd, args[2], raw);
4865
5014
  } else if (subcommand === 'patch') {
4866
5015
  const patches = {};
4867
5016
  for (let i = 2; i < args.length; i += 2) {
@@ -4871,16 +5020,18 @@ async function main() {
4871
5020
  patches[key] = value;
4872
5021
  }
4873
5022
  }
4874
- cmdStatePatch(cwd, patches, raw);
5023
+ stateLib.cmdStatePatch(cwd, patches, raw);
5024
+ } else if (subcommand === 'json') {
5025
+ stateLib.cmdStateJson(cwd, raw);
4875
5026
  } else if (subcommand === 'advance-plan') {
4876
- cmdStateAdvancePlan(cwd, raw);
5027
+ stateLib.cmdStateAdvancePlan(cwd, raw);
4877
5028
  } else if (subcommand === 'record-metric') {
4878
5029
  const phaseIdx = args.indexOf('--phase');
4879
5030
  const planIdx = args.indexOf('--plan');
4880
5031
  const durationIdx = args.indexOf('--duration');
4881
5032
  const tasksIdx = args.indexOf('--tasks');
4882
5033
  const filesIdx = args.indexOf('--files');
4883
- cmdStateRecordMetric(cwd, {
5034
+ stateLib.cmdStateRecordMetric(cwd, {
4884
5035
  phase: phaseIdx !== -1 ? args[phaseIdx + 1] : null,
4885
5036
  plan: planIdx !== -1 ? args[planIdx + 1] : null,
4886
5037
  duration: durationIdx !== -1 ? args[durationIdx + 1] : null,
@@ -4888,42 +5039,50 @@ async function main() {
4888
5039
  files: filesIdx !== -1 ? args[filesIdx + 1] : null,
4889
5040
  }, raw);
4890
5041
  } else if (subcommand === 'update-progress') {
4891
- cmdStateUpdateProgress(cwd, raw);
5042
+ stateLib.cmdStateUpdateProgress(cwd, raw);
4892
5043
  } else if (subcommand === 'add-decision') {
4893
5044
  const phaseIdx = args.indexOf('--phase');
4894
5045
  const summaryIdx = args.indexOf('--summary');
5046
+ const summaryFileIdx = args.indexOf('--summary-file');
4895
5047
  const rationaleIdx = args.indexOf('--rationale');
4896
- cmdStateAddDecision(cwd, {
5048
+ const rationaleFileIdx = args.indexOf('--rationale-file');
5049
+ stateLib.cmdStateAddDecision(cwd, {
4897
5050
  phase: phaseIdx !== -1 ? args[phaseIdx + 1] : null,
4898
5051
  summary: summaryIdx !== -1 ? args[summaryIdx + 1] : null,
5052
+ summary_file: summaryFileIdx !== -1 ? args[summaryFileIdx + 1] : null,
4899
5053
  rationale: rationaleIdx !== -1 ? args[rationaleIdx + 1] : '',
5054
+ rationale_file: rationaleFileIdx !== -1 ? args[rationaleFileIdx + 1] : null,
4900
5055
  }, raw);
4901
5056
  } else if (subcommand === 'add-blocker') {
4902
5057
  const textIdx = args.indexOf('--text');
4903
- cmdStateAddBlocker(cwd, textIdx !== -1 ? args[textIdx + 1] : null, raw);
5058
+ const textFileIdx = args.indexOf('--text-file');
5059
+ stateLib.cmdStateAddBlocker(cwd, {
5060
+ text: textIdx !== -1 ? args[textIdx + 1] : null,
5061
+ text_file: textFileIdx !== -1 ? args[textFileIdx + 1] : null,
5062
+ }, raw);
4904
5063
  } else if (subcommand === 'resolve-blocker') {
4905
5064
  const textIdx = args.indexOf('--text');
4906
- cmdStateResolveBlocker(cwd, textIdx !== -1 ? args[textIdx + 1] : null, raw);
5065
+ stateLib.cmdStateResolveBlocker(cwd, textIdx !== -1 ? args[textIdx + 1] : null, raw);
4907
5066
  } else if (subcommand === 'record-session') {
4908
5067
  const stoppedIdx = args.indexOf('--stopped-at');
4909
5068
  const resumeIdx = args.indexOf('--resume-file');
4910
- cmdStateRecordSession(cwd, {
5069
+ stateLib.cmdStateRecordSession(cwd, {
4911
5070
  stopped_at: stoppedIdx !== -1 ? args[stoppedIdx + 1] : null,
4912
5071
  resume_file: resumeIdx !== -1 ? args[resumeIdx + 1] : 'None',
4913
5072
  }, raw);
4914
5073
  } else {
4915
- cmdStateLoad(cwd, raw);
5074
+ stateLib.cmdStateLoad(cwd, raw);
4916
5075
  }
4917
5076
  break;
4918
5077
  }
4919
5078
 
4920
5079
  case 'resolve-model': {
4921
- cmdResolveModel(cwd, args[1], raw);
5080
+ commandsLib.cmdResolveModel(cwd, args[1], raw);
4922
5081
  break;
4923
5082
  }
4924
5083
 
4925
5084
  case 'find-phase': {
4926
- cmdFindPhase(cwd, args[1], raw);
5085
+ phaseLib.cmdFindPhase(cwd, args[1], raw);
4927
5086
  break;
4928
5087
  }
4929
5088
 
@@ -4933,7 +5092,7 @@ async function main() {
4933
5092
  // Parse --files flag (collect args after --files, stopping at other flags)
4934
5093
  const filesIndex = args.indexOf('--files');
4935
5094
  const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
4936
- cmdCommit(cwd, message, files, raw, amend);
5095
+ commandsLib.cmdCommit(cwd, message, files, raw, amend);
4937
5096
  break;
4938
5097
  }
4939
5098
 
@@ -4941,14 +5100,14 @@ async function main() {
4941
5100
  const summaryPath = args[1];
4942
5101
  const countIndex = args.indexOf('--check-count');
4943
5102
  const checkCount = countIndex !== -1 ? parseInt(args[countIndex + 1], 10) : 2;
4944
- cmdVerifySummary(cwd, summaryPath, checkCount, raw);
5103
+ verifyLib.cmdVerifySummary(cwd, summaryPath, checkCount, raw);
4945
5104
  break;
4946
5105
  }
4947
5106
 
4948
5107
  case 'template': {
4949
5108
  const subcommand = args[1];
4950
5109
  if (subcommand === 'select') {
4951
- cmdTemplateSelect(cwd, args[2], raw);
5110
+ templateLib.cmdTemplateSelect(cwd, args[2], raw);
4952
5111
  } else if (subcommand === 'fill') {
4953
5112
  const templateType = args[2];
4954
5113
  const phaseIdx = args.indexOf('--phase');
@@ -4957,7 +5116,7 @@ async function main() {
4957
5116
  const typeIdx = args.indexOf('--type');
4958
5117
  const waveIdx = args.indexOf('--wave');
4959
5118
  const fieldsIdx = args.indexOf('--fields');
4960
- cmdTemplateFill(cwd, templateType, {
5119
+ templateLib.cmdTemplateFill(cwd, templateType, {
4961
5120
  phase: phaseIdx !== -1 ? args[phaseIdx + 1] : null,
4962
5121
  plan: planIdx !== -1 ? args[planIdx + 1] : null,
4963
5122
  name: nameIdx !== -1 ? args[nameIdx + 1] : null,
@@ -4976,17 +5135,17 @@ async function main() {
4976
5135
  const file = args[2];
4977
5136
  if (subcommand === 'get') {
4978
5137
  const fieldIdx = args.indexOf('--field');
4979
- cmdFrontmatterGet(cwd, file, fieldIdx !== -1 ? args[fieldIdx + 1] : null, raw);
5138
+ frontmatterLib.cmdFrontmatterGet(cwd, file, fieldIdx !== -1 ? args[fieldIdx + 1] : null, raw);
4980
5139
  } else if (subcommand === 'set') {
4981
5140
  const fieldIdx = args.indexOf('--field');
4982
5141
  const valueIdx = args.indexOf('--value');
4983
- cmdFrontmatterSet(cwd, file, fieldIdx !== -1 ? args[fieldIdx + 1] : null, valueIdx !== -1 ? args[valueIdx + 1] : undefined, raw);
5142
+ frontmatterLib.cmdFrontmatterSet(cwd, file, fieldIdx !== -1 ? args[fieldIdx + 1] : null, valueIdx !== -1 ? args[valueIdx + 1] : undefined, raw);
4984
5143
  } else if (subcommand === 'merge') {
4985
5144
  const dataIdx = args.indexOf('--data');
4986
- cmdFrontmatterMerge(cwd, file, dataIdx !== -1 ? args[dataIdx + 1] : null, raw);
5145
+ frontmatterLib.cmdFrontmatterMerge(cwd, file, dataIdx !== -1 ? args[dataIdx + 1] : null, raw);
4987
5146
  } else if (subcommand === 'validate') {
4988
5147
  const schemaIdx = args.indexOf('--schema');
4989
- cmdFrontmatterValidate(cwd, file, schemaIdx !== -1 ? args[schemaIdx + 1] : null, raw);
5148
+ frontmatterLib.cmdFrontmatterValidate(cwd, file, schemaIdx !== -1 ? args[schemaIdx + 1] : null, raw);
4990
5149
  } else {
4991
5150
  error('Unknown frontmatter subcommand. Available: get, set, merge, validate');
4992
5151
  }
@@ -4996,17 +5155,17 @@ async function main() {
4996
5155
  case 'verify': {
4997
5156
  const subcommand = args[1];
4998
5157
  if (subcommand === 'plan-structure') {
4999
- cmdVerifyPlanStructure(cwd, args[2], raw);
5158
+ verifyLib.cmdVerifyPlanStructure(cwd, args[2], raw);
5000
5159
  } else if (subcommand === 'phase-completeness') {
5001
- cmdVerifyPhaseCompleteness(cwd, args[2], raw);
5160
+ verifyLib.cmdVerifyPhaseCompleteness(cwd, args[2], raw);
5002
5161
  } else if (subcommand === 'references') {
5003
- cmdVerifyReferences(cwd, args[2], raw);
5162
+ verifyLib.cmdVerifyReferences(cwd, args[2], raw);
5004
5163
  } else if (subcommand === 'commits') {
5005
- cmdVerifyCommits(cwd, args.slice(2), raw);
5164
+ verifyLib.cmdVerifyCommits(cwd, args.slice(2), raw);
5006
5165
  } else if (subcommand === 'artifacts') {
5007
- cmdVerifyArtifacts(cwd, args[2], raw);
5166
+ verifyLib.cmdVerifyArtifacts(cwd, args[2], raw);
5008
5167
  } else if (subcommand === 'key-links') {
5009
- cmdVerifyKeyLinks(cwd, args[2], raw);
5168
+ verifyLib.cmdVerifyKeyLinks(cwd, args[2], raw);
5010
5169
  } else {
5011
5170
  error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links');
5012
5171
  }
@@ -5014,42 +5173,42 @@ async function main() {
5014
5173
  }
5015
5174
 
5016
5175
  case 'generate-slug': {
5017
- cmdGenerateSlug(args[1], raw);
5176
+ commandsLib.cmdGenerateSlug(args[1], raw);
5018
5177
  break;
5019
5178
  }
5020
5179
 
5021
5180
  case 'current-timestamp': {
5022
- cmdCurrentTimestamp(args[1] || 'full', raw);
5181
+ commandsLib.cmdCurrentTimestamp(args[1] || 'full', raw);
5023
5182
  break;
5024
5183
  }
5025
5184
 
5026
5185
  case 'list-todos': {
5027
- cmdListTodos(cwd, args[1], raw);
5186
+ commandsLib.cmdListTodos(cwd, args[1], raw);
5028
5187
  break;
5029
5188
  }
5030
5189
 
5031
5190
  case 'verify-path-exists': {
5032
- cmdVerifyPathExists(cwd, args[1], raw);
5191
+ commandsLib.cmdVerifyPathExists(cwd, args[1], raw);
5033
5192
  break;
5034
5193
  }
5035
5194
 
5036
5195
  case 'config-ensure-section': {
5037
- cmdConfigEnsureSection(cwd, raw);
5196
+ configLib.cmdConfigEnsureSection(cwd, raw);
5038
5197
  break;
5039
5198
  }
5040
5199
 
5041
5200
  case 'config-set': {
5042
- cmdConfigSet(cwd, args[1], args[2], raw);
5201
+ configLib.cmdConfigSet(cwd, args[1], args[2], raw);
5043
5202
  break;
5044
5203
  }
5045
5204
 
5046
5205
  case 'config-get': {
5047
- cmdConfigGet(cwd, args[1], raw);
5206
+ configLib.cmdConfigGet(cwd, args[1], raw);
5048
5207
  break;
5049
5208
  }
5050
5209
 
5051
5210
  case 'history-digest': {
5052
- cmdHistoryDigest(cwd, raw);
5211
+ commandsLib.cmdHistoryDigest(cwd, raw);
5053
5212
  break;
5054
5213
  }
5055
5214
 
@@ -5063,7 +5222,7 @@ async function main() {
5063
5222
  phase: phaseIndex !== -1 ? args[phaseIndex + 1] : null,
5064
5223
  includeArchived: args.includes('--include-archived'),
5065
5224
  };
5066
- cmdPhasesList(cwd, options, raw);
5225
+ phaseLib.cmdPhasesList(cwd, options, raw);
5067
5226
  } else {
5068
5227
  error('Unknown phases subcommand. Available: list');
5069
5228
  }
@@ -5073,11 +5232,11 @@ async function main() {
5073
5232
  case 'roadmap': {
5074
5233
  const subcommand = args[1];
5075
5234
  if (subcommand === 'get-phase') {
5076
- cmdRoadmapGetPhase(cwd, args[2], raw);
5235
+ roadmapLib.cmdRoadmapGetPhase(cwd, args[2], raw);
5077
5236
  } else if (subcommand === 'analyze') {
5078
- cmdRoadmapAnalyze(cwd, raw);
5237
+ roadmapLib.cmdRoadmapAnalyze(cwd, raw);
5079
5238
  } else if (subcommand === 'update-plan-progress') {
5080
- cmdRoadmapUpdatePlanProgress(cwd, args[2], raw);
5239
+ roadmapLib.cmdRoadmapUpdatePlanProgress(cwd, args[2], raw);
5081
5240
  } else {
5082
5241
  error('Unknown roadmap subcommand. Available: get-phase, analyze, update-plan-progress');
5083
5242
  }
@@ -5087,22 +5246,32 @@ async function main() {
5087
5246
  case 'phase': {
5088
5247
  const subcommand = args[1];
5089
5248
  if (subcommand === 'next-decimal') {
5090
- cmdPhaseNextDecimal(cwd, args[2], raw);
5249
+ phaseLib.cmdPhaseNextDecimal(cwd, args[2], raw);
5091
5250
  } else if (subcommand === 'add') {
5092
- cmdPhaseAdd(cwd, args.slice(2).join(' '), raw);
5251
+ phaseLib.cmdPhaseAdd(cwd, args.slice(2).join(' '), raw);
5093
5252
  } else if (subcommand === 'insert') {
5094
- cmdPhaseInsert(cwd, args[2], args.slice(3).join(' '), raw);
5253
+ phaseLib.cmdPhaseInsert(cwd, args[2], args.slice(3).join(' '), raw);
5095
5254
  } else if (subcommand === 'remove') {
5096
5255
  const forceFlag = args.includes('--force');
5097
- cmdPhaseRemove(cwd, args[2], { force: forceFlag }, raw);
5256
+ phaseLib.cmdPhaseRemove(cwd, args[2], { force: forceFlag }, raw);
5098
5257
  } else if (subcommand === 'complete') {
5099
- cmdPhaseComplete(cwd, args[2], raw);
5258
+ phaseLib.cmdPhaseComplete(cwd, args[2], raw);
5100
5259
  } else {
5101
5260
  error('Unknown phase subcommand. Available: next-decimal, add, insert, remove, complete');
5102
5261
  }
5103
5262
  break;
5104
5263
  }
5105
5264
 
5265
+ case 'requirements': {
5266
+ const subcommand = args[1];
5267
+ if (subcommand === 'mark-complete') {
5268
+ milestoneLib.cmdRequirementsMarkComplete(cwd, args.slice(2), raw);
5269
+ } else {
5270
+ error('Unknown requirements subcommand. Available: mark-complete');
5271
+ }
5272
+ break;
5273
+ }
5274
+
5106
5275
  case 'milestone': {
5107
5276
  const subcommand = args[1];
5108
5277
  if (subcommand === 'complete') {
@@ -5118,7 +5287,7 @@ async function main() {
5118
5287
  }
5119
5288
  milestoneName = nameArgs.join(' ') || null;
5120
5289
  }
5121
- cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases }, raw);
5290
+ milestoneLib.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases }, raw);
5122
5291
  } else {
5123
5292
  error('Unknown milestone subcommand. Available: complete');
5124
5293
  }
@@ -5128,10 +5297,10 @@ async function main() {
5128
5297
  case 'validate': {
5129
5298
  const subcommand = args[1];
5130
5299
  if (subcommand === 'consistency') {
5131
- cmdValidateConsistency(cwd, raw);
5300
+ verifyLib.cmdValidateConsistency(cwd, raw);
5132
5301
  } else if (subcommand === 'health') {
5133
5302
  const repairFlag = args.includes('--repair');
5134
- cmdValidateHealth(cwd, { repair: repairFlag }, raw);
5303
+ verifyLib.cmdValidateHealth(cwd, { repair: repairFlag }, raw);
5135
5304
  } else {
5136
5305
  error('Unknown validate subcommand. Available: consistency, health');
5137
5306
  }
@@ -5140,14 +5309,14 @@ async function main() {
5140
5309
 
5141
5310
  case 'progress': {
5142
5311
  const subcommand = args[1] || 'json';
5143
- cmdProgressRender(cwd, subcommand, raw);
5312
+ commandsLib.cmdProgressRender(cwd, subcommand, raw);
5144
5313
  break;
5145
5314
  }
5146
5315
 
5147
5316
  case 'todo': {
5148
5317
  const subcommand = args[1];
5149
5318
  if (subcommand === 'complete') {
5150
- cmdTodoComplete(cwd, args[2], raw);
5319
+ commandsLib.cmdTodoComplete(cwd, args[2], raw);
5151
5320
  } else {
5152
5321
  error('Unknown todo subcommand. Available: complete');
5153
5322
  }
@@ -5162,7 +5331,7 @@ async function main() {
5162
5331
  phase: phaseIndex !== -1 ? args[phaseIndex + 1] : null,
5163
5332
  name: nameIndex !== -1 ? args.slice(nameIndex + 1).join(' ') : null,
5164
5333
  };
5165
- cmdScaffold(cwd, scaffoldType, scaffoldOptions, raw);
5334
+ commandsLib.cmdScaffold(cwd, scaffoldType, scaffoldOptions, raw);
5166
5335
  break;
5167
5336
  }
5168
5337
 
@@ -5171,40 +5340,40 @@ async function main() {
5171
5340
  const includes = parseIncludeFlag(args);
5172
5341
  switch (workflow) {
5173
5342
  case 'execute-phase':
5174
- cmdInitExecutePhase(cwd, args[2], includes, raw);
5343
+ initLib.cmdInitExecutePhase(cwd, args[2], raw);
5175
5344
  break;
5176
5345
  case 'plan-phase':
5177
- cmdInitPlanPhase(cwd, args[2], includes, raw);
5346
+ initLib.cmdInitPlanPhase(cwd, args[2], raw);
5178
5347
  break;
5179
5348
  case 'new-project':
5180
- cmdInitNewProject(cwd, raw);
5349
+ initLib.cmdInitNewProject(cwd, raw);
5181
5350
  break;
5182
5351
  case 'new-milestone':
5183
- cmdInitNewMilestone(cwd, raw);
5352
+ initLib.cmdInitNewMilestone(cwd, raw);
5184
5353
  break;
5185
5354
  case 'quick':
5186
- cmdInitQuick(cwd, args.slice(2).join(' '), raw);
5355
+ initLib.cmdInitQuick(cwd, args.slice(2).join(' '), raw);
5187
5356
  break;
5188
5357
  case 'resume':
5189
- cmdInitResume(cwd, raw);
5358
+ initLib.cmdInitResume(cwd, raw);
5190
5359
  break;
5191
5360
  case 'verify-work':
5192
- cmdInitVerifyWork(cwd, args[2], raw);
5361
+ initLib.cmdInitVerifyWork(cwd, args[2], raw);
5193
5362
  break;
5194
5363
  case 'phase-op':
5195
- cmdInitPhaseOp(cwd, args[2], raw);
5364
+ initLib.cmdInitPhaseOp(cwd, args[2], raw);
5196
5365
  break;
5197
5366
  case 'todos':
5198
- cmdInitTodos(cwd, args[2], raw);
5367
+ initLib.cmdInitTodos(cwd, args[2], raw);
5199
5368
  break;
5200
5369
  case 'milestone-op':
5201
- cmdInitMilestoneOp(cwd, raw);
5370
+ initLib.cmdInitMilestoneOp(cwd, raw);
5202
5371
  break;
5203
5372
  case 'map-codebase':
5204
- cmdInitMapCodebase(cwd, raw);
5373
+ initLib.cmdInitMapCodebase(cwd, raw);
5205
5374
  break;
5206
5375
  case 'progress':
5207
- cmdInitProgress(cwd, includes, raw);
5376
+ initLib.cmdInitProgress(cwd, includes, raw);
5208
5377
  break;
5209
5378
  default:
5210
5379
  error(`Unknown init workflow: ${workflow}\nAvailable: execute-phase, plan-phase, new-project, new-milestone, quick, resume, verify-work, phase-op, todos, milestone-op, map-codebase, progress`);
@@ -5213,12 +5382,12 @@ async function main() {
5213
5382
  }
5214
5383
 
5215
5384
  case 'phase-plan-index': {
5216
- cmdPhasePlanIndex(cwd, args[1], raw);
5385
+ phaseLib.cmdPhasePlanIndex(cwd, args[1], raw);
5217
5386
  break;
5218
5387
  }
5219
5388
 
5220
5389
  case 'state-snapshot': {
5221
- cmdStateSnapshot(cwd, raw);
5390
+ stateLib.cmdStateSnapshot(cwd, raw);
5222
5391
  break;
5223
5392
  }
5224
5393
 
@@ -5226,7 +5395,7 @@ async function main() {
5226
5395
  const summaryPath = args[1];
5227
5396
  const fieldsIndex = args.indexOf('--fields');
5228
5397
  const fields = fieldsIndex !== -1 ? args[fieldsIndex + 1].split(',') : null;
5229
- cmdSummaryExtract(cwd, summaryPath, fields, raw);
5398
+ commandsLib.cmdSummaryExtract(cwd, summaryPath, fields, raw);
5230
5399
  break;
5231
5400
  }
5232
5401
 
@@ -5234,7 +5403,7 @@ async function main() {
5234
5403
  const query = args[1];
5235
5404
  const limitIdx = args.indexOf('--limit');
5236
5405
  const freshnessIdx = args.indexOf('--freshness');
5237
- await cmdWebsearch(query, {
5406
+ await commandsLib.cmdWebsearch(query, {
5238
5407
  limit: limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 10,
5239
5408
  freshness: freshnessIdx !== -1 ? args[freshnessIdx + 1] : null,
5240
5409
  }, raw);
@@ -16,21 +16,21 @@ function toPosixPath(p) {
16
16
  // ─── Model Profile Table ─────────────────────────────────────────────────────
17
17
 
18
18
  const MODEL_PROFILES = {
19
- // quality balanced budget
20
- 'gsd-planner': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
21
- 'gsd-roadmapper': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
22
- 'gsd-executor': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
23
- 'gsd-phase-researcher': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'low' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
24
- 'gsd-project-researcher': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'low' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
25
- 'gsd-research-synthesizer': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'low' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
26
- 'gsd-debugger': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
27
- 'gsd-codebase-mapper': { quality: { m: 'gpt-5.3-codex', t: 'low' }, balanced: { m: 'gpt-5.3-codex', t: 'low' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
28
- 'gsd-verifier': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
29
- 'gsd-plan-checker': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'low' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
30
- 'gsd-integration-checker': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'low' }, budget: { m: 'gpt-5.3-codex', t: 'low' } },
19
+ // quality balanced budget
20
+ 'gsd-planner': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'xhigh' }, budget: { m: 'gpt-5.3-codex', t: 'high' } },
21
+ 'gsd-roadmapper': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
22
+ 'gsd-executor': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
23
+ 'gsd-phase-researcher': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
24
+ 'gsd-project-researcher': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
25
+ 'gsd-research-synthesizer': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
26
+ 'gsd-debugger': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'xhigh' }, budget: { m: 'gpt-5.3-codex', t: 'high' } },
27
+ 'gsd-codebase-mapper': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
28
+ 'gsd-verifier': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
29
+ 'gsd-plan-checker': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
30
+ 'gsd-integration-checker': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
31
31
  };
32
32
 
33
- const DEFAULT_ENTRY = { m: 'gpt-5.3-codex', t: 'medium' };
33
+ const DEFAULT_ENTRY = { m: 'gpt-5.3-codex', t: 'high' };
34
34
 
35
35
  // ─── Output helpers ───────────────────────────────────────────────────────────
36
36
 
@@ -366,15 +366,15 @@ function resolveModelInternal(cwd, agentType) {
366
366
  if (override) {
367
367
  // Override can be a string (thinking level) or { m, t } object
368
368
  if (typeof override === 'string') {
369
- return { model: 'inherit', thinking: override === 'high' || override === 'medium' || override === 'low' ? override : 'medium' };
369
+ return { model: 'inherit', thinking: override === 'xhigh' || override === 'high' || override === 'medium' || override === 'low' ? override : 'high' };
370
370
  }
371
- return { model: 'inherit', thinking: override.t || 'medium' };
371
+ return { model: 'inherit', thinking: override.t || 'high' };
372
372
  }
373
373
 
374
374
  // Fall back to profile lookup
375
375
  const profile = config.model_profile || 'balanced';
376
376
  const agentModels = MODEL_PROFILES[agentType];
377
- if (!agentModels) return { model: 'inherit', thinking: 'medium' };
377
+ if (!agentModels) return { model: 'inherit', thinking: 'high' };
378
378
  const entry = agentModels[profile] || agentModels['balanced'] || DEFAULT_ENTRY;
379
379
  return { model: 'inherit', thinking: entry.t };
380
380
  }
@@ -17,7 +17,7 @@ Default: `balanced` if not set or config missing.
17
17
  Look up the agent in the table for the resolved profile. Each entry returns:
18
18
 
19
19
  ```json
20
- { "model": "inherit", "thinking": "high" }
20
+ { "model": "inherit", "thinking": "xhigh" }
21
21
  ```
22
22
 
23
23
  All agents use `gpt-5.3-codex` (via `"inherit"`). The `thinking` field controls reasoning effort.
@@ -29,7 +29,7 @@ Task(
29
29
  prompt="...",
30
30
  subagent_type="gsd-planner",
31
31
  model="inherit",
32
- thinking="{resolved_thinking}" # "high", "medium", or "low"
32
+ thinking="{resolved_thinking}" # "xhigh", "high", "medium", or "low"
33
33
  )
34
34
  ```
35
35
 
@@ -6,17 +6,17 @@ Model profiles control the reasoning effort level for each GSD agent. All agents
6
6
 
7
7
  | Agent | `quality` | `balanced` | `budget` |
8
8
  | ------------------------ | --------- | ---------- | --------- |
9
- | gsd-planner | 🟒 high | 🟒 high | 🟑 medium |
10
- | gsd-roadmapper | 🟒 high | 🟑 medium | πŸ”΅ low |
11
- | gsd-executor | 🟒 high | 🟑 medium | πŸ”΅ low |
12
- | gsd-phase-researcher | 🟑 medium | πŸ”΅ low | πŸ”΅ low |
13
- | gsd-project-researcher | 🟑 medium | πŸ”΅ low | πŸ”΅ low |
14
- | gsd-research-synthesizer | 🟑 medium | πŸ”΅ low | πŸ”΅ low |
15
- | gsd-debugger | 🟒 high | 🟒 high | 🟑 medium |
16
- | gsd-codebase-mapper | πŸ”΅ low | πŸ”΅ low | πŸ”΅ low |
17
- | gsd-verifier | 🟑 medium | 🟑 medium | πŸ”΅ low |
18
- | gsd-plan-checker | 🟑 medium | πŸ”΅ low | πŸ”΅ low |
19
- | gsd-integration-checker | 🟑 medium | πŸ”΅ low | πŸ”΅ low |
9
+ | gsd-planner | πŸ”΄ xhigh | πŸ”΄ xhigh | 🟒 high |
10
+ | gsd-roadmapper | πŸ”΄ xhigh | 🟒 high | 🟑 medium |
11
+ | gsd-executor | πŸ”΄ xhigh | 🟒 high | 🟑 medium |
12
+ | gsd-phase-researcher | 🟒 high | 🟑 medium | 🟑 medium |
13
+ | gsd-project-researcher | 🟒 high | 🟑 medium | 🟑 medium |
14
+ | gsd-research-synthesizer | 🟒 high | 🟑 medium | 🟑 medium |
15
+ | gsd-debugger | πŸ”΄ xhigh | πŸ”΄ xhigh | 🟒 high |
16
+ | gsd-codebase-mapper | 🟑 medium | 🟑 medium | 🟑 medium |
17
+ | gsd-verifier | 🟒 high | 🟒 high | 🟑 medium |
18
+ | gsd-plan-checker | 🟒 high | 🟑 medium | 🟑 medium |
19
+ | gsd-integration-checker | 🟒 high | 🟑 medium | 🟑 medium |
20
20
 
21
21
  All entries resolve to `model: "inherit"` (uses the session's gpt-5.3-codex). The `thinking` field controls reasoning effort.
22
22
 
@@ -24,39 +24,39 @@ All entries resolve to `model: "inherit"` (uses the session's gpt-5.3-codex). Th
24
24
 
25
25
  **quality** - Maximum reasoning for every role
26
26
 
27
- - 🟒 **high** for decision-makers: planner, roadmapper, executor, debugger
28
- - 🟑 **medium** for analysis: researchers, verifiers, checkers
29
- - πŸ”΅ **low** for read-only mapping
27
+ - πŸ”΄ **xhigh** for decision-makers: planner, roadmapper, executor, debugger
28
+ - 🟒 **high** for analysis: researchers, verifiers, checkers
29
+ - 🟑 **medium** for read-only mapping
30
30
  - Use when: critical architecture work, complex debugging
31
31
 
32
32
  **balanced** (default) - Smart thinking allocation
33
33
 
34
- - 🟒 **high** only for planner and debugger (highest-impact decisions)
35
- - 🟑 **medium** for executor and verifier (needs reasoning but follows plans)
36
- - πŸ”΅ **low** for everything else (structured output, scanning)
34
+ - πŸ”΄ **xhigh** only for planner and debugger (highest-impact decisions)
35
+ - 🟒 **high** for executor and verifier (needs reasoning but follows plans)
36
+ - 🟑 **medium** for everything else (structured output, scanning)
37
37
  - Use when: normal development
38
38
 
39
39
  **budget** - Minimal reasoning budget
40
40
 
41
- - 🟑 **medium** for planner and debugger (always need some reasoning)
42
- - πŸ”΅ **low** for everything else
41
+ - 🟒 **high** for planner and debugger (always need some reasoning)
42
+ - 🟑 **medium** for everything else
43
43
  - Use when: high-volume work, less critical phases
44
44
 
45
45
  ## Role-Based Thinking Rationale
46
46
 
47
- **Why high thinking for gsd-planner?**
47
+ **Why xhigh thinking for gsd-planner?**
48
48
  Planning involves architecture decisions, goal decomposition, and task design. These decisions cascade through the entire phase β€” worth the extra reasoning budget.
49
49
 
50
- **Why high thinking for gsd-debugger even in balanced?**
50
+ **Why xhigh thinking for gsd-debugger even in balanced?**
51
51
  Root cause analysis requires deep reasoning. A debugger that misdiagnoses wastes more tokens in re-runs than the reasoning cost.
52
52
 
53
- **Why low thinking for gsd-codebase-mapper?**
53
+ **Why medium thinking for gsd-codebase-mapper?**
54
54
  Read-only file scanning and pattern extraction. No decisions to make β€” just structured output from file contents.
55
55
 
56
- **Why medium thinking for gsd-verifier in balanced?**
57
- Verification requires goal-backward reasoning β€” checking if code _delivers_ what the phase promised. Low thinking may miss subtle gaps.
56
+ **Why high thinking for gsd-verifier in balanced?**
57
+ Verification requires goal-backward reasoning β€” checking if code _delivers_ what the phase promised. Medium thinking may miss subtle gaps.
58
58
 
59
- **Why low thinking for researchers in balanced?**
59
+ **Why medium thinking for researchers in balanced?**
60
60
  Research agents scan and collect information. The synthesis happens elsewhere. They don't need deep reasoning for reading files.
61
61
 
62
62
  ## Resolution Logic
@@ -70,7 +70,7 @@ Orchestrators resolve model and thinking before spawning:
70
70
  4. Pass model + thinking to Task call
71
71
  ```
72
72
 
73
- Returns: `{ model: "inherit", thinking: "high"|"medium"|"low" }`
73
+ Returns: `{ model: "inherit", thinking: "xhigh"|"high"|"medium"|"low" }`
74
74
 
75
75
  ## Per-Agent Overrides
76
76
 
@@ -80,13 +80,13 @@ Override thinking level for specific agents:
80
80
  {
81
81
  "model_profile": "balanced",
82
82
  "model_overrides": {
83
- "gsd-executor": "high",
84
- "gsd-codebase-mapper": "medium"
83
+ "gsd-executor": "xhigh",
84
+ "gsd-codebase-mapper": "high"
85
85
  }
86
86
  }
87
87
  ```
88
88
 
89
- Valid override values: `"high"`, `"medium"`, `"low"`.
89
+ Valid override values: `"xhigh"`, `"high"`, `"medium"`, `"low"`.
90
90
 
91
91
  ## Switching Profiles
92
92
 
@@ -309,9 +309,9 @@ Usage: `$gsd-settings`
309
309
  **`$gsd-set-profile <profile>`**
310
310
  Quick switch model profile for GSD agents.
311
311
 
312
- - `quality` β€” high thinking for decision-makers, medium for analysis agents
313
- - `balanced` β€” high thinking for planner/debugger, medium/low for others (default)
314
- - `budget` β€” minimal thinking β€” medium for planner/debugger, low everywhere else
312
+ - `quality` β€” xhigh thinking for decision-makers, high for analysis agents
313
+ - `balanced` β€” xhigh thinking for planner/debugger, high/medium for others (default)
314
+ - `budget` β€” minimal thinking β€” high for planner/debugger, medium everywhere else
315
315
 
316
316
  Usage: `$gsd-set-profile budget`
317
317
 
@@ -164,9 +164,9 @@ AskUserQuestion([
164
164
  question: "Which AI models for planning agents?",
165
165
  multiSelect: false,
166
166
  options: [
167
- { label: "Balanced (Recommended)", description: "gpt-5.3-codex β€” smart thinking allocation per role" },
168
- { label: "Quality", description: "gpt-5.3-codex β€” high thinking for all decision-makers" },
169
- { label: "Budget", description: "gpt-5.3-codex β€” minimal thinking, fastest/cheapest" }
167
+ { label: "Balanced (Recommended)", description: "gpt-5.3-codex β€” xhigh/high/medium thinking allocation per role" },
168
+ { label: "Quality", description: "gpt-5.3-codex β€” xhigh thinking for all decision-makers" },
169
+ { label: "Budget", description: "gpt-5.3-codex β€” high/medium thinking, fastest/cheapest" }
170
170
  ]
171
171
  }
172
172
  ])
@@ -459,9 +459,9 @@ questions: [
459
459
  question: "Which AI models for planning agents?",
460
460
  multiSelect: false,
461
461
  options: [
462
- { label: "Balanced (Recommended)", description: "gpt-5.3-codex β€” smart thinking allocation per role" },
463
- { label: "Quality", description: "gpt-5.3-codex β€” high thinking for all decision-makers" },
464
- { label: "Budget", description: "gpt-5.3-codex β€” minimal thinking, fastest/cheapest" }
462
+ { label: "Balanced (Recommended)", description: "gpt-5.3-codex β€” xhigh/high/medium thinking allocation per role" },
463
+ { label: "Quality", description: "gpt-5.3-codex β€” xhigh thinking for all decision-makers" },
464
+ { label: "Budget", description: "gpt-5.3-codex β€” high/medium thinking, fastest/cheapest" }
465
465
  ]
466
466
  }
467
467
  ]
@@ -58,9 +58,9 @@ Agents will now use:
58
58
  Example:
59
59
  | Agent | Model | Thinking |
60
60
  |-------|-------|----------|
61
- | gsd-planner | gpt-5.3-codex | high |
62
- | gsd-executor | gpt-5.3-codex | medium |
63
- | gsd-verifier | gpt-5.3-codex | medium |
61
+ | gsd-planner | gpt-5.3-codex | xhigh |
62
+ | gsd-executor | gpt-5.3-codex | high |
63
+ | gsd-verifier | gpt-5.3-codex | high |
64
64
  | ... | ... | ... |
65
65
 
66
66
  Next spawned agents will use the new profile.
@@ -43,9 +43,9 @@ AskUserQuestion([
43
43
  header: "Model",
44
44
  multiSelect: false,
45
45
  options: [
46
- { label: "Quality", description: "gpt-5.3-codex with high thinking for decision-makers, medium for analysis" },
47
- { label: "Balanced (Recommended)", description: "gpt-5.3-codex with high thinking for planner/debugger, lower for others" },
48
- { label: "Budget", description: "gpt-5.3-codex with minimal thinking β€” fastest, lowest cost" }
46
+ { label: "Quality", description: "gpt-5.3-codex with xhigh thinking for decision-makers, high for analysis" },
47
+ { label: "Balanced (Recommended)", description: "gpt-5.3-codex with xhigh thinking for planner/debugger, high/medium for others" },
48
+ { label: "Budget", description: "gpt-5.3-codex with high thinking for planner/debugger, medium everywhere else" }
49
49
  ]
50
50
  },
51
51
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@undeemed/get-shit-done-codex",
3
- "version": "1.24.1",
3
+ "version": "1.24.2",
4
4
  "description": "A meta-prompting, context engineering and spec-driven development system for OpenAI Codex (CLI and Desktop). Fork of get-shit-done by TΓ‚CHES, adapted for Codex.",
5
5
  "bin": {
6
6
  "get-shit-done-codex": "bin/install.js"
@@ -67,6 +67,7 @@
67
67
  "scripts": {
68
68
  "build:hooks": "node scripts/build-hooks.js",
69
69
  "prepublishOnly": "npm run build:hooks",
70
- "test": "node --test tests/*.test.cjs"
70
+ "test": "node scripts/run-tests.cjs",
71
+ "test:coverage": "node scripts/run-tests.cjs --coverage"
71
72
  }
72
73
  }
@@ -1,29 +1,43 @@
1
1
  #!/usr/bin/env node
2
- // Cross-platform test runner β€” resolves test file globs via Node
3
- // instead of relying on shell expansion (which fails on Windows PowerShell/cmd).
4
- // Propagates NODE_V8_COVERAGE so c8 collects coverage from the child process.
5
- 'use strict';
6
-
7
- const { readdirSync } = require('fs');
8
- const { join } = require('path');
9
- const { execFileSync } = require('child_process');
10
-
11
- const testDir = join(__dirname, '..', 'tests');
12
- const files = readdirSync(testDir)
13
- .filter(f => f.endsWith('.test.cjs'))
14
- .sort()
15
- .map(f => join('tests', f));
16
-
17
- if (files.length === 0) {
18
- console.error('No test files found in tests/');
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { spawnSync } = require("node:child_process");
6
+
7
+ function collectTests(dir) {
8
+ if (!fs.existsSync(dir)) return [];
9
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
10
+ const files = [];
11
+
12
+ for (const entry of entries) {
13
+ const fullPath = path.join(dir, entry.name);
14
+ if (entry.isDirectory()) {
15
+ files.push(...collectTests(fullPath));
16
+ continue;
17
+ }
18
+ if (entry.isFile() && entry.name.endsWith(".test.cjs")) {
19
+ files.push(fullPath);
20
+ }
21
+ }
22
+
23
+ return files;
24
+ }
25
+
26
+ const withCoverage = process.argv.includes("--coverage");
27
+ const testFiles = collectTests(path.resolve(__dirname, "..", "tests")).sort();
28
+
29
+ if (testFiles.length === 0) {
30
+ console.error("No test files found in ./tests");
19
31
  process.exit(1);
20
32
  }
21
33
 
22
- try {
23
- execFileSync(process.execPath, ['--test', ...files], {
24
- stdio: 'inherit',
25
- env: { ...process.env },
26
- });
27
- } catch (err) {
28
- process.exit(err.status || 1);
34
+ const args = [];
35
+ if (withCoverage) args.push("--experimental-test-coverage");
36
+ args.push("--test", ...testFiles);
37
+
38
+ const result = spawnSync(process.execPath, args, { stdio: "inherit" });
39
+ if (result.error) {
40
+ console.error(result.error.message);
41
+ process.exit(1);
29
42
  }
43
+ process.exit(result.status ?? 1);