gsd-opencode 1.30.0 → 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-debugger.md +0 -1
- package/agents/gsd-doc-verifier.md +207 -0
- package/agents/gsd-doc-writer.md +608 -0
- package/agents/gsd-executor.md +22 -1
- package/agents/gsd-phase-researcher.md +41 -0
- package/agents/gsd-plan-checker.md +82 -0
- package/agents/gsd-planner.md +123 -194
- package/agents/gsd-security-auditor.md +129 -0
- package/agents/gsd-ui-auditor.md +40 -0
- package/agents/gsd-user-profiler.md +2 -2
- package/agents/gsd-verifier.md +84 -18
- package/commands/gsd/gsd-add-backlog.md +1 -1
- package/commands/gsd/gsd-analyze-dependencies.md +34 -0
- package/commands/gsd/gsd-autonomous.md +6 -2
- package/commands/gsd/gsd-cleanup.md +5 -0
- package/commands/gsd/gsd-debug.md +24 -21
- package/commands/gsd/gsd-discuss-phase.md +7 -2
- package/commands/gsd/gsd-docs-update.md +48 -0
- package/commands/gsd/gsd-execute-phase.md +4 -0
- package/commands/gsd/gsd-help.md +2 -0
- package/commands/gsd/gsd-join-discord.md +2 -1
- package/commands/gsd/gsd-manager.md +1 -0
- package/commands/gsd/gsd-new-project.md +4 -0
- package/commands/gsd/gsd-plan-phase.md +5 -0
- package/commands/gsd/gsd-quick.md +5 -3
- package/commands/gsd/gsd-reapply-patches.md +171 -39
- package/commands/gsd/gsd-research-phase.md +2 -12
- package/commands/gsd/gsd-review-backlog.md +1 -0
- package/commands/gsd/gsd-review.md +3 -2
- package/commands/gsd/gsd-secure-phase.md +35 -0
- package/commands/gsd/gsd-thread.md +1 -1
- package/commands/gsd/gsd-workstreams.md +7 -2
- package/get-shit-done/bin/gsd-tools.cjs +42 -8
- package/get-shit-done/bin/lib/commands.cjs +68 -14
- package/get-shit-done/bin/lib/config.cjs +18 -10
- package/get-shit-done/bin/lib/core.cjs +383 -80
- package/get-shit-done/bin/lib/docs.cjs +267 -0
- package/get-shit-done/bin/lib/frontmatter.cjs +47 -2
- package/get-shit-done/bin/lib/init.cjs +85 -5
- package/get-shit-done/bin/lib/milestone.cjs +21 -0
- package/get-shit-done/bin/lib/model-profiles.cjs +2 -0
- package/get-shit-done/bin/lib/phase.cjs +232 -189
- package/get-shit-done/bin/lib/profile-output.cjs +97 -1
- package/get-shit-done/bin/lib/roadmap.cjs +137 -113
- package/get-shit-done/bin/lib/schema-detect.cjs +238 -0
- package/get-shit-done/bin/lib/security.cjs +5 -3
- package/get-shit-done/bin/lib/state.cjs +366 -44
- package/get-shit-done/bin/lib/verify.cjs +158 -14
- package/get-shit-done/bin/lib/workstream.cjs +6 -2
- 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/context-budget.md +49 -0
- package/get-shit-done/references/continuation-format.md +15 -15
- 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/model-profiles.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 +15 -0
- 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/workstream-flag.md +56 -3
- package/get-shit-done/templates/SECURITY.md +61 -0
- package/get-shit-done/templates/VALIDATION.md +3 -3
- package/get-shit-done/templates/claude-md.md +27 -4
- package/get-shit-done/templates/config.json +4 -0
- package/get-shit-done/templates/debug-subagent-prompt.md +2 -6
- package/get-shit-done/templates/planner-subagent-prompt.md +2 -10
- package/get-shit-done/workflows/add-phase.md +2 -2
- package/get-shit-done/workflows/add-todo.md +1 -1
- package/get-shit-done/workflows/analyze-dependencies.md +96 -0
- package/get-shit-done/workflows/audit-milestone.md +8 -12
- package/get-shit-done/workflows/autonomous.md +158 -13
- package/get-shit-done/workflows/check-todos.md +2 -2
- package/get-shit-done/workflows/complete-milestone.md +13 -4
- package/get-shit-done/workflows/diagnose-issues.md +8 -6
- package/get-shit-done/workflows/discovery-phase.md +1 -1
- package/get-shit-done/workflows/discuss-phase-assumptions.md +22 -4
- package/get-shit-done/workflows/discuss-phase-power.md +291 -0
- package/get-shit-done/workflows/discuss-phase.md +149 -11
- package/get-shit-done/workflows/docs-update.md +1093 -0
- package/get-shit-done/workflows/execute-phase.md +362 -66
- package/get-shit-done/workflows/execute-plan.md +1 -1
- package/get-shit-done/workflows/help.md +9 -6
- package/get-shit-done/workflows/insert-phase.md +2 -2
- package/get-shit-done/workflows/manager.md +27 -26
- package/get-shit-done/workflows/map-codebase.md +10 -32
- package/get-shit-done/workflows/new-milestone.md +14 -8
- package/get-shit-done/workflows/new-project.md +48 -25
- package/get-shit-done/workflows/next.md +1 -1
- package/get-shit-done/workflows/note.md +1 -1
- package/get-shit-done/workflows/pause-work.md +73 -10
- package/get-shit-done/workflows/plan-milestone-gaps.md +2 -2
- package/get-shit-done/workflows/plan-phase.md +184 -32
- package/get-shit-done/workflows/progress.md +20 -20
- package/get-shit-done/workflows/quick.md +102 -84
- package/get-shit-done/workflows/research-phase.md +2 -6
- package/get-shit-done/workflows/resume-project.md +4 -4
- package/get-shit-done/workflows/review.md +56 -3
- package/get-shit-done/workflows/secure-phase.md +154 -0
- package/get-shit-done/workflows/settings.md +13 -2
- package/get-shit-done/workflows/ship.md +13 -4
- package/get-shit-done/workflows/transition.md +6 -6
- package/get-shit-done/workflows/ui-phase.md +4 -14
- package/get-shit-done/workflows/ui-review.md +25 -7
- package/get-shit-done/workflows/update.md +165 -16
- package/get-shit-done/workflows/validate-phase.md +1 -11
- package/get-shit-done/workflows/verify-phase.md +127 -6
- package/get-shit-done/workflows/verify-work.md +69 -21
- package/package.json +1 -1
|
@@ -141,29 +141,28 @@ function cmdStatePatch(cwd, patches, raw) {
|
|
|
141
141
|
|
|
142
142
|
const statePath = planningPaths(cwd).state;
|
|
143
143
|
try {
|
|
144
|
-
let content = fs.readFileSync(statePath, 'utf-8');
|
|
145
144
|
const results = { updated: [], failed: [] };
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
}
|
|
161
163
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (results.updated.length > 0) {
|
|
165
|
-
writeStateMd(statePath, content, cwd);
|
|
166
|
-
}
|
|
164
|
+
return content;
|
|
165
|
+
}, cwd);
|
|
167
166
|
|
|
168
167
|
output(results, raw, results.updated.length > 0 ? 'true' : 'false');
|
|
169
168
|
} catch {
|
|
@@ -236,6 +235,12 @@ function stateReplaceFieldWithFallback(content, primary, fallback, value) {
|
|
|
236
235
|
result = stateReplaceField(content, fallback, value);
|
|
237
236
|
if (result) return result;
|
|
238
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
|
+
);
|
|
239
244
|
return content;
|
|
240
245
|
}
|
|
241
246
|
|
|
@@ -699,8 +704,14 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
699
704
|
} catch { /* intentionally empty */ }
|
|
700
705
|
}
|
|
701
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.
|
|
702
711
|
let progressPercent = null;
|
|
703
|
-
if (
|
|
712
|
+
if (totalPlans !== null && totalPlans > 0 && completedPlans !== null) {
|
|
713
|
+
progressPercent = Math.min(100, Math.round(completedPlans / totalPlans * 100));
|
|
714
|
+
} else if (progressRaw) {
|
|
704
715
|
const pctMatch = progressRaw.match(/(\d+)%/);
|
|
705
716
|
if (pctMatch) progressPercent = parseInt(pctMatch[1], 10);
|
|
706
717
|
}
|
|
@@ -781,55 +792,80 @@ function syncStateFrontmatter(content, cwd) {
|
|
|
781
792
|
}
|
|
782
793
|
|
|
783
794
|
/**
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
* Uses a simple lockfile to prevent parallel agents from overwriting
|
|
787
|
-
* each other's changes (race condition with read-modify-write cycle).
|
|
795
|
+
* Acquire a lockfile for STATE.md operations.
|
|
796
|
+
* Returns the lock path for later release.
|
|
788
797
|
*/
|
|
789
|
-
function
|
|
790
|
-
const synced = syncStateFrontmatter(content, cwd);
|
|
798
|
+
function acquireStateLock(statePath) {
|
|
791
799
|
const lockPath = statePath + '.lock';
|
|
792
800
|
const maxRetries = 10;
|
|
793
801
|
const retryDelay = 200; // ms
|
|
794
802
|
|
|
795
|
-
// Acquire lock (spin with backoff)
|
|
796
803
|
for (let i = 0; i < maxRetries; i++) {
|
|
797
804
|
try {
|
|
798
|
-
// O_EXCL fails if file already exists — atomic lock
|
|
799
805
|
const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
|
800
806
|
fs.writeSync(fd, String(process.pid));
|
|
801
807
|
fs.closeSync(fd);
|
|
802
|
-
|
|
808
|
+
return lockPath;
|
|
803
809
|
} catch (err) {
|
|
804
810
|
if (err.code === 'EEXIST') {
|
|
805
|
-
// Check for stale lock (> 10s old)
|
|
806
811
|
try {
|
|
807
812
|
const stat = fs.statSync(lockPath);
|
|
808
813
|
if (Date.now() - stat.mtimeMs > 10000) {
|
|
809
814
|
fs.unlinkSync(lockPath);
|
|
810
|
-
continue;
|
|
815
|
+
continue;
|
|
811
816
|
}
|
|
812
817
|
} catch { /* lock was released between check — retry */ }
|
|
813
818
|
|
|
814
819
|
if (i === maxRetries - 1) {
|
|
815
|
-
// Last resort: write anyway rather than losing data
|
|
816
820
|
try { fs.unlinkSync(lockPath); } catch {}
|
|
817
|
-
|
|
821
|
+
return lockPath;
|
|
818
822
|
}
|
|
819
|
-
// Spin-wait with small jitter
|
|
820
823
|
const jitter = Math.floor(Math.random() * 50);
|
|
821
824
|
const start = Date.now();
|
|
822
825
|
while (Date.now() - start < retryDelay + jitter) { /* busy wait */ }
|
|
823
826
|
continue;
|
|
824
827
|
}
|
|
825
|
-
|
|
828
|
+
return lockPath; // non-EEXIST error — proceed without lock
|
|
826
829
|
}
|
|
827
830
|
}
|
|
831
|
+
return statePath + '.lock';
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function releaseStateLock(lockPath) {
|
|
835
|
+
try { fs.unlinkSync(lockPath); } catch { /* lock already gone */ }
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* write STATE.md with synchronized YAML frontmatter.
|
|
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).
|
|
843
|
+
*/
|
|
844
|
+
function writeStateMd(statePath, content, cwd) {
|
|
845
|
+
const synced = syncStateFrontmatter(content, cwd);
|
|
846
|
+
const lockPath = acquireStateLock(statePath);
|
|
847
|
+
try {
|
|
848
|
+
fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8');
|
|
849
|
+
} finally {
|
|
850
|
+
releaseStateLock(lockPath);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
828
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);
|
|
829
862
|
try {
|
|
863
|
+
const content = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8') : '';
|
|
864
|
+
const modified = transformFn(content);
|
|
865
|
+
const synced = syncStateFrontmatter(modified, cwd);
|
|
830
866
|
fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8');
|
|
831
867
|
} finally {
|
|
832
|
-
|
|
868
|
+
releaseStateLock(lockPath);
|
|
833
869
|
}
|
|
834
870
|
}
|
|
835
871
|
|
|
@@ -841,16 +877,27 @@ function cmdStateJson(cwd, raw) {
|
|
|
841
877
|
}
|
|
842
878
|
|
|
843
879
|
const content = fs.readFileSync(statePath, 'utf-8');
|
|
844
|
-
const
|
|
880
|
+
const existingFm = extractFrontmatter(content);
|
|
881
|
+
const body = stripFrontmatter(content);
|
|
845
882
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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;
|
|
851
898
|
}
|
|
852
899
|
|
|
853
|
-
output(
|
|
900
|
+
output(built, raw, JSON.stringify(built, null, 2));
|
|
854
901
|
}
|
|
855
902
|
|
|
856
903
|
/**
|
|
@@ -1007,11 +1054,283 @@ function cmdSignalResume(cwd, raw) {
|
|
|
1007
1054
|
output({ resumed: true, removed }, raw, removed ? 'true' : 'false');
|
|
1008
1055
|
}
|
|
1009
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
|
+
}
|
|
1182
|
+
|
|
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);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
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);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1010
1328
|
module.exports = {
|
|
1011
1329
|
stateExtractField,
|
|
1012
1330
|
stateReplaceField,
|
|
1013
1331
|
stateReplaceFieldWithFallback,
|
|
1014
1332
|
writeStateMd,
|
|
1333
|
+
updatePerformanceMetricsSection,
|
|
1015
1334
|
cmdStateLoad,
|
|
1016
1335
|
cmdStateGet,
|
|
1017
1336
|
cmdStatePatch,
|
|
@@ -1026,6 +1345,9 @@ module.exports = {
|
|
|
1026
1345
|
cmdStateSnapshot,
|
|
1027
1346
|
cmdStateJson,
|
|
1028
1347
|
cmdStateBeginPhase,
|
|
1348
|
+
cmdStatePlannedPhase,
|
|
1349
|
+
cmdStateValidate,
|
|
1350
|
+
cmdStateSync,
|
|
1029
1351
|
cmdSignalWaiting,
|
|
1030
1352
|
cmdSignalResume,
|
|
1031
1353
|
};
|