gsd-antigravity-kit 2.0.1 → 2.1.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/.agent/skills/gsd/SKILL.md +26 -4
- package/.agent/skills/gsd/VERSION +1 -1
- package/.agent/skills/gsd/assets/templates/AI-SPEC.md +246 -0
- package/.agent/skills/gsd/assets/templates/DEBUG.md +7 -2
- package/.agent/skills/gsd/assets/templates/config.json +56 -48
- package/.agent/skills/gsd/assets/templates/research.md +40 -0
- package/.agent/skills/gsd/assets/templates/spec.md +307 -0
- package/.agent/skills/gsd/assets/templates/state.md +8 -0
- package/.agent/skills/gsd/bin/gsd-tools.cjs +212 -11
- package/.agent/skills/gsd/bin/help-manifest.json +8 -2
- package/.agent/skills/gsd/bin/hooks/gsd-check-update-worker.js +108 -0
- package/.agent/skills/gsd/bin/hooks/gsd-check-update.js +14 -89
- package/.agent/skills/gsd/bin/hooks/gsd-context-monitor.js +34 -5
- package/.agent/skills/gsd/bin/hooks/gsd-phase-boundary.sh +1 -0
- package/.agent/skills/gsd/bin/hooks/gsd-prompt-guard.js +1 -1
- package/.agent/skills/gsd/bin/hooks/gsd-read-guard.js +6 -1
- package/.agent/skills/gsd/bin/hooks/gsd-session-state.sh +1 -0
- package/.agent/skills/gsd/bin/hooks/gsd-statusline.js +150 -16
- package/.agent/skills/gsd/bin/hooks/gsd-validate-commit.sh +1 -0
- package/.agent/skills/gsd/bin/hooks/gsd-workflow-guard.js +1 -1
- package/.agent/skills/gsd/bin/lib/audit.cjs +757 -0
- package/.agent/skills/gsd/bin/lib/commands.cjs +17 -7
- package/.agent/skills/gsd/bin/lib/config.cjs +66 -20
- package/.agent/skills/gsd/bin/lib/core.cjs +212 -12
- package/.agent/skills/gsd/bin/lib/frontmatter.cjs +6 -8
- package/.agent/skills/gsd/bin/lib/graphify.cjs +494 -0
- package/.agent/skills/gsd/bin/lib/gsd2-import.cjs +511 -0
- package/.agent/skills/gsd/bin/lib/init.cjs +371 -18
- package/.agent/skills/gsd/bin/lib/intel.cjs +9 -30
- package/.agent/skills/gsd/bin/lib/milestone.cjs +18 -17
- package/.agent/skills/gsd/bin/lib/model-profiles.cjs +1 -0
- package/.agent/skills/gsd/bin/lib/phase.cjs +225 -98
- package/.agent/skills/gsd/bin/lib/profile-output.cjs +17 -5
- package/.agent/skills/gsd/bin/lib/roadmap.cjs +12 -5
- package/.agent/skills/gsd/bin/lib/state.cjs +394 -129
- package/.agent/skills/gsd/bin/lib/template.cjs +8 -4
- package/.agent/skills/gsd/bin/lib/uat.cjs +2 -1
- package/.agent/skills/gsd/bin/lib/verify.cjs +111 -42
- package/.agent/skills/gsd/migration_report.md +2 -2
- package/.agent/skills/gsd/references/agents/gsd-advisor-researcher.md +23 -0
- package/.agent/skills/gsd/references/agents/gsd-ai-researcher.md +133 -0
- package/.agent/skills/gsd/references/agents/gsd-code-fixer.md +11 -10
- package/.agent/skills/gsd/references/agents/gsd-code-reviewer.md +2 -2
- package/.agent/skills/gsd/references/agents/gsd-codebase-mapper.md +13 -2
- package/.agent/skills/gsd/references/agents/gsd-debug-session-manager.md +314 -0
- package/.agent/skills/gsd/references/agents/gsd-debugger.md +147 -76
- package/.agent/skills/gsd/references/agents/gsd-doc-verifier.md +1 -1
- package/.agent/skills/gsd/references/agents/gsd-doc-writer.md +615 -602
- package/.agent/skills/gsd/references/agents/gsd-domain-researcher.md +153 -0
- package/.agent/skills/gsd/references/agents/gsd-eval-auditor.md +175 -0
- package/.agent/skills/gsd/references/agents/gsd-eval-planner.md +154 -0
- package/.agent/skills/gsd/references/agents/gsd-executor.md +108 -38
- package/.agent/skills/gsd/references/agents/gsd-framework-selector.md +160 -0
- package/.agent/skills/gsd/references/agents/gsd-integration-checker.md +454 -443
- package/.agent/skills/gsd/references/agents/gsd-intel-updater.md +40 -20
- package/.agent/skills/gsd/references/agents/gsd-nyquist-auditor.md +187 -176
- package/.agent/skills/gsd/references/agents/gsd-pattern-mapper.md +335 -0
- package/.agent/skills/gsd/references/agents/gsd-phase-researcher.md +112 -13
- package/.agent/skills/gsd/references/agents/gsd-plan-checker.md +104 -10
- package/.agent/skills/gsd/references/agents/gsd-planner.md +125 -167
- package/.agent/skills/gsd/references/agents/gsd-project-researcher.md +25 -2
- package/.agent/skills/gsd/references/agents/gsd-research-synthesizer.md +3 -3
- package/.agent/skills/gsd/references/agents/gsd-roadmapper.md +12 -1
- package/.agent/skills/gsd/references/agents/gsd-security-auditor.md +139 -128
- package/.agent/skills/gsd/references/agents/gsd-ui-auditor.md +3 -3
- package/.agent/skills/gsd/references/agents/gsd-ui-checker.md +11 -2
- package/.agent/skills/gsd/references/agents/gsd-ui-researcher.md +27 -4
- package/.agent/skills/gsd/references/agents/gsd-verifier.md +13 -19
- package/.agent/skills/gsd/references/commands/atomic/add-todo.md +2 -2
- package/.agent/skills/gsd/references/commands/atomic/check-todos.md +2 -2
- package/.agent/skills/gsd/references/commands/atomic/cleanup.md +2 -2
- package/.agent/skills/gsd/references/commands/atomic/do.md +2 -2
- package/.agent/skills/gsd/references/commands/atomic/help.md +2 -2
- package/.agent/skills/gsd/references/commands/atomic/join-discord.md +2 -2
- package/.agent/skills/gsd/references/commands/atomic/note.md +2 -2
- package/.agent/skills/gsd/references/commands/atomic/session-report.md +2 -2
- package/.agent/skills/gsd/references/commands/atomic/ship.md +2 -2
- package/.agent/skills/gsd/references/commands/atomic/stats.md +2 -2
- package/.agent/skills/gsd/references/commands/atomic/thread.md +141 -41
- package/.agent/skills/gsd/references/commands/atomic/undo.md +2 -2
- package/.agent/skills/gsd/references/commands/milestone/add-backlog.md +15 -12
- package/.agent/skills/gsd/references/commands/milestone/audit-milestone.md +2 -2
- package/.agent/skills/gsd/references/commands/milestone/complete-milestone.md +2 -2
- package/.agent/skills/gsd/references/commands/milestone/milestone-summary.md +2 -2
- package/.agent/skills/gsd/references/commands/milestone/new-milestone.md +2 -2
- package/.agent/skills/gsd/references/commands/milestone/plan-milestone-gaps.md +2 -2
- package/.agent/skills/gsd/references/commands/milestone/plant-seed.md +2 -2
- package/.agent/skills/gsd/references/commands/milestone/review-backlog.md +4 -4
- package/.agent/skills/gsd/references/commands/misc/ai-integration-phase.md +38 -0
- package/.agent/skills/gsd/references/commands/misc/audit-fix.md +2 -2
- package/.agent/skills/gsd/references/commands/misc/audit-uat.md +2 -2
- package/.agent/skills/gsd/references/commands/misc/eval-review.md +34 -0
- package/.agent/skills/gsd/references/commands/misc/extract_learnings.md +24 -0
- package/.agent/skills/gsd/references/commands/misc/from-gsd2.md +49 -0
- package/.agent/skills/gsd/references/commands/misc/graphify.md +203 -0
- package/.agent/skills/gsd/references/commands/misc/inbox.md +40 -0
- package/.agent/skills/gsd/references/commands/misc/next.md +5 -3
- package/.agent/skills/gsd/references/commands/misc/progress.md +4 -3
- package/.agent/skills/gsd/references/commands/misc/sketch-wrap-up.md +33 -0
- package/.agent/skills/gsd/references/commands/misc/sketch.md +47 -0
- package/.agent/skills/gsd/references/commands/misc/spec-phase.md +64 -0
- package/.agent/skills/gsd/references/commands/misc/spike-wrap-up.md +33 -0
- package/.agent/skills/gsd/references/commands/misc/spike.md +43 -0
- package/.agent/skills/gsd/references/commands/misc/verify-work.md +2 -2
- package/.agent/skills/gsd/references/commands/phase/add-phase.md +2 -2
- package/.agent/skills/gsd/references/commands/phase/add-tests.md +2 -2
- package/.agent/skills/gsd/references/commands/phase/discuss-phase.md +5 -5
- package/.agent/skills/gsd/references/commands/phase/execute-phase.md +4 -4
- package/.agent/skills/gsd/references/commands/phase/insert-phase.md +2 -2
- package/.agent/skills/gsd/references/commands/phase/list-phase-assumptions.md +2 -2
- package/.agent/skills/gsd/references/commands/phase/plan-phase.md +3 -3
- package/.agent/skills/gsd/references/commands/phase/remove-phase.md +2 -2
- package/.agent/skills/gsd/references/commands/phase/research-phase.md +5 -5
- package/.agent/skills/gsd/references/commands/phase/secure-phase.md +2 -2
- package/.agent/skills/gsd/references/commands/phase/ui-phase.md +2 -2
- package/.agent/skills/gsd/references/commands/phase/ui-review.md +2 -2
- package/.agent/skills/gsd/references/commands/phase/validate-phase.md +2 -2
- package/.agent/skills/gsd/references/commands/phase/workstreams.md +9 -9
- package/.agent/skills/gsd/references/commands/project/analyze-dependencies.md +2 -2
- package/.agent/skills/gsd/references/commands/project/explore.md +2 -2
- package/.agent/skills/gsd/references/commands/project/import.md +2 -2
- package/.agent/skills/gsd/references/commands/project/intel.md +10 -10
- package/.agent/skills/gsd/references/commands/project/list-workspaces.md +2 -2
- package/.agent/skills/gsd/references/commands/project/map-codebase.md +2 -2
- package/.agent/skills/gsd/references/commands/project/new-project.md +2 -2
- package/.agent/skills/gsd/references/commands/project/new-workspace.md +2 -2
- package/.agent/skills/gsd/references/commands/project/remove-workspace.md +2 -2
- package/.agent/skills/gsd/references/commands/project/scan.md +2 -2
- package/.agent/skills/gsd/references/commands/system/autonomous.md +4 -3
- package/.agent/skills/gsd/references/commands/system/code-review-fix.md +3 -3
- package/.agent/skills/gsd/references/commands/system/code-review.md +3 -3
- package/.agent/skills/gsd/references/commands/system/debug.md +177 -100
- package/.agent/skills/gsd/references/commands/system/docs-update.md +2 -2
- package/.agent/skills/gsd/references/commands/system/fast.md +2 -2
- package/.agent/skills/gsd/references/commands/system/forensics.md +2 -2
- package/.agent/skills/gsd/references/commands/system/gsd-tools.md +153 -6
- package/.agent/skills/gsd/references/commands/system/health.md +2 -2
- package/.agent/skills/gsd/references/commands/system/manager.md +3 -3
- package/.agent/skills/gsd/references/commands/system/pause-work.md +2 -2
- package/.agent/skills/gsd/references/commands/system/pr-branch.md +2 -2
- package/.agent/skills/gsd/references/commands/system/profile-user.md +2 -2
- package/.agent/skills/gsd/references/commands/system/quick.md +127 -3
- package/.agent/skills/gsd/references/commands/system/reapply-patches.md +45 -6
- package/.agent/skills/gsd/references/commands/system/resume-work.md +2 -2
- package/.agent/skills/gsd/references/commands/system/review.md +6 -4
- package/.agent/skills/gsd/references/commands/system/set-profile.md +3 -3
- package/.agent/skills/gsd/references/commands/system/settings.md +2 -2
- package/.agent/skills/gsd/references/commands/system/update.md +2 -2
- package/.agent/skills/gsd/references/docs/ai-evals.md +156 -0
- package/.agent/skills/gsd/references/docs/ai-frameworks.md +186 -0
- package/.agent/skills/gsd/references/docs/artifact-types.md +18 -0
- package/.agent/skills/gsd/references/docs/autonomous-smart-discuss.md +277 -0
- package/.agent/skills/gsd/references/docs/checkpoints.md +30 -0
- package/.agent/skills/gsd/references/docs/common-bug-patterns.md +49 -49
- package/.agent/skills/gsd/references/docs/continuation-format.md +11 -7
- package/.agent/skills/gsd/references/docs/debugger-philosophy.md +76 -0
- package/.agent/skills/gsd/references/docs/decimal-phase-calculation.md +64 -64
- package/.agent/skills/gsd/references/docs/executor-examples.md +110 -0
- package/.agent/skills/gsd/references/docs/git-integration.md +4 -4
- package/.agent/skills/gsd/references/docs/git-planning-commit.md +40 -38
- package/.agent/skills/gsd/references/docs/ios-scaffold.md +123 -0
- package/.agent/skills/gsd/references/docs/mandatory-initial-read.md +2 -0
- package/.agent/skills/gsd/references/docs/phase-argument-parsing.md +61 -61
- package/.agent/skills/gsd/references/docs/planner-antipatterns.md +89 -0
- package/.agent/skills/gsd/references/docs/planner-revision.md +87 -87
- package/.agent/skills/gsd/references/docs/planner-source-audit.md +73 -0
- package/.agent/skills/gsd/references/docs/planning-config.md +33 -8
- package/.agent/skills/gsd/references/docs/project-skills-discovery.md +19 -0
- package/.agent/skills/gsd/references/docs/sketch-interactivity.md +41 -0
- package/.agent/skills/gsd/references/docs/sketch-theme-system.md +94 -0
- package/.agent/skills/gsd/references/docs/sketch-tooling.md +45 -0
- package/.agent/skills/gsd/references/docs/sketch-variant-patterns.md +81 -0
- package/.agent/skills/gsd/references/docs/tdd.md +67 -0
- package/.agent/skills/gsd/references/docs/universal-anti-patterns.md +5 -0
- package/.agent/skills/gsd/references/docs/workstream-flag.md +11 -11
- package/.agent/skills/gsd/references/mapping.md +1 -1
- package/.agent/skills/gsd/references/workflows/add-phase.md +112 -112
- package/.agent/skills/gsd/references/workflows/add-tests.md +6 -3
- package/.agent/skills/gsd/references/workflows/add-todo.md +5 -3
- package/.agent/skills/gsd/references/workflows/ai-integration-phase.md +284 -0
- package/.agent/skills/gsd/references/workflows/audit-fix.md +157 -157
- package/.agent/skills/gsd/references/workflows/audit-milestone.md +340 -340
- package/.agent/skills/gsd/references/workflows/audit-uat.md +109 -109
- package/.agent/skills/gsd/references/workflows/autonomous.md +20 -288
- package/.agent/skills/gsd/references/workflows/check-todos.md +4 -2
- package/.agent/skills/gsd/references/workflows/cleanup.md +3 -1
- package/.agent/skills/gsd/references/workflows/code-review-fix.md +497 -497
- package/.agent/skills/gsd/references/workflows/code-review.md +515 -515
- package/.agent/skills/gsd/references/workflows/complete-milestone.md +97 -24
- package/.agent/skills/gsd/references/workflows/diagnose-issues.md +238 -238
- package/.agent/skills/gsd/references/workflows/discovery-phase.md +2 -0
- package/.agent/skills/gsd/references/workflows/discuss-phase-assumptions.md +11 -11
- package/.agent/skills/gsd/references/workflows/discuss-phase.md +143 -19
- package/.agent/skills/gsd/references/workflows/do.md +8 -2
- package/.agent/skills/gsd/references/workflows/docs-update.md +5 -3
- package/.agent/skills/gsd/references/workflows/eval-review.md +155 -0
- package/.agent/skills/gsd/references/workflows/execute-phase.md +338 -54
- package/.agent/skills/gsd/references/workflows/execute-plan.md +80 -104
- package/.agent/skills/gsd/references/workflows/explore.md +3 -1
- package/.agent/skills/gsd/references/workflows/extract_learnings.md +232 -0
- package/.agent/skills/gsd/references/workflows/forensics.md +3 -3
- package/.agent/skills/gsd/references/workflows/health.md +2 -2
- package/.agent/skills/gsd/references/workflows/help.md +59 -1
- package/.agent/skills/gsd/references/workflows/import.md +3 -1
- package/.agent/skills/gsd/references/workflows/inbox.md +387 -384
- package/.agent/skills/gsd/references/workflows/insert-phase.md +130 -130
- package/.agent/skills/gsd/references/workflows/list-workspaces.md +56 -56
- package/.agent/skills/gsd/references/workflows/manager.md +5 -3
- package/.agent/skills/gsd/references/workflows/map-codebase.md +19 -5
- package/.agent/skills/gsd/references/workflows/milestone-summary.md +6 -6
- package/.agent/skills/gsd/references/workflows/new-milestone.md +63 -9
- package/.agent/skills/gsd/references/workflows/new-project.md +126 -22
- package/.agent/skills/gsd/references/workflows/new-workspace.md +6 -4
- package/.agent/skills/gsd/references/workflows/next.md +220 -153
- package/.agent/skills/gsd/references/workflows/note.md +2 -0
- package/.agent/skills/gsd/references/workflows/pause-work.md +11 -7
- package/.agent/skills/gsd/references/workflows/plan-milestone-gaps.md +273 -273
- package/.agent/skills/gsd/references/workflows/plan-phase.md +281 -62
- package/.agent/skills/gsd/references/workflows/plant-seed.md +4 -1
- package/.agent/skills/gsd/references/workflows/pr-branch.md +41 -13
- package/.agent/skills/gsd/references/workflows/profile-user.md +15 -13
- package/.agent/skills/gsd/references/workflows/progress.md +133 -21
- package/.agent/skills/gsd/references/workflows/quick.md +67 -27
- package/.agent/skills/gsd/references/workflows/remove-phase.md +155 -155
- package/.agent/skills/gsd/references/workflows/remove-workspace.md +4 -2
- package/.agent/skills/gsd/references/workflows/research-phase.md +3 -3
- package/.agent/skills/gsd/references/workflows/resume-project.md +3 -3
- package/.agent/skills/gsd/references/workflows/review.md +71 -8
- package/.agent/skills/gsd/references/workflows/scan.md +102 -102
- package/.agent/skills/gsd/references/workflows/secure-phase.md +7 -5
- package/.agent/skills/gsd/references/workflows/settings.md +24 -7
- package/.agent/skills/gsd/references/workflows/ship.md +71 -6
- package/.agent/skills/gsd/references/workflows/sketch-wrap-up.md +283 -0
- package/.agent/skills/gsd/references/workflows/sketch.md +263 -0
- package/.agent/skills/gsd/references/workflows/spec-phase.md +262 -0
- package/.agent/skills/gsd/references/workflows/spike-wrap-up.md +273 -0
- package/.agent/skills/gsd/references/workflows/spike.md +270 -0
- package/.agent/skills/gsd/references/workflows/stats.md +60 -60
- package/.agent/skills/gsd/references/workflows/transition.md +671 -671
- package/.agent/skills/gsd/references/workflows/ui-phase.md +33 -12
- package/.agent/skills/gsd/references/workflows/ui-review.md +6 -4
- package/.agent/skills/gsd/references/workflows/undo.md +3 -1
- package/.agent/skills/gsd/references/workflows/update.md +113 -2
- package/.agent/skills/gsd/references/workflows/validate-phase.md +7 -5
- package/.agent/skills/gsd/references/workflows/verify-phase.md +93 -10
- package/.agent/skills/gsd/references/workflows/verify-work.md +50 -10
- package/.agent/skills/gsd-converter/references/mapping.md +1 -1
- package/.agent/skills/gsd-converter/scripts/convert.py +36 -17
- package/.agent/skills/gsd-converter/scripts/regression_test.py +68 -33
- package/README.md +3 -2
- package/package.json +1 -1
|
@@ -4,14 +4,29 @@
|
|
|
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
|
+
// Cache disk scan results from buildStateFrontmatter per cwd per process (#1967).
|
|
11
|
+
// Avoids re-reading N+1 directories on every state write when the phase structure
|
|
12
|
+
// hasn't changed within the same gsd-tools invocation.
|
|
13
|
+
const _diskScanCache = new Map();
|
|
14
|
+
|
|
10
15
|
/** Shorthand — every state command needs this path */
|
|
11
16
|
function getStatePath(cwd) {
|
|
12
17
|
return planningPaths(cwd).state;
|
|
13
18
|
}
|
|
14
19
|
|
|
20
|
+
// Track all lock files held by this process so they can be removed on exit.
|
|
21
|
+
// process.on('exit') fires even on process.exit(1), unlike try/finally which is
|
|
22
|
+
// skipped when error() calls process.exit(1) inside a locked region (#1916).
|
|
23
|
+
const _heldStateLocks = new Set();
|
|
24
|
+
process.on('exit', () => {
|
|
25
|
+
for (const lockPath of _heldStateLocks) {
|
|
26
|
+
try { require('fs').unlinkSync(lockPath); } catch { /* already gone */ }
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
15
30
|
// Shared helper: extract a field value from STATE.md content.
|
|
16
31
|
// Supports both **Field:** bold and plain Field: format.
|
|
17
32
|
function stateExtractField(content, fieldName) {
|
|
@@ -184,18 +199,22 @@ function cmdStateUpdate(cwd, field, value) {
|
|
|
184
199
|
|
|
185
200
|
const statePath = planningPaths(cwd).state;
|
|
186
201
|
try {
|
|
187
|
-
let
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
202
|
+
let updated = false;
|
|
203
|
+
readModifyWriteStateMd(statePath, (content) => {
|
|
204
|
+
const fieldEscaped = escapeRegex(field);
|
|
205
|
+
// Try **Field:** bold format first, then plain Field: format
|
|
206
|
+
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
|
207
|
+
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
|
208
|
+
if (boldPattern.test(content)) {
|
|
209
|
+
updated = true;
|
|
210
|
+
return content.replace(boldPattern, (_match, prefix) => `${prefix}${value}`);
|
|
211
|
+
} else if (plainPattern.test(content)) {
|
|
212
|
+
updated = true;
|
|
213
|
+
return content.replace(plainPattern, (_match, prefix) => `${prefix}${value}`);
|
|
214
|
+
}
|
|
215
|
+
return content;
|
|
216
|
+
}, cwd);
|
|
217
|
+
if (updated) {
|
|
199
218
|
output({ updated: true });
|
|
200
219
|
} else {
|
|
201
220
|
output({ updated: false, reason: `Field "${field}" not found in STATE.md` });
|
|
@@ -274,9 +293,10 @@ function cmdStateAdvancePlan(cwd, raw) {
|
|
|
274
293
|
const statePath = planningPaths(cwd).state;
|
|
275
294
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
276
295
|
|
|
277
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
278
296
|
const today = new Date().toISOString().split('T')[0];
|
|
297
|
+
let result = null;
|
|
279
298
|
|
|
299
|
+
readModifyWriteStateMd(statePath, (content) => {
|
|
280
300
|
// Try legacy separate fields first, then compound "Plan: X of Y" format
|
|
281
301
|
const legacyPlan = stateExtractField(content, 'Current Plan');
|
|
282
302
|
const legacyTotal = stateExtractField(content, 'Total Plans in Phase');
|
|
@@ -286,43 +306,54 @@ function cmdStateAdvancePlan(cwd, raw) {
|
|
|
286
306
|
let useCompoundFormat = false;
|
|
287
307
|
|
|
288
308
|
if (legacyPlan && legacyTotal) {
|
|
289
|
-
|
|
290
|
-
|
|
309
|
+
currentPlan = parseInt(legacyPlan, 10);
|
|
310
|
+
totalPlans = parseInt(legacyTotal, 10);
|
|
291
311
|
} else if (planField) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
312
|
+
// Compound format: "2 of 6 in current phase" or "2 of 6"
|
|
313
|
+
currentPlan = parseInt(planField, 10);
|
|
314
|
+
const ofMatch = planField.match(/of\s+(\d+)/);
|
|
315
|
+
totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
|
|
316
|
+
useCompoundFormat = true;
|
|
297
317
|
}
|
|
298
318
|
|
|
299
319
|
if (isNaN(currentPlan) || isNaN(totalPlans)) {
|
|
300
|
-
|
|
301
|
-
|
|
320
|
+
result = { error: true };
|
|
321
|
+
return content;
|
|
302
322
|
}
|
|
303
323
|
|
|
304
324
|
if (currentPlan >= totalPlans) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
output({ advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' }, raw, 'false');
|
|
325
|
+
content = stateReplaceFieldWithFallback(content, 'Status', null, 'Phase complete — ready for verification');
|
|
326
|
+
content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
|
|
327
|
+
content = updateCurrentPositionFields(content, { status: 'Phase complete — ready for verification', lastActivity: today });
|
|
328
|
+
result = { advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' };
|
|
310
329
|
} else {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
330
|
+
const newPlan = currentPlan + 1;
|
|
331
|
+
let planDisplayValue;
|
|
332
|
+
if (useCompoundFormat) {
|
|
314
333
|
// Preserve compound format: "X of Y in current phase" → replace X only
|
|
315
334
|
planDisplayValue = planField.replace(/^\d+/, String(newPlan));
|
|
316
335
|
content = stateReplaceField(content, 'Plan', planDisplayValue) || content;
|
|
317
|
-
|
|
336
|
+
} else {
|
|
318
337
|
planDisplayValue = `${newPlan} of ${totalPlans}`;
|
|
319
338
|
content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
|
|
339
|
+
}
|
|
340
|
+
content = stateReplaceFieldWithFallback(content, 'Status', null, 'Ready to execute');
|
|
341
|
+
content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
|
|
342
|
+
content = updateCurrentPositionFields(content, { status: 'Ready to execute', lastActivity: today, plan: planDisplayValue });
|
|
343
|
+
result = { advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans };
|
|
320
344
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
output({
|
|
345
|
+
return content;
|
|
346
|
+
}, cwd);
|
|
347
|
+
|
|
348
|
+
if (!result || result.error) {
|
|
349
|
+
output({ error: 'Cannot parse Current Plan or Total Plans in Phase from STATE.md' }, raw);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (result.advanced === false) {
|
|
354
|
+
output(result, raw, 'false');
|
|
355
|
+
} else {
|
|
356
|
+
output(result, raw, 'true');
|
|
326
357
|
}
|
|
327
358
|
}
|
|
328
359
|
|
|
@@ -330,7 +361,6 @@ function cmdStateRecordMetric(cwd, options, raw) {
|
|
|
330
361
|
const statePath = planningPaths(cwd).state;
|
|
331
362
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
332
363
|
|
|
333
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
334
364
|
const { phase, plan, duration, tasks, files } = options;
|
|
335
365
|
|
|
336
366
|
if (!phase || !plan || !duration) {
|
|
@@ -338,22 +368,29 @@ function cmdStateRecordMetric(cwd, options, raw) {
|
|
|
338
368
|
return;
|
|
339
369
|
}
|
|
340
370
|
|
|
371
|
+
let recorded = false;
|
|
372
|
+
readModifyWriteStateMd(statePath, (content) => {
|
|
341
373
|
// Find Performance Metrics section and its table
|
|
342
374
|
const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n)([\s\S]*?)(?=\n##|\n$|$)/i;
|
|
343
375
|
const metricsMatch = content.match(metricsPattern);
|
|
344
376
|
|
|
345
377
|
if (metricsMatch) {
|
|
346
|
-
|
|
347
|
-
|
|
378
|
+
let tableBody = metricsMatch[2].trimEnd();
|
|
379
|
+
const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
|
|
348
380
|
|
|
349
|
-
|
|
381
|
+
if (tableBody.trim() === '' || tableBody.includes('None yet')) {
|
|
350
382
|
tableBody = newRow;
|
|
351
|
-
|
|
383
|
+
} else {
|
|
352
384
|
tableBody = tableBody + '\n' + newRow;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
recorded = true;
|
|
388
|
+
return content.replace(metricsPattern, (_match, header) => `${header}${tableBody}\n`);
|
|
353
389
|
}
|
|
390
|
+
return content;
|
|
391
|
+
}, cwd);
|
|
354
392
|
|
|
355
|
-
|
|
356
|
-
writeStateMd(statePath, content, cwd);
|
|
393
|
+
if (recorded) {
|
|
357
394
|
output({ recorded: true, phase, plan, duration }, raw, 'true');
|
|
358
395
|
} else {
|
|
359
396
|
output({ recorded: false, reason: 'Performance Metrics section not found in STATE.md' }, raw, 'false');
|
|
@@ -364,9 +401,7 @@ function cmdStateUpdateProgress(cwd, raw) {
|
|
|
364
401
|
const statePath = planningPaths(cwd).state;
|
|
365
402
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
366
403
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
// Count summaries across current milestone phases only
|
|
404
|
+
// Count summaries across current milestone phases only (outside lock — read-only)
|
|
370
405
|
const phasesDir = planningPaths(cwd).phases;
|
|
371
406
|
let totalPlans = 0;
|
|
372
407
|
let totalSummaries = 0;
|
|
@@ -389,17 +424,26 @@ function cmdStateUpdateProgress(cwd, raw) {
|
|
|
389
424
|
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
390
425
|
const progressStr = `[${bar}] ${percent}%`;
|
|
391
426
|
|
|
427
|
+
let updated = false;
|
|
428
|
+
const _totalPlans = totalPlans;
|
|
429
|
+
const _totalSummaries = totalSummaries;
|
|
430
|
+
|
|
431
|
+
readModifyWriteStateMd(statePath, (content) => {
|
|
392
432
|
// Try **Progress:** bold format first, then plain Progress: format
|
|
393
433
|
const boldProgressPattern = /(\*\*Progress:\*\*\s*).*/i;
|
|
394
434
|
const plainProgressPattern = /^(Progress:\s*).*/im;
|
|
395
435
|
if (boldProgressPattern.test(content)) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
|
|
436
|
+
updated = true;
|
|
437
|
+
return content.replace(boldProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
|
|
399
438
|
} else if (plainProgressPattern.test(content)) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
439
|
+
updated = true;
|
|
440
|
+
return content.replace(plainProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
|
|
441
|
+
}
|
|
442
|
+
return content;
|
|
443
|
+
}, cwd);
|
|
444
|
+
|
|
445
|
+
if (updated) {
|
|
446
|
+
output({ updated: true, percent, completed: _totalSummaries, total: _totalPlans, bar: progressStr }, raw, progressStr);
|
|
403
447
|
} else {
|
|
404
448
|
output({ updated: false, reason: 'Progress field not found in STATE.md' }, raw, 'false');
|
|
405
449
|
}
|
|
@@ -423,20 +467,26 @@ function cmdStateAddDecision(cwd, options, raw) {
|
|
|
423
467
|
|
|
424
468
|
if (!summaryText) { output({ error: 'summary required' }, raw); return; }
|
|
425
469
|
|
|
426
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
427
470
|
const entry = `- [Phase ${phase || '?'}]: ${summaryText}${rationaleText ? ` — ${rationaleText}` : ''}`;
|
|
471
|
+
let added = false;
|
|
428
472
|
|
|
473
|
+
readModifyWriteStateMd(statePath, (content) => {
|
|
429
474
|
// Find Decisions section (various heading patterns)
|
|
430
475
|
const sectionPattern = /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
431
476
|
const match = content.match(sectionPattern);
|
|
432
477
|
|
|
433
478
|
if (match) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
479
|
+
let sectionBody = match[2];
|
|
480
|
+
// Remove placeholders
|
|
481
|
+
sectionBody = sectionBody.replace(/None yet\.?\s*\n?/gi, '').replace(/No decisions yet\.?\s*\n?/gi, '');
|
|
482
|
+
sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
|
|
483
|
+
added = true;
|
|
484
|
+
return content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
|
|
485
|
+
}
|
|
486
|
+
return content;
|
|
487
|
+
}, cwd);
|
|
488
|
+
|
|
489
|
+
if (added) {
|
|
440
490
|
output({ added: true, decision: entry }, raw, 'true');
|
|
441
491
|
} else {
|
|
442
492
|
output({ added: false, reason: 'Decisions section not found in STATE.md' }, raw, 'false');
|
|
@@ -458,18 +508,24 @@ function cmdStateAddBlocker(cwd, text, raw) {
|
|
|
458
508
|
|
|
459
509
|
if (!blockerText) { output({ error: 'text required' }, raw); return; }
|
|
460
510
|
|
|
461
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
462
511
|
const entry = `- ${blockerText}`;
|
|
512
|
+
let added = false;
|
|
463
513
|
|
|
514
|
+
readModifyWriteStateMd(statePath, (content) => {
|
|
464
515
|
const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
465
516
|
const match = content.match(sectionPattern);
|
|
466
517
|
|
|
467
518
|
if (match) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
519
|
+
let sectionBody = match[2];
|
|
520
|
+
sectionBody = sectionBody.replace(/None\.?\s*\n?/gi, '').replace(/None yet\.?\s*\n?/gi, '');
|
|
521
|
+
sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
|
|
522
|
+
added = true;
|
|
523
|
+
return content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
|
|
524
|
+
}
|
|
525
|
+
return content;
|
|
526
|
+
}, cwd);
|
|
527
|
+
|
|
528
|
+
if (added) {
|
|
473
529
|
output({ added: true, blocker: blockerText }, raw, 'true');
|
|
474
530
|
} else {
|
|
475
531
|
output({ added: false, reason: 'Blockers section not found in STATE.md' }, raw, 'false');
|
|
@@ -481,27 +537,33 @@ function cmdStateResolveBlocker(cwd, text, raw) {
|
|
|
481
537
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
482
538
|
if (!text) { output({ error: 'text required' }, raw); return; }
|
|
483
539
|
|
|
484
|
-
let
|
|
540
|
+
let resolved = false;
|
|
485
541
|
|
|
542
|
+
readModifyWriteStateMd(statePath, (content) => {
|
|
486
543
|
const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
487
544
|
const match = content.match(sectionPattern);
|
|
488
545
|
|
|
489
546
|
if (match) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
547
|
+
const sectionBody = match[2];
|
|
548
|
+
const lines = sectionBody.split('\n');
|
|
549
|
+
const filtered = lines.filter(line => {
|
|
493
550
|
if (!line.startsWith('- ')) return true;
|
|
494
551
|
return !line.toLowerCase().includes(text.toLowerCase());
|
|
495
|
-
|
|
552
|
+
});
|
|
496
553
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
554
|
+
let newBody = filtered.join('\n');
|
|
555
|
+
// If section is now empty, add placeholder
|
|
556
|
+
if (!newBody.trim() || !newBody.includes('- ')) {
|
|
500
557
|
newBody = 'None\n';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
resolved = true;
|
|
561
|
+
return content.replace(sectionPattern, (_match, header) => `${header}${newBody}`);
|
|
501
562
|
}
|
|
563
|
+
return content;
|
|
564
|
+
}, cwd);
|
|
502
565
|
|
|
503
|
-
|
|
504
|
-
writeStateMd(statePath, content, cwd);
|
|
566
|
+
if (resolved) {
|
|
505
567
|
output({ resolved: true, blocker: text }, raw, 'true');
|
|
506
568
|
} else {
|
|
507
569
|
output({ resolved: false, reason: 'Blockers section not found in STATE.md' }, raw, 'false');
|
|
@@ -512,10 +574,10 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
512
574
|
const statePath = planningPaths(cwd).state;
|
|
513
575
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
514
576
|
|
|
515
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
516
577
|
const now = new Date().toISOString();
|
|
517
578
|
const updated = [];
|
|
518
579
|
|
|
580
|
+
readModifyWriteStateMd(statePath, (content) => {
|
|
519
581
|
// Update Last session / Last Date
|
|
520
582
|
let result = stateReplaceField(content, 'Last session', now);
|
|
521
583
|
if (result) { content = result; updated.push('Last session'); }
|
|
@@ -524,9 +586,9 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
524
586
|
|
|
525
587
|
// Update Stopped at
|
|
526
588
|
if (options.stopped_at) {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
589
|
+
result = stateReplaceField(content, 'Stopped At', options.stopped_at);
|
|
590
|
+
if (!result) result = stateReplaceField(content, 'Stopped at', options.stopped_at);
|
|
591
|
+
if (result) { content = result; updated.push('Stopped At'); }
|
|
530
592
|
}
|
|
531
593
|
|
|
532
594
|
// Update Resume file
|
|
@@ -535,8 +597,10 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
535
597
|
if (!result) result = stateReplaceField(content, 'Resume file', resumeFile);
|
|
536
598
|
if (result) { content = result; updated.push('Resume File'); }
|
|
537
599
|
|
|
600
|
+
return content;
|
|
601
|
+
}, cwd);
|
|
602
|
+
|
|
538
603
|
if (updated.length > 0) {
|
|
539
|
-
writeStateMd(statePath, content, cwd);
|
|
540
604
|
output({ recorded: true, updated }, raw, 'true');
|
|
541
605
|
} else {
|
|
542
606
|
output({ recorded: false, reason: 'No session fields found in STATE.md' }, raw, 'false');
|
|
@@ -678,28 +742,40 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
678
742
|
try {
|
|
679
743
|
const phasesDir = planningPaths(cwd).phases;
|
|
680
744
|
if (fs.existsSync(phasesDir)) {
|
|
681
|
-
|
|
682
|
-
|
|
745
|
+
// Use cached disk scan when available — avoids N+1 readdirSync calls
|
|
746
|
+
// on repeated buildStateFrontmatter invocations within the same process (#1967)
|
|
747
|
+
let cached = _diskScanCache.get(cwd);
|
|
748
|
+
if (!cached) {
|
|
749
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
750
|
+
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
683
751
|
.filter(e => e.isDirectory()).map(e => e.name)
|
|
684
752
|
.filter(isDirInMilestone);
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
753
|
+
let diskTotalPlans = 0;
|
|
754
|
+
let diskTotalSummaries = 0;
|
|
755
|
+
let diskCompletedPhases = 0;
|
|
688
756
|
|
|
689
|
-
|
|
757
|
+
for (const dir of phaseDirs) {
|
|
690
758
|
const files = fs.readdirSync(path.join(phasesDir, dir));
|
|
691
759
|
const plans = files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
|
692
760
|
const summaries = files.filter(f => f.match(/-SUMMARY\.md$/i)).length;
|
|
693
761
|
diskTotalPlans += plans;
|
|
694
762
|
diskTotalSummaries += summaries;
|
|
695
763
|
if (plans > 0 && summaries >= plans) diskCompletedPhases++;
|
|
764
|
+
}
|
|
765
|
+
cached = {
|
|
766
|
+
totalPhases: isDirInMilestone.phaseCount > 0
|
|
767
|
+
? Math.max(phaseDirs.length, isDirInMilestone.phaseCount)
|
|
768
|
+
: phaseDirs.length,
|
|
769
|
+
completedPhases: diskCompletedPhases,
|
|
770
|
+
totalPlans: diskTotalPlans,
|
|
771
|
+
completedPlans: diskTotalSummaries,
|
|
772
|
+
};
|
|
773
|
+
_diskScanCache.set(cwd, cached);
|
|
696
774
|
}
|
|
697
|
-
totalPhases =
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
totalPlans = diskTotalPlans;
|
|
702
|
-
completedPlans = diskTotalSummaries;
|
|
775
|
+
totalPhases = cached.totalPhases;
|
|
776
|
+
completedPhases = cached.completedPhases;
|
|
777
|
+
totalPlans = cached.totalPlans;
|
|
778
|
+
completedPlans = cached.completedPlans;
|
|
703
779
|
}
|
|
704
780
|
} catch { /* intentionally empty */ }
|
|
705
781
|
}
|
|
@@ -805,6 +881,9 @@ function acquireStateLock(statePath) {
|
|
|
805
881
|
const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
|
806
882
|
fs.writeSync(fd, String(process.pid));
|
|
807
883
|
fs.closeSync(fd);
|
|
884
|
+
// Register for exit-time cleanup so process.exit(1) inside a locked region
|
|
885
|
+
// cannot leave a stale lock file (#1916).
|
|
886
|
+
_heldStateLocks.add(lockPath);
|
|
808
887
|
return lockPath;
|
|
809
888
|
} catch (err) {
|
|
810
889
|
if (err.code === 'EEXIST') {
|
|
@@ -821,8 +900,7 @@ function acquireStateLock(statePath) {
|
|
|
821
900
|
return lockPath;
|
|
822
901
|
}
|
|
823
902
|
const jitter = Math.floor(Math.random() * 50);
|
|
824
|
-
|
|
825
|
-
while (Date.now() - start < retryDelay + jitter) { /* busy wait */ }
|
|
903
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, retryDelay + jitter);
|
|
826
904
|
continue;
|
|
827
905
|
}
|
|
828
906
|
return lockPath; // non-EEXIST error — proceed without lock
|
|
@@ -832,6 +910,7 @@ function acquireStateLock(statePath) {
|
|
|
832
910
|
}
|
|
833
911
|
|
|
834
912
|
function releaseStateLock(lockPath) {
|
|
913
|
+
_heldStateLocks.delete(lockPath);
|
|
835
914
|
try { fs.unlinkSync(lockPath); } catch { /* lock already gone */ }
|
|
836
915
|
}
|
|
837
916
|
|
|
@@ -842,10 +921,14 @@ function releaseStateLock(lockPath) {
|
|
|
842
921
|
* each other's changes (race condition with read-modify-write cycle).
|
|
843
922
|
*/
|
|
844
923
|
function writeStateMd(statePath, content, cwd) {
|
|
924
|
+
// Invalidate disk scan cache before computing new frontmatter — the write
|
|
925
|
+
// may create new PLAN/SUMMARY files that buildStateFrontmatter must see.
|
|
926
|
+
// Safe for any calling pattern, not just short-lived CLI processes (#1967).
|
|
927
|
+
if (cwd) _diskScanCache.delete(cwd);
|
|
845
928
|
const synced = syncStateFrontmatter(content, cwd);
|
|
846
929
|
const lockPath = acquireStateLock(statePath);
|
|
847
930
|
try {
|
|
848
|
-
|
|
931
|
+
atomicWriteFileSync(statePath, normalizeMd(synced), 'utf-8');
|
|
849
932
|
} finally {
|
|
850
933
|
releaseStateLock(lockPath);
|
|
851
934
|
}
|
|
@@ -863,7 +946,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd) {
|
|
|
863
946
|
const content = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8') : '';
|
|
864
947
|
const modified = transformFn(content);
|
|
865
948
|
const synced = syncStateFrontmatter(modified, cwd);
|
|
866
|
-
|
|
949
|
+
atomicWriteFileSync(statePath, normalizeMd(synced), 'utf-8');
|
|
867
950
|
} finally {
|
|
868
951
|
releaseStateLock(lockPath);
|
|
869
952
|
}
|
|
@@ -913,10 +996,10 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
913
996
|
return;
|
|
914
997
|
}
|
|
915
998
|
|
|
916
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
917
999
|
const today = new Date().toISOString().split('T')[0];
|
|
918
1000
|
const updated = [];
|
|
919
1001
|
|
|
1002
|
+
readModifyWriteStateMd(statePath, (content) => {
|
|
920
1003
|
// Update Status field
|
|
921
1004
|
const statusValue = `Executing Phase ${phaseNumber}`;
|
|
922
1005
|
let result = stateReplaceField(content, 'Status', statusValue);
|
|
@@ -937,8 +1020,8 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
937
1020
|
|
|
938
1021
|
// Update Current Phase Name
|
|
939
1022
|
if (phaseName) {
|
|
940
|
-
|
|
941
|
-
|
|
1023
|
+
result = stateReplaceField(content, 'Current Phase Name', phaseName);
|
|
1024
|
+
if (result) { content = result; updated.push('Current Phase Name'); }
|
|
942
1025
|
}
|
|
943
1026
|
|
|
944
1027
|
// Update Current Plan to 1 (starting from the first plan)
|
|
@@ -947,16 +1030,16 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
947
1030
|
|
|
948
1031
|
// Update Total Plans in Phase
|
|
949
1032
|
if (planCount) {
|
|
950
|
-
|
|
951
|
-
|
|
1033
|
+
result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
|
|
1034
|
+
if (result) { content = result; updated.push('Total Plans in Phase'); }
|
|
952
1035
|
}
|
|
953
1036
|
|
|
954
1037
|
// Update **Current focus:** body text line (#1104)
|
|
955
1038
|
const focusLabel = phaseName ? `Phase ${phaseNumber} — ${phaseName}` : `Phase ${phaseNumber}`;
|
|
956
1039
|
const focusPattern = /(\*\*Current focus:\*\*\s*).*/i;
|
|
957
1040
|
if (focusPattern.test(content)) {
|
|
958
|
-
|
|
959
|
-
|
|
1041
|
+
content = content.replace(focusPattern, (_match, prefix) => `${prefix}${focusLabel}`);
|
|
1042
|
+
updated.push('Current focus');
|
|
960
1043
|
}
|
|
961
1044
|
|
|
962
1045
|
// Update ## Current Position section (#1104, #1365)
|
|
@@ -965,44 +1048,43 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
965
1048
|
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
|
966
1049
|
const positionMatch = content.match(positionPattern);
|
|
967
1050
|
if (positionMatch) {
|
|
968
|
-
|
|
969
|
-
|
|
1051
|
+
const header = positionMatch[1];
|
|
1052
|
+
let posBody = positionMatch[2];
|
|
970
1053
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1054
|
+
// Update or insert Phase line
|
|
1055
|
+
const newPhase = `Phase: ${phaseNumber}${phaseName ? ` (${phaseName})` : ''} — EXECUTING`;
|
|
1056
|
+
if (/^Phase:/m.test(posBody)) {
|
|
974
1057
|
posBody = posBody.replace(/^Phase:.*$/m, newPhase);
|
|
975
|
-
|
|
1058
|
+
} else {
|
|
976
1059
|
posBody = newPhase + '\n' + posBody;
|
|
977
|
-
|
|
1060
|
+
}
|
|
978
1061
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1062
|
+
// Update or insert Plan line
|
|
1063
|
+
const newPlan = `Plan: 1 of ${planCount || '?'}`;
|
|
1064
|
+
if (/^Plan:/m.test(posBody)) {
|
|
982
1065
|
posBody = posBody.replace(/^Plan:.*$/m, newPlan);
|
|
983
|
-
|
|
1066
|
+
} else {
|
|
984
1067
|
posBody = posBody.replace(/^(Phase:.*$)/m, `$1\n${newPlan}`);
|
|
985
|
-
|
|
1068
|
+
}
|
|
986
1069
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1070
|
+
// Update Status line if present
|
|
1071
|
+
const newStatus = `Status: Executing Phase ${phaseNumber}`;
|
|
1072
|
+
if (/^Status:/m.test(posBody)) {
|
|
990
1073
|
posBody = posBody.replace(/^Status:.*$/m, newStatus);
|
|
991
|
-
|
|
1074
|
+
}
|
|
992
1075
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1076
|
+
// Update Last activity line if present
|
|
1077
|
+
const newActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution started`;
|
|
1078
|
+
if (/^Last activity:/im.test(posBody)) {
|
|
996
1079
|
posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
|
|
997
|
-
|
|
1080
|
+
}
|
|
998
1081
|
|
|
999
|
-
|
|
1000
|
-
|
|
1082
|
+
content = content.replace(positionPattern, `${header}${posBody}`);
|
|
1083
|
+
updated.push('Current Position');
|
|
1001
1084
|
}
|
|
1002
1085
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
}
|
|
1086
|
+
return content;
|
|
1087
|
+
}, cwd);
|
|
1006
1088
|
|
|
1007
1089
|
output({ updated, phase: phaseNumber, phase_name: phaseName || null, plan_count: planCount || null }, raw, updated.length > 0 ? 'true' : 'false');
|
|
1008
1090
|
}
|
|
@@ -1325,11 +1407,193 @@ function cmdStateSync(cwd, options, raw) {
|
|
|
1325
1407
|
output({ synced: true, changes, dry_run: false }, raw);
|
|
1326
1408
|
}
|
|
1327
1409
|
|
|
1410
|
+
/**
|
|
1411
|
+
* Prune old entries from STATE.md sections that grow unboundedly (#1970).
|
|
1412
|
+
* Moves decisions, recently-completed summaries, and resolved blockers
|
|
1413
|
+
* older than keepRecent phases to STATE-ARCHIVE.md.
|
|
1414
|
+
*
|
|
1415
|
+
* Options:
|
|
1416
|
+
* keepRecent: number of recent phases to retain (default: 3)
|
|
1417
|
+
* dryRun: if true, return what would be pruned without modifying STATE.md
|
|
1418
|
+
*/
|
|
1419
|
+
function cmdStatePrune(cwd, options, raw) {
|
|
1420
|
+
const silent = !!options.silent;
|
|
1421
|
+
const emit = silent ? () => {} : (result, r, v) => output(result, r, v);
|
|
1422
|
+
const statePath = planningPaths(cwd).state;
|
|
1423
|
+
if (!fs.existsSync(statePath)) { emit({ error: 'STATE.md not found' }, raw); return; }
|
|
1424
|
+
|
|
1425
|
+
const keepRecent = parseInt(options.keepRecent, 10) || 3;
|
|
1426
|
+
const dryRun = !!options.dryRun;
|
|
1427
|
+
const currentPhaseRaw = stateExtractField(fs.readFileSync(statePath, 'utf-8'), 'Current Phase');
|
|
1428
|
+
const currentPhase = parseInt(currentPhaseRaw, 10) || 0;
|
|
1429
|
+
const cutoff = currentPhase - keepRecent;
|
|
1430
|
+
|
|
1431
|
+
if (cutoff <= 0) {
|
|
1432
|
+
emit({ pruned: false, reason: `Only ${currentPhase} phases — nothing to prune with --keep-recent ${keepRecent}` }, raw, 'false');
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const archivePath = path.join(path.dirname(statePath), 'STATE-ARCHIVE.md');
|
|
1437
|
+
const archived = [];
|
|
1438
|
+
|
|
1439
|
+
// Shared pruning logic applied to both dry-run and real passes.
|
|
1440
|
+
// Returns { newContent, archivedSections }.
|
|
1441
|
+
function prunePass(content) {
|
|
1442
|
+
const sections = [];
|
|
1443
|
+
|
|
1444
|
+
// Prune Decisions section: entries like "- [Phase N]: ..."
|
|
1445
|
+
const decisionPattern = /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
1446
|
+
const decMatch = content.match(decisionPattern);
|
|
1447
|
+
if (decMatch) {
|
|
1448
|
+
const lines = decMatch[2].split('\n');
|
|
1449
|
+
const keep = [];
|
|
1450
|
+
const archive = [];
|
|
1451
|
+
for (const line of lines) {
|
|
1452
|
+
const phaseMatch = line.match(/^\s*-\s*\[Phase\s+(\d+)/i);
|
|
1453
|
+
if (phaseMatch && parseInt(phaseMatch[1], 10) <= cutoff) {
|
|
1454
|
+
archive.push(line);
|
|
1455
|
+
} else {
|
|
1456
|
+
keep.push(line);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
if (archive.length > 0) {
|
|
1460
|
+
sections.push({ section: 'Decisions', count: archive.length, lines: archive });
|
|
1461
|
+
content = content.replace(decisionPattern, (_m, header) => `${header}${keep.join('\n')}`);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Prune Recently Completed section: entries mentioning phase numbers
|
|
1466
|
+
const recentPattern = /(###?\s*Recently Completed\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
1467
|
+
const recMatch = content.match(recentPattern);
|
|
1468
|
+
if (recMatch) {
|
|
1469
|
+
const lines = recMatch[2].split('\n');
|
|
1470
|
+
const keep = [];
|
|
1471
|
+
const archive = [];
|
|
1472
|
+
for (const line of lines) {
|
|
1473
|
+
const phaseMatch = line.match(/Phase\s+(\d+)/i);
|
|
1474
|
+
if (phaseMatch && parseInt(phaseMatch[1], 10) <= cutoff) {
|
|
1475
|
+
archive.push(line);
|
|
1476
|
+
} else {
|
|
1477
|
+
keep.push(line);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (archive.length > 0) {
|
|
1481
|
+
sections.push({ section: 'Recently Completed', count: archive.length, lines: archive });
|
|
1482
|
+
content = content.replace(recentPattern, (_m, header) => `${header}${keep.join('\n')}`);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Prune resolved blockers: lines marked as resolved (strikethrough ~~text~~
|
|
1487
|
+
// or "[RESOLVED]" prefix) with a phase reference older than cutoff
|
|
1488
|
+
const blockersPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Blockers\s*&\s*Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
1489
|
+
const blockersMatch = content.match(blockersPattern);
|
|
1490
|
+
if (blockersMatch) {
|
|
1491
|
+
const lines = blockersMatch[2].split('\n');
|
|
1492
|
+
const keep = [];
|
|
1493
|
+
const archive = [];
|
|
1494
|
+
for (const line of lines) {
|
|
1495
|
+
const isResolved = /~~.*~~|\[RESOLVED\]/i.test(line);
|
|
1496
|
+
const phaseMatch = line.match(/Phase\s+(\d+)/i);
|
|
1497
|
+
if (isResolved && phaseMatch && parseInt(phaseMatch[1], 10) <= cutoff) {
|
|
1498
|
+
archive.push(line);
|
|
1499
|
+
} else {
|
|
1500
|
+
keep.push(line);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (archive.length > 0) {
|
|
1504
|
+
sections.push({ section: 'Blockers (resolved)', count: archive.length, lines: archive });
|
|
1505
|
+
content = content.replace(blockersPattern, (_m, header) => `${header}${keep.join('\n')}`);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// Prune Performance Metrics table rows: keep only rows for phases > cutoff.
|
|
1510
|
+
// Preserves header rows (| Phase | ... and |---|...) and any prose around the table.
|
|
1511
|
+
const metricsPattern = /(###?\s*Performance Metrics\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
1512
|
+
const metricsMatch = content.match(metricsPattern);
|
|
1513
|
+
if (metricsMatch) {
|
|
1514
|
+
const sectionLines = metricsMatch[2].split('\n');
|
|
1515
|
+
const keep = [];
|
|
1516
|
+
const archive = [];
|
|
1517
|
+
for (const line of sectionLines) {
|
|
1518
|
+
// Table data row: starts with | followed by a number (phase)
|
|
1519
|
+
const tableRowMatch = line.match(/^\|\s*(\d+)\s*\|/);
|
|
1520
|
+
if (tableRowMatch) {
|
|
1521
|
+
const rowPhase = parseInt(tableRowMatch[1], 10);
|
|
1522
|
+
if (rowPhase <= cutoff) {
|
|
1523
|
+
archive.push(line);
|
|
1524
|
+
} else {
|
|
1525
|
+
keep.push(line);
|
|
1526
|
+
}
|
|
1527
|
+
} else {
|
|
1528
|
+
// Header row, separator row, or prose — always keep
|
|
1529
|
+
keep.push(line);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
if (archive.length > 0) {
|
|
1533
|
+
sections.push({ section: 'Performance Metrics', count: archive.length, lines: archive });
|
|
1534
|
+
content = content.replace(metricsPattern, (_m, header) => `${header}${keep.join('\n')}`);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
return { newContent: content, archivedSections: sections };
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
if (dryRun) {
|
|
1542
|
+
// Dry-run: compute what would be pruned without writing anything
|
|
1543
|
+
const content = fs.readFileSync(statePath, 'utf-8');
|
|
1544
|
+
const result = prunePass(content);
|
|
1545
|
+
const totalPruned = result.archivedSections.reduce((sum, s) => sum + s.count, 0);
|
|
1546
|
+
emit({
|
|
1547
|
+
pruned: false,
|
|
1548
|
+
dry_run: true,
|
|
1549
|
+
cutoff_phase: cutoff,
|
|
1550
|
+
keep_recent: keepRecent,
|
|
1551
|
+
sections: result.archivedSections.map(s => ({ section: s.section, entries_would_archive: s.count })),
|
|
1552
|
+
total_would_archive: totalPruned,
|
|
1553
|
+
note: totalPruned > 0 ? 'Run without --dry-run to actually prune' : 'Nothing to prune',
|
|
1554
|
+
}, raw, totalPruned > 0 ? 'true' : 'false');
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
readModifyWriteStateMd(statePath, (content) => {
|
|
1559
|
+
const result = prunePass(content);
|
|
1560
|
+
archived.push(...result.archivedSections);
|
|
1561
|
+
return result.newContent;
|
|
1562
|
+
}, cwd);
|
|
1563
|
+
|
|
1564
|
+
// Write archived entries to STATE-ARCHIVE.md
|
|
1565
|
+
if (archived.length > 0) {
|
|
1566
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
1567
|
+
let archiveContent = '';
|
|
1568
|
+
if (fs.existsSync(archivePath)) {
|
|
1569
|
+
archiveContent = fs.readFileSync(archivePath, 'utf-8');
|
|
1570
|
+
} else {
|
|
1571
|
+
archiveContent = '# STATE Archive\n\nPruned entries from STATE.md. Recoverable but no longer loaded into agent context.\n\n';
|
|
1572
|
+
}
|
|
1573
|
+
archiveContent += `## Pruned ${timestamp} (phases 1-${cutoff}, kept recent ${keepRecent})\n\n`;
|
|
1574
|
+
for (const section of archived) {
|
|
1575
|
+
archiveContent += `### ${section.section}\n\n${section.lines.join('\n')}\n\n`;
|
|
1576
|
+
}
|
|
1577
|
+
atomicWriteFileSync(archivePath, archiveContent);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
const totalPruned = archived.reduce((sum, s) => sum + s.count, 0);
|
|
1581
|
+
emit({
|
|
1582
|
+
pruned: totalPruned > 0,
|
|
1583
|
+
cutoff_phase: cutoff,
|
|
1584
|
+
keep_recent: keepRecent,
|
|
1585
|
+
sections: archived.map(s => ({ section: s.section, entries_archived: s.count })),
|
|
1586
|
+
total_archived: totalPruned,
|
|
1587
|
+
archive_file: totalPruned > 0 ? 'STATE-ARCHIVE.md' : null,
|
|
1588
|
+
}, raw, totalPruned > 0 ? 'true' : 'false');
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1328
1591
|
module.exports = {
|
|
1329
1592
|
stateExtractField,
|
|
1330
1593
|
stateReplaceField,
|
|
1331
1594
|
stateReplaceFieldWithFallback,
|
|
1332
1595
|
writeStateMd,
|
|
1596
|
+
readModifyWriteStateMd,
|
|
1333
1597
|
updatePerformanceMetricsSection,
|
|
1334
1598
|
cmdStateLoad,
|
|
1335
1599
|
cmdStateGet,
|
|
@@ -1348,6 +1612,7 @@ module.exports = {
|
|
|
1348
1612
|
cmdStatePlannedPhase,
|
|
1349
1613
|
cmdStateValidate,
|
|
1350
1614
|
cmdStateSync,
|
|
1615
|
+
cmdStatePrune,
|
|
1351
1616
|
cmdSignalWaiting,
|
|
1352
1617
|
cmdSignalResume,
|
|
1353
1618
|
};
|