gsd-opencode 1.22.1 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/gsd-advisor-researcher.md +112 -0
- package/agents/gsd-assumptions-analyzer.md +110 -0
- package/agents/gsd-codebase-mapper.md +0 -2
- package/agents/gsd-debugger.md +117 -2
- package/agents/gsd-doc-verifier.md +207 -0
- package/agents/gsd-doc-writer.md +608 -0
- package/agents/gsd-executor.md +45 -4
- package/agents/gsd-integration-checker.md +0 -2
- package/agents/gsd-nyquist-auditor.md +0 -2
- package/agents/gsd-phase-researcher.md +191 -5
- package/agents/gsd-plan-checker.md +152 -5
- package/agents/gsd-planner.md +131 -157
- package/agents/gsd-project-researcher.md +28 -3
- package/agents/gsd-research-synthesizer.md +0 -2
- package/agents/gsd-roadmapper.md +29 -2
- package/agents/gsd-security-auditor.md +129 -0
- package/agents/gsd-ui-auditor.md +485 -0
- package/agents/gsd-ui-checker.md +305 -0
- package/agents/gsd-ui-researcher.md +368 -0
- package/agents/gsd-user-profiler.md +173 -0
- package/agents/gsd-verifier.md +207 -22
- package/commands/gsd/gsd-add-backlog.md +76 -0
- package/commands/gsd/gsd-analyze-dependencies.md +34 -0
- package/commands/gsd/gsd-audit-uat.md +24 -0
- package/commands/gsd/gsd-autonomous.md +45 -0
- package/commands/gsd/gsd-cleanup.md +5 -0
- package/commands/gsd/gsd-debug.md +29 -21
- package/commands/gsd/gsd-discuss-phase.md +15 -36
- package/commands/gsd/gsd-do.md +30 -0
- package/commands/gsd/gsd-docs-update.md +48 -0
- package/commands/gsd/gsd-execute-phase.md +24 -2
- package/commands/gsd/gsd-fast.md +30 -0
- package/commands/gsd/gsd-forensics.md +56 -0
- package/commands/gsd/gsd-help.md +2 -0
- package/commands/gsd/gsd-join-discord.md +2 -1
- package/commands/gsd/gsd-list-workspaces.md +19 -0
- package/commands/gsd/gsd-manager.md +40 -0
- package/commands/gsd/gsd-milestone-summary.md +51 -0
- package/commands/gsd/gsd-new-project.md +4 -0
- package/commands/gsd/gsd-new-workspace.md +44 -0
- package/commands/gsd/gsd-next.md +24 -0
- package/commands/gsd/gsd-note.md +34 -0
- package/commands/gsd/gsd-plan-phase.md +8 -1
- package/commands/gsd/gsd-plant-seed.md +28 -0
- package/commands/gsd/gsd-pr-branch.md +25 -0
- package/commands/gsd/gsd-profile-user.md +46 -0
- package/commands/gsd/gsd-quick.md +7 -3
- package/commands/gsd/gsd-reapply-patches.md +178 -45
- package/commands/gsd/gsd-remove-workspace.md +26 -0
- package/commands/gsd/gsd-research-phase.md +7 -12
- package/commands/gsd/gsd-review-backlog.md +62 -0
- package/commands/gsd/gsd-review.md +38 -0
- package/commands/gsd/gsd-secure-phase.md +35 -0
- package/commands/gsd/gsd-session-report.md +19 -0
- package/commands/gsd/gsd-set-profile.md +24 -23
- package/commands/gsd/gsd-ship.md +23 -0
- package/commands/gsd/gsd-stats.md +18 -0
- package/commands/gsd/gsd-thread.md +127 -0
- package/commands/gsd/gsd-ui-phase.md +34 -0
- package/commands/gsd/gsd-ui-review.md +32 -0
- package/commands/gsd/gsd-workstreams.md +71 -0
- package/get-shit-done/bin/gsd-tools.cjs +450 -90
- package/get-shit-done/bin/lib/commands.cjs +489 -24
- package/get-shit-done/bin/lib/config.cjs +329 -48
- package/get-shit-done/bin/lib/core.cjs +1143 -102
- package/get-shit-done/bin/lib/docs.cjs +267 -0
- package/get-shit-done/bin/lib/frontmatter.cjs +125 -43
- package/get-shit-done/bin/lib/init.cjs +918 -106
- package/get-shit-done/bin/lib/milestone.cjs +65 -33
- package/get-shit-done/bin/lib/model-profiles.cjs +70 -0
- package/get-shit-done/bin/lib/phase.cjs +434 -404
- package/get-shit-done/bin/lib/profile-output.cjs +1048 -0
- package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
- package/get-shit-done/bin/lib/roadmap.cjs +156 -101
- package/get-shit-done/bin/lib/schema-detect.cjs +238 -0
- package/get-shit-done/bin/lib/security.cjs +384 -0
- package/get-shit-done/bin/lib/state.cjs +711 -79
- package/get-shit-done/bin/lib/template.cjs +2 -2
- package/get-shit-done/bin/lib/uat.cjs +282 -0
- package/get-shit-done/bin/lib/verify.cjs +254 -42
- package/get-shit-done/bin/lib/workstream.cjs +495 -0
- package/get-shit-done/references/agent-contracts.md +79 -0
- package/get-shit-done/references/artifact-types.md +113 -0
- package/get-shit-done/references/checkpoints.md +12 -10
- package/get-shit-done/references/context-budget.md +49 -0
- package/get-shit-done/references/continuation-format.md +15 -15
- package/get-shit-done/references/decimal-phase-calculation.md +2 -3
- package/get-shit-done/references/domain-probes.md +125 -0
- package/get-shit-done/references/gate-prompts.md +100 -0
- package/get-shit-done/references/git-integration.md +47 -0
- package/get-shit-done/references/model-profile-resolution.md +2 -0
- package/get-shit-done/references/model-profiles.md +62 -16
- package/get-shit-done/references/phase-argument-parsing.md +2 -2
- package/get-shit-done/references/planner-gap-closure.md +62 -0
- package/get-shit-done/references/planner-reviews.md +39 -0
- package/get-shit-done/references/planner-revision.md +87 -0
- package/get-shit-done/references/planning-config.md +18 -1
- package/get-shit-done/references/revision-loop.md +97 -0
- package/get-shit-done/references/ui-brand.md +2 -2
- package/get-shit-done/references/universal-anti-patterns.md +58 -0
- package/get-shit-done/references/user-profiling.md +681 -0
- package/get-shit-done/references/workstream-flag.md +111 -0
- package/get-shit-done/templates/SECURITY.md +61 -0
- package/get-shit-done/templates/UAT.md +21 -3
- package/get-shit-done/templates/UI-SPEC.md +100 -0
- package/get-shit-done/templates/VALIDATION.md +3 -3
- package/get-shit-done/templates/claude-md.md +145 -0
- package/get-shit-done/templates/config.json +14 -3
- package/get-shit-done/templates/context.md +61 -6
- package/get-shit-done/templates/debug-subagent-prompt.md +2 -6
- package/get-shit-done/templates/dev-preferences.md +21 -0
- package/get-shit-done/templates/discussion-log.md +63 -0
- package/get-shit-done/templates/phase-prompt.md +46 -5
- package/get-shit-done/templates/planner-subagent-prompt.md +2 -10
- package/get-shit-done/templates/project.md +2 -0
- package/get-shit-done/templates/state.md +2 -2
- package/get-shit-done/templates/user-profile.md +146 -0
- package/get-shit-done/workflows/add-phase.md +4 -4
- package/get-shit-done/workflows/add-tests.md +4 -4
- package/get-shit-done/workflows/add-todo.md +4 -4
- package/get-shit-done/workflows/analyze-dependencies.md +96 -0
- package/get-shit-done/workflows/audit-milestone.md +20 -16
- package/get-shit-done/workflows/audit-uat.md +109 -0
- package/get-shit-done/workflows/autonomous.md +1036 -0
- package/get-shit-done/workflows/check-todos.md +4 -4
- package/get-shit-done/workflows/cleanup.md +4 -4
- package/get-shit-done/workflows/complete-milestone.md +22 -10
- package/get-shit-done/workflows/diagnose-issues.md +21 -7
- package/get-shit-done/workflows/discovery-phase.md +2 -2
- package/get-shit-done/workflows/discuss-phase-assumptions.md +671 -0
- package/get-shit-done/workflows/discuss-phase-power.md +291 -0
- package/get-shit-done/workflows/discuss-phase.md +558 -47
- package/get-shit-done/workflows/do.md +104 -0
- package/get-shit-done/workflows/docs-update.md +1093 -0
- package/get-shit-done/workflows/execute-phase.md +741 -58
- package/get-shit-done/workflows/execute-plan.md +77 -12
- package/get-shit-done/workflows/fast.md +105 -0
- package/get-shit-done/workflows/forensics.md +265 -0
- package/get-shit-done/workflows/health.md +28 -6
- package/get-shit-done/workflows/help.md +127 -7
- package/get-shit-done/workflows/insert-phase.md +4 -4
- package/get-shit-done/workflows/list-phase-assumptions.md +2 -2
- package/get-shit-done/workflows/list-workspaces.md +56 -0
- package/get-shit-done/workflows/manager.md +363 -0
- package/get-shit-done/workflows/map-codebase.md +83 -44
- package/get-shit-done/workflows/milestone-summary.md +223 -0
- package/get-shit-done/workflows/new-milestone.md +133 -25
- package/get-shit-done/workflows/new-project.md +216 -54
- package/get-shit-done/workflows/new-workspace.md +237 -0
- package/get-shit-done/workflows/next.md +97 -0
- package/get-shit-done/workflows/node-repair.md +92 -0
- package/get-shit-done/workflows/note.md +156 -0
- package/get-shit-done/workflows/pause-work.md +132 -15
- package/get-shit-done/workflows/plan-milestone-gaps.md +6 -7
- package/get-shit-done/workflows/plan-phase.md +513 -62
- package/get-shit-done/workflows/plant-seed.md +169 -0
- package/get-shit-done/workflows/pr-branch.md +129 -0
- package/get-shit-done/workflows/profile-user.md +450 -0
- package/get-shit-done/workflows/progress.md +154 -29
- package/get-shit-done/workflows/quick.md +285 -111
- package/get-shit-done/workflows/remove-phase.md +2 -2
- package/get-shit-done/workflows/remove-workspace.md +90 -0
- package/get-shit-done/workflows/research-phase.md +13 -9
- package/get-shit-done/workflows/resume-project.md +37 -18
- package/get-shit-done/workflows/review.md +281 -0
- package/get-shit-done/workflows/secure-phase.md +154 -0
- package/get-shit-done/workflows/session-report.md +146 -0
- package/get-shit-done/workflows/set-profile.md +2 -2
- package/get-shit-done/workflows/settings.md +91 -11
- package/get-shit-done/workflows/ship.md +237 -0
- package/get-shit-done/workflows/stats.md +60 -0
- package/get-shit-done/workflows/transition.md +150 -23
- package/get-shit-done/workflows/ui-phase.md +292 -0
- package/get-shit-done/workflows/ui-review.md +183 -0
- package/get-shit-done/workflows/update.md +262 -30
- package/get-shit-done/workflows/validate-phase.md +14 -17
- package/get-shit-done/workflows/verify-phase.md +143 -11
- package/get-shit-done/workflows/verify-work.md +141 -39
- package/package.json +1 -1
- package/skills/gsd-audit-milestone/SKILL.md +29 -0
- package/skills/gsd-cleanup/SKILL.md +19 -0
- package/skills/gsd-complete-milestone/SKILL.md +131 -0
- package/skills/gsd-discuss-phase/SKILL.md +54 -0
- package/skills/gsd-execute-phase/SKILL.md +49 -0
- package/skills/gsd-plan-phase/SKILL.md +37 -0
- package/skills/gsd-ui-phase/SKILL.md +24 -0
- package/skills/gsd-ui-review/SKILL.md +24 -0
- package/skills/gsd-verify-work/SKILL.md +30 -0
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, output, error } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningDir, planningPaths, output, error } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
|
9
9
|
|
|
10
|
+
/** Shorthand — every state command needs this path */
|
|
11
|
+
function getStatePath(cwd) {
|
|
12
|
+
return planningPaths(cwd).state;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
// Shared helper: extract a field value from STATE.md content.
|
|
11
16
|
// Supports both **Field:** bold and plain Field: format.
|
|
12
17
|
function stateExtractField(content, fieldName) {
|
|
@@ -21,15 +26,15 @@ function stateExtractField(content, fieldName) {
|
|
|
21
26
|
|
|
22
27
|
function cmdStateLoad(cwd, raw) {
|
|
23
28
|
const config = loadConfig(cwd);
|
|
24
|
-
const
|
|
29
|
+
const planDir = planningPaths(cwd).planning;
|
|
25
30
|
|
|
26
31
|
let stateRaw = '';
|
|
27
32
|
try {
|
|
28
|
-
stateRaw = fs.readFileSync(path.join(
|
|
29
|
-
} catch {}
|
|
33
|
+
stateRaw = fs.readFileSync(path.join(planDir, 'STATE.md'), 'utf-8');
|
|
34
|
+
} catch { /* intentionally empty */ }
|
|
30
35
|
|
|
31
|
-
const configExists = fs.existsSync(path.join(
|
|
32
|
-
const roadmapExists = fs.existsSync(path.join(
|
|
36
|
+
const configExists = fs.existsSync(path.join(planDir, 'config.json'));
|
|
37
|
+
const roadmapExists = fs.existsSync(path.join(planDir, 'ROADMAP.md'));
|
|
33
38
|
const stateExists = stateRaw.length > 0;
|
|
34
39
|
|
|
35
40
|
const result = {
|
|
@@ -65,7 +70,7 @@ function cmdStateLoad(cwd, raw) {
|
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
function cmdStateGet(cwd, section, raw) {
|
|
68
|
-
const statePath =
|
|
73
|
+
const statePath = planningPaths(cwd).state;
|
|
69
74
|
try {
|
|
70
75
|
const content = fs.readFileSync(statePath, 'utf-8');
|
|
71
76
|
|
|
@@ -75,7 +80,7 @@ function cmdStateGet(cwd, section, raw) {
|
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
// Try to find markdown section or field
|
|
78
|
-
const fieldEscaped = section
|
|
83
|
+
const fieldEscaped = escapeRegex(section);
|
|
79
84
|
|
|
80
85
|
// Check for **field:** value (bold format)
|
|
81
86
|
const boldPattern = new RegExp(`\\*\\*${fieldEscaped}:\\*\\*\\s*(.*)`, 'i');
|
|
@@ -110,40 +115,54 @@ function cmdStateGet(cwd, section, raw) {
|
|
|
110
115
|
function readTextArgOrFile(cwd, value, filePath, label) {
|
|
111
116
|
if (!filePath) return value;
|
|
112
117
|
|
|
113
|
-
|
|
118
|
+
// Path traversal guard: ensure file resolves within project directory
|
|
119
|
+
const { validatePath } = require('./security.cjs');
|
|
120
|
+
const pathCheck = validatePath(filePath, cwd, { allowAbsolute: true });
|
|
121
|
+
if (!pathCheck.safe) {
|
|
122
|
+
throw new Error(`${label} path rejected: ${pathCheck.error}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
try {
|
|
115
|
-
return fs.readFileSync(
|
|
126
|
+
return fs.readFileSync(pathCheck.resolved, 'utf-8').trimEnd();
|
|
116
127
|
} catch {
|
|
117
128
|
throw new Error(`${label} file not found: ${filePath}`);
|
|
118
129
|
}
|
|
119
130
|
}
|
|
120
131
|
|
|
121
132
|
function cmdStatePatch(cwd, patches, raw) {
|
|
122
|
-
|
|
133
|
+
// Validate all field names before processing
|
|
134
|
+
const { validateFieldName } = require('./security.cjs');
|
|
135
|
+
for (const field of Object.keys(patches)) {
|
|
136
|
+
const fieldCheck = validateFieldName(field);
|
|
137
|
+
if (!fieldCheck.valid) {
|
|
138
|
+
error(`state patch: ${fieldCheck.error}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const statePath = planningPaths(cwd).state;
|
|
123
143
|
try {
|
|
124
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
125
144
|
const results = { updated: [], failed: [] };
|
|
126
145
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
// Use atomic read-modify-write to prevent lost updates from concurrent agents
|
|
147
|
+
readModifyWriteStateMd(statePath, (content) => {
|
|
148
|
+
for (const [field, value] of Object.entries(patches)) {
|
|
149
|
+
const fieldEscaped = escapeRegex(field);
|
|
150
|
+
// Try **Field:** bold format first, then plain Field: format
|
|
151
|
+
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
|
152
|
+
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
|
153
|
+
|
|
154
|
+
if (boldPattern.test(content)) {
|
|
155
|
+
content = content.replace(boldPattern, (_match, prefix) => `${prefix}${value}`);
|
|
156
|
+
results.updated.push(field);
|
|
157
|
+
} else if (plainPattern.test(content)) {
|
|
158
|
+
content = content.replace(plainPattern, (_match, prefix) => `${prefix}${value}`);
|
|
159
|
+
results.updated.push(field);
|
|
160
|
+
} else {
|
|
161
|
+
results.failed.push(field);
|
|
162
|
+
}
|
|
141
163
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (results.updated.length > 0) {
|
|
145
|
-
writeStateMd(statePath, content, cwd);
|
|
146
|
-
}
|
|
164
|
+
return content;
|
|
165
|
+
}, cwd);
|
|
147
166
|
|
|
148
167
|
output(results, raw, results.updated.length > 0 ? 'true' : 'false');
|
|
149
168
|
} catch {
|
|
@@ -156,10 +175,17 @@ function cmdStateUpdate(cwd, field, value) {
|
|
|
156
175
|
error('field and value required for state update');
|
|
157
176
|
}
|
|
158
177
|
|
|
159
|
-
|
|
178
|
+
// Validate field name to prevent regex injection via crafted field names
|
|
179
|
+
const { validateFieldName } = require('./security.cjs');
|
|
180
|
+
const fieldCheck = validateFieldName(field);
|
|
181
|
+
if (!fieldCheck.valid) {
|
|
182
|
+
error(`state update: ${fieldCheck.error}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const statePath = planningPaths(cwd).state;
|
|
160
186
|
try {
|
|
161
187
|
let content = fs.readFileSync(statePath, 'utf-8');
|
|
162
|
-
const fieldEscaped = field
|
|
188
|
+
const fieldEscaped = escapeRegex(field);
|
|
163
189
|
// Try **Field:** bold format first, then plain Field: format
|
|
164
190
|
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
|
165
191
|
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
|
@@ -180,21 +206,10 @@ function cmdStateUpdate(cwd, field, value) {
|
|
|
180
206
|
}
|
|
181
207
|
|
|
182
208
|
// ─── State Progression Engine ────────────────────────────────────────────────
|
|
183
|
-
|
|
184
|
-
function stateExtractField(content, fieldName) {
|
|
185
|
-
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
186
|
-
// Try **Field:** bold format first
|
|
187
|
-
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.+)`, 'i');
|
|
188
|
-
const boldMatch = content.match(boldPattern);
|
|
189
|
-
if (boldMatch) return boldMatch[1].trim();
|
|
190
|
-
// Fall back to plain Field: format
|
|
191
|
-
const plainPattern = new RegExp(`^${escaped}:\\s*(.+)`, 'im');
|
|
192
|
-
const plainMatch = content.match(plainPattern);
|
|
193
|
-
return plainMatch ? plainMatch[1].trim() : null;
|
|
194
|
-
}
|
|
209
|
+
// stateExtractField is defined above (shared helper) — do not duplicate.
|
|
195
210
|
|
|
196
211
|
function stateReplaceField(content, fieldName, newValue) {
|
|
197
|
-
const escaped = fieldName
|
|
212
|
+
const escaped = escapeRegex(fieldName);
|
|
198
213
|
// Try **Field:** bold format first, then plain Field: format
|
|
199
214
|
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
|
200
215
|
if (boldPattern.test(content)) {
|
|
@@ -207,37 +222,112 @@ function stateReplaceField(content, fieldName, newValue) {
|
|
|
207
222
|
return null;
|
|
208
223
|
}
|
|
209
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Replace a STATE.md field with fallback field name support.
|
|
227
|
+
* Tries `primary` first, then `fallback` (if provided), returns content unchanged
|
|
228
|
+
* if neither matches. This consolidates the replaceWithFallback pattern that was
|
|
229
|
+
* previously duplicated inline across phase.cjs, milestone.cjs, and state.cjs.
|
|
230
|
+
*/
|
|
231
|
+
function stateReplaceFieldWithFallback(content, primary, fallback, value) {
|
|
232
|
+
let result = stateReplaceField(content, primary, value);
|
|
233
|
+
if (result) return result;
|
|
234
|
+
if (fallback) {
|
|
235
|
+
result = stateReplaceField(content, fallback, value);
|
|
236
|
+
if (result) return result;
|
|
237
|
+
}
|
|
238
|
+
// Neither pattern matched — field may have been reformatted or removed.
|
|
239
|
+
// Log diagnostic so template drift is detected early rather than silently swallowed.
|
|
240
|
+
process.stderr.write(
|
|
241
|
+
`[gsd-tools] WARNING: STATE.md field "${primary}"${fallback ? ` (fallback: "${fallback}")` : ''} not found — update skipped. ` +
|
|
242
|
+
`This may indicate STATE.md was externally modified or uses an unexpected format.\n`
|
|
243
|
+
);
|
|
244
|
+
return content;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Update fields within the ## Current Position section of STATE.md.
|
|
249
|
+
* This keeps the Current Position body in sync with the bold frontmatter fields.
|
|
250
|
+
* Only updates fields that already exist in the section; does not add new lines.
|
|
251
|
+
* Fixes #1365: advance-plan could not update Status/Last activity after begin-phase.
|
|
252
|
+
*/
|
|
253
|
+
function updateCurrentPositionFields(content, fields) {
|
|
254
|
+
const posPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
|
255
|
+
const posMatch = content.match(posPattern);
|
|
256
|
+
if (!posMatch) return content;
|
|
257
|
+
|
|
258
|
+
let posBody = posMatch[2];
|
|
259
|
+
|
|
260
|
+
if (fields.status && /^Status:/m.test(posBody)) {
|
|
261
|
+
posBody = posBody.replace(/^Status:.*$/m, `Status: ${fields.status}`);
|
|
262
|
+
}
|
|
263
|
+
if (fields.lastActivity && /^Last activity:/im.test(posBody)) {
|
|
264
|
+
posBody = posBody.replace(/^Last activity:.*$/im, `Last activity: ${fields.lastActivity}`);
|
|
265
|
+
}
|
|
266
|
+
if (fields.plan && /^Plan:/m.test(posBody)) {
|
|
267
|
+
posBody = posBody.replace(/^Plan:.*$/m, `Plan: ${fields.plan}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return content.replace(posPattern, `${posMatch[1]}${posBody}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
210
273
|
function cmdStateAdvancePlan(cwd, raw) {
|
|
211
|
-
const statePath =
|
|
274
|
+
const statePath = planningPaths(cwd).state;
|
|
212
275
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
213
276
|
|
|
214
277
|
let content = fs.readFileSync(statePath, 'utf-8');
|
|
215
|
-
const currentPlan = parseInt(stateExtractField(content, 'Current Plan'), 10);
|
|
216
|
-
const totalPlans = parseInt(stateExtractField(content, 'Total Plans in Phase'), 10);
|
|
217
278
|
const today = new Date().toISOString().split('T')[0];
|
|
218
279
|
|
|
280
|
+
// Try legacy separate fields first, then compound "Plan: X of Y" format
|
|
281
|
+
const legacyPlan = stateExtractField(content, 'Current Plan');
|
|
282
|
+
const legacyTotal = stateExtractField(content, 'Total Plans in Phase');
|
|
283
|
+
const planField = stateExtractField(content, 'Plan');
|
|
284
|
+
|
|
285
|
+
let currentPlan, totalPlans;
|
|
286
|
+
let useCompoundFormat = false;
|
|
287
|
+
|
|
288
|
+
if (legacyPlan && legacyTotal) {
|
|
289
|
+
currentPlan = parseInt(legacyPlan, 10);
|
|
290
|
+
totalPlans = parseInt(legacyTotal, 10);
|
|
291
|
+
} else if (planField) {
|
|
292
|
+
// Compound format: "2 of 6 in current phase" or "2 of 6"
|
|
293
|
+
currentPlan = parseInt(planField, 10);
|
|
294
|
+
const ofMatch = planField.match(/of\s+(\d+)/);
|
|
295
|
+
totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
|
|
296
|
+
useCompoundFormat = true;
|
|
297
|
+
}
|
|
298
|
+
|
|
219
299
|
if (isNaN(currentPlan) || isNaN(totalPlans)) {
|
|
220
300
|
output({ error: 'Cannot parse Current Plan or Total Plans in Phase from STATE.md' }, raw);
|
|
221
301
|
return;
|
|
222
302
|
}
|
|
223
303
|
|
|
224
304
|
if (currentPlan >= totalPlans) {
|
|
225
|
-
content =
|
|
226
|
-
content =
|
|
305
|
+
content = stateReplaceFieldWithFallback(content, 'Status', null, 'Phase complete — ready for verification');
|
|
306
|
+
content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
|
|
307
|
+
content = updateCurrentPositionFields(content, { status: 'Phase complete — ready for verification', lastActivity: today });
|
|
227
308
|
writeStateMd(statePath, content, cwd);
|
|
228
309
|
output({ advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' }, raw, 'false');
|
|
229
310
|
} else {
|
|
230
311
|
const newPlan = currentPlan + 1;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
312
|
+
let planDisplayValue;
|
|
313
|
+
if (useCompoundFormat) {
|
|
314
|
+
// Preserve compound format: "X of Y in current phase" → replace X only
|
|
315
|
+
planDisplayValue = planField.replace(/^\d+/, String(newPlan));
|
|
316
|
+
content = stateReplaceField(content, 'Plan', planDisplayValue) || content;
|
|
317
|
+
} else {
|
|
318
|
+
planDisplayValue = `${newPlan} of ${totalPlans}`;
|
|
319
|
+
content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
|
|
320
|
+
}
|
|
321
|
+
content = stateReplaceFieldWithFallback(content, 'Status', null, 'Ready to execute');
|
|
322
|
+
content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
|
|
323
|
+
content = updateCurrentPositionFields(content, { status: 'Ready to execute', lastActivity: today, plan: planDisplayValue });
|
|
234
324
|
writeStateMd(statePath, content, cwd);
|
|
235
325
|
output({ advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans }, raw, 'true');
|
|
236
326
|
}
|
|
237
327
|
}
|
|
238
328
|
|
|
239
329
|
function cmdStateRecordMetric(cwd, options, raw) {
|
|
240
|
-
const statePath =
|
|
330
|
+
const statePath = planningPaths(cwd).state;
|
|
241
331
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
242
332
|
|
|
243
333
|
let content = fs.readFileSync(statePath, 'utf-8');
|
|
@@ -271,19 +361,21 @@ function cmdStateRecordMetric(cwd, options, raw) {
|
|
|
271
361
|
}
|
|
272
362
|
|
|
273
363
|
function cmdStateUpdateProgress(cwd, raw) {
|
|
274
|
-
const statePath =
|
|
364
|
+
const statePath = planningPaths(cwd).state;
|
|
275
365
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
276
366
|
|
|
277
367
|
let content = fs.readFileSync(statePath, 'utf-8');
|
|
278
368
|
|
|
279
|
-
// Count summaries across
|
|
280
|
-
const phasesDir =
|
|
369
|
+
// Count summaries across current milestone phases only
|
|
370
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
281
371
|
let totalPlans = 0;
|
|
282
372
|
let totalSummaries = 0;
|
|
283
373
|
|
|
284
374
|
if (fs.existsSync(phasesDir)) {
|
|
375
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
285
376
|
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
286
|
-
.filter(e => e.isDirectory()).map(e => e.name)
|
|
377
|
+
.filter(e => e.isDirectory()).map(e => e.name)
|
|
378
|
+
.filter(isDirInMilestone);
|
|
287
379
|
for (const dir of phaseDirs) {
|
|
288
380
|
const files = fs.readdirSync(path.join(phasesDir, dir));
|
|
289
381
|
totalPlans += files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
|
@@ -314,7 +406,7 @@ function cmdStateUpdateProgress(cwd, raw) {
|
|
|
314
406
|
}
|
|
315
407
|
|
|
316
408
|
function cmdStateAddDecision(cwd, options, raw) {
|
|
317
|
-
const statePath =
|
|
409
|
+
const statePath = planningPaths(cwd).state;
|
|
318
410
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
319
411
|
|
|
320
412
|
const { phase, summary, summary_file, rationale, rationale_file } = options;
|
|
@@ -352,7 +444,7 @@ function cmdStateAddDecision(cwd, options, raw) {
|
|
|
352
444
|
}
|
|
353
445
|
|
|
354
446
|
function cmdStateAddBlocker(cwd, text, raw) {
|
|
355
|
-
const statePath =
|
|
447
|
+
const statePath = planningPaths(cwd).state;
|
|
356
448
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
357
449
|
const blockerOptions = typeof text === 'object' && text !== null ? text : { text };
|
|
358
450
|
let blockerText = null;
|
|
@@ -385,7 +477,7 @@ function cmdStateAddBlocker(cwd, text, raw) {
|
|
|
385
477
|
}
|
|
386
478
|
|
|
387
479
|
function cmdStateResolveBlocker(cwd, text, raw) {
|
|
388
|
-
const statePath =
|
|
480
|
+
const statePath = planningPaths(cwd).state;
|
|
389
481
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
390
482
|
if (!text) { output({ error: 'text required' }, raw); return; }
|
|
391
483
|
|
|
@@ -417,7 +509,7 @@ function cmdStateResolveBlocker(cwd, text, raw) {
|
|
|
417
509
|
}
|
|
418
510
|
|
|
419
511
|
function cmdStateRecordSession(cwd, options, raw) {
|
|
420
|
-
const statePath =
|
|
512
|
+
const statePath = planningPaths(cwd).state;
|
|
421
513
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
422
514
|
|
|
423
515
|
let content = fs.readFileSync(statePath, 'utf-8');
|
|
@@ -452,7 +544,7 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
452
544
|
}
|
|
453
545
|
|
|
454
546
|
function cmdStateSnapshot(cwd, raw) {
|
|
455
|
-
const statePath =
|
|
547
|
+
const statePath = planningPaths(cwd).state;
|
|
456
548
|
|
|
457
549
|
if (!fs.existsSync(statePath)) {
|
|
458
550
|
output({ error: 'STATE.md not found' }, raw);
|
|
@@ -574,7 +666,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
574
666
|
const info = getMilestoneInfo(cwd);
|
|
575
667
|
milestone = info.version;
|
|
576
668
|
milestoneName = info.name;
|
|
577
|
-
} catch {}
|
|
669
|
+
} catch { /* intentionally empty */ }
|
|
578
670
|
}
|
|
579
671
|
|
|
580
672
|
let totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
|
|
@@ -584,7 +676,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
584
676
|
|
|
585
677
|
if (cwd) {
|
|
586
678
|
try {
|
|
587
|
-
const phasesDir =
|
|
679
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
588
680
|
if (fs.existsSync(phasesDir)) {
|
|
589
681
|
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
590
682
|
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
@@ -609,11 +701,17 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
609
701
|
totalPlans = diskTotalPlans;
|
|
610
702
|
completedPlans = diskTotalSummaries;
|
|
611
703
|
}
|
|
612
|
-
} catch {}
|
|
704
|
+
} catch { /* intentionally empty */ }
|
|
613
705
|
}
|
|
614
706
|
|
|
707
|
+
// Derive percent from disk counts when available (ground truth).
|
|
708
|
+
// Only falls back to the body Progress: field when no plan files exist on disk
|
|
709
|
+
// (phases directory empty or absent), which means disk has no authoritative data.
|
|
710
|
+
// This prevents a stale body "0%" from overriding the real 100% completion state.
|
|
615
711
|
let progressPercent = null;
|
|
616
|
-
if (
|
|
712
|
+
if (totalPlans !== null && totalPlans > 0 && completedPlans !== null) {
|
|
713
|
+
progressPercent = Math.min(100, Math.round(completedPlans / totalPlans * 100));
|
|
714
|
+
} else if (progressRaw) {
|
|
617
715
|
const pctMatch = progressRaw.match(/(\d+)%/);
|
|
618
716
|
if (pctMatch) progressPercent = parseInt(pctMatch[1], 10);
|
|
619
717
|
}
|
|
@@ -662,49 +760,577 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
662
760
|
}
|
|
663
761
|
|
|
664
762
|
function stripFrontmatter(content) {
|
|
665
|
-
|
|
763
|
+
// Strip ALL frontmatter blocks at the start of the file.
|
|
764
|
+
// Handles CRLF line endings and multiple stacked blocks (corruption recovery).
|
|
765
|
+
// Greedy: keeps stripping ---...--- blocks separated by optional whitespace.
|
|
766
|
+
let result = content;
|
|
767
|
+
// eslint-disable-next-line no-constant-condition
|
|
768
|
+
while (true) {
|
|
769
|
+
const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
|
|
770
|
+
if (stripped === result) break;
|
|
771
|
+
result = stripped;
|
|
772
|
+
}
|
|
773
|
+
return result;
|
|
666
774
|
}
|
|
667
775
|
|
|
668
776
|
function syncStateFrontmatter(content, cwd) {
|
|
777
|
+
// read existing frontmatter BEFORE stripping — it may contain values
|
|
778
|
+
// that the body no longer has (e.g., Status field removed by an agent).
|
|
779
|
+
const existingFm = extractFrontmatter(content);
|
|
669
780
|
const body = stripFrontmatter(content);
|
|
670
|
-
const
|
|
671
|
-
|
|
781
|
+
const derivedFm = buildStateFrontmatter(body, cwd);
|
|
782
|
+
|
|
783
|
+
// Preserve existing frontmatter status when body-derived status is 'unknown'.
|
|
784
|
+
// This prevents a missing Status: field in the body from overwriting a
|
|
785
|
+
// previously valid status (e.g., 'executing' → 'unknown').
|
|
786
|
+
if (derivedFm.status === 'unknown' && existingFm.status && existingFm.status !== 'unknown') {
|
|
787
|
+
derivedFm.status = existingFm.status;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const yamlStr = reconstructFrontmatter(derivedFm);
|
|
672
791
|
return `---\n${yamlStr}\n---\n\n${body}`;
|
|
673
792
|
}
|
|
674
793
|
|
|
794
|
+
/**
|
|
795
|
+
* Acquire a lockfile for STATE.md operations.
|
|
796
|
+
* Returns the lock path for later release.
|
|
797
|
+
*/
|
|
798
|
+
function acquireStateLock(statePath) {
|
|
799
|
+
const lockPath = statePath + '.lock';
|
|
800
|
+
const maxRetries = 10;
|
|
801
|
+
const retryDelay = 200; // ms
|
|
802
|
+
|
|
803
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
804
|
+
try {
|
|
805
|
+
const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
|
806
|
+
fs.writeSync(fd, String(process.pid));
|
|
807
|
+
fs.closeSync(fd);
|
|
808
|
+
return lockPath;
|
|
809
|
+
} catch (err) {
|
|
810
|
+
if (err.code === 'EEXIST') {
|
|
811
|
+
try {
|
|
812
|
+
const stat = fs.statSync(lockPath);
|
|
813
|
+
if (Date.now() - stat.mtimeMs > 10000) {
|
|
814
|
+
fs.unlinkSync(lockPath);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
} catch { /* lock was released between check — retry */ }
|
|
818
|
+
|
|
819
|
+
if (i === maxRetries - 1) {
|
|
820
|
+
try { fs.unlinkSync(lockPath); } catch {}
|
|
821
|
+
return lockPath;
|
|
822
|
+
}
|
|
823
|
+
const jitter = Math.floor(Math.random() * 50);
|
|
824
|
+
const start = Date.now();
|
|
825
|
+
while (Date.now() - start < retryDelay + jitter) { /* busy wait */ }
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
return lockPath; // non-EEXIST error — proceed without lock
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return statePath + '.lock';
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function releaseStateLock(lockPath) {
|
|
835
|
+
try { fs.unlinkSync(lockPath); } catch { /* lock already gone */ }
|
|
836
|
+
}
|
|
837
|
+
|
|
675
838
|
/**
|
|
676
839
|
* write STATE.md with synchronized YAML frontmatter.
|
|
677
840
|
* All STATE.md writes should use this instead of raw writeFileSync.
|
|
841
|
+
* Uses a simple lockfile to prevent parallel agents from overwriting
|
|
842
|
+
* each other's changes (race condition with read-modify-write cycle).
|
|
678
843
|
*/
|
|
679
844
|
function writeStateMd(statePath, content, cwd) {
|
|
680
845
|
const synced = syncStateFrontmatter(content, cwd);
|
|
681
|
-
|
|
846
|
+
const lockPath = acquireStateLock(statePath);
|
|
847
|
+
try {
|
|
848
|
+
fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8');
|
|
849
|
+
} finally {
|
|
850
|
+
releaseStateLock(lockPath);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Atomic read-modify-write for STATE.md.
|
|
856
|
+
* Holds the lock across the entire read -> transform -> write cycle,
|
|
857
|
+
* preventing the lost-update problem where two agents read the same
|
|
858
|
+
* content and the second write clobbers the first.
|
|
859
|
+
*/
|
|
860
|
+
function readModifyWriteStateMd(statePath, transformFn, cwd) {
|
|
861
|
+
const lockPath = acquireStateLock(statePath);
|
|
862
|
+
try {
|
|
863
|
+
const content = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8') : '';
|
|
864
|
+
const modified = transformFn(content);
|
|
865
|
+
const synced = syncStateFrontmatter(modified, cwd);
|
|
866
|
+
fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8');
|
|
867
|
+
} finally {
|
|
868
|
+
releaseStateLock(lockPath);
|
|
869
|
+
}
|
|
682
870
|
}
|
|
683
871
|
|
|
684
872
|
function cmdStateJson(cwd, raw) {
|
|
685
|
-
const statePath =
|
|
873
|
+
const statePath = planningPaths(cwd).state;
|
|
686
874
|
if (!fs.existsSync(statePath)) {
|
|
687
875
|
output({ error: 'STATE.md not found' }, raw, 'STATE.md not found');
|
|
688
876
|
return;
|
|
689
877
|
}
|
|
690
878
|
|
|
691
879
|
const content = fs.readFileSync(statePath, 'utf-8');
|
|
692
|
-
const
|
|
880
|
+
const existingFm = extractFrontmatter(content);
|
|
881
|
+
const body = stripFrontmatter(content);
|
|
882
|
+
|
|
883
|
+
// Always rebuild from body + disk so progress counters reflect current state.
|
|
884
|
+
// Returning cached frontmatter directly causes stale percent/completed_plans
|
|
885
|
+
// when SUMMARY files were added after the last STATE.md write (#1589).
|
|
886
|
+
const built = buildStateFrontmatter(body, cwd);
|
|
887
|
+
|
|
888
|
+
// Preserve frontmatter-only fields that cannot be recovered from the body.
|
|
889
|
+
if (existingFm && existingFm.stopped_at && !built.stopped_at) {
|
|
890
|
+
built.stopped_at = existingFm.stopped_at;
|
|
891
|
+
}
|
|
892
|
+
if (existingFm && existingFm.paused_at && !built.paused_at) {
|
|
893
|
+
built.paused_at = existingFm.paused_at;
|
|
894
|
+
}
|
|
895
|
+
// Preserve existing status when body-derived status is 'unknown' (same logic as syncStateFrontmatter).
|
|
896
|
+
if (built.status === 'unknown' && existingFm && existingFm.status && existingFm.status !== 'unknown') {
|
|
897
|
+
built.status = existingFm.status;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
output(built, raw, JSON.stringify(built, null, 2));
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Update STATE.md when a new phase begins execution.
|
|
905
|
+
* Updates body text fields (Current focus, Status, Last Activity, Current Position)
|
|
906
|
+
* and synchronizes frontmatter via writeStateMd.
|
|
907
|
+
* Fixes: #1102 (plan counts), #1103 (status/last_activity), #1104 (body text).
|
|
908
|
+
*/
|
|
909
|
+
function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
910
|
+
const statePath = planningPaths(cwd).state;
|
|
911
|
+
if (!fs.existsSync(statePath)) {
|
|
912
|
+
output({ error: 'STATE.md not found' }, raw);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
let content = fs.readFileSync(statePath, 'utf-8');
|
|
917
|
+
const today = new Date().toISOString().split('T')[0];
|
|
918
|
+
const updated = [];
|
|
919
|
+
|
|
920
|
+
// Update Status field
|
|
921
|
+
const statusValue = `Executing Phase ${phaseNumber}`;
|
|
922
|
+
let result = stateReplaceField(content, 'Status', statusValue);
|
|
923
|
+
if (result) { content = result; updated.push('Status'); }
|
|
924
|
+
|
|
925
|
+
// Update Last Activity
|
|
926
|
+
result = stateReplaceField(content, 'Last Activity', today);
|
|
927
|
+
if (result) { content = result; updated.push('Last Activity'); }
|
|
928
|
+
|
|
929
|
+
// Update Last Activity Description if it exists
|
|
930
|
+
const activityDesc = `Phase ${phaseNumber} execution started`;
|
|
931
|
+
result = stateReplaceField(content, 'Last Activity Description', activityDesc);
|
|
932
|
+
if (result) { content = result; updated.push('Last Activity Description'); }
|
|
933
|
+
|
|
934
|
+
// Update Current Phase
|
|
935
|
+
result = stateReplaceField(content, 'Current Phase', String(phaseNumber));
|
|
936
|
+
if (result) { content = result; updated.push('Current Phase'); }
|
|
937
|
+
|
|
938
|
+
// Update Current Phase Name
|
|
939
|
+
if (phaseName) {
|
|
940
|
+
result = stateReplaceField(content, 'Current Phase Name', phaseName);
|
|
941
|
+
if (result) { content = result; updated.push('Current Phase Name'); }
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Update Current Plan to 1 (starting from the first plan)
|
|
945
|
+
result = stateReplaceField(content, 'Current Plan', '1');
|
|
946
|
+
if (result) { content = result; updated.push('Current Plan'); }
|
|
947
|
+
|
|
948
|
+
// Update Total Plans in Phase
|
|
949
|
+
if (planCount) {
|
|
950
|
+
result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
|
|
951
|
+
if (result) { content = result; updated.push('Total Plans in Phase'); }
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Update **Current focus:** body text line (#1104)
|
|
955
|
+
const focusLabel = phaseName ? `Phase ${phaseNumber} — ${phaseName}` : `Phase ${phaseNumber}`;
|
|
956
|
+
const focusPattern = /(\*\*Current focus:\*\*\s*).*/i;
|
|
957
|
+
if (focusPattern.test(content)) {
|
|
958
|
+
content = content.replace(focusPattern, (_match, prefix) => `${prefix}${focusLabel}`);
|
|
959
|
+
updated.push('Current focus');
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Update ## Current Position section (#1104, #1365)
|
|
963
|
+
// Update individual fields within Current Position instead of replacing the
|
|
964
|
+
// entire section, so that Status, Last activity, and Progress are preserved.
|
|
965
|
+
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
|
966
|
+
const positionMatch = content.match(positionPattern);
|
|
967
|
+
if (positionMatch) {
|
|
968
|
+
const header = positionMatch[1];
|
|
969
|
+
let posBody = positionMatch[2];
|
|
970
|
+
|
|
971
|
+
// Update or insert Phase line
|
|
972
|
+
const newPhase = `Phase: ${phaseNumber}${phaseName ? ` (${phaseName})` : ''} — EXECUTING`;
|
|
973
|
+
if (/^Phase:/m.test(posBody)) {
|
|
974
|
+
posBody = posBody.replace(/^Phase:.*$/m, newPhase);
|
|
975
|
+
} else {
|
|
976
|
+
posBody = newPhase + '\n' + posBody;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Update or insert Plan line
|
|
980
|
+
const newPlan = `Plan: 1 of ${planCount || '?'}`;
|
|
981
|
+
if (/^Plan:/m.test(posBody)) {
|
|
982
|
+
posBody = posBody.replace(/^Plan:.*$/m, newPlan);
|
|
983
|
+
} else {
|
|
984
|
+
posBody = posBody.replace(/^(Phase:.*$)/m, `$1\n${newPlan}`);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Update Status line if present
|
|
988
|
+
const newStatus = `Status: Executing Phase ${phaseNumber}`;
|
|
989
|
+
if (/^Status:/m.test(posBody)) {
|
|
990
|
+
posBody = posBody.replace(/^Status:.*$/m, newStatus);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Update Last activity line if present
|
|
994
|
+
const newActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution started`;
|
|
995
|
+
if (/^Last activity:/im.test(posBody)) {
|
|
996
|
+
posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
content = content.replace(positionPattern, `${header}${posBody}`);
|
|
1000
|
+
updated.push('Current Position');
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (updated.length > 0) {
|
|
1004
|
+
writeStateMd(statePath, content, cwd);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
output({ updated, phase: phaseNumber, phase_name: phaseName || null, plan_count: planCount || null }, raw, updated.length > 0 ? 'true' : 'false');
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* write a WAITING.json signal file when GSD hits a decision point.
|
|
1012
|
+
* External watchers (fswatch, polling, orchestrators) can detect this.
|
|
1013
|
+
* File is written to .planning/WAITING.json (or .gsd/WAITING.json if .gsd exists).
|
|
1014
|
+
* Fixes #1034.
|
|
1015
|
+
*/
|
|
1016
|
+
function cmdSignalWaiting(cwd, type, question, options, phase, raw) {
|
|
1017
|
+
const gsdDir = fs.existsSync(path.join(cwd, '.gsd')) ? path.join(cwd, '.gsd') : planningDir(cwd);
|
|
1018
|
+
const waitingPath = path.join(gsdDir, 'WAITING.json');
|
|
1019
|
+
|
|
1020
|
+
const signal = {
|
|
1021
|
+
status: 'waiting',
|
|
1022
|
+
type: type || 'decision_point',
|
|
1023
|
+
question: question || null,
|
|
1024
|
+
options: options ? options.split('|').map(o => o.trim()) : [],
|
|
1025
|
+
since: new Date().toISOString(),
|
|
1026
|
+
phase: phase || null,
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
fs.mkdirSync(gsdDir, { recursive: true });
|
|
1031
|
+
fs.writeFileSync(waitingPath, JSON.stringify(signal, null, 2), 'utf-8');
|
|
1032
|
+
output({ signaled: true, path: waitingPath }, raw, 'true');
|
|
1033
|
+
} catch (e) {
|
|
1034
|
+
output({ signaled: false, error: e.message }, raw, 'false');
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Remove the WAITING.json signal file when user answers and agent resumes.
|
|
1040
|
+
*/
|
|
1041
|
+
function cmdSignalResume(cwd, raw) {
|
|
1042
|
+
const paths = [
|
|
1043
|
+
path.join(cwd, '.gsd', 'WAITING.json'),
|
|
1044
|
+
path.join(planningDir(cwd), 'WAITING.json'),
|
|
1045
|
+
];
|
|
1046
|
+
|
|
1047
|
+
let removed = false;
|
|
1048
|
+
for (const p of paths) {
|
|
1049
|
+
if (fs.existsSync(p)) {
|
|
1050
|
+
try { fs.unlinkSync(p); removed = true; } catch {}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
output({ resumed: true, removed }, raw, removed ? 'true' : 'false');
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// ─── Gate Functions (STATE.md consistency enforcement) ────────────────────────
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Update the ## Performance Metrics section in STATE.md content.
|
|
1061
|
+
* Increments Velocity totals and upserts a By Phase table row.
|
|
1062
|
+
* Returns modified content string.
|
|
1063
|
+
*/
|
|
1064
|
+
function updatePerformanceMetricsSection(content, cwd, phaseNum, planCount, summaryCount) {
|
|
1065
|
+
// Update Velocity: Total plans completed
|
|
1066
|
+
const totalMatch = content.match(/Total plans completed:\s*(\d+|\[N\])/);
|
|
1067
|
+
const prevTotal = totalMatch && totalMatch[1] !== '[N]' ? parseInt(totalMatch[1], 10) : 0;
|
|
1068
|
+
const newTotal = prevTotal + summaryCount;
|
|
1069
|
+
content = content.replace(
|
|
1070
|
+
/Total plans completed:\s*(\d+|\[N\])/,
|
|
1071
|
+
`Total plans completed: ${newTotal}`
|
|
1072
|
+
);
|
|
1073
|
+
|
|
1074
|
+
// Update By Phase table — upsert row for this phase
|
|
1075
|
+
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;
|
|
1076
|
+
const byPhaseMatch = content.match(byPhaseTablePattern);
|
|
1077
|
+
if (byPhaseMatch) {
|
|
1078
|
+
let tableBody = byPhaseMatch[2].trim();
|
|
1079
|
+
const phaseRowPattern = new RegExp(`^\\|\\s*${escapeRegex(String(phaseNum))}\\s*\\|.*$`, 'm');
|
|
1080
|
+
const newRow = `| ${phaseNum} | ${summaryCount} | - | - |`;
|
|
1081
|
+
|
|
1082
|
+
if (phaseRowPattern.test(tableBody)) {
|
|
1083
|
+
// Update existing row
|
|
1084
|
+
tableBody = tableBody.replace(phaseRowPattern, newRow);
|
|
1085
|
+
} else {
|
|
1086
|
+
// Remove placeholder row and add new row
|
|
1087
|
+
tableBody = tableBody.replace(/^\|\s*-\s*\|\s*-\s*\|\s*-\s*\|\s*-\s*\|$/m, '').trim();
|
|
1088
|
+
tableBody = tableBody ? tableBody + '\n' + newRow : newRow;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
content = content.replace(byPhaseTablePattern, `$1${tableBody}\n`);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return content;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Gate 3a: Record state after plan-phase completes.
|
|
1099
|
+
* Updates Status to "Ready to execute", Total Plans, Last Activity.
|
|
1100
|
+
*/
|
|
1101
|
+
function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
|
1102
|
+
const statePath = planningPaths(cwd).state;
|
|
1103
|
+
if (!fs.existsSync(statePath)) {
|
|
1104
|
+
output({ error: 'STATE.md not found' }, raw);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
let content = fs.readFileSync(statePath, 'utf-8');
|
|
1109
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1110
|
+
const updated = [];
|
|
1111
|
+
|
|
1112
|
+
// Update Status
|
|
1113
|
+
let result = stateReplaceField(content, 'Status', 'Ready to execute');
|
|
1114
|
+
if (result) { content = result; updated.push('Status'); }
|
|
1115
|
+
|
|
1116
|
+
// Update Total Plans in Phase
|
|
1117
|
+
if (planCount !== null && planCount !== undefined) {
|
|
1118
|
+
result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
|
|
1119
|
+
if (result) { content = result; updated.push('Total Plans in Phase'); }
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Update Last Activity
|
|
1123
|
+
result = stateReplaceField(content, 'Last Activity', today);
|
|
1124
|
+
if (result) { content = result; updated.push('Last Activity'); }
|
|
1125
|
+
|
|
1126
|
+
// Update Last Activity Description
|
|
1127
|
+
result = stateReplaceField(content, 'Last Activity Description', `Phase ${phaseNumber} planning complete — ${planCount || '?'} plans ready`);
|
|
1128
|
+
if (result) { content = result; updated.push('Last Activity Description'); }
|
|
1129
|
+
|
|
1130
|
+
// Update Current Position section
|
|
1131
|
+
content = updateCurrentPositionFields(content, {
|
|
1132
|
+
status: 'Ready to execute',
|
|
1133
|
+
lastActivity: `${today} -- Phase ${phaseNumber} planning complete`,
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
if (updated.length > 0) {
|
|
1137
|
+
writeStateMd(statePath, content, cwd);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
output({ updated, phase: phaseNumber, plan_count: planCount }, raw, updated.length > 0 ? 'true' : 'false');
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Gate 1: Validate STATE.md against filesystem.
|
|
1145
|
+
* Returns { valid, warnings, drift } JSON.
|
|
1146
|
+
*/
|
|
1147
|
+
function cmdStateValidate(cwd, raw) {
|
|
1148
|
+
const statePath = planningPaths(cwd).state;
|
|
1149
|
+
if (!fs.existsSync(statePath)) {
|
|
1150
|
+
output({ error: 'STATE.md not found' }, raw);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const content = fs.readFileSync(statePath, 'utf-8');
|
|
1155
|
+
const warnings = [];
|
|
1156
|
+
const drift = {};
|
|
1157
|
+
|
|
1158
|
+
const status = stateExtractField(content, 'Status') || '';
|
|
1159
|
+
const currentPhase = stateExtractField(content, 'Current Phase');
|
|
1160
|
+
const totalPlansRaw = stateExtractField(content, 'Total Plans in Phase');
|
|
1161
|
+
const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
|
|
1162
|
+
|
|
1163
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
1164
|
+
|
|
1165
|
+
// Scan disk for current phase
|
|
1166
|
+
if (currentPhase && fs.existsSync(phasesDir)) {
|
|
1167
|
+
const normalized = currentPhase.replace(/\s+of\s+\d+.*/, '').trim();
|
|
1168
|
+
try {
|
|
1169
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
1170
|
+
const phaseDir = entries.find(e => e.isDirectory() && e.name.startsWith(normalized.replace(/^0+/, '').padStart(2, '0')));
|
|
1171
|
+
if (phaseDir) {
|
|
1172
|
+
const phaseDirPath = path.join(phasesDir, phaseDir.name);
|
|
1173
|
+
const files = fs.readdirSync(phaseDirPath);
|
|
1174
|
+
const diskPlans = files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
|
1175
|
+
const diskSummaries = files.filter(f => f.match(/-SUMMARY\.md$/i)).length;
|
|
1176
|
+
|
|
1177
|
+
// Check plan count mismatch
|
|
1178
|
+
if (totalPlansInPhase !== null && diskPlans !== totalPlansInPhase) {
|
|
1179
|
+
warnings.push(`Plan count mismatch: STATE.md says ${totalPlansInPhase} plans, disk has ${diskPlans}`);
|
|
1180
|
+
drift.plan_count = { state: totalPlansInPhase, disk: diskPlans };
|
|
1181
|
+
}
|
|
693
1182
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1183
|
+
// Check for VERIFICATION.md
|
|
1184
|
+
const verificationFiles = files.filter(f => f.includes('VERIFICATION') && f.endsWith('.md'));
|
|
1185
|
+
for (const vf of verificationFiles) {
|
|
1186
|
+
try {
|
|
1187
|
+
const vContent = fs.readFileSync(path.join(phaseDirPath, vf), 'utf-8');
|
|
1188
|
+
if (/status:\s*passed/i.test(vContent) && /executing/i.test(status)) {
|
|
1189
|
+
warnings.push(`Status drift: STATE.md says "${status}" but ${vf} shows verification passed — phase may be complete`);
|
|
1190
|
+
drift.verification_status = { state_status: status, verification: 'passed' };
|
|
1191
|
+
}
|
|
1192
|
+
} catch { /* intentionally empty */ }
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Check if all plans have summaries but status still says executing
|
|
1196
|
+
if (diskPlans > 0 && diskSummaries >= diskPlans && /executing/i.test(status)) {
|
|
1197
|
+
// Only warn if no verification exists (if verification passed, the above warning covers it)
|
|
1198
|
+
if (verificationFiles.length === 0) {
|
|
1199
|
+
warnings.push(`All ${diskPlans} plans have summaries but status is still "${status}" — phase may be ready for verification`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
} catch { /* intentionally empty */ }
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const valid = warnings.length === 0;
|
|
1207
|
+
output({ valid, warnings, drift }, raw);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Gate 2: Sync STATE.md from filesystem ground truth.
|
|
1212
|
+
* Scans phase dirs, reconstructs counters, progress, metrics.
|
|
1213
|
+
* Supports --verify for dry-run mode.
|
|
1214
|
+
*/
|
|
1215
|
+
function cmdStateSync(cwd, options, raw) {
|
|
1216
|
+
const statePath = planningPaths(cwd).state;
|
|
1217
|
+
if (!fs.existsSync(statePath)) {
|
|
1218
|
+
output({ error: 'STATE.md not found' }, raw);
|
|
698
1219
|
return;
|
|
699
1220
|
}
|
|
700
1221
|
|
|
701
|
-
|
|
1222
|
+
const verify = options && options.verify;
|
|
1223
|
+
const content = fs.readFileSync(statePath, 'utf-8');
|
|
1224
|
+
const changes = [];
|
|
1225
|
+
let modified = content;
|
|
1226
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1227
|
+
|
|
1228
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
1229
|
+
if (!fs.existsSync(phasesDir)) {
|
|
1230
|
+
output({ synced: true, changes: [], dry_run: !!verify }, raw);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Scan all phases
|
|
1235
|
+
let entries;
|
|
1236
|
+
try {
|
|
1237
|
+
entries = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
1238
|
+
.filter(e => e.isDirectory())
|
|
1239
|
+
.map(e => e.name)
|
|
1240
|
+
.sort();
|
|
1241
|
+
} catch {
|
|
1242
|
+
output({ synced: true, changes: [], dry_run: !!verify }, raw);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
let totalDiskPlans = 0;
|
|
1247
|
+
let totalDiskSummaries = 0;
|
|
1248
|
+
let highestIncompletePhase = null;
|
|
1249
|
+
let highestIncompletePhaseNum = null;
|
|
1250
|
+
let highestIncompletePhaseplanCount = 0;
|
|
1251
|
+
let highestIncompletePhaseSummaryCount = 0;
|
|
1252
|
+
|
|
1253
|
+
for (const dir of entries) {
|
|
1254
|
+
const dirPath = path.join(phasesDir, dir);
|
|
1255
|
+
const files = fs.readdirSync(dirPath);
|
|
1256
|
+
const plans = files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
|
1257
|
+
const summaries = files.filter(f => f.match(/-SUMMARY\.md$/i)).length;
|
|
1258
|
+
totalDiskPlans += plans;
|
|
1259
|
+
totalDiskSummaries += summaries;
|
|
1260
|
+
|
|
1261
|
+
// Track the highest phase with incomplete plans (or any plans)
|
|
1262
|
+
const phaseMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
|
1263
|
+
if (phaseMatch && plans > 0) {
|
|
1264
|
+
if (summaries < plans) {
|
|
1265
|
+
// Incomplete phase — this is likely the current one
|
|
1266
|
+
highestIncompletePhase = dir;
|
|
1267
|
+
highestIncompletePhaseNum = phaseMatch[1];
|
|
1268
|
+
highestIncompletePhaseplanCount = plans;
|
|
1269
|
+
highestIncompletePhaseSummaryCount = summaries;
|
|
1270
|
+
} else if (!highestIncompletePhase) {
|
|
1271
|
+
// All complete, track as potential current
|
|
1272
|
+
highestIncompletePhase = dir;
|
|
1273
|
+
highestIncompletePhaseNum = phaseMatch[1];
|
|
1274
|
+
highestIncompletePhaseplanCount = plans;
|
|
1275
|
+
highestIncompletePhaseSummaryCount = summaries;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Sync Total Plans in Phase
|
|
1281
|
+
if (highestIncompletePhase) {
|
|
1282
|
+
const currentPlansField = stateExtractField(modified, 'Total Plans in Phase');
|
|
1283
|
+
if (currentPlansField && parseInt(currentPlansField, 10) !== highestIncompletePhaseplanCount) {
|
|
1284
|
+
changes.push(`Total Plans in Phase: ${currentPlansField} -> ${highestIncompletePhaseplanCount}`);
|
|
1285
|
+
const result = stateReplaceField(modified, 'Total Plans in Phase', String(highestIncompletePhaseplanCount));
|
|
1286
|
+
if (result) modified = result;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Sync Progress
|
|
1291
|
+
const percent = totalDiskPlans > 0 ? Math.min(100, Math.round(totalDiskSummaries / totalDiskPlans * 100)) : 0;
|
|
1292
|
+
const currentProgress = stateExtractField(modified, 'Progress');
|
|
1293
|
+
if (currentProgress) {
|
|
1294
|
+
const currentPercent = parseInt(currentProgress.replace(/[^\d]/g, ''), 10);
|
|
1295
|
+
if (currentPercent !== percent) {
|
|
1296
|
+
const barWidth = 10;
|
|
1297
|
+
const filled = Math.round(percent / 100 * barWidth);
|
|
1298
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
1299
|
+
const progressStr = `[${bar}] ${percent}%`;
|
|
1300
|
+
changes.push(`Progress: ${currentProgress} -> ${progressStr}`);
|
|
1301
|
+
const result = stateReplaceField(modified, 'Progress', progressStr);
|
|
1302
|
+
if (result) modified = result;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// Sync Last Activity
|
|
1307
|
+
const result = stateReplaceField(modified, 'Last Activity', today);
|
|
1308
|
+
if (result) {
|
|
1309
|
+
const oldActivity = stateExtractField(modified, 'Last Activity');
|
|
1310
|
+
if (oldActivity !== today) {
|
|
1311
|
+
changes.push(`Last Activity: ${oldActivity} -> ${today}`);
|
|
1312
|
+
}
|
|
1313
|
+
modified = result;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (verify) {
|
|
1317
|
+
output({ synced: false, changes, dry_run: true }, raw);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
if (changes.length > 0 || modified !== content) {
|
|
1322
|
+
writeStateMd(statePath, modified, cwd);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
output({ synced: true, changes, dry_run: false }, raw);
|
|
702
1326
|
}
|
|
703
1327
|
|
|
704
1328
|
module.exports = {
|
|
705
1329
|
stateExtractField,
|
|
706
1330
|
stateReplaceField,
|
|
1331
|
+
stateReplaceFieldWithFallback,
|
|
707
1332
|
writeStateMd,
|
|
1333
|
+
updatePerformanceMetricsSection,
|
|
708
1334
|
cmdStateLoad,
|
|
709
1335
|
cmdStateGet,
|
|
710
1336
|
cmdStatePatch,
|
|
@@ -718,4 +1344,10 @@ module.exports = {
|
|
|
718
1344
|
cmdStateRecordSession,
|
|
719
1345
|
cmdStateSnapshot,
|
|
720
1346
|
cmdStateJson,
|
|
1347
|
+
cmdStateBeginPhase,
|
|
1348
|
+
cmdStatePlannedPhase,
|
|
1349
|
+
cmdStateValidate,
|
|
1350
|
+
cmdStateSync,
|
|
1351
|
+
cmdSignalWaiting,
|
|
1352
|
+
cmdSignalResume,
|
|
721
1353
|
};
|