@opengsd/gsd-core 1.8.0 → 1.9.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +31 -1
- package/agents/gsd-code-fixer.md +107 -34
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +36 -0
- package/agents/gsd-executor.md +20 -7
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +15 -23
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/bin/install.js +236 -107
- package/commands/gsd/plan-review-convergence.md +5 -1
- package/gsd-core/bin/gsd-tools.cjs +882 -4
- package/gsd-core/bin/lib/api-coverage.cjs +22 -8
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/check-command-router.cjs +12 -2
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
- package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
- package/gsd-core/bin/lib/commands.cjs +246 -18
- package/gsd-core/bin/lib/config-loader.cjs +200 -28
- package/gsd-core/bin/lib/config.cjs +90 -5
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +44 -19
- package/gsd-core/bin/lib/install-engine.cjs +1 -0
- package/gsd-core/bin/lib/milestone.cjs +36 -9
- package/gsd-core/bin/lib/model-catalog.cjs +51 -1
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +278 -5
- package/gsd-core/bin/lib/phase.cjs +61 -6
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +1 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
- package/gsd-core/bin/lib/roadmap.cjs +10 -4
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +1 -1
- package/gsd-core/bin/lib/state-document.cjs +164 -20
- package/gsd-core/bin/lib/state-transition.cjs +28 -10
- package/gsd-core/bin/lib/state.cjs +141 -21
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +9 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +32 -0
- package/gsd-core/bin/lib/verification.cjs +51 -14
- package/gsd-core/bin/lib/verify.cjs +146 -22
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -21
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/ai-integration-phase.md +4 -4
- package/gsd-core/workflows/audit-fix.md +4 -0
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -21
- package/gsd-core/workflows/debug.md +10 -2
- package/gsd-core/workflows/diagnose-issues.md +4 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/docs-update.md +8 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase.md +85 -115
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/help/modes/full.md +3 -3
- package/gsd-core/workflows/import.md +4 -1
- package/gsd-core/workflows/ingest-docs.md +4 -0
- package/gsd-core/workflows/map-codebase.md +13 -6
- package/gsd-core/workflows/new-milestone.md +10 -2
- package/gsd-core/workflows/new-project.md +11 -4
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/plan-phase.md +42 -46
- package/gsd-core/workflows/plan-review-convergence.md +18 -14
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +14 -3
- package/gsd-core/workflows/review.md +146 -575
- package/gsd-core/workflows/scan.md +9 -1
- package/gsd-core/workflows/secure-phase.md +10 -2
- package/gsd-core/workflows/ship.md +41 -11
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/ui-phase.md +8 -1
- package/gsd-core/workflows/ui-review.md +8 -1
- package/gsd-core/workflows/update.md +104 -5
- package/gsd-core/workflows/validate-phase.md +10 -2
- package/gsd-core/workflows/verify-work.md +8 -1
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +9 -6
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +9 -6
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +7 -7
- package/pi/gsd.cjs +26 -1
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +24 -0
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +6 -4
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +372 -94
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/validate-registry.cjs +10 -6
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -39,6 +39,7 @@ const { transitionCore, applyStatePreservation, sliceCurrentPositionSection } =
|
|
|
39
39
|
const state_document_cjs_1 = require("./state-document.cjs");
|
|
40
40
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
41
41
|
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
42
|
+
const validate_cjs_1 = require("./validate.cjs");
|
|
42
43
|
const STATE_PROGRESS_RESYNC_FIELDS = new Set([
|
|
43
44
|
'Progress',
|
|
44
45
|
'Total Plans in Phase',
|
|
@@ -359,6 +360,7 @@ function cmdStateAdvancePlan(cwd, raw) {
|
|
|
359
360
|
const deps = {
|
|
360
361
|
clock: clock_cjs_1.realClock,
|
|
361
362
|
progressProvider: () => null,
|
|
363
|
+
sourcePath: statePath,
|
|
362
364
|
};
|
|
363
365
|
let resultData;
|
|
364
366
|
readModifyWriteStateMd(statePath, (content) => {
|
|
@@ -1150,6 +1152,38 @@ function matchSessionSection(body) {
|
|
|
1150
1152
|
?? (0, markdown_sectionizer_cjs_1.collectSection)(body, isSessionContinuity, { levelBounded: true });
|
|
1151
1153
|
return section ? section.body : null;
|
|
1152
1154
|
}
|
|
1155
|
+
/**
|
|
1156
|
+
* #2567: prevent a stale archive "Last activity:" line from overwriting a
|
|
1157
|
+
* newer frontmatter value. `stateExtractField` matches the first body
|
|
1158
|
+
* occurrence, which may be a historical line in an archive section. Unlike
|
|
1159
|
+
* Stopped At / Paused At (which canonically live in `## Session`), Last
|
|
1160
|
+
* Activity has no single canonical section — it appears in the preamble,
|
|
1161
|
+
* `## Configuration`, and `## Current Position` across STATE.md layouts, so a
|
|
1162
|
+
* section scope cannot reliably exclude archive copies. Guard the
|
|
1163
|
+
* information-losing direction instead: when the body-derived date is OLDER
|
|
1164
|
+
* than the existing frontmatter date, keep the existing value and its
|
|
1165
|
+
* description. Applied at both the write seam (syncStateFrontmatter) and the
|
|
1166
|
+
* read seam (cmdStateJson) so they agree. Date fields only — non-date values
|
|
1167
|
+
* pass through unchanged.
|
|
1168
|
+
*/
|
|
1169
|
+
function preferNewerLastActivity(existingFm, derivedFm) {
|
|
1170
|
+
if (!existingFm)
|
|
1171
|
+
return;
|
|
1172
|
+
const exRaw = existingFm['last_activity'];
|
|
1173
|
+
const derRaw = derivedFm['last_activity'];
|
|
1174
|
+
if (typeof exRaw !== 'string' || typeof derRaw !== 'string')
|
|
1175
|
+
return;
|
|
1176
|
+
const exDate = exRaw.slice(0, 10);
|
|
1177
|
+
const derDate = derRaw.slice(0, 10);
|
|
1178
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(exDate) || !/^\d{4}-\d{2}-\d{2}$/.test(derDate))
|
|
1179
|
+
return;
|
|
1180
|
+
if (derDate < exDate) {
|
|
1181
|
+
derivedFm['last_activity'] = exRaw;
|
|
1182
|
+
if (existingFm['last_activity_desc'] !== undefined) {
|
|
1183
|
+
derivedFm['last_activity_desc'] = existingFm['last_activity_desc'];
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1153
1187
|
function parseProsePhaseField(value) {
|
|
1154
1188
|
// #2121 Phase 2 (#2125): delegate to the canonical anchored parser so this
|
|
1155
1189
|
// module holds no independent prose phase-id regex. Drives #2111 — the
|
|
@@ -1180,7 +1214,9 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
1180
1214
|
// Bug #3265: prefer YAML frontmatter for canonical scalar fields so that a
|
|
1181
1215
|
// body table cell containing **Status:** Y cannot shadow the authoritative
|
|
1182
1216
|
// frontmatter value. Mirrors the fix in sdk/src/query/state.ts.
|
|
1183
|
-
|
|
1217
|
+
// Pass statePath so a truncated STATE.md is named in the #1882 diagnostic rather than
|
|
1218
|
+
// reported under a content digest — STATE.md is one of the artefacts epic #1879 is about.
|
|
1219
|
+
const fm = extractFrontmatter(content, statePath);
|
|
1184
1220
|
const body = stripFrontmatter(content);
|
|
1185
1221
|
// Helper: return frontmatter scalar value when present and non-empty.
|
|
1186
1222
|
// Accepts strings, numbers, and booleans — coercing non-string primitives to
|
|
@@ -1363,8 +1399,8 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1363
1399
|
const proseLastActivity = parseProseLastActivityField(rawLastActivity);
|
|
1364
1400
|
const lastActivity = proseLastActivity.date ?? rawLastActivity;
|
|
1365
1401
|
const lastActivityDesc = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Last Activity Description') ?? proseLastActivity.description;
|
|
1366
|
-
// Bug #2444: scope Stopped At extraction to the
|
|
1367
|
-
//
|
|
1402
|
+
// Bug #2444 / #2567: scope Stopped At AND Paused At extraction to the
|
|
1403
|
+
// ## Session section so historical prose elsewhere in the body (e.g. in a
|
|
1368
1404
|
// Session Continuity Archive section) never overwrites the current value.
|
|
1369
1405
|
// Fall back to full-body search only when no ## Session section exists.
|
|
1370
1406
|
// #1101: prefer the canonical `## Session` block, falling back to the bootstrap
|
|
@@ -1372,7 +1408,9 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1372
1408
|
const sessionSectionMatch = matchSessionSection(bodyContent);
|
|
1373
1409
|
const sessionBodyScope = sessionSectionMatch ?? bodyContent;
|
|
1374
1410
|
const stoppedAt = (0, state_document_cjs_1.stateExtractField)(sessionBodyScope, 'Stopped At') || (0, state_document_cjs_1.stateExtractField)(sessionBodyScope, 'Stopped at');
|
|
1375
|
-
|
|
1411
|
+
// #2567: Paused At is a session field — scope it to ## Session too so a
|
|
1412
|
+
// stale "Paused At:" line in an archive section cannot overwrite the value.
|
|
1413
|
+
const pausedAt = (0, state_document_cjs_1.stateExtractField)(sessionBodyScope, 'Paused At');
|
|
1376
1414
|
let milestone = null;
|
|
1377
1415
|
let milestoneName = null;
|
|
1378
1416
|
if (cwd) {
|
|
@@ -1498,10 +1536,20 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1498
1536
|
const versionedHeading = new RegExp(`^#{1,3}\\s+(?!Phase\\s+\\S).*${escapeRegex(String(milestone).trim())}`, 'mi');
|
|
1499
1537
|
milestoneBounded = versionedHeading.test(roadmapRaw);
|
|
1500
1538
|
}
|
|
1539
|
+
// #2828: distinguish a FLAT unmilestoned roadmap (no milestone sectioning
|
|
1540
|
+
// at all — only Phase headings) from a MILESTONED-but-unbounded one
|
|
1541
|
+
// (milestone/version headings exist but the asserted one isn't among them).
|
|
1542
|
+
// On a flat roadmap the whole-doc count is correct (no sibling milestones to
|
|
1543
|
+
// conflate); on a sectioned-but-unbounded one it conflates siblings (#1761),
|
|
1544
|
+
// so fall back to phaseDirs.length.
|
|
1545
|
+
const hasMilestoneSectioning = roadmapRaw !== null
|
|
1546
|
+
&& /^#{2,3}\s+(?!Phase\s+\S)/mi.test(roadmapRaw);
|
|
1547
|
+
const safeToUseRoadmapCount = milestoneBounded
|
|
1548
|
+
|| (roadmapPhaseCount > 0 && !hasMilestoneSectioning);
|
|
1501
1549
|
return {
|
|
1502
|
-
totalPhases:
|
|
1503
|
-
? phaseDirs.length
|
|
1504
|
-
:
|
|
1550
|
+
totalPhases: safeToUseRoadmapCount
|
|
1551
|
+
? Math.max(phaseDirs.length, roadmapPhaseCount)
|
|
1552
|
+
: phaseDirs.length,
|
|
1505
1553
|
milestoneBounded,
|
|
1506
1554
|
completedPhases: diskCompletedPhases,
|
|
1507
1555
|
totalPlans: diskTotalPlans,
|
|
@@ -1578,10 +1626,12 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1578
1626
|
fm['progress'] = progress;
|
|
1579
1627
|
return fm;
|
|
1580
1628
|
}
|
|
1581
|
-
function syncStateFrontmatter(content, cwd) {
|
|
1629
|
+
function syncStateFrontmatter(content, cwd, authoritativeFm) {
|
|
1582
1630
|
// Read existing frontmatter BEFORE stripping — it may contain values
|
|
1583
1631
|
// that the body no longer has (e.g., Status field removed by an agent).
|
|
1584
|
-
|
|
1632
|
+
// `cwd` already identifies the workspace this content came from, so the STATE.md path is
|
|
1633
|
+
// derivable here without widening the signature (#1882).
|
|
1634
|
+
const existingFm = extractFrontmatter(content, cwd ? planningPaths(cwd).state : undefined);
|
|
1585
1635
|
const body = stripFrontmatter(content);
|
|
1586
1636
|
const derivedFm = buildStateFrontmatter(body, cwd);
|
|
1587
1637
|
// Preserve existing frontmatter status when body-derived status is 'unknown'.
|
|
@@ -1665,6 +1715,23 @@ function syncStateFrontmatter(content, cwd) {
|
|
|
1665
1715
|
derivedFm[key] = existingFm[key];
|
|
1666
1716
|
}
|
|
1667
1717
|
}
|
|
1718
|
+
// #2567: guard the information-losing direction — a stale archive
|
|
1719
|
+
// "Last activity:" line must not overwrite a newer frontmatter value.
|
|
1720
|
+
preferNewerLastActivity(existingFm, derivedFm);
|
|
1721
|
+
// #2736: intent-first override, applied last. A transition adapter that
|
|
1722
|
+
// already holds the exact value (completePhase's next-phase display name,
|
|
1723
|
+
// beginPhase's phase name) passes it here, so the body-prose re-derivation
|
|
1724
|
+
// above — which is lossy by construction for names containing a
|
|
1725
|
+
// parenthetical (`Closer-ruling measurement (D1a)` → `D1a`) — never runs
|
|
1726
|
+
// the final word on a field the transition just resolved. The prose parser
|
|
1727
|
+
// remains the fallback for genuinely unknown prose only.
|
|
1728
|
+
if (authoritativeFm) {
|
|
1729
|
+
for (const [key, value] of Object.entries(authoritativeFm)) {
|
|
1730
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
1731
|
+
derivedFm[key] = value;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1668
1735
|
const yamlStr = reconstructFrontmatter(derivedFm);
|
|
1669
1736
|
return `---\n${yamlStr}\n---\n\n${body}`;
|
|
1670
1737
|
}
|
|
@@ -1960,7 +2027,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
1960
2027
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(statePath) || '';
|
|
1961
2028
|
// Snapshot the existing progress block BEFORE the transform so we can
|
|
1962
2029
|
// restore it when resync is false.
|
|
1963
|
-
const preFm = resync ? null : extractFrontmatter(content);
|
|
2030
|
+
const preFm = resync ? null : extractFrontmatter(content, statePath);
|
|
1964
2031
|
// Bug #1230: delta heuristic — snapshot pre-transform body source fields so
|
|
1965
2032
|
// we can detect whether THIS write changed them. syncStateFrontmatter
|
|
1966
2033
|
// re-derives frontmatter status/stopped_at from the body on every write;
|
|
@@ -1972,7 +2039,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
1972
2039
|
// Strip frontmatter before calling stateExtractField so the YAML `status:`
|
|
1973
2040
|
// key in the frontmatter block cannot shadow the body field we are tracking.
|
|
1974
2041
|
const preBody = stripFrontmatter(content);
|
|
1975
|
-
const preFmSnapshot = extractFrontmatter(content);
|
|
2042
|
+
const preFmSnapshot = extractFrontmatter(content, statePath);
|
|
1976
2043
|
const preBodyStatus = (0, state_document_cjs_1.stateExtractField)(preBody, 'Status');
|
|
1977
2044
|
// Bug #1230 / Change B: scope stopped_at delta to the ## Session section,
|
|
1978
2045
|
// mirroring buildStateFrontmatter's sessionBodyScope logic (line ~1172).
|
|
@@ -1999,7 +2066,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
1999
2066
|
if (modified === content) {
|
|
2000
2067
|
return;
|
|
2001
2068
|
}
|
|
2002
|
-
let synced = syncStateFrontmatter(modified, cwd);
|
|
2069
|
+
let synced = syncStateFrontmatter(modified, cwd, options?.authoritativeFm);
|
|
2003
2070
|
// Post-transform body source fields used for the delta comparison (#1230).
|
|
2004
2071
|
// Use `modified` (not `synced`): syncStateFrontmatter only rewrites the frontmatter block, so the body is identical in both — and we need the body the transform produced.
|
|
2005
2072
|
// Strip frontmatter so the YAML status key cannot shadow the body field we are tracking.
|
|
@@ -2020,7 +2087,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
2020
2087
|
// one policy source, not three drifting encodings. Behavior-identical to
|
|
2021
2088
|
// the pre-#1796 inline block; this is the absorption ADR-1769 / CONTEXT.md
|
|
2022
2089
|
// already claimed shipped.
|
|
2023
|
-
const postFm = extractFrontmatter(synced);
|
|
2090
|
+
const postFm = extractFrontmatter(synced, statePath);
|
|
2024
2091
|
const preservation = applyStatePreservation({
|
|
2025
2092
|
preFm, postFm, preFmSnapshot, resync,
|
|
2026
2093
|
deriveProgressKeys: options?.deriveProgressKeys === true,
|
|
@@ -2028,7 +2095,21 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
2028
2095
|
preBodyStoppedAt, postBodyStoppedAt,
|
|
2029
2096
|
preBodyPhaseSource, postBodyPhaseSource,
|
|
2030
2097
|
});
|
|
2031
|
-
|
|
2098
|
+
// #2736: re-assert the intent-first values AFTER preservation. On STATE.md
|
|
2099
|
+
// layouts with no body `Phase:` line, both phase-source snapshots are null
|
|
2100
|
+
// (equal), so the #1695 restore fires and would put the stale pre-transition
|
|
2101
|
+
// name back over the authoritative one. Intent beats both the prose
|
|
2102
|
+
// re-derivation and the curated restore — the transition just resolved it.
|
|
2103
|
+
let authoritativeReasserted = false;
|
|
2104
|
+
if (options?.authoritativeFm) {
|
|
2105
|
+
for (const [key, value] of Object.entries(options.authoritativeFm)) {
|
|
2106
|
+
if (typeof value === 'string' && value.trim().length > 0 && preservation.postFm[key] !== value) {
|
|
2107
|
+
preservation.postFm[key] = value;
|
|
2108
|
+
authoritativeReasserted = true;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
if (preservation.mutated || authoritativeReasserted) {
|
|
2032
2113
|
const yamlStr = reconstructFrontmatter(preservation.postFm);
|
|
2033
2114
|
const body = stripFrontmatter(synced);
|
|
2034
2115
|
synced = `---\n${yamlStr}\n---\n\n${body}`;
|
|
@@ -2046,7 +2127,7 @@ function cmdStateJson(cwd, raw) {
|
|
|
2046
2127
|
return;
|
|
2047
2128
|
}
|
|
2048
2129
|
const content = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
2049
|
-
const existingFm = extractFrontmatter(content);
|
|
2130
|
+
const existingFm = extractFrontmatter(content, statePath);
|
|
2050
2131
|
const body = stripFrontmatter(content);
|
|
2051
2132
|
// Always rebuild from body + disk so progress counters reflect current state.
|
|
2052
2133
|
// Returning cached frontmatter directly causes stale percent/completed_plans
|
|
@@ -2080,6 +2161,10 @@ function cmdStateJson(cwd, raw) {
|
|
|
2080
2161
|
if (existingFm && (0, state_document_cjs_1.shouldPreserveExistingProgress)(existingFm['progress'], built['progress'])) {
|
|
2081
2162
|
built['progress'] = (0, state_document_cjs_1.normalizeProgressNumbers)(existingFm['progress']);
|
|
2082
2163
|
}
|
|
2164
|
+
// #2567: guard the information-losing direction — a stale archive
|
|
2165
|
+
// "Last activity:" line must not surface as the current value. Mirrors the
|
|
2166
|
+
// syncStateFrontmatter guard so the read path agrees with the write path.
|
|
2167
|
+
preferNewerLastActivity(existingFm, built);
|
|
2083
2168
|
output(built, raw, JSON.stringify(built, null, 2));
|
|
2084
2169
|
}
|
|
2085
2170
|
/**
|
|
@@ -2109,13 +2194,30 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
2109
2194
|
const deps = {
|
|
2110
2195
|
clock: clock_cjs_1.realClock,
|
|
2111
2196
|
progressProvider: () => null, // beginPhase doesn't consult disk progress; syncStateFrontmatter's scan is authoritative
|
|
2197
|
+
sourcePath: statePath,
|
|
2198
|
+
};
|
|
2199
|
+
// #2736: the transition holds the exact display name; without this the
|
|
2200
|
+
// post-transform sync re-derives current_phase_name from the freshly
|
|
2201
|
+
// written `Phase: N (Name) — EXECUTING` line, which truncates any name
|
|
2202
|
+
// that itself contains a parenthetical. The #1695 delta-gate preservation
|
|
2203
|
+
// still runs after the sync; the override is re-asserted after it inside
|
|
2204
|
+
// readModifyWriteStateMd for layouts with no body `Phase:` line.
|
|
2205
|
+
const rmwOptions = {
|
|
2206
|
+
authoritativeFm: intent.phaseName ? { current_phase_name: intent.phaseName } : undefined,
|
|
2112
2207
|
};
|
|
2113
2208
|
let updated = [];
|
|
2114
2209
|
readModifyWriteStateMd(statePath, (content) => {
|
|
2115
2210
|
const result = transitionCore(content, intent, deps);
|
|
2116
2211
|
updated = result.updated;
|
|
2212
|
+
// #3127 resume: the core preserved the mid-flight Current Phase Name, so
|
|
2213
|
+
// the intent-first override must not fire — it would drift frontmatter
|
|
2214
|
+
// away from the preserved body value. Dropping it here is safe because
|
|
2215
|
+
// readModifyWriteStateMd consults options only after this callback returns.
|
|
2216
|
+
if (result.data?.['resumed']) {
|
|
2217
|
+
delete rmwOptions.authoritativeFm;
|
|
2218
|
+
}
|
|
2117
2219
|
return result.content;
|
|
2118
|
-
}, cwd);
|
|
2220
|
+
}, cwd, rmwOptions);
|
|
2119
2221
|
output({ updated, phase: phaseNumber, phase_name: phaseName || null, plan_count: planCount || null }, raw, updated.length > 0 ? 'true' : 'false');
|
|
2120
2222
|
}
|
|
2121
2223
|
/**
|
|
@@ -2361,6 +2463,7 @@ function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
|
|
2361
2463
|
const deps = {
|
|
2362
2464
|
clock: clock_cjs_1.realClock,
|
|
2363
2465
|
progressProvider: () => null,
|
|
2466
|
+
sourcePath: statePath,
|
|
2364
2467
|
};
|
|
2365
2468
|
let updated = [];
|
|
2366
2469
|
readModifyWriteStateMd(statePath, (content) => {
|
|
@@ -2368,7 +2471,10 @@ function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
|
|
2368
2471
|
updated = result.updated;
|
|
2369
2472
|
return result.content;
|
|
2370
2473
|
}, cwd, { resync: false, deriveProgressKeys: true });
|
|
2371
|
-
|
|
2474
|
+
const result = updated.length === 0
|
|
2475
|
+
? { updated, phase: phaseNumber, plan_count: planCount, warning: 'STATE.md Current Position has no recognized labels — transition was a no-op. Verify STATE.md uses the canonical labeled format (Status:, Total Plans in Phase:, etc.).' }
|
|
2476
|
+
: { updated, phase: phaseNumber, plan_count: planCount };
|
|
2477
|
+
output(result, raw, updated.length > 0 ? 'true' : 'false');
|
|
2372
2478
|
}
|
|
2373
2479
|
/**
|
|
2374
2480
|
* Bug #2630: reset STATE.md for a new milestone cycle.
|
|
@@ -2390,7 +2496,7 @@ function cmdStateMilestoneSwitch(cwd, version, name, raw) {
|
|
|
2390
2496
|
// milestoneSwitch rebuilds frontmatter directly and must not run the
|
|
2391
2497
|
// steady-state syncStateFrontmatter post-sync.
|
|
2392
2498
|
const intent = { kind: 'milestoneSwitch', version, name: resolvedName };
|
|
2393
|
-
const deps = { clock: clock_cjs_1.realClock, progressProvider: () => null };
|
|
2499
|
+
const deps = { clock: clock_cjs_1.realClock, progressProvider: () => null, sourcePath: statePath };
|
|
2394
2500
|
const lockPath = acquireStateLock(statePath);
|
|
2395
2501
|
try {
|
|
2396
2502
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(statePath) || '';
|
|
@@ -2413,6 +2519,14 @@ function cmdStateValidate(cwd, raw) {
|
|
|
2413
2519
|
return;
|
|
2414
2520
|
}
|
|
2415
2521
|
const content = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
2522
|
+
// #2701: fail loud on NUL/binary corruption before drift checks. A corrupt
|
|
2523
|
+
// STATE.md otherwise validates as clean and is silently skipped by recursive
|
|
2524
|
+
// searchers downstream, reading as "absent" rather than "corrupt."
|
|
2525
|
+
const encErr = (0, validate_cjs_1.textEncodingError)(content, 'STATE.md');
|
|
2526
|
+
if (encErr) {
|
|
2527
|
+
output({ valid: false, warnings: [encErr], drift: {} }, raw, undefined);
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2416
2530
|
const warnings = [];
|
|
2417
2531
|
const drift = {};
|
|
2418
2532
|
const status = (0, state_document_cjs_1.stateExtractField)(content, 'Status') || '';
|
|
@@ -2590,7 +2704,7 @@ function cmdStateSync(cwd, options, raw) {
|
|
|
2590
2704
|
// set — leave Progress untouched (percent=null) rather than silently writing
|
|
2591
2705
|
// fallback-derived wrong values. Projects without a milestone version (the common
|
|
2592
2706
|
// sync-test shape) are unaffected: the gate only fires when a version is asserted.
|
|
2593
|
-
const fmVersion = extractFrontmatter(content).milestone;
|
|
2707
|
+
const fmVersion = extractFrontmatter(content, statePath).milestone;
|
|
2594
2708
|
const versionStr = typeof fmVersion === 'string' && fmVersion.trim() ? fmVersion.trim() : null;
|
|
2595
2709
|
let milestoneBounded = true;
|
|
2596
2710
|
if (versionStr !== null && syncRoadmapRaw !== null) {
|
|
@@ -2648,7 +2762,7 @@ function cmdStatePrune(cwd, options, raw) {
|
|
|
2648
2762
|
// the explicit `Current Phase` field are unambiguous, so they stay document-wide;
|
|
2649
2763
|
// the shared extractor is not narrowed for any other caller.
|
|
2650
2764
|
const rawState = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
2651
|
-
const fm = extractFrontmatter(rawState);
|
|
2765
|
+
const fm = extractFrontmatter(rawState, statePath);
|
|
2652
2766
|
const body = stripFrontmatter(rawState);
|
|
2653
2767
|
// Mirror buildStateFrontmatter's fmScalar: only string/number/boolean
|
|
2654
2768
|
// frontmatter scalars are usable (an object/array `current_phase` is ignored,
|
|
@@ -2789,6 +2903,12 @@ function cmdStateRebuild(cwd, options, raw) {
|
|
|
2789
2903
|
progressProvider: () => null,
|
|
2790
2904
|
clock: clock_cjs_1.realClock,
|
|
2791
2905
|
phaseInventoryProvider,
|
|
2906
|
+
// Without this, `state rebuild --dry-run` reported a truncated STATE.md anonymously: the
|
|
2907
|
+
// write path is named only because readModifyWriteStateMd parses with the path first, and
|
|
2908
|
+
// the dry-run branch reads the file directly and never does. Dry-run is the read-only mode
|
|
2909
|
+
// an operator reaches for first when they suspect corruption, so it is the one that most
|
|
2910
|
+
// needs to name the file (#1882).
|
|
2911
|
+
sourcePath: statePath,
|
|
2792
2912
|
};
|
|
2793
2913
|
const runRebuild = (content) => transitionCore(content, { kind: 'rebuild' }, deps);
|
|
2794
2914
|
const emitVerboseLog = (log) => {
|
|
@@ -2887,7 +3007,7 @@ function cmdStateCompletePhase(cwd, raw, overridePhase) {
|
|
|
2887
3007
|
const currentPhase = resolvedPhase;
|
|
2888
3008
|
// Bug #1255: operate on body only so the YAML frontmatter `status:` key
|
|
2889
3009
|
// cannot shadow the body Status field (pipe-table or inline).
|
|
2890
|
-
const existingFm = extractFrontmatter(content);
|
|
3010
|
+
const existingFm = extractFrontmatter(content, statePath);
|
|
2891
3011
|
const hasFrontmatter = Object.keys(existingFm).length > 0;
|
|
2892
3012
|
let body = stripFrontmatter(content);
|
|
2893
3013
|
const reassemble = (b) => hasFrontmatter ? `---\n${reconstructFrontmatter(existingFm)}\n---\n\n${b}` : b;
|
|
@@ -194,9 +194,10 @@ function evaluateUatPassed(phaseFullDir, opts) {
|
|
|
194
194
|
// ── Process UAT files ──────────────────────────────────────────────────────
|
|
195
195
|
for (const file of uatFileNames) {
|
|
196
196
|
uatFiles.push(file);
|
|
197
|
+
const uatFilePath = node_path_1.default.join(phaseFullDir, file);
|
|
197
198
|
let raw = '';
|
|
198
199
|
try {
|
|
199
|
-
raw = node_fs_1.default.readFileSync(
|
|
200
|
+
raw = node_fs_1.default.readFileSync(uatFilePath, 'utf-8');
|
|
200
201
|
}
|
|
201
202
|
catch {
|
|
202
203
|
blockers.push(`${file}: could not read file`);
|
|
@@ -210,7 +211,7 @@ function evaluateUatPassed(phaseFullDir, opts) {
|
|
|
210
211
|
if (unterminatedFence || unterminatedComment) {
|
|
211
212
|
blockers.push(`${file}: malformed markdown (unterminated fence or comment)`);
|
|
212
213
|
}
|
|
213
|
-
const fm = extractFrontmatter(raw);
|
|
214
|
+
const fm = extractFrontmatter(raw, uatFilePath);
|
|
214
215
|
// File-level frontmatter status check
|
|
215
216
|
if (fm['status'] && BLOCKING_UAT_FM_STATUSES.has(fm['status'])) {
|
|
216
217
|
blockers.push(`${file}: frontmatter status=${fm['status']}`);
|
|
@@ -240,15 +241,16 @@ function evaluateUatPassed(phaseFullDir, opts) {
|
|
|
240
241
|
let hasPassingVerification = false;
|
|
241
242
|
for (const file of verFileNames) {
|
|
242
243
|
verificationFiles.push(file);
|
|
244
|
+
const verificationFilePath = node_path_1.default.join(phaseFullDir, file);
|
|
243
245
|
let raw = '';
|
|
244
246
|
try {
|
|
245
|
-
raw = node_fs_1.default.readFileSync(
|
|
247
|
+
raw = node_fs_1.default.readFileSync(verificationFilePath, 'utf-8');
|
|
246
248
|
}
|
|
247
249
|
catch {
|
|
248
250
|
blockers.push(`${file}: could not read verification file`);
|
|
249
251
|
continue;
|
|
250
252
|
}
|
|
251
|
-
const vfm = extractFrontmatter(raw);
|
|
253
|
+
const vfm = extractFrontmatter(raw, verificationFilePath);
|
|
252
254
|
const vStatus = vfm['status'];
|
|
253
255
|
if (vStatus && BLOCKING_VERIFICATION_FM_STATUSES.has(vStatus)) {
|
|
254
256
|
blockers.push(`${file}: verification status=${vStatus}`);
|
package/gsd-core/bin/lib/uat.cjs
CHANGED
|
@@ -63,7 +63,8 @@ function cmdAuditUat(cwd, raw) {
|
|
|
63
63
|
const files = node_fs_1.default.readdirSync(phaseDir);
|
|
64
64
|
// Process UAT files
|
|
65
65
|
for (const file of files.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
|
|
66
|
-
const
|
|
66
|
+
const uatFilePath = node_path_1.default.join(phaseDir, file);
|
|
67
|
+
const content = node_fs_1.default.readFileSync(uatFilePath, 'utf-8');
|
|
67
68
|
const items = parseUatItems(content);
|
|
68
69
|
if (items.length > 0) {
|
|
69
70
|
results.push({
|
|
@@ -72,17 +73,18 @@ function cmdAuditUat(cwd, raw) {
|
|
|
72
73
|
file,
|
|
73
74
|
file_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(phaseDir, file))),
|
|
74
75
|
type: 'uat',
|
|
75
|
-
status: (extractFrontmatter(content).status || 'unknown'),
|
|
76
|
+
status: (extractFrontmatter(content, uatFilePath).status || 'unknown'),
|
|
76
77
|
items,
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
// Process VERIFICATION files
|
|
81
82
|
for (const file of files.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
83
|
+
const verificationFilePath = node_path_1.default.join(phaseDir, file);
|
|
84
|
+
const content = node_fs_1.default.readFileSync(verificationFilePath, 'utf-8');
|
|
85
|
+
const status = extractFrontmatter(content, verificationFilePath).status || 'unknown';
|
|
84
86
|
if (status === 'human_needed' || status === 'gaps_found') {
|
|
85
|
-
const items = parseVerificationItems(content, status);
|
|
87
|
+
const items = parseVerificationItems(content, status, verificationFilePath);
|
|
86
88
|
if (items.length > 0) {
|
|
87
89
|
results.push({
|
|
88
90
|
phase: phaseNum,
|
|
@@ -624,7 +626,7 @@ function rawGapEntryText(entryLines) {
|
|
|
624
626
|
.trim();
|
|
625
627
|
}
|
|
626
628
|
// ─── parseVerificationItems ───────────────────────────────────────────────────
|
|
627
|
-
function parseVerificationItems(content, status) {
|
|
629
|
+
function parseVerificationItems(content, status, sourcePath) {
|
|
628
630
|
const items = [];
|
|
629
631
|
if (status === 'human_needed') {
|
|
630
632
|
// #2286: the frontmatter's structured `human_verification:` YAML array
|
|
@@ -633,7 +635,7 @@ function parseVerificationItems(content, status) {
|
|
|
633
635
|
// whose frontmatter declares the array doesn't require any particular
|
|
634
636
|
// `## Human Verification` body shape at all. An absent or empty array
|
|
635
637
|
// (length 0) falls back to the body scan unchanged.
|
|
636
|
-
const frontmatter = extractFrontmatter(content);
|
|
638
|
+
const frontmatter = extractFrontmatter(content, sourcePath);
|
|
637
639
|
const humanVerification = frontmatter.human_verification;
|
|
638
640
|
if (Array.isArray(humanVerification) && humanVerification.length > 0) {
|
|
639
641
|
humanVerification.forEach((entry, idx) => {
|
|
@@ -66,8 +66,8 @@ function classifyElement(text) {
|
|
|
66
66
|
*/
|
|
67
67
|
exports.UI_TAXONOMY = [
|
|
68
68
|
{ id: 'empty', name: 'Empty / no data', elements: ['form', 'list-collection', 'media'], consideration: 'What is shown when there is no data — zero items, an unfilled form, or absent media?' },
|
|
69
|
-
{ id: 'loading', name: 'Loading / in-flight', elements: ['form', 'list-collection', 'media', 'nav'], consideration: 'What is shown while data or content is still loading (skeleton, spinner, progressive reveal)?' },
|
|
70
|
-
{ id: 'error', name: 'Error / failure', elements: ['form', 'list-collection', 'media', 'nav'], consideration: 'What is shown when the load or submit fails (message, retry affordance, partial fallback)?' },
|
|
69
|
+
{ id: 'loading', name: 'Loading / in-flight', elements: ['form', 'list-collection', 'media', 'nav', 'interactive-control'], consideration: 'What is shown while data or content is still loading (skeleton, spinner, progressive reveal)?' },
|
|
70
|
+
{ id: 'error', name: 'Error / failure', elements: ['form', 'list-collection', 'media', 'nav', 'interactive-control'], consideration: 'What is shown when the load or submit fails (message, retry affordance, partial fallback)?' },
|
|
71
71
|
{ id: 'populated', name: 'Populated / happy path', elements: ['list-collection', 'media'], consideration: 'What does the normal populated (happy-path) state look like at a typical volume of content?' },
|
|
72
72
|
{ id: 'partial', name: 'Partial / incomplete', elements: ['form', 'list-collection'], consideration: 'What is shown for partial or incomplete data — some fields or rows present, others missing?' },
|
|
73
73
|
{ id: 'overflow', name: 'Overflow / truncation', elements: ['list-collection', 'nav', 'static-content'], consideration: 'What happens when content exceeds its container — scroll, clip, wrap, or truncate?' },
|