gsd-opencode 1.33.3 → 1.35.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 (118) hide show
  1. package/agents/gsd-advisor-researcher.md +23 -0
  2. package/agents/gsd-ai-researcher.md +142 -0
  3. package/agents/gsd-code-fixer.md +523 -0
  4. package/agents/gsd-code-reviewer.md +361 -0
  5. package/agents/gsd-debugger.md +14 -1
  6. package/agents/gsd-domain-researcher.md +162 -0
  7. package/agents/gsd-eval-auditor.md +170 -0
  8. package/agents/gsd-eval-planner.md +161 -0
  9. package/agents/gsd-executor.md +70 -7
  10. package/agents/gsd-framework-selector.md +167 -0
  11. package/agents/gsd-intel-updater.md +320 -0
  12. package/agents/gsd-phase-researcher.md +26 -0
  13. package/agents/gsd-plan-checker.md +12 -0
  14. package/agents/gsd-planner.md +16 -6
  15. package/agents/gsd-project-researcher.md +23 -0
  16. package/agents/gsd-ui-researcher.md +23 -0
  17. package/agents/gsd-verifier.md +55 -1
  18. package/commands/gsd/gsd-ai-integration-phase.md +36 -0
  19. package/commands/gsd/gsd-audit-fix.md +33 -0
  20. package/commands/gsd/gsd-autonomous.md +1 -0
  21. package/commands/gsd/gsd-code-review-fix.md +52 -0
  22. package/commands/gsd/gsd-code-review.md +55 -0
  23. package/commands/gsd/gsd-eval-review.md +32 -0
  24. package/commands/gsd/gsd-explore.md +27 -0
  25. package/commands/gsd/gsd-from-gsd2.md +45 -0
  26. package/commands/gsd/gsd-import.md +36 -0
  27. package/commands/gsd/gsd-intel.md +183 -0
  28. package/commands/gsd/gsd-next.md +2 -0
  29. package/commands/gsd/gsd-reapply-patches.md +58 -3
  30. package/commands/gsd/gsd-review.md +4 -2
  31. package/commands/gsd/gsd-scan.md +26 -0
  32. package/commands/gsd/gsd-undo.md +34 -0
  33. package/commands/gsd/gsd-workstreams.md +6 -6
  34. package/get-shit-done/bin/gsd-tools.cjs +143 -5
  35. package/get-shit-done/bin/lib/commands.cjs +10 -2
  36. package/get-shit-done/bin/lib/config.cjs +71 -37
  37. package/get-shit-done/bin/lib/core.cjs +70 -8
  38. package/get-shit-done/bin/lib/gsd2-import.cjs +511 -0
  39. package/get-shit-done/bin/lib/init.cjs +20 -6
  40. package/get-shit-done/bin/lib/intel.cjs +660 -0
  41. package/get-shit-done/bin/lib/learnings.cjs +378 -0
  42. package/get-shit-done/bin/lib/milestone.cjs +25 -15
  43. package/get-shit-done/bin/lib/model-profiles.cjs +17 -17
  44. package/get-shit-done/bin/lib/phase.cjs +148 -112
  45. package/get-shit-done/bin/lib/roadmap.cjs +12 -5
  46. package/get-shit-done/bin/lib/security.cjs +119 -0
  47. package/get-shit-done/bin/lib/state.cjs +283 -221
  48. package/get-shit-done/bin/lib/template.cjs +8 -4
  49. package/get-shit-done/bin/lib/verify.cjs +42 -5
  50. package/get-shit-done/references/ai-evals.md +156 -0
  51. package/get-shit-done/references/ai-frameworks.md +186 -0
  52. package/get-shit-done/references/common-bug-patterns.md +114 -0
  53. package/get-shit-done/references/few-shot-examples/plan-checker.md +73 -0
  54. package/get-shit-done/references/few-shot-examples/verifier.md +109 -0
  55. package/get-shit-done/references/gates.md +70 -0
  56. package/get-shit-done/references/ios-scaffold.md +123 -0
  57. package/get-shit-done/references/model-profile-resolution.md +6 -7
  58. package/get-shit-done/references/model-profiles.md +20 -14
  59. package/get-shit-done/references/planning-config.md +237 -0
  60. package/get-shit-done/references/thinking-models-debug.md +44 -0
  61. package/get-shit-done/references/thinking-models-execution.md +50 -0
  62. package/get-shit-done/references/thinking-models-planning.md +62 -0
  63. package/get-shit-done/references/thinking-models-research.md +50 -0
  64. package/get-shit-done/references/thinking-models-verification.md +55 -0
  65. package/get-shit-done/references/thinking-partner.md +96 -0
  66. package/get-shit-done/references/universal-anti-patterns.md +6 -1
  67. package/get-shit-done/references/verification-overrides.md +227 -0
  68. package/get-shit-done/templates/AI-SPEC.md +246 -0
  69. package/get-shit-done/workflows/add-tests.md +3 -0
  70. package/get-shit-done/workflows/add-todo.md +2 -0
  71. package/get-shit-done/workflows/ai-integration-phase.md +284 -0
  72. package/get-shit-done/workflows/audit-fix.md +154 -0
  73. package/get-shit-done/workflows/autonomous.md +33 -2
  74. package/get-shit-done/workflows/check-todos.md +2 -0
  75. package/get-shit-done/workflows/cleanup.md +2 -0
  76. package/get-shit-done/workflows/code-review-fix.md +497 -0
  77. package/get-shit-done/workflows/code-review.md +515 -0
  78. package/get-shit-done/workflows/complete-milestone.md +40 -15
  79. package/get-shit-done/workflows/diagnose-issues.md +1 -1
  80. package/get-shit-done/workflows/discovery-phase.md +3 -1
  81. package/get-shit-done/workflows/discuss-phase-assumptions.md +1 -1
  82. package/get-shit-done/workflows/discuss-phase.md +21 -7
  83. package/get-shit-done/workflows/do.md +2 -0
  84. package/get-shit-done/workflows/docs-update.md +2 -0
  85. package/get-shit-done/workflows/eval-review.md +155 -0
  86. package/get-shit-done/workflows/execute-phase.md +307 -57
  87. package/get-shit-done/workflows/execute-plan.md +64 -93
  88. package/get-shit-done/workflows/explore.md +136 -0
  89. package/get-shit-done/workflows/help.md +1 -1
  90. package/get-shit-done/workflows/import.md +273 -0
  91. package/get-shit-done/workflows/inbox.md +387 -0
  92. package/get-shit-done/workflows/manager.md +4 -10
  93. package/get-shit-done/workflows/new-milestone.md +3 -1
  94. package/get-shit-done/workflows/new-project.md +2 -0
  95. package/get-shit-done/workflows/new-workspace.md +2 -0
  96. package/get-shit-done/workflows/next.md +56 -0
  97. package/get-shit-done/workflows/note.md +2 -0
  98. package/get-shit-done/workflows/plan-phase.md +97 -17
  99. package/get-shit-done/workflows/plant-seed.md +3 -0
  100. package/get-shit-done/workflows/pr-branch.md +41 -13
  101. package/get-shit-done/workflows/profile-user.md +4 -2
  102. package/get-shit-done/workflows/quick.md +99 -4
  103. package/get-shit-done/workflows/remove-workspace.md +2 -0
  104. package/get-shit-done/workflows/review.md +53 -6
  105. package/get-shit-done/workflows/scan.md +98 -0
  106. package/get-shit-done/workflows/secure-phase.md +2 -0
  107. package/get-shit-done/workflows/settings.md +18 -3
  108. package/get-shit-done/workflows/ship.md +3 -0
  109. package/get-shit-done/workflows/ui-phase.md +10 -2
  110. package/get-shit-done/workflows/ui-review.md +2 -0
  111. package/get-shit-done/workflows/undo.md +314 -0
  112. package/get-shit-done/workflows/update.md +2 -0
  113. package/get-shit-done/workflows/validate-phase.md +2 -0
  114. package/get-shit-done/workflows/verify-phase.md +83 -0
  115. package/get-shit-done/workflows/verify-work.md +12 -1
  116. package/package.json +1 -1
  117. package/skills/gsd-code-review/SKILL.md +48 -0
  118. package/skills/gsd-code-review-fix/SKILL.md +44 -0
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningDir, planningPaths, output, error } = require('./core.cjs');
7
+ const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningDir, planningPaths, output, error, atomicWriteFileSync } = require('./core.cjs');
8
8
  const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
9
9
 
10
10
  /** Shorthand — every state command needs this path */
@@ -12,6 +12,16 @@ function getStatePath(cwd) {
12
12
  return planningPaths(cwd).state;
13
13
  }
14
14
 
15
+ // Track all lock files held by this process so they can be removed on exit.
16
+ // process.on('exit') fires even on process.exit(1), unlike try/finally which is
17
+ // skipped when error() calls process.exit(1) inside a locked region (#1916).
18
+ const _heldStateLocks = new Set();
19
+ process.on('exit', () => {
20
+ for (const lockPath of _heldStateLocks) {
21
+ try { require('fs').unlinkSync(lockPath); } catch { /* already gone */ }
22
+ }
23
+ });
24
+
15
25
  // Shared helper: extract a field value from STATE.md content.
16
26
  // Supports both **Field:** bold and plain Field: format.
17
27
  function stateExtractField(content, fieldName) {
@@ -184,18 +194,22 @@ function cmdStateUpdate(cwd, field, value) {
184
194
 
185
195
  const statePath = planningPaths(cwd).state;
186
196
  try {
187
- let content = fs.readFileSync(statePath, 'utf-8');
188
- const fieldEscaped = escapeRegex(field);
189
- // Try **Field:** bold format first, then plain Field: format
190
- const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
191
- const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
192
- if (boldPattern.test(content)) {
193
- content = content.replace(boldPattern, (_match, prefix) => `${prefix}${value}`);
194
- writeStateMd(statePath, content, cwd);
195
- output({ updated: true });
196
- } else if (plainPattern.test(content)) {
197
- content = content.replace(plainPattern, (_match, prefix) => `${prefix}${value}`);
198
- writeStateMd(statePath, content, cwd);
197
+ let updated = false;
198
+ readModifyWriteStateMd(statePath, (content) => {
199
+ const fieldEscaped = escapeRegex(field);
200
+ // Try **Field:** bold format first, then plain Field: format
201
+ const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
202
+ const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
203
+ if (boldPattern.test(content)) {
204
+ updated = true;
205
+ return content.replace(boldPattern, (_match, prefix) => `${prefix}${value}`);
206
+ } else if (plainPattern.test(content)) {
207
+ updated = true;
208
+ return content.replace(plainPattern, (_match, prefix) => `${prefix}${value}`);
209
+ }
210
+ return content;
211
+ }, cwd);
212
+ if (updated) {
199
213
  output({ updated: true });
200
214
  } else {
201
215
  output({ updated: false, reason: `Field "${field}" not found in STATE.md` });
@@ -274,55 +288,67 @@ function cmdStateAdvancePlan(cwd, raw) {
274
288
  const statePath = planningPaths(cwd).state;
275
289
  if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
276
290
 
277
- let content = fs.readFileSync(statePath, 'utf-8');
278
291
  const today = new Date().toISOString().split('T')[0];
292
+ let result = null;
293
+
294
+ readModifyWriteStateMd(statePath, (content) => {
295
+ // Try legacy separate fields first, then compound "Plan: X of Y" format
296
+ const legacyPlan = stateExtractField(content, 'Current Plan');
297
+ const legacyTotal = stateExtractField(content, 'Total Plans in Phase');
298
+ const planField = stateExtractField(content, 'Plan');
299
+
300
+ let currentPlan, totalPlans;
301
+ let useCompoundFormat = false;
302
+
303
+ if (legacyPlan && legacyTotal) {
304
+ currentPlan = parseInt(legacyPlan, 10);
305
+ totalPlans = parseInt(legacyTotal, 10);
306
+ } else if (planField) {
307
+ // Compound format: "2 of 6 in current phase" or "2 of 6"
308
+ currentPlan = parseInt(planField, 10);
309
+ const ofMatch = planField.match(/of\s+(\d+)/);
310
+ totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
311
+ useCompoundFormat = true;
312
+ }
279
313
 
280
- // Try legacy separate fields first, then compound "Plan: X of Y" format
281
- const legacyPlan = stateExtractField(content, 'Current Plan');
282
- const legacyTotal = stateExtractField(content, 'Total Plans in Phase');
283
- const planField = stateExtractField(content, 'Plan');
284
-
285
- let currentPlan, totalPlans;
286
- let useCompoundFormat = false;
287
-
288
- if (legacyPlan && legacyTotal) {
289
- currentPlan = parseInt(legacyPlan, 10);
290
- totalPlans = parseInt(legacyTotal, 10);
291
- } else if (planField) {
292
- // Compound format: "2 of 6 in current phase" or "2 of 6"
293
- currentPlan = parseInt(planField, 10);
294
- const ofMatch = planField.match(/of\s+(\d+)/);
295
- totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
296
- useCompoundFormat = true;
297
- }
314
+ if (isNaN(currentPlan) || isNaN(totalPlans)) {
315
+ result = { error: true };
316
+ return content;
317
+ }
318
+
319
+ if (currentPlan >= totalPlans) {
320
+ content = stateReplaceFieldWithFallback(content, 'Status', null, 'Phase complete — ready for verification');
321
+ content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
322
+ content = updateCurrentPositionFields(content, { status: 'Phase complete — ready for verification', lastActivity: today });
323
+ result = { advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' };
324
+ } else {
325
+ const newPlan = currentPlan + 1;
326
+ let planDisplayValue;
327
+ if (useCompoundFormat) {
328
+ // Preserve compound format: "X of Y in current phase" → replace X only
329
+ planDisplayValue = planField.replace(/^\d+/, String(newPlan));
330
+ content = stateReplaceField(content, 'Plan', planDisplayValue) || content;
331
+ } else {
332
+ planDisplayValue = `${newPlan} of ${totalPlans}`;
333
+ content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
334
+ }
335
+ content = stateReplaceFieldWithFallback(content, 'Status', null, 'Ready to execute');
336
+ content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
337
+ content = updateCurrentPositionFields(content, { status: 'Ready to execute', lastActivity: today, plan: planDisplayValue });
338
+ result = { advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans };
339
+ }
340
+ return content;
341
+ }, cwd);
298
342
 
299
- if (isNaN(currentPlan) || isNaN(totalPlans)) {
343
+ if (!result || result.error) {
300
344
  output({ error: 'Cannot parse Current Plan or Total Plans in Phase from STATE.md' }, raw);
301
345
  return;
302
346
  }
303
347
 
304
- if (currentPlan >= totalPlans) {
305
- content = stateReplaceFieldWithFallback(content, 'Status', null, 'Phase complete — ready for verification');
306
- content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
307
- content = updateCurrentPositionFields(content, { status: 'Phase complete — ready for verification', lastActivity: today });
308
- writeStateMd(statePath, content, cwd);
309
- output({ advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' }, raw, 'false');
348
+ if (result.advanced === false) {
349
+ output(result, raw, 'false');
310
350
  } else {
311
- const newPlan = currentPlan + 1;
312
- let planDisplayValue;
313
- if (useCompoundFormat) {
314
- // Preserve compound format: "X of Y in current phase" → replace X only
315
- planDisplayValue = planField.replace(/^\d+/, String(newPlan));
316
- content = stateReplaceField(content, 'Plan', planDisplayValue) || content;
317
- } else {
318
- planDisplayValue = `${newPlan} of ${totalPlans}`;
319
- content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
320
- }
321
- content = stateReplaceFieldWithFallback(content, 'Status', null, 'Ready to execute');
322
- content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
323
- content = updateCurrentPositionFields(content, { status: 'Ready to execute', lastActivity: today, plan: planDisplayValue });
324
- writeStateMd(statePath, content, cwd);
325
- output({ advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans }, raw, 'true');
351
+ output(result, raw, 'true');
326
352
  }
327
353
  }
328
354
 
@@ -330,7 +356,6 @@ function cmdStateRecordMetric(cwd, options, raw) {
330
356
  const statePath = planningPaths(cwd).state;
331
357
  if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
332
358
 
333
- let content = fs.readFileSync(statePath, 'utf-8');
334
359
  const { phase, plan, duration, tasks, files } = options;
335
360
 
336
361
  if (!phase || !plan || !duration) {
@@ -338,22 +363,29 @@ function cmdStateRecordMetric(cwd, options, raw) {
338
363
  return;
339
364
  }
340
365
 
341
- // Find Performance Metrics section and its table
342
- const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n)([\s\S]*?)(?=\n##|\n$|$)/i;
343
- const metricsMatch = content.match(metricsPattern);
366
+ let recorded = false;
367
+ readModifyWriteStateMd(statePath, (content) => {
368
+ // Find Performance Metrics section and its table
369
+ const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n)([\s\S]*?)(?=\n##|\n$|$)/i;
370
+ const metricsMatch = content.match(metricsPattern);
344
371
 
345
- if (metricsMatch) {
346
- let tableBody = metricsMatch[2].trimEnd();
347
- const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
372
+ if (metricsMatch) {
373
+ let tableBody = metricsMatch[2].trimEnd();
374
+ const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
348
375
 
349
- if (tableBody.trim() === '' || tableBody.includes('None yet')) {
350
- tableBody = newRow;
351
- } else {
352
- tableBody = tableBody + '\n' + newRow;
376
+ if (tableBody.trim() === '' || tableBody.includes('None yet')) {
377
+ tableBody = newRow;
378
+ } else {
379
+ tableBody = tableBody + '\n' + newRow;
380
+ }
381
+
382
+ recorded = true;
383
+ return content.replace(metricsPattern, (_match, header) => `${header}${tableBody}\n`);
353
384
  }
385
+ return content;
386
+ }, cwd);
354
387
 
355
- content = content.replace(metricsPattern, (_match, header) => `${header}${tableBody}\n`);
356
- writeStateMd(statePath, content, cwd);
388
+ if (recorded) {
357
389
  output({ recorded: true, phase, plan, duration }, raw, 'true');
358
390
  } else {
359
391
  output({ recorded: false, reason: 'Performance Metrics section not found in STATE.md' }, raw, 'false');
@@ -364,9 +396,7 @@ function cmdStateUpdateProgress(cwd, raw) {
364
396
  const statePath = planningPaths(cwd).state;
365
397
  if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
366
398
 
367
- let content = fs.readFileSync(statePath, 'utf-8');
368
-
369
- // Count summaries across current milestone phases only
399
+ // Count summaries across current milestone phases only (outside lock — read-only)
370
400
  const phasesDir = planningPaths(cwd).phases;
371
401
  let totalPlans = 0;
372
402
  let totalSummaries = 0;
@@ -389,17 +419,26 @@ function cmdStateUpdateProgress(cwd, raw) {
389
419
  const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
390
420
  const progressStr = `[${bar}] ${percent}%`;
391
421
 
392
- // Try **Progress:** bold format first, then plain Progress: format
393
- const boldProgressPattern = /(\*\*Progress:\*\*\s*).*/i;
394
- const plainProgressPattern = /^(Progress:\s*).*/im;
395
- if (boldProgressPattern.test(content)) {
396
- content = content.replace(boldProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
397
- writeStateMd(statePath, content, cwd);
398
- output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
399
- } else if (plainProgressPattern.test(content)) {
400
- content = content.replace(plainProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
401
- writeStateMd(statePath, content, cwd);
402
- output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
422
+ let updated = false;
423
+ const _totalPlans = totalPlans;
424
+ const _totalSummaries = totalSummaries;
425
+
426
+ readModifyWriteStateMd(statePath, (content) => {
427
+ // Try **Progress:** bold format first, then plain Progress: format
428
+ const boldProgressPattern = /(\*\*Progress:\*\*\s*).*/i;
429
+ const plainProgressPattern = /^(Progress:\s*).*/im;
430
+ if (boldProgressPattern.test(content)) {
431
+ updated = true;
432
+ return content.replace(boldProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
433
+ } else if (plainProgressPattern.test(content)) {
434
+ updated = true;
435
+ return content.replace(plainProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
436
+ }
437
+ return content;
438
+ }, cwd);
439
+
440
+ if (updated) {
441
+ output({ updated: true, percent, completed: _totalSummaries, total: _totalPlans, bar: progressStr }, raw, progressStr);
403
442
  } else {
404
443
  output({ updated: false, reason: 'Progress field not found in STATE.md' }, raw, 'false');
405
444
  }
@@ -423,20 +462,26 @@ function cmdStateAddDecision(cwd, options, raw) {
423
462
 
424
463
  if (!summaryText) { output({ error: 'summary required' }, raw); return; }
425
464
 
426
- let content = fs.readFileSync(statePath, 'utf-8');
427
465
  const entry = `- [Phase ${phase || '?'}]: ${summaryText}${rationaleText ? ` — ${rationaleText}` : ''}`;
466
+ let added = false;
467
+
468
+ readModifyWriteStateMd(statePath, (content) => {
469
+ // Find Decisions section (various heading patterns)
470
+ const sectionPattern = /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
471
+ const match = content.match(sectionPattern);
472
+
473
+ if (match) {
474
+ let sectionBody = match[2];
475
+ // Remove placeholders
476
+ sectionBody = sectionBody.replace(/None yet\.?\s*\n?/gi, '').replace(/No decisions yet\.?\s*\n?/gi, '');
477
+ sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
478
+ added = true;
479
+ return content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
480
+ }
481
+ return content;
482
+ }, cwd);
428
483
 
429
- // Find Decisions section (various heading patterns)
430
- const sectionPattern = /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
431
- const match = content.match(sectionPattern);
432
-
433
- if (match) {
434
- let sectionBody = match[2];
435
- // Remove placeholders
436
- sectionBody = sectionBody.replace(/None yet\.?\s*\n?/gi, '').replace(/No decisions yet\.?\s*\n?/gi, '');
437
- sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
438
- content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
439
- writeStateMd(statePath, content, cwd);
484
+ if (added) {
440
485
  output({ added: true, decision: entry }, raw, 'true');
441
486
  } else {
442
487
  output({ added: false, reason: 'Decisions section not found in STATE.md' }, raw, 'false');
@@ -458,18 +503,24 @@ function cmdStateAddBlocker(cwd, text, raw) {
458
503
 
459
504
  if (!blockerText) { output({ error: 'text required' }, raw); return; }
460
505
 
461
- let content = fs.readFileSync(statePath, 'utf-8');
462
506
  const entry = `- ${blockerText}`;
507
+ let added = false;
508
+
509
+ readModifyWriteStateMd(statePath, (content) => {
510
+ const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
511
+ const match = content.match(sectionPattern);
512
+
513
+ if (match) {
514
+ let sectionBody = match[2];
515
+ sectionBody = sectionBody.replace(/None\.?\s*\n?/gi, '').replace(/None yet\.?\s*\n?/gi, '');
516
+ sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
517
+ added = true;
518
+ return content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
519
+ }
520
+ return content;
521
+ }, cwd);
463
522
 
464
- const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
465
- const match = content.match(sectionPattern);
466
-
467
- if (match) {
468
- let sectionBody = match[2];
469
- sectionBody = sectionBody.replace(/None\.?\s*\n?/gi, '').replace(/None yet\.?\s*\n?/gi, '');
470
- sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
471
- content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
472
- writeStateMd(statePath, content, cwd);
523
+ if (added) {
473
524
  output({ added: true, blocker: blockerText }, raw, 'true');
474
525
  } else {
475
526
  output({ added: false, reason: 'Blockers section not found in STATE.md' }, raw, 'false');
@@ -481,27 +532,33 @@ function cmdStateResolveBlocker(cwd, text, raw) {
481
532
  if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
482
533
  if (!text) { output({ error: 'text required' }, raw); return; }
483
534
 
484
- let content = fs.readFileSync(statePath, 'utf-8');
535
+ let resolved = false;
536
+
537
+ readModifyWriteStateMd(statePath, (content) => {
538
+ const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
539
+ const match = content.match(sectionPattern);
485
540
 
486
- const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
487
- const match = content.match(sectionPattern);
488
-
489
- if (match) {
490
- const sectionBody = match[2];
491
- const lines = sectionBody.split('\n');
492
- const filtered = lines.filter(line => {
493
- if (!line.startsWith('- ')) return true;
494
- return !line.toLowerCase().includes(text.toLowerCase());
495
- });
496
-
497
- let newBody = filtered.join('\n');
498
- // If section is now empty, add placeholder
499
- if (!newBody.trim() || !newBody.includes('- ')) {
500
- newBody = 'None\n';
541
+ if (match) {
542
+ const sectionBody = match[2];
543
+ const lines = sectionBody.split('\n');
544
+ const filtered = lines.filter(line => {
545
+ if (!line.startsWith('- ')) return true;
546
+ return !line.toLowerCase().includes(text.toLowerCase());
547
+ });
548
+
549
+ let newBody = filtered.join('\n');
550
+ // If section is now empty, add placeholder
551
+ if (!newBody.trim() || !newBody.includes('- ')) {
552
+ newBody = 'None\n';
553
+ }
554
+
555
+ resolved = true;
556
+ return content.replace(sectionPattern, (_match, header) => `${header}${newBody}`);
501
557
  }
558
+ return content;
559
+ }, cwd);
502
560
 
503
- content = content.replace(sectionPattern, (_match, header) => `${header}${newBody}`);
504
- writeStateMd(statePath, content, cwd);
561
+ if (resolved) {
505
562
  output({ resolved: true, blocker: text }, raw, 'true');
506
563
  } else {
507
564
  output({ resolved: false, reason: 'Blockers section not found in STATE.md' }, raw, 'false');
@@ -512,31 +569,33 @@ function cmdStateRecordSession(cwd, options, raw) {
512
569
  const statePath = planningPaths(cwd).state;
513
570
  if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
514
571
 
515
- let content = fs.readFileSync(statePath, 'utf-8');
516
572
  const now = new Date().toISOString();
517
573
  const updated = [];
518
574
 
519
- // Update Last session / Last Date
520
- let result = stateReplaceField(content, 'Last session', now);
521
- if (result) { content = result; updated.push('Last session'); }
522
- result = stateReplaceField(content, 'Last Date', now);
523
- if (result) { content = result; updated.push('Last Date'); }
524
-
525
- // Update Stopped at
526
- if (options.stopped_at) {
527
- result = stateReplaceField(content, 'Stopped At', options.stopped_at);
528
- if (!result) result = stateReplaceField(content, 'Stopped at', options.stopped_at);
529
- if (result) { content = result; updated.push('Stopped At'); }
530
- }
575
+ readModifyWriteStateMd(statePath, (content) => {
576
+ // Update Last session / Last Date
577
+ let result = stateReplaceField(content, 'Last session', now);
578
+ if (result) { content = result; updated.push('Last session'); }
579
+ result = stateReplaceField(content, 'Last Date', now);
580
+ if (result) { content = result; updated.push('Last Date'); }
581
+
582
+ // Update Stopped at
583
+ if (options.stopped_at) {
584
+ result = stateReplaceField(content, 'Stopped At', options.stopped_at);
585
+ if (!result) result = stateReplaceField(content, 'Stopped at', options.stopped_at);
586
+ if (result) { content = result; updated.push('Stopped At'); }
587
+ }
588
+
589
+ // Update Resume file
590
+ const resumeFile = options.resume_file || 'None';
591
+ result = stateReplaceField(content, 'Resume File', resumeFile);
592
+ if (!result) result = stateReplaceField(content, 'Resume file', resumeFile);
593
+ if (result) { content = result; updated.push('Resume File'); }
531
594
 
532
- // Update Resume file
533
- const resumeFile = options.resume_file || 'None';
534
- result = stateReplaceField(content, 'Resume File', resumeFile);
535
- if (!result) result = stateReplaceField(content, 'Resume file', resumeFile);
536
- if (result) { content = result; updated.push('Resume File'); }
595
+ return content;
596
+ }, cwd);
537
597
 
538
598
  if (updated.length > 0) {
539
- writeStateMd(statePath, content, cwd);
540
599
  output({ recorded: true, updated }, raw, 'true');
541
600
  } else {
542
601
  output({ recorded: false, reason: 'No session fields found in STATE.md' }, raw, 'false');
@@ -805,6 +864,9 @@ function acquireStateLock(statePath) {
805
864
  const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
806
865
  fs.writeSync(fd, String(process.pid));
807
866
  fs.closeSync(fd);
867
+ // Register for exit-time cleanup so process.exit(1) inside a locked region
868
+ // cannot leave a stale lock file (#1916).
869
+ _heldStateLocks.add(lockPath);
808
870
  return lockPath;
809
871
  } catch (err) {
810
872
  if (err.code === 'EEXIST') {
@@ -821,8 +883,7 @@ function acquireStateLock(statePath) {
821
883
  return lockPath;
822
884
  }
823
885
  const jitter = Math.floor(Math.random() * 50);
824
- const start = Date.now();
825
- while (Date.now() - start < retryDelay + jitter) { /* busy wait */ }
886
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, retryDelay + jitter);
826
887
  continue;
827
888
  }
828
889
  return lockPath; // non-EEXIST error — proceed without lock
@@ -832,6 +893,7 @@ function acquireStateLock(statePath) {
832
893
  }
833
894
 
834
895
  function releaseStateLock(lockPath) {
896
+ _heldStateLocks.delete(lockPath);
835
897
  try { fs.unlinkSync(lockPath); } catch { /* lock already gone */ }
836
898
  }
837
899
 
@@ -845,7 +907,7 @@ function writeStateMd(statePath, content, cwd) {
845
907
  const synced = syncStateFrontmatter(content, cwd);
846
908
  const lockPath = acquireStateLock(statePath);
847
909
  try {
848
- fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8');
910
+ atomicWriteFileSync(statePath, normalizeMd(synced), 'utf-8');
849
911
  } finally {
850
912
  releaseStateLock(lockPath);
851
913
  }
@@ -863,7 +925,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd) {
863
925
  const content = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8') : '';
864
926
  const modified = transformFn(content);
865
927
  const synced = syncStateFrontmatter(modified, cwd);
866
- fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8');
928
+ atomicWriteFileSync(statePath, normalizeMd(synced), 'utf-8');
867
929
  } finally {
868
930
  releaseStateLock(lockPath);
869
931
  }
@@ -913,96 +975,95 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
913
975
  return;
914
976
  }
915
977
 
916
- let content = fs.readFileSync(statePath, 'utf-8');
917
978
  const today = new Date().toISOString().split('T')[0];
918
979
  const updated = [];
919
980
 
920
- // Update Status field
921
- const statusValue = `Executing Phase ${phaseNumber}`;
922
- let result = stateReplaceField(content, 'Status', statusValue);
923
- if (result) { content = result; updated.push('Status'); }
924
-
925
- // Update Last Activity
926
- result = stateReplaceField(content, 'Last Activity', today);
927
- if (result) { content = result; updated.push('Last Activity'); }
928
-
929
- // Update Last Activity Description if it exists
930
- const activityDesc = `Phase ${phaseNumber} execution started`;
931
- result = stateReplaceField(content, 'Last Activity Description', activityDesc);
932
- if (result) { content = result; updated.push('Last Activity Description'); }
981
+ readModifyWriteStateMd(statePath, (content) => {
982
+ // Update Status field
983
+ const statusValue = `Executing Phase ${phaseNumber}`;
984
+ let result = stateReplaceField(content, 'Status', statusValue);
985
+ if (result) { content = result; updated.push('Status'); }
986
+
987
+ // Update Last Activity
988
+ result = stateReplaceField(content, 'Last Activity', today);
989
+ if (result) { content = result; updated.push('Last Activity'); }
990
+
991
+ // Update Last Activity Description if it exists
992
+ const activityDesc = `Phase ${phaseNumber} execution started`;
993
+ result = stateReplaceField(content, 'Last Activity Description', activityDesc);
994
+ if (result) { content = result; updated.push('Last Activity Description'); }
995
+
996
+ // Update Current Phase
997
+ result = stateReplaceField(content, 'Current Phase', String(phaseNumber));
998
+ if (result) { content = result; updated.push('Current Phase'); }
999
+
1000
+ // Update Current Phase Name
1001
+ if (phaseName) {
1002
+ result = stateReplaceField(content, 'Current Phase Name', phaseName);
1003
+ if (result) { content = result; updated.push('Current Phase Name'); }
1004
+ }
933
1005
 
934
- // Update Current Phase
935
- result = stateReplaceField(content, 'Current Phase', String(phaseNumber));
936
- if (result) { content = result; updated.push('Current Phase'); }
1006
+ // Update Current Plan to 1 (starting from the first plan)
1007
+ result = stateReplaceField(content, 'Current Plan', '1');
1008
+ if (result) { content = result; updated.push('Current Plan'); }
937
1009
 
938
- // Update Current Phase Name
939
- if (phaseName) {
940
- result = stateReplaceField(content, 'Current Phase Name', phaseName);
941
- if (result) { content = result; updated.push('Current Phase Name'); }
942
- }
943
-
944
- // Update Current Plan to 1 (starting from the first plan)
945
- result = stateReplaceField(content, 'Current Plan', '1');
946
- if (result) { content = result; updated.push('Current Plan'); }
1010
+ // Update Total Plans in Phase
1011
+ if (planCount) {
1012
+ result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
1013
+ if (result) { content = result; updated.push('Total Plans in Phase'); }
1014
+ }
947
1015
 
948
- // Update Total Plans in Phase
949
- if (planCount) {
950
- result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
951
- if (result) { content = result; updated.push('Total Plans in Phase'); }
952
- }
1016
+ // Update **Current focus:** body text line (#1104)
1017
+ const focusLabel = phaseName ? `Phase ${phaseNumber} — ${phaseName}` : `Phase ${phaseNumber}`;
1018
+ const focusPattern = /(\*\*Current focus:\*\*\s*).*/i;
1019
+ if (focusPattern.test(content)) {
1020
+ content = content.replace(focusPattern, (_match, prefix) => `${prefix}${focusLabel}`);
1021
+ updated.push('Current focus');
1022
+ }
953
1023
 
954
- // Update **Current focus:** body text line (#1104)
955
- const focusLabel = phaseName ? `Phase ${phaseNumber} ${phaseName}` : `Phase ${phaseNumber}`;
956
- const focusPattern = /(\*\*Current focus:\*\*\s*).*/i;
957
- if (focusPattern.test(content)) {
958
- content = content.replace(focusPattern, (_match, prefix) => `${prefix}${focusLabel}`);
959
- updated.push('Current focus');
960
- }
1024
+ // Update ## Current Position section (#1104, #1365)
1025
+ // Update individual fields within Current Position instead of replacing the
1026
+ // entire section, so that Status, Last activity, and Progress are preserved.
1027
+ const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
1028
+ const positionMatch = content.match(positionPattern);
1029
+ if (positionMatch) {
1030
+ const header = positionMatch[1];
1031
+ let posBody = positionMatch[2];
1032
+
1033
+ // Update or insert Phase line
1034
+ const newPhase = `Phase: ${phaseNumber}${phaseName ? ` (${phaseName})` : ''} — EXECUTING`;
1035
+ if (/^Phase:/m.test(posBody)) {
1036
+ posBody = posBody.replace(/^Phase:.*$/m, newPhase);
1037
+ } else {
1038
+ posBody = newPhase + '\n' + posBody;
1039
+ }
961
1040
 
962
- // Update ## Current Position section (#1104, #1365)
963
- // Update individual fields within Current Position instead of replacing the
964
- // entire section, so that Status, Last activity, and Progress are preserved.
965
- const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
966
- const positionMatch = content.match(positionPattern);
967
- if (positionMatch) {
968
- const header = positionMatch[1];
969
- let posBody = positionMatch[2];
970
-
971
- // Update or insert Phase line
972
- const newPhase = `Phase: ${phaseNumber}${phaseName ? ` (${phaseName})` : ''} — EXECUTING`;
973
- if (/^Phase:/m.test(posBody)) {
974
- posBody = posBody.replace(/^Phase:.*$/m, newPhase);
975
- } else {
976
- posBody = newPhase + '\n' + posBody;
977
- }
1041
+ // Update or insert Plan line
1042
+ const newPlan = `Plan: 1 of ${planCount || '?'}`;
1043
+ if (/^Plan:/m.test(posBody)) {
1044
+ posBody = posBody.replace(/^Plan:.*$/m, newPlan);
1045
+ } else {
1046
+ posBody = posBody.replace(/^(Phase:.*$)/m, `$1\n${newPlan}`);
1047
+ }
978
1048
 
979
- // Update or insert Plan line
980
- const newPlan = `Plan: 1 of ${planCount || '?'}`;
981
- if (/^Plan:/m.test(posBody)) {
982
- posBody = posBody.replace(/^Plan:.*$/m, newPlan);
983
- } else {
984
- posBody = posBody.replace(/^(Phase:.*$)/m, `$1\n${newPlan}`);
985
- }
1049
+ // Update Status line if present
1050
+ const newStatus = `Status: Executing Phase ${phaseNumber}`;
1051
+ if (/^Status:/m.test(posBody)) {
1052
+ posBody = posBody.replace(/^Status:.*$/m, newStatus);
1053
+ }
986
1054
 
987
- // Update Status line if present
988
- const newStatus = `Status: Executing Phase ${phaseNumber}`;
989
- if (/^Status:/m.test(posBody)) {
990
- posBody = posBody.replace(/^Status:.*$/m, newStatus);
991
- }
1055
+ // Update Last activity line if present
1056
+ const newActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution started`;
1057
+ if (/^Last activity:/im.test(posBody)) {
1058
+ posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
1059
+ }
992
1060
 
993
- // Update Last activity line if present
994
- const newActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution started`;
995
- if (/^Last activity:/im.test(posBody)) {
996
- posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
1061
+ content = content.replace(positionPattern, `${header}${posBody}`);
1062
+ updated.push('Current Position');
997
1063
  }
998
1064
 
999
- content = content.replace(positionPattern, `${header}${posBody}`);
1000
- updated.push('Current Position');
1001
- }
1002
-
1003
- if (updated.length > 0) {
1004
- writeStateMd(statePath, content, cwd);
1005
- }
1065
+ return content;
1066
+ }, cwd);
1006
1067
 
1007
1068
  output({ updated, phase: phaseNumber, phase_name: phaseName || null, plan_count: planCount || null }, raw, updated.length > 0 ? 'true' : 'false');
1008
1069
  }
@@ -1330,6 +1391,7 @@ module.exports = {
1330
1391
  stateReplaceField,
1331
1392
  stateReplaceFieldWithFallback,
1332
1393
  writeStateMd,
1394
+ readModifyWriteStateMd,
1333
1395
  updatePerformanceMetricsSection,
1334
1396
  cmdStateLoad,
1335
1397
  cmdStateGet,