@bhargavvc/sdd-cc 1.30.0 → 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/README.ja-JP.md +144 -110
- package/README.ko-KR.md +143 -107
- package/README.md +183 -112
- package/README.pt-BR.md +90 -52
- package/README.zh-CN.md +141 -101
- package/agents/sdd-advisor-researcher.md +23 -0
- package/agents/sdd-ai-researcher.md +133 -0
- package/agents/sdd-code-fixer.md +516 -0
- package/agents/sdd-code-reviewer.md +355 -0
- package/agents/sdd-codebase-mapper.md +3 -3
- package/agents/sdd-debugger.md +17 -5
- package/agents/sdd-doc-verifier.md +201 -0
- package/agents/sdd-doc-writer.md +602 -0
- package/agents/sdd-domain-researcher.md +153 -0
- package/agents/sdd-eval-auditor.md +164 -0
- package/agents/sdd-eval-planner.md +154 -0
- package/agents/sdd-executor.md +87 -4
- package/agents/sdd-framework-selector.md +160 -0
- package/agents/sdd-intel-updater.md +314 -0
- package/agents/sdd-nyquist-auditor.md +1 -1
- package/agents/sdd-phase-researcher.md +71 -4
- package/agents/sdd-plan-checker.md +100 -6
- package/agents/sdd-planner.md +145 -206
- package/agents/sdd-project-researcher.md +25 -2
- package/agents/sdd-research-synthesizer.md +3 -3
- package/agents/sdd-roadmapper.md +6 -6
- package/agents/sdd-security-auditor.md +128 -0
- package/agents/sdd-ui-auditor.md +43 -3
- package/agents/sdd-ui-checker.md +5 -5
- package/agents/sdd-ui-researcher.md +27 -4
- package/agents/sdd-user-profiler.md +2 -2
- package/agents/sdd-verifier.md +142 -22
- package/bin/install.js +2151 -551
- package/commands/sdd/add-backlog.md +5 -5
- package/commands/sdd/add-tests.md +2 -2
- package/commands/sdd/ai-integration-phase.md +36 -0
- package/commands/sdd/analyze-dependencies.md +34 -0
- package/commands/sdd/audit-fix.md +33 -0
- package/commands/sdd/autonomous.md +7 -2
- package/commands/sdd/cleanup.md +5 -0
- package/commands/sdd/code-review-fix.md +52 -0
- package/commands/sdd/code-review.md +55 -0
- package/commands/sdd/complete-milestone.md +6 -6
- package/commands/sdd/debug.md +22 -9
- package/commands/sdd/discuss-phase.md +7 -2
- package/commands/sdd/do.md +1 -1
- package/commands/sdd/docs-update.md +48 -0
- package/commands/sdd/eval-review.md +32 -0
- package/commands/sdd/execute-phase.md +4 -0
- package/commands/sdd/explore.md +27 -0
- package/commands/sdd/fast.md +2 -2
- package/commands/sdd/from-sdd2.md +45 -0
- package/commands/sdd/help.md +2 -0
- package/commands/sdd/import.md +36 -0
- package/commands/sdd/intel.md +179 -0
- package/commands/sdd/join-discord.md +2 -1
- package/commands/sdd/manager.md +1 -0
- package/commands/sdd/map-codebase.md +3 -3
- package/commands/sdd/new-milestone.md +1 -1
- package/commands/sdd/new-project.md +5 -1
- package/commands/sdd/new-workspace.md +1 -1
- package/commands/sdd/next.md +2 -0
- package/commands/sdd/plan-milestone-gaps.md +2 -2
- package/commands/sdd/plan-phase.md +6 -1
- package/commands/sdd/plant-seed.md +1 -1
- package/commands/sdd/profile-user.md +1 -1
- package/commands/sdd/quick.md +5 -3
- package/commands/sdd/reapply-patches.md +230 -42
- package/commands/sdd/research-phase.md +3 -3
- package/commands/sdd/review-backlog.md +1 -0
- package/commands/sdd/review.md +6 -3
- package/commands/sdd/scan.md +26 -0
- package/commands/sdd/secure-phase.md +35 -0
- package/commands/sdd/ship.md +1 -1
- package/commands/sdd/thread.md +5 -5
- package/commands/sdd/undo.md +34 -0
- package/commands/sdd/verify-work.md +1 -1
- package/commands/sdd/workstreams.md +17 -11
- package/hooks/dist/sdd-check-update.js +33 -8
- package/hooks/dist/sdd-context-monitor.js +17 -8
- package/hooks/dist/sdd-phase-boundary.sh +27 -0
- package/hooks/dist/sdd-prompt-guard.js +1 -0
- package/hooks/dist/sdd-read-guard.js +82 -0
- package/hooks/dist/sdd-session-state.sh +33 -0
- package/hooks/dist/sdd-statusline.js +137 -15
- package/hooks/dist/sdd-validate-commit.sh +47 -0
- package/hooks/dist/sdd-workflow-guard.js +4 -4
- package/hooks/sdd-check-update.js +139 -0
- package/hooks/sdd-context-monitor.js +165 -0
- package/hooks/sdd-phase-boundary.sh +27 -0
- package/hooks/sdd-prompt-guard.js +97 -0
- package/hooks/sdd-read-guard.js +82 -0
- package/hooks/sdd-session-state.sh +33 -0
- package/hooks/sdd-statusline.js +241 -0
- package/hooks/sdd-validate-commit.sh +47 -0
- package/hooks/sdd-workflow-guard.js +94 -0
- package/package.json +3 -3
- package/scripts/build-hooks.js +18 -7
- package/scripts/prompt-injection-scan.sh +1 -0
- package/scripts/rebrand-gsd-to-sdd.sh +221 -220
- package/scripts/run-tests.cjs +5 -1
- package/scripts/sync-upstream.sh +1 -1
- package/sdd/bin/lib/commands.cjs +79 -17
- package/sdd/bin/lib/config.cjs +90 -48
- package/sdd/bin/lib/core.cjs +452 -87
- package/sdd/bin/lib/docs.cjs +267 -0
- package/sdd/bin/lib/frontmatter.cjs +381 -336
- package/sdd/bin/lib/init.cjs +110 -16
- package/sdd/bin/lib/intel.cjs +660 -0
- package/sdd/bin/lib/learnings.cjs +378 -0
- package/sdd/bin/lib/milestone.cjs +42 -11
- package/sdd/bin/lib/model-profiles.cjs +17 -15
- package/sdd/bin/lib/phase.cjs +367 -288
- package/sdd/bin/lib/profile-output.cjs +106 -10
- package/sdd/bin/lib/roadmap.cjs +146 -115
- package/sdd/bin/lib/schema-detect.cjs +238 -0
- package/sdd/bin/lib/sdd2-import.cjs +511 -0
- package/sdd/bin/lib/security.cjs +124 -3
- package/sdd/bin/lib/state.cjs +648 -264
- package/sdd/bin/lib/template.cjs +8 -4
- package/sdd/bin/lib/verify.cjs +209 -28
- package/sdd/bin/lib/workstream.cjs +7 -3
- package/sdd/bin/sdd-tools.cjs +184 -12
- package/sdd/contexts/dev.md +21 -0
- package/sdd/contexts/research.md +22 -0
- package/sdd/contexts/review.md +22 -0
- package/sdd/references/agent-contracts.md +79 -0
- package/sdd/references/ai-evals.md +156 -0
- package/sdd/references/ai-frameworks.md +186 -0
- package/sdd/references/artifact-types.md +113 -0
- package/sdd/references/common-bug-patterns.md +114 -0
- package/sdd/references/context-budget.md +49 -0
- package/sdd/references/continuation-format.md +25 -25
- package/sdd/references/domain-probes.md +125 -0
- package/sdd/references/few-shot-examples/plan-checker.md +73 -0
- package/sdd/references/few-shot-examples/verifier.md +109 -0
- package/sdd/references/gate-prompts.md +100 -0
- package/sdd/references/gates.md +70 -0
- package/sdd/references/git-integration.md +1 -1
- package/sdd/references/ios-scaffold.md +123 -0
- package/sdd/references/model-profile-resolution.md +2 -0
- package/sdd/references/model-profiles.md +24 -18
- package/sdd/references/planner-gap-closure.md +62 -0
- package/sdd/references/planner-reviews.md +39 -0
- package/sdd/references/planner-revision.md +87 -0
- package/sdd/references/planning-config.md +252 -0
- package/sdd/references/revision-loop.md +97 -0
- package/sdd/references/thinking-models-debug.md +44 -0
- package/sdd/references/thinking-models-execution.md +50 -0
- package/sdd/references/thinking-models-planning.md +62 -0
- package/sdd/references/thinking-models-research.md +50 -0
- package/sdd/references/thinking-models-verification.md +55 -0
- package/sdd/references/thinking-partner.md +96 -0
- package/sdd/references/ui-brand.md +4 -4
- package/sdd/references/universal-anti-patterns.md +63 -0
- package/sdd/references/verification-overrides.md +227 -0
- package/sdd/references/workstream-flag.md +56 -3
- package/sdd/templates/AI-SPEC.md +246 -0
- package/sdd/templates/DEBUG.md +1 -1
- package/sdd/templates/SECURITY.md +61 -0
- package/sdd/templates/UAT.md +4 -4
- package/sdd/templates/VALIDATION.md +4 -4
- package/sdd/templates/claude-md.md +32 -9
- package/sdd/templates/config.json +4 -0
- package/sdd/templates/debug-subagent-prompt.md +1 -1
- package/sdd/templates/dev-preferences.md +1 -1
- package/sdd/templates/discovery.md +2 -2
- package/sdd/templates/phase-prompt.md +1 -1
- package/sdd/templates/planner-subagent-prompt.md +3 -3
- package/sdd/templates/project.md +1 -1
- package/sdd/templates/research.md +1 -1
- package/sdd/templates/state.md +2 -2
- package/sdd/workflows/add-phase.md +8 -8
- package/sdd/workflows/add-tests.md +12 -9
- package/sdd/workflows/add-todo.md +5 -3
- package/sdd/workflows/ai-integration-phase.md +284 -0
- package/sdd/workflows/analyze-dependencies.md +96 -0
- package/sdd/workflows/audit-fix.md +157 -0
- package/sdd/workflows/audit-milestone.md +11 -11
- package/sdd/workflows/audit-uat.md +2 -2
- package/sdd/workflows/autonomous.md +195 -27
- package/sdd/workflows/check-todos.md +12 -10
- package/sdd/workflows/cleanup.md +2 -0
- package/sdd/workflows/code-review-fix.md +497 -0
- package/sdd/workflows/code-review.md +515 -0
- package/sdd/workflows/complete-milestone.md +56 -22
- package/sdd/workflows/diagnose-issues.md +10 -3
- package/sdd/workflows/discovery-phase.md +5 -3
- package/sdd/workflows/discuss-phase-assumptions.md +24 -6
- package/sdd/workflows/discuss-phase-power.md +291 -0
- package/sdd/workflows/discuss-phase.md +173 -21
- package/sdd/workflows/do.md +23 -21
- package/sdd/workflows/docs-update.md +1155 -0
- package/sdd/workflows/eval-review.md +155 -0
- package/sdd/workflows/execute-phase.md +594 -38
- package/sdd/workflows/execute-plan.md +67 -96
- package/sdd/workflows/explore.md +139 -0
- package/sdd/workflows/fast.md +5 -5
- package/sdd/workflows/forensics.md +2 -2
- package/sdd/workflows/health.md +4 -4
- package/sdd/workflows/help.md +122 -119
- package/sdd/workflows/import.md +276 -0
- package/sdd/workflows/inbox.md +387 -0
- package/sdd/workflows/insert-phase.md +7 -7
- package/sdd/workflows/list-phase-assumptions.md +4 -4
- package/sdd/workflows/list-workspaces.md +2 -2
- package/sdd/workflows/manager.md +35 -32
- package/sdd/workflows/map-codebase.md +7 -5
- package/sdd/workflows/milestone-summary.md +2 -2
- package/sdd/workflows/new-milestone.md +17 -9
- package/sdd/workflows/new-project.md +50 -25
- package/sdd/workflows/new-workspace.md +7 -5
- package/sdd/workflows/next.md +67 -11
- package/sdd/workflows/note.md +9 -7
- package/sdd/workflows/pause-work.md +75 -12
- package/sdd/workflows/plan-milestone-gaps.md +8 -8
- package/sdd/workflows/plan-phase.md +294 -42
- package/sdd/workflows/plant-seed.md +6 -3
- package/sdd/workflows/pr-branch.md +42 -14
- package/sdd/workflows/profile-user.md +9 -7
- package/sdd/workflows/progress.md +45 -45
- package/sdd/workflows/quick.md +195 -47
- package/sdd/workflows/remove-phase.md +6 -6
- package/sdd/workflows/remove-workspace.md +3 -1
- package/sdd/workflows/research-phase.md +2 -2
- package/sdd/workflows/resume-project.md +12 -12
- package/sdd/workflows/review.md +109 -9
- package/sdd/workflows/scan.md +102 -0
- package/sdd/workflows/secure-phase.md +166 -0
- package/sdd/workflows/session-report.md +2 -2
- package/sdd/workflows/settings.md +38 -12
- package/sdd/workflows/ship.md +21 -9
- package/sdd/workflows/stats.md +1 -1
- package/sdd/workflows/transition.md +23 -23
- package/sdd/workflows/ui-phase.md +15 -7
- package/sdd/workflows/ui-review.md +29 -4
- package/sdd/workflows/undo.md +314 -0
- package/sdd/workflows/update.md +171 -20
- package/sdd/workflows/validate-phase.md +6 -4
- package/sdd/workflows/verify-phase.md +210 -6
- package/sdd/workflows/verify-work.md +83 -9
- package/sdd/commands/sdd/workstreams.md +0 -63
package/sdd/bin/lib/state.cjs
CHANGED
|
@@ -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) {
|
|
@@ -141,29 +151,28 @@ function cmdStatePatch(cwd, patches, raw) {
|
|
|
141
151
|
|
|
142
152
|
const statePath = planningPaths(cwd).state;
|
|
143
153
|
try {
|
|
144
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
145
154
|
const results = { updated: [], failed: [] };
|
|
146
155
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
// Use atomic read-modify-write to prevent lost updates from concurrent agents
|
|
157
|
+
readModifyWriteStateMd(statePath, (content) => {
|
|
158
|
+
for (const [field, value] of Object.entries(patches)) {
|
|
159
|
+
const fieldEscaped = escapeRegex(field);
|
|
160
|
+
// Try **Field:** bold format first, then plain Field: format
|
|
161
|
+
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
|
162
|
+
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
|
163
|
+
|
|
164
|
+
if (boldPattern.test(content)) {
|
|
165
|
+
content = content.replace(boldPattern, (_match, prefix) => `${prefix}${value}`);
|
|
166
|
+
results.updated.push(field);
|
|
167
|
+
} else if (plainPattern.test(content)) {
|
|
168
|
+
content = content.replace(plainPattern, (_match, prefix) => `${prefix}${value}`);
|
|
169
|
+
results.updated.push(field);
|
|
170
|
+
} else {
|
|
171
|
+
results.failed.push(field);
|
|
172
|
+
}
|
|
161
173
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (results.updated.length > 0) {
|
|
165
|
-
writeStateMd(statePath, content, cwd);
|
|
166
|
-
}
|
|
174
|
+
return content;
|
|
175
|
+
}, cwd);
|
|
167
176
|
|
|
168
177
|
output(results, raw, results.updated.length > 0 ? 'true' : 'false');
|
|
169
178
|
} catch {
|
|
@@ -185,18 +194,22 @@ function cmdStateUpdate(cwd, field, value) {
|
|
|
185
194
|
|
|
186
195
|
const statePath = planningPaths(cwd).state;
|
|
187
196
|
try {
|
|
188
|
-
let
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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) {
|
|
200
213
|
output({ updated: true });
|
|
201
214
|
} else {
|
|
202
215
|
output({ updated: false, reason: `Field "${field}" not found in STATE.md` });
|
|
@@ -236,6 +249,12 @@ function stateReplaceFieldWithFallback(content, primary, fallback, value) {
|
|
|
236
249
|
result = stateReplaceField(content, fallback, value);
|
|
237
250
|
if (result) return result;
|
|
238
251
|
}
|
|
252
|
+
// Neither pattern matched — field may have been reformatted or removed.
|
|
253
|
+
// Log diagnostic so template drift is detected early rather than silently swallowed.
|
|
254
|
+
process.stderr.write(
|
|
255
|
+
`[sdd-tools] WARNING: STATE.md field "${primary}"${fallback ? ` (fallback: "${fallback}")` : ''} not found — update skipped. ` +
|
|
256
|
+
`This may indicate STATE.md was externally modified or uses an unexpected format.\n`
|
|
257
|
+
);
|
|
239
258
|
return content;
|
|
240
259
|
}
|
|
241
260
|
|
|
@@ -269,55 +288,67 @@ function cmdStateAdvancePlan(cwd, raw) {
|
|
|
269
288
|
const statePath = planningPaths(cwd).state;
|
|
270
289
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
271
290
|
|
|
272
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
273
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
|
+
}
|
|
274
313
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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);
|
|
293
342
|
|
|
294
|
-
if (
|
|
343
|
+
if (!result || result.error) {
|
|
295
344
|
output({ error: 'Cannot parse Current Plan or Total Plans in Phase from STATE.md' }, raw);
|
|
296
345
|
return;
|
|
297
346
|
}
|
|
298
347
|
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
|
|
302
|
-
content = updateCurrentPositionFields(content, { status: 'Phase complete — ready for verification', lastActivity: today });
|
|
303
|
-
writeStateMd(statePath, content, cwd);
|
|
304
|
-
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');
|
|
305
350
|
} else {
|
|
306
|
-
|
|
307
|
-
let planDisplayValue;
|
|
308
|
-
if (useCompoundFormat) {
|
|
309
|
-
// Preserve compound format: "X of Y in current phase" → replace X only
|
|
310
|
-
planDisplayValue = planField.replace(/^\d+/, String(newPlan));
|
|
311
|
-
content = stateReplaceField(content, 'Plan', planDisplayValue) || content;
|
|
312
|
-
} else {
|
|
313
|
-
planDisplayValue = `${newPlan} of ${totalPlans}`;
|
|
314
|
-
content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
|
|
315
|
-
}
|
|
316
|
-
content = stateReplaceFieldWithFallback(content, 'Status', null, 'Ready to execute');
|
|
317
|
-
content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
|
|
318
|
-
content = updateCurrentPositionFields(content, { status: 'Ready to execute', lastActivity: today, plan: planDisplayValue });
|
|
319
|
-
writeStateMd(statePath, content, cwd);
|
|
320
|
-
output({ advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans }, raw, 'true');
|
|
351
|
+
output(result, raw, 'true');
|
|
321
352
|
}
|
|
322
353
|
}
|
|
323
354
|
|
|
@@ -325,7 +356,6 @@ function cmdStateRecordMetric(cwd, options, raw) {
|
|
|
325
356
|
const statePath = planningPaths(cwd).state;
|
|
326
357
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
327
358
|
|
|
328
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
329
359
|
const { phase, plan, duration, tasks, files } = options;
|
|
330
360
|
|
|
331
361
|
if (!phase || !plan || !duration) {
|
|
@@ -333,22 +363,29 @@ function cmdStateRecordMetric(cwd, options, raw) {
|
|
|
333
363
|
return;
|
|
334
364
|
}
|
|
335
365
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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);
|
|
339
371
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
372
|
+
if (metricsMatch) {
|
|
373
|
+
let tableBody = metricsMatch[2].trimEnd();
|
|
374
|
+
const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
|
|
343
375
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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`);
|
|
348
384
|
}
|
|
385
|
+
return content;
|
|
386
|
+
}, cwd);
|
|
349
387
|
|
|
350
|
-
|
|
351
|
-
writeStateMd(statePath, content, cwd);
|
|
388
|
+
if (recorded) {
|
|
352
389
|
output({ recorded: true, phase, plan, duration }, raw, 'true');
|
|
353
390
|
} else {
|
|
354
391
|
output({ recorded: false, reason: 'Performance Metrics section not found in STATE.md' }, raw, 'false');
|
|
@@ -359,9 +396,7 @@ function cmdStateUpdateProgress(cwd, raw) {
|
|
|
359
396
|
const statePath = planningPaths(cwd).state;
|
|
360
397
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
361
398
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
// Count summaries across current milestone phases only
|
|
399
|
+
// Count summaries across current milestone phases only (outside lock — read-only)
|
|
365
400
|
const phasesDir = planningPaths(cwd).phases;
|
|
366
401
|
let totalPlans = 0;
|
|
367
402
|
let totalSummaries = 0;
|
|
@@ -384,17 +419,26 @@ function cmdStateUpdateProgress(cwd, raw) {
|
|
|
384
419
|
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
385
420
|
const progressStr = `[${bar}] ${percent}%`;
|
|
386
421
|
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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);
|
|
398
442
|
} else {
|
|
399
443
|
output({ updated: false, reason: 'Progress field not found in STATE.md' }, raw, 'false');
|
|
400
444
|
}
|
|
@@ -418,20 +462,26 @@ function cmdStateAddDecision(cwd, options, raw) {
|
|
|
418
462
|
|
|
419
463
|
if (!summaryText) { output({ error: 'summary required' }, raw); return; }
|
|
420
464
|
|
|
421
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
422
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);
|
|
423
483
|
|
|
424
|
-
|
|
425
|
-
const sectionPattern = /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
426
|
-
const match = content.match(sectionPattern);
|
|
427
|
-
|
|
428
|
-
if (match) {
|
|
429
|
-
let sectionBody = match[2];
|
|
430
|
-
// Remove placeholders
|
|
431
|
-
sectionBody = sectionBody.replace(/None yet\.?\s*\n?/gi, '').replace(/No decisions yet\.?\s*\n?/gi, '');
|
|
432
|
-
sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
|
|
433
|
-
content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
|
|
434
|
-
writeStateMd(statePath, content, cwd);
|
|
484
|
+
if (added) {
|
|
435
485
|
output({ added: true, decision: entry }, raw, 'true');
|
|
436
486
|
} else {
|
|
437
487
|
output({ added: false, reason: 'Decisions section not found in STATE.md' }, raw, 'false');
|
|
@@ -453,18 +503,24 @@ function cmdStateAddBlocker(cwd, text, raw) {
|
|
|
453
503
|
|
|
454
504
|
if (!blockerText) { output({ error: 'text required' }, raw); return; }
|
|
455
505
|
|
|
456
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
457
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);
|
|
458
522
|
|
|
459
|
-
|
|
460
|
-
const match = content.match(sectionPattern);
|
|
461
|
-
|
|
462
|
-
if (match) {
|
|
463
|
-
let sectionBody = match[2];
|
|
464
|
-
sectionBody = sectionBody.replace(/None\.?\s*\n?/gi, '').replace(/None yet\.?\s*\n?/gi, '');
|
|
465
|
-
sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
|
|
466
|
-
content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
|
|
467
|
-
writeStateMd(statePath, content, cwd);
|
|
523
|
+
if (added) {
|
|
468
524
|
output({ added: true, blocker: blockerText }, raw, 'true');
|
|
469
525
|
} else {
|
|
470
526
|
output({ added: false, reason: 'Blockers section not found in STATE.md' }, raw, 'false');
|
|
@@ -476,27 +532,33 @@ function cmdStateResolveBlocker(cwd, text, raw) {
|
|
|
476
532
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
477
533
|
if (!text) { output({ error: 'text required' }, raw); return; }
|
|
478
534
|
|
|
479
|
-
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);
|
|
540
|
+
|
|
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
|
+
});
|
|
480
548
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
return !line.toLowerCase().includes(text.toLowerCase());
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
let newBody = filtered.join('\n');
|
|
493
|
-
// If section is now empty, add placeholder
|
|
494
|
-
if (!newBody.trim() || !newBody.includes('- ')) {
|
|
495
|
-
newBody = 'None\n';
|
|
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}`);
|
|
496
557
|
}
|
|
558
|
+
return content;
|
|
559
|
+
}, cwd);
|
|
497
560
|
|
|
498
|
-
|
|
499
|
-
writeStateMd(statePath, content, cwd);
|
|
561
|
+
if (resolved) {
|
|
500
562
|
output({ resolved: true, blocker: text }, raw, 'true');
|
|
501
563
|
} else {
|
|
502
564
|
output({ resolved: false, reason: 'Blockers section not found in STATE.md' }, raw, 'false');
|
|
@@ -507,31 +569,33 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
507
569
|
const statePath = planningPaths(cwd).state;
|
|
508
570
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
509
571
|
|
|
510
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
511
572
|
const now = new Date().toISOString();
|
|
512
573
|
const updated = [];
|
|
513
574
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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'); }
|
|
526
594
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
result = stateReplaceField(content, 'Resume File', resumeFile);
|
|
530
|
-
if (!result) result = stateReplaceField(content, 'Resume file', resumeFile);
|
|
531
|
-
if (result) { content = result; updated.push('Resume File'); }
|
|
595
|
+
return content;
|
|
596
|
+
}, cwd);
|
|
532
597
|
|
|
533
598
|
if (updated.length > 0) {
|
|
534
|
-
writeStateMd(statePath, content, cwd);
|
|
535
599
|
output({ recorded: true, updated }, raw, 'true');
|
|
536
600
|
} else {
|
|
537
601
|
output({ recorded: false, reason: 'No session fields found in STATE.md' }, raw, 'false');
|
|
@@ -699,8 +763,14 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
699
763
|
} catch { /* intentionally empty */ }
|
|
700
764
|
}
|
|
701
765
|
|
|
766
|
+
// Derive percent from disk counts when available (ground truth).
|
|
767
|
+
// Only falls back to the body Progress: field when no plan files exist on disk
|
|
768
|
+
// (phases directory empty or absent), which means disk has no authoritative data.
|
|
769
|
+
// This prevents a stale body "0%" from overriding the real 100% completion state.
|
|
702
770
|
let progressPercent = null;
|
|
703
|
-
if (
|
|
771
|
+
if (totalPlans !== null && totalPlans > 0 && completedPlans !== null) {
|
|
772
|
+
progressPercent = Math.min(100, Math.round(completedPlans / totalPlans * 100));
|
|
773
|
+
} else if (progressRaw) {
|
|
704
774
|
const pctMatch = progressRaw.match(/(\d+)%/);
|
|
705
775
|
if (pctMatch) progressPercent = parseInt(pctMatch[1], 10);
|
|
706
776
|
}
|
|
@@ -781,55 +851,83 @@ function syncStateFrontmatter(content, cwd) {
|
|
|
781
851
|
}
|
|
782
852
|
|
|
783
853
|
/**
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
* Uses a simple lockfile to prevent parallel agents from overwriting
|
|
787
|
-
* each other's changes (race condition with read-modify-write cycle).
|
|
854
|
+
* Acquire a lockfile for STATE.md operations.
|
|
855
|
+
* Returns the lock path for later release.
|
|
788
856
|
*/
|
|
789
|
-
function
|
|
790
|
-
const synced = syncStateFrontmatter(content, cwd);
|
|
857
|
+
function acquireStateLock(statePath) {
|
|
791
858
|
const lockPath = statePath + '.lock';
|
|
792
859
|
const maxRetries = 10;
|
|
793
860
|
const retryDelay = 200; // ms
|
|
794
861
|
|
|
795
|
-
// Acquire lock (spin with backoff)
|
|
796
862
|
for (let i = 0; i < maxRetries; i++) {
|
|
797
863
|
try {
|
|
798
|
-
// O_EXCL fails if file already exists — atomic lock
|
|
799
864
|
const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
|
800
865
|
fs.writeSync(fd, String(process.pid));
|
|
801
866
|
fs.closeSync(fd);
|
|
802
|
-
|
|
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);
|
|
870
|
+
return lockPath;
|
|
803
871
|
} catch (err) {
|
|
804
872
|
if (err.code === 'EEXIST') {
|
|
805
|
-
// Check for stale lock (> 10s old)
|
|
806
873
|
try {
|
|
807
874
|
const stat = fs.statSync(lockPath);
|
|
808
875
|
if (Date.now() - stat.mtimeMs > 10000) {
|
|
809
876
|
fs.unlinkSync(lockPath);
|
|
810
|
-
continue;
|
|
877
|
+
continue;
|
|
811
878
|
}
|
|
812
879
|
} catch { /* lock was released between check — retry */ }
|
|
813
880
|
|
|
814
881
|
if (i === maxRetries - 1) {
|
|
815
|
-
// Last resort: write anyway rather than losing data
|
|
816
882
|
try { fs.unlinkSync(lockPath); } catch {}
|
|
817
|
-
|
|
883
|
+
return lockPath;
|
|
818
884
|
}
|
|
819
|
-
// Spin-wait with small jitter
|
|
820
885
|
const jitter = Math.floor(Math.random() * 50);
|
|
821
|
-
|
|
822
|
-
while (Date.now() - start < retryDelay + jitter) { /* busy wait */ }
|
|
886
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, retryDelay + jitter);
|
|
823
887
|
continue;
|
|
824
888
|
}
|
|
825
|
-
|
|
889
|
+
return lockPath; // non-EEXIST error — proceed without lock
|
|
826
890
|
}
|
|
827
891
|
}
|
|
892
|
+
return statePath + '.lock';
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function releaseStateLock(lockPath) {
|
|
896
|
+
_heldStateLocks.delete(lockPath);
|
|
897
|
+
try { fs.unlinkSync(lockPath); } catch { /* lock already gone */ }
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Write STATE.md with synchronized YAML frontmatter.
|
|
902
|
+
* All STATE.md writes should use this instead of raw writeFileSync.
|
|
903
|
+
* Uses a simple lockfile to prevent parallel agents from overwriting
|
|
904
|
+
* each other's changes (race condition with read-modify-write cycle).
|
|
905
|
+
*/
|
|
906
|
+
function writeStateMd(statePath, content, cwd) {
|
|
907
|
+
const synced = syncStateFrontmatter(content, cwd);
|
|
908
|
+
const lockPath = acquireStateLock(statePath);
|
|
909
|
+
try {
|
|
910
|
+
atomicWriteFileSync(statePath, normalizeMd(synced), 'utf-8');
|
|
911
|
+
} finally {
|
|
912
|
+
releaseStateLock(lockPath);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
828
915
|
|
|
916
|
+
/**
|
|
917
|
+
* Atomic read-modify-write for STATE.md.
|
|
918
|
+
* Holds the lock across the entire read -> transform -> write cycle,
|
|
919
|
+
* preventing the lost-update problem where two agents read the same
|
|
920
|
+
* content and the second write clobbers the first.
|
|
921
|
+
*/
|
|
922
|
+
function readModifyWriteStateMd(statePath, transformFn, cwd) {
|
|
923
|
+
const lockPath = acquireStateLock(statePath);
|
|
829
924
|
try {
|
|
830
|
-
fs.
|
|
925
|
+
const content = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8') : '';
|
|
926
|
+
const modified = transformFn(content);
|
|
927
|
+
const synced = syncStateFrontmatter(modified, cwd);
|
|
928
|
+
atomicWriteFileSync(statePath, normalizeMd(synced), 'utf-8');
|
|
831
929
|
} finally {
|
|
832
|
-
|
|
930
|
+
releaseStateLock(lockPath);
|
|
833
931
|
}
|
|
834
932
|
}
|
|
835
933
|
|
|
@@ -841,16 +939,27 @@ function cmdStateJson(cwd, raw) {
|
|
|
841
939
|
}
|
|
842
940
|
|
|
843
941
|
const content = fs.readFileSync(statePath, 'utf-8');
|
|
844
|
-
const
|
|
942
|
+
const existingFm = extractFrontmatter(content);
|
|
943
|
+
const body = stripFrontmatter(content);
|
|
845
944
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
945
|
+
// Always rebuild from body + disk so progress counters reflect current state.
|
|
946
|
+
// Returning cached frontmatter directly causes stale percent/completed_plans
|
|
947
|
+
// when SUMMARY files were added after the last STATE.md write (#1589).
|
|
948
|
+
const built = buildStateFrontmatter(body, cwd);
|
|
949
|
+
|
|
950
|
+
// Preserve frontmatter-only fields that cannot be recovered from the body.
|
|
951
|
+
if (existingFm && existingFm.stopped_at && !built.stopped_at) {
|
|
952
|
+
built.stopped_at = existingFm.stopped_at;
|
|
953
|
+
}
|
|
954
|
+
if (existingFm && existingFm.paused_at && !built.paused_at) {
|
|
955
|
+
built.paused_at = existingFm.paused_at;
|
|
956
|
+
}
|
|
957
|
+
// Preserve existing status when body-derived status is 'unknown' (same logic as syncStateFrontmatter).
|
|
958
|
+
if (built.status === 'unknown' && existingFm && existingFm.status && existingFm.status !== 'unknown') {
|
|
959
|
+
built.status = existingFm.status;
|
|
851
960
|
}
|
|
852
961
|
|
|
853
|
-
output(
|
|
962
|
+
output(built, raw, JSON.stringify(built, null, 2));
|
|
854
963
|
}
|
|
855
964
|
|
|
856
965
|
/**
|
|
@@ -866,96 +975,95 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
866
975
|
return;
|
|
867
976
|
}
|
|
868
977
|
|
|
869
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
870
978
|
const today = new Date().toISOString().split('T')[0];
|
|
871
979
|
const updated = [];
|
|
872
980
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
+
}
|
|
890
1005
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
result =
|
|
894
|
-
if (result) { content = result; updated.push('Current Phase Name'); }
|
|
895
|
-
}
|
|
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'); }
|
|
896
1009
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
+
}
|
|
900
1015
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
if (
|
|
905
|
-
|
|
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
|
+
}
|
|
906
1023
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
+
}
|
|
914
1040
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
let posBody = positionMatch[2];
|
|
923
|
-
|
|
924
|
-
// Update or insert Phase line
|
|
925
|
-
const newPhase = `Phase: ${phaseNumber}${phaseName ? ` (${phaseName})` : ''} — EXECUTING`;
|
|
926
|
-
if (/^Phase:/m.test(posBody)) {
|
|
927
|
-
posBody = posBody.replace(/^Phase:.*$/m, newPhase);
|
|
928
|
-
} else {
|
|
929
|
-
posBody = newPhase + '\n' + posBody;
|
|
930
|
-
}
|
|
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
|
+
}
|
|
931
1048
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
posBody = posBody.replace(/^(Phase:.*$)/m, `$1\n${newPlan}`);
|
|
938
|
-
}
|
|
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
|
+
}
|
|
939
1054
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
+
}
|
|
945
1060
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
if (/^Last activity:/im.test(posBody)) {
|
|
949
|
-
posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
|
|
1061
|
+
content = content.replace(positionPattern, `${header}${posBody}`);
|
|
1062
|
+
updated.push('Current Position');
|
|
950
1063
|
}
|
|
951
1064
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
if (updated.length > 0) {
|
|
957
|
-
writeStateMd(statePath, content, cwd);
|
|
958
|
-
}
|
|
1065
|
+
return content;
|
|
1066
|
+
}, cwd);
|
|
959
1067
|
|
|
960
1068
|
output({ updated, phase: phaseNumber, phase_name: phaseName || null, plan_count: planCount || null }, raw, updated.length > 0 ? 'true' : 'false');
|
|
961
1069
|
}
|
|
@@ -1007,11 +1115,284 @@ function cmdSignalResume(cwd, raw) {
|
|
|
1007
1115
|
output({ resumed: true, removed }, raw, removed ? 'true' : 'false');
|
|
1008
1116
|
}
|
|
1009
1117
|
|
|
1118
|
+
// ─── Gate Functions (STATE.md consistency enforcement) ────────────────────────
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Update the ## Performance Metrics section in STATE.md content.
|
|
1122
|
+
* Increments Velocity totals and upserts a By Phase table row.
|
|
1123
|
+
* Returns modified content string.
|
|
1124
|
+
*/
|
|
1125
|
+
function updatePerformanceMetricsSection(content, cwd, phaseNum, planCount, summaryCount) {
|
|
1126
|
+
// Update Velocity: Total plans completed
|
|
1127
|
+
const totalMatch = content.match(/Total plans completed:\s*(\d+|\[N\])/);
|
|
1128
|
+
const prevTotal = totalMatch && totalMatch[1] !== '[N]' ? parseInt(totalMatch[1], 10) : 0;
|
|
1129
|
+
const newTotal = prevTotal + summaryCount;
|
|
1130
|
+
content = content.replace(
|
|
1131
|
+
/Total plans completed:\s*(\d+|\[N\])/,
|
|
1132
|
+
`Total plans completed: ${newTotal}`
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
// Update By Phase table — upsert row for this phase
|
|
1136
|
+
const byPhaseTablePattern = /(\|\s*Phase\s*\|\s*Plans\s*\|\s*Total\s*\|\s*Avg\/Plan\s*\|[ \t]*\n\|(?:[- :\t]+\|)+[ \t]*\n)((?:[ \t]*\|[^\n]*\n)*)(?=\n|$)/i;
|
|
1137
|
+
const byPhaseMatch = content.match(byPhaseTablePattern);
|
|
1138
|
+
if (byPhaseMatch) {
|
|
1139
|
+
let tableBody = byPhaseMatch[2].trim();
|
|
1140
|
+
const phaseRowPattern = new RegExp(`^\\|\\s*${escapeRegex(String(phaseNum))}\\s*\\|.*$`, 'm');
|
|
1141
|
+
const newRow = `| ${phaseNum} | ${summaryCount} | - | - |`;
|
|
1142
|
+
|
|
1143
|
+
if (phaseRowPattern.test(tableBody)) {
|
|
1144
|
+
// Update existing row
|
|
1145
|
+
tableBody = tableBody.replace(phaseRowPattern, newRow);
|
|
1146
|
+
} else {
|
|
1147
|
+
// Remove placeholder row and add new row
|
|
1148
|
+
tableBody = tableBody.replace(/^\|\s*-\s*\|\s*-\s*\|\s*-\s*\|\s*-\s*\|$/m, '').trim();
|
|
1149
|
+
tableBody = tableBody ? tableBody + '\n' + newRow : newRow;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
content = content.replace(byPhaseTablePattern, `$1${tableBody}\n`);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return content;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Gate 3a: Record state after plan-phase completes.
|
|
1160
|
+
* Updates Status to "Ready to execute", Total Plans, Last Activity.
|
|
1161
|
+
*/
|
|
1162
|
+
function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
|
1163
|
+
const statePath = planningPaths(cwd).state;
|
|
1164
|
+
if (!fs.existsSync(statePath)) {
|
|
1165
|
+
output({ error: 'STATE.md not found' }, raw);
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
let content = fs.readFileSync(statePath, 'utf-8');
|
|
1170
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1171
|
+
const updated = [];
|
|
1172
|
+
|
|
1173
|
+
// Update Status
|
|
1174
|
+
let result = stateReplaceField(content, 'Status', 'Ready to execute');
|
|
1175
|
+
if (result) { content = result; updated.push('Status'); }
|
|
1176
|
+
|
|
1177
|
+
// Update Total Plans in Phase
|
|
1178
|
+
if (planCount !== null && planCount !== undefined) {
|
|
1179
|
+
result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
|
|
1180
|
+
if (result) { content = result; updated.push('Total Plans in Phase'); }
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Update Last Activity
|
|
1184
|
+
result = stateReplaceField(content, 'Last Activity', today);
|
|
1185
|
+
if (result) { content = result; updated.push('Last Activity'); }
|
|
1186
|
+
|
|
1187
|
+
// Update Last Activity Description
|
|
1188
|
+
result = stateReplaceField(content, 'Last Activity Description', `Phase ${phaseNumber} planning complete — ${planCount || '?'} plans ready`);
|
|
1189
|
+
if (result) { content = result; updated.push('Last Activity Description'); }
|
|
1190
|
+
|
|
1191
|
+
// Update Current Position section
|
|
1192
|
+
content = updateCurrentPositionFields(content, {
|
|
1193
|
+
status: 'Ready to execute',
|
|
1194
|
+
lastActivity: `${today} -- Phase ${phaseNumber} planning complete`,
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
if (updated.length > 0) {
|
|
1198
|
+
writeStateMd(statePath, content, cwd);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
output({ updated, phase: phaseNumber, plan_count: planCount }, raw, updated.length > 0 ? 'true' : 'false');
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Gate 1: Validate STATE.md against filesystem.
|
|
1206
|
+
* Returns { valid, warnings, drift } JSON.
|
|
1207
|
+
*/
|
|
1208
|
+
function cmdStateValidate(cwd, raw) {
|
|
1209
|
+
const statePath = planningPaths(cwd).state;
|
|
1210
|
+
if (!fs.existsSync(statePath)) {
|
|
1211
|
+
output({ error: 'STATE.md not found' }, raw);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const content = fs.readFileSync(statePath, 'utf-8');
|
|
1216
|
+
const warnings = [];
|
|
1217
|
+
const drift = {};
|
|
1218
|
+
|
|
1219
|
+
const status = stateExtractField(content, 'Status') || '';
|
|
1220
|
+
const currentPhase = stateExtractField(content, 'Current Phase');
|
|
1221
|
+
const totalPlansRaw = stateExtractField(content, 'Total Plans in Phase');
|
|
1222
|
+
const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
|
|
1223
|
+
|
|
1224
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
1225
|
+
|
|
1226
|
+
// Scan disk for current phase
|
|
1227
|
+
if (currentPhase && fs.existsSync(phasesDir)) {
|
|
1228
|
+
const normalized = currentPhase.replace(/\s+of\s+\d+.*/, '').trim();
|
|
1229
|
+
try {
|
|
1230
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
1231
|
+
const phaseDir = entries.find(e => e.isDirectory() && e.name.startsWith(normalized.replace(/^0+/, '').padStart(2, '0')));
|
|
1232
|
+
if (phaseDir) {
|
|
1233
|
+
const phaseDirPath = path.join(phasesDir, phaseDir.name);
|
|
1234
|
+
const files = fs.readdirSync(phaseDirPath);
|
|
1235
|
+
const diskPlans = files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
|
1236
|
+
const diskSummaries = files.filter(f => f.match(/-SUMMARY\.md$/i)).length;
|
|
1237
|
+
|
|
1238
|
+
// Check plan count mismatch
|
|
1239
|
+
if (totalPlansInPhase !== null && diskPlans !== totalPlansInPhase) {
|
|
1240
|
+
warnings.push(`Plan count mismatch: STATE.md says ${totalPlansInPhase} plans, disk has ${diskPlans}`);
|
|
1241
|
+
drift.plan_count = { state: totalPlansInPhase, disk: diskPlans };
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Check for VERIFICATION.md
|
|
1245
|
+
const verificationFiles = files.filter(f => f.includes('VERIFICATION') && f.endsWith('.md'));
|
|
1246
|
+
for (const vf of verificationFiles) {
|
|
1247
|
+
try {
|
|
1248
|
+
const vContent = fs.readFileSync(path.join(phaseDirPath, vf), 'utf-8');
|
|
1249
|
+
if (/status:\s*passed/i.test(vContent) && /executing/i.test(status)) {
|
|
1250
|
+
warnings.push(`Status drift: STATE.md says "${status}" but ${vf} shows verification passed — phase may be complete`);
|
|
1251
|
+
drift.verification_status = { state_status: status, verification: 'passed' };
|
|
1252
|
+
}
|
|
1253
|
+
} catch { /* intentionally empty */ }
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Check if all plans have summaries but status still says executing
|
|
1257
|
+
if (diskPlans > 0 && diskSummaries >= diskPlans && /executing/i.test(status)) {
|
|
1258
|
+
// Only warn if no verification exists (if verification passed, the above warning covers it)
|
|
1259
|
+
if (verificationFiles.length === 0) {
|
|
1260
|
+
warnings.push(`All ${diskPlans} plans have summaries but status is still "${status}" — phase may be ready for verification`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
} catch { /* intentionally empty */ }
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const valid = warnings.length === 0;
|
|
1268
|
+
output({ valid, warnings, drift }, raw);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Gate 2: Sync STATE.md from filesystem ground truth.
|
|
1273
|
+
* Scans phase dirs, reconstructs counters, progress, metrics.
|
|
1274
|
+
* Supports --verify for dry-run mode.
|
|
1275
|
+
*/
|
|
1276
|
+
function cmdStateSync(cwd, options, raw) {
|
|
1277
|
+
const statePath = planningPaths(cwd).state;
|
|
1278
|
+
if (!fs.existsSync(statePath)) {
|
|
1279
|
+
output({ error: 'STATE.md not found' }, raw);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
const verify = options && options.verify;
|
|
1284
|
+
const content = fs.readFileSync(statePath, 'utf-8');
|
|
1285
|
+
const changes = [];
|
|
1286
|
+
let modified = content;
|
|
1287
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1288
|
+
|
|
1289
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
1290
|
+
if (!fs.existsSync(phasesDir)) {
|
|
1291
|
+
output({ synced: true, changes: [], dry_run: !!verify }, raw);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Scan all phases
|
|
1296
|
+
let entries;
|
|
1297
|
+
try {
|
|
1298
|
+
entries = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
1299
|
+
.filter(e => e.isDirectory())
|
|
1300
|
+
.map(e => e.name)
|
|
1301
|
+
.sort();
|
|
1302
|
+
} catch {
|
|
1303
|
+
output({ synced: true, changes: [], dry_run: !!verify }, raw);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
let totalDiskPlans = 0;
|
|
1308
|
+
let totalDiskSummaries = 0;
|
|
1309
|
+
let highestIncompletePhase = null;
|
|
1310
|
+
let highestIncompletePhaseNum = null;
|
|
1311
|
+
let highestIncompletePhaseplanCount = 0;
|
|
1312
|
+
let highestIncompletePhaseSummaryCount = 0;
|
|
1313
|
+
|
|
1314
|
+
for (const dir of entries) {
|
|
1315
|
+
const dirPath = path.join(phasesDir, dir);
|
|
1316
|
+
const files = fs.readdirSync(dirPath);
|
|
1317
|
+
const plans = files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
|
1318
|
+
const summaries = files.filter(f => f.match(/-SUMMARY\.md$/i)).length;
|
|
1319
|
+
totalDiskPlans += plans;
|
|
1320
|
+
totalDiskSummaries += summaries;
|
|
1321
|
+
|
|
1322
|
+
// Track the highest phase with incomplete plans (or any plans)
|
|
1323
|
+
const phaseMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
|
1324
|
+
if (phaseMatch && plans > 0) {
|
|
1325
|
+
if (summaries < plans) {
|
|
1326
|
+
// Incomplete phase — this is likely the current one
|
|
1327
|
+
highestIncompletePhase = dir;
|
|
1328
|
+
highestIncompletePhaseNum = phaseMatch[1];
|
|
1329
|
+
highestIncompletePhaseplanCount = plans;
|
|
1330
|
+
highestIncompletePhaseSummaryCount = summaries;
|
|
1331
|
+
} else if (!highestIncompletePhase) {
|
|
1332
|
+
// All complete, track as potential current
|
|
1333
|
+
highestIncompletePhase = dir;
|
|
1334
|
+
highestIncompletePhaseNum = phaseMatch[1];
|
|
1335
|
+
highestIncompletePhaseplanCount = plans;
|
|
1336
|
+
highestIncompletePhaseSummaryCount = summaries;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Sync Total Plans in Phase
|
|
1342
|
+
if (highestIncompletePhase) {
|
|
1343
|
+
const currentPlansField = stateExtractField(modified, 'Total Plans in Phase');
|
|
1344
|
+
if (currentPlansField && parseInt(currentPlansField, 10) !== highestIncompletePhaseplanCount) {
|
|
1345
|
+
changes.push(`Total Plans in Phase: ${currentPlansField} -> ${highestIncompletePhaseplanCount}`);
|
|
1346
|
+
const result = stateReplaceField(modified, 'Total Plans in Phase', String(highestIncompletePhaseplanCount));
|
|
1347
|
+
if (result) modified = result;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Sync Progress
|
|
1352
|
+
const percent = totalDiskPlans > 0 ? Math.min(100, Math.round(totalDiskSummaries / totalDiskPlans * 100)) : 0;
|
|
1353
|
+
const currentProgress = stateExtractField(modified, 'Progress');
|
|
1354
|
+
if (currentProgress) {
|
|
1355
|
+
const currentPercent = parseInt(currentProgress.replace(/[^\d]/g, ''), 10);
|
|
1356
|
+
if (currentPercent !== percent) {
|
|
1357
|
+
const barWidth = 10;
|
|
1358
|
+
const filled = Math.round(percent / 100 * barWidth);
|
|
1359
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
1360
|
+
const progressStr = `[${bar}] ${percent}%`;
|
|
1361
|
+
changes.push(`Progress: ${currentProgress} -> ${progressStr}`);
|
|
1362
|
+
const result = stateReplaceField(modified, 'Progress', progressStr);
|
|
1363
|
+
if (result) modified = result;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// Sync Last Activity
|
|
1368
|
+
const result = stateReplaceField(modified, 'Last Activity', today);
|
|
1369
|
+
if (result) {
|
|
1370
|
+
const oldActivity = stateExtractField(modified, 'Last Activity');
|
|
1371
|
+
if (oldActivity !== today) {
|
|
1372
|
+
changes.push(`Last Activity: ${oldActivity} -> ${today}`);
|
|
1373
|
+
}
|
|
1374
|
+
modified = result;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
if (verify) {
|
|
1378
|
+
output({ synced: false, changes, dry_run: true }, raw);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (changes.length > 0 || modified !== content) {
|
|
1383
|
+
writeStateMd(statePath, modified, cwd);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
output({ synced: true, changes, dry_run: false }, raw);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1010
1389
|
module.exports = {
|
|
1011
1390
|
stateExtractField,
|
|
1012
1391
|
stateReplaceField,
|
|
1013
1392
|
stateReplaceFieldWithFallback,
|
|
1014
1393
|
writeStateMd,
|
|
1394
|
+
readModifyWriteStateMd,
|
|
1395
|
+
updatePerformanceMetricsSection,
|
|
1015
1396
|
cmdStateLoad,
|
|
1016
1397
|
cmdStateGet,
|
|
1017
1398
|
cmdStatePatch,
|
|
@@ -1026,6 +1407,9 @@ module.exports = {
|
|
|
1026
1407
|
cmdStateSnapshot,
|
|
1027
1408
|
cmdStateJson,
|
|
1028
1409
|
cmdStateBeginPhase,
|
|
1410
|
+
cmdStatePlannedPhase,
|
|
1411
|
+
cmdStateValidate,
|
|
1412
|
+
cmdStateSync,
|
|
1029
1413
|
cmdSignalWaiting,
|
|
1030
1414
|
cmdSignalResume,
|
|
1031
1415
|
};
|