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.
- package/agents/gsd-advisor-researcher.md +23 -0
- package/agents/gsd-ai-researcher.md +142 -0
- package/agents/gsd-code-fixer.md +523 -0
- package/agents/gsd-code-reviewer.md +361 -0
- package/agents/gsd-debugger.md +14 -1
- package/agents/gsd-domain-researcher.md +162 -0
- package/agents/gsd-eval-auditor.md +170 -0
- package/agents/gsd-eval-planner.md +161 -0
- package/agents/gsd-executor.md +70 -7
- package/agents/gsd-framework-selector.md +167 -0
- package/agents/gsd-intel-updater.md +320 -0
- package/agents/gsd-phase-researcher.md +26 -0
- package/agents/gsd-plan-checker.md +12 -0
- package/agents/gsd-planner.md +16 -6
- package/agents/gsd-project-researcher.md +23 -0
- package/agents/gsd-ui-researcher.md +23 -0
- package/agents/gsd-verifier.md +55 -1
- package/commands/gsd/gsd-ai-integration-phase.md +36 -0
- package/commands/gsd/gsd-audit-fix.md +33 -0
- package/commands/gsd/gsd-autonomous.md +1 -0
- package/commands/gsd/gsd-code-review-fix.md +52 -0
- package/commands/gsd/gsd-code-review.md +55 -0
- package/commands/gsd/gsd-eval-review.md +32 -0
- package/commands/gsd/gsd-explore.md +27 -0
- package/commands/gsd/gsd-from-gsd2.md +45 -0
- package/commands/gsd/gsd-import.md +36 -0
- package/commands/gsd/gsd-intel.md +183 -0
- package/commands/gsd/gsd-next.md +2 -0
- package/commands/gsd/gsd-reapply-patches.md +58 -3
- package/commands/gsd/gsd-review.md +4 -2
- package/commands/gsd/gsd-scan.md +26 -0
- package/commands/gsd/gsd-undo.md +34 -0
- package/commands/gsd/gsd-workstreams.md +6 -6
- package/get-shit-done/bin/gsd-tools.cjs +143 -5
- package/get-shit-done/bin/lib/commands.cjs +10 -2
- package/get-shit-done/bin/lib/config.cjs +71 -37
- package/get-shit-done/bin/lib/core.cjs +70 -8
- package/get-shit-done/bin/lib/gsd2-import.cjs +511 -0
- package/get-shit-done/bin/lib/init.cjs +20 -6
- package/get-shit-done/bin/lib/intel.cjs +660 -0
- package/get-shit-done/bin/lib/learnings.cjs +378 -0
- package/get-shit-done/bin/lib/milestone.cjs +25 -15
- package/get-shit-done/bin/lib/model-profiles.cjs +17 -17
- package/get-shit-done/bin/lib/phase.cjs +148 -112
- package/get-shit-done/bin/lib/roadmap.cjs +12 -5
- package/get-shit-done/bin/lib/security.cjs +119 -0
- package/get-shit-done/bin/lib/state.cjs +283 -221
- package/get-shit-done/bin/lib/template.cjs +8 -4
- package/get-shit-done/bin/lib/verify.cjs +42 -5
- package/get-shit-done/references/ai-evals.md +156 -0
- package/get-shit-done/references/ai-frameworks.md +186 -0
- package/get-shit-done/references/common-bug-patterns.md +114 -0
- package/get-shit-done/references/few-shot-examples/plan-checker.md +73 -0
- package/get-shit-done/references/few-shot-examples/verifier.md +109 -0
- package/get-shit-done/references/gates.md +70 -0
- package/get-shit-done/references/ios-scaffold.md +123 -0
- package/get-shit-done/references/model-profile-resolution.md +6 -7
- package/get-shit-done/references/model-profiles.md +20 -14
- package/get-shit-done/references/planning-config.md +237 -0
- package/get-shit-done/references/thinking-models-debug.md +44 -0
- package/get-shit-done/references/thinking-models-execution.md +50 -0
- package/get-shit-done/references/thinking-models-planning.md +62 -0
- package/get-shit-done/references/thinking-models-research.md +50 -0
- package/get-shit-done/references/thinking-models-verification.md +55 -0
- package/get-shit-done/references/thinking-partner.md +96 -0
- package/get-shit-done/references/universal-anti-patterns.md +6 -1
- package/get-shit-done/references/verification-overrides.md +227 -0
- package/get-shit-done/templates/AI-SPEC.md +246 -0
- package/get-shit-done/workflows/add-tests.md +3 -0
- package/get-shit-done/workflows/add-todo.md +2 -0
- package/get-shit-done/workflows/ai-integration-phase.md +284 -0
- package/get-shit-done/workflows/audit-fix.md +154 -0
- package/get-shit-done/workflows/autonomous.md +33 -2
- package/get-shit-done/workflows/check-todos.md +2 -0
- package/get-shit-done/workflows/cleanup.md +2 -0
- package/get-shit-done/workflows/code-review-fix.md +497 -0
- package/get-shit-done/workflows/code-review.md +515 -0
- package/get-shit-done/workflows/complete-milestone.md +40 -15
- package/get-shit-done/workflows/diagnose-issues.md +1 -1
- package/get-shit-done/workflows/discovery-phase.md +3 -1
- package/get-shit-done/workflows/discuss-phase-assumptions.md +1 -1
- package/get-shit-done/workflows/discuss-phase.md +21 -7
- package/get-shit-done/workflows/do.md +2 -0
- package/get-shit-done/workflows/docs-update.md +2 -0
- package/get-shit-done/workflows/eval-review.md +155 -0
- package/get-shit-done/workflows/execute-phase.md +307 -57
- package/get-shit-done/workflows/execute-plan.md +64 -93
- package/get-shit-done/workflows/explore.md +136 -0
- package/get-shit-done/workflows/help.md +1 -1
- package/get-shit-done/workflows/import.md +273 -0
- package/get-shit-done/workflows/inbox.md +387 -0
- package/get-shit-done/workflows/manager.md +4 -10
- package/get-shit-done/workflows/new-milestone.md +3 -1
- package/get-shit-done/workflows/new-project.md +2 -0
- package/get-shit-done/workflows/new-workspace.md +2 -0
- package/get-shit-done/workflows/next.md +56 -0
- package/get-shit-done/workflows/note.md +2 -0
- package/get-shit-done/workflows/plan-phase.md +97 -17
- package/get-shit-done/workflows/plant-seed.md +3 -0
- package/get-shit-done/workflows/pr-branch.md +41 -13
- package/get-shit-done/workflows/profile-user.md +4 -2
- package/get-shit-done/workflows/quick.md +99 -4
- package/get-shit-done/workflows/remove-workspace.md +2 -0
- package/get-shit-done/workflows/review.md +53 -6
- package/get-shit-done/workflows/scan.md +98 -0
- package/get-shit-done/workflows/secure-phase.md +2 -0
- package/get-shit-done/workflows/settings.md +18 -3
- package/get-shit-done/workflows/ship.md +3 -0
- package/get-shit-done/workflows/ui-phase.md +10 -2
- package/get-shit-done/workflows/ui-review.md +2 -0
- package/get-shit-done/workflows/undo.md +314 -0
- package/get-shit-done/workflows/update.md +2 -0
- package/get-shit-done/workflows/validate-phase.md +2 -0
- package/get-shit-done/workflows/verify-phase.md +83 -0
- package/get-shit-done/workflows/verify-work.md +12 -1
- package/package.json +1 -1
- package/skills/gsd-code-review/SKILL.md +48 -0
- 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
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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 (
|
|
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 (
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
372
|
+
if (metricsMatch) {
|
|
373
|
+
let tableBody = metricsMatch[2].trimEnd();
|
|
374
|
+
const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
|
|
348
375
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
393
|
-
const
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
533
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
if (
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
994
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
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,
|