@opengsd/gsd-core 1.9.0 → 1.10.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +131 -34
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-executor.md +7 -5
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-plan-checker.md +9 -0
- package/agents/gsd-planner.md +5 -8
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-verifier.md +14 -70
- package/bin/install.js +503 -341
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +607 -63
- package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
- package/gsd-core/bin/lib/api-coverage.cjs +120 -0
- package/gsd-core/bin/lib/audit.cjs +89 -1
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-registry.cjs +96 -110
- package/gsd-core/bin/lib/capability-validator.cjs +12 -2
- package/gsd-core/bin/lib/check-command-router.cjs +43 -1
- package/gsd-core/bin/lib/command-aliases.cjs +72 -0
- package/gsd-core/bin/lib/commands.cjs +26 -25
- package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +12 -1
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +91 -12
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/frontmatter.cjs +84 -12
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1021 -57
- package/gsd-core/bin/lib/install-engine.cjs +64 -10
- package/gsd-core/bin/lib/install-profiles.cjs +27 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
- package/gsd-core/bin/lib/io.cjs +28 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone.cjs +106 -51
- package/gsd-core/bin/lib/phase-id.cjs +63 -0
- package/gsd-core/bin/lib/phase-locator.cjs +138 -45
- package/gsd-core/bin/lib/phase.cjs +260 -25
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
- package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
- package/gsd-core/bin/lib/roadmap.cjs +37 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
- package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
- package/gsd-core/bin/lib/smart-entry.cjs +12 -0
- package/gsd-core/bin/lib/state-transition.cjs +73 -8
- package/gsd-core/bin/lib/state.cjs +151 -62
- package/gsd-core/bin/lib/surface.cjs +12 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
- package/gsd-core/bin/lib/uat.cjs +320 -21
- package/gsd-core/bin/lib/unusable-input.cjs +9 -0
- package/gsd-core/bin/lib/verification.cjs +29 -12
- package/gsd-core/bin/lib/verify.cjs +29 -5
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
- package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
- package/gsd-core/bin/lib/workstream.cjs +6 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/specless-probe-fallback.md +7 -6
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +32 -69
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
- package/gsd-core/workflows/code-review.md +42 -145
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +23 -81
- package/gsd-core/workflows/debug.md +9 -12
- package/gsd-core/workflows/diagnose-issues.md +22 -0
- package/gsd-core/workflows/discovery-phase.md +4 -4
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +65 -137
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/ingest-docs.md +2 -1
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +21 -38
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +13 -226
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +49 -193
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +11 -153
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +20 -390
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +15 -8
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +17 -14
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +49 -11
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +8 -21
- package/gsd-core/workflows/ui-phase.md +8 -7
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +18 -7
- package/gsd-core/workflows/verify-phase.md +4 -7
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +8 -58
- package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
- package/hooks/dist/gsd-check-update-worker.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
- package/hooks/dist/gsd-read-injection-scanner.js +7 -0
- package/hooks/dist/gsd-statusline.js +72 -6
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/isolation-sentinel.js +268 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +428 -0
- package/hooks/gsd-check-update-worker.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +532 -26
- package/hooks/gsd-read-injection-scanner.js +7 -0
- package/hooks/gsd-statusline.js +72 -6
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/isolation-sentinel.js +268 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +14 -5
- package/pi/gsd.cjs +57 -12
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +9 -2
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-context-index.cjs +448 -0
- package/scripts/gen-inventory-manifest.cjs +101 -1
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-test-file-count.allowlist.json +27 -1
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/prompt-injection-scan.sh +27 -6
- package/scripts/registry-schema.cjs +323 -94
- package/scripts/run-tests.cjs +3 -2
- package/scripts/validate-registry.cjs +10 -6
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +2 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-emitted-baseline.cjs +0 -145
|
@@ -19,7 +19,7 @@ const configLoaderMod = require("./config-loader.cjs");
|
|
|
19
19
|
const { loadConfig } = configLoaderMod;
|
|
20
20
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
21
|
const phaseIdMod = require("./phase-id.cjs");
|
|
22
|
-
const { escapeRegex,
|
|
22
|
+
const { escapeRegex, parsePhaseFromProse, PHASE_NUMBER_TOKEN_SOURCE, phaseKeyFromToken, phaseKeyFromDir } = phaseIdMod;
|
|
23
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
24
|
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
25
25
|
const { getMilestoneInfo, getMilestonePhaseFilter, extractCurrentMilestone } = roadmapParserMod;
|
|
@@ -123,25 +123,38 @@ function _stateHolderVerifiedLive(lockPath) {
|
|
|
123
123
|
return pid !== null && _stateLockIsPidAlive(pid);
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* promptly) from an EMPTY/unparseable one (the create→write window — do not steal while
|
|
129
|
-
* fresh) is what `_stateHolderVerifiedLive` alone cannot express, so the steal decision
|
|
130
|
-
* in acquireStateLock reads the pid directly (PR #1532 review, window a).
|
|
126
|
+
* Read + classify the lock body at `lockPath`. See `LockBodyStatus` for the
|
|
127
|
+
* three-way distinction the steal decision in `acquireStateLock` relies on.
|
|
131
128
|
*/
|
|
132
|
-
function
|
|
129
|
+
function _stateLockBodyStatus(lockPath) {
|
|
133
130
|
let body;
|
|
134
131
|
try {
|
|
135
132
|
body = node_fs_1.default.readFileSync(lockPath, 'utf-8');
|
|
136
133
|
}
|
|
137
134
|
catch {
|
|
138
|
-
return
|
|
135
|
+
return { kind: 'unreadable' };
|
|
139
136
|
}
|
|
140
137
|
const trimmed = body.trim();
|
|
141
138
|
const pid = parseInt(trimmed, 10);
|
|
142
139
|
if (!Number.isInteger(pid) || pid <= 0 || String(pid) !== trimmed)
|
|
143
|
-
return
|
|
144
|
-
return pid;
|
|
140
|
+
return { kind: 'empty' };
|
|
141
|
+
return { kind: 'pid', pid };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Parse the lock body to its recorded pid, or null when the body is empty / non-numeric
|
|
145
|
+
* / unreadable (legacy or mid-creation). Distinguishing a COMPLETE dead-pid body (steal
|
|
146
|
+
* promptly) from an EMPTY/unparseable one (the create→write window — do not steal while
|
|
147
|
+
* fresh) is what `_stateHolderVerifiedLive` alone cannot express, so the steal decision
|
|
148
|
+
* in acquireStateLock reads the pid directly (PR #1532 review, window a).
|
|
149
|
+
*
|
|
150
|
+
* NOTE: this collapses "genuinely empty" and "unreadable" to the same `null` —
|
|
151
|
+
* that is fine for `_stateHolderVerifiedLive` (both mean "not verified-live"
|
|
152
|
+
* either way), but the STEAL-TIMING decision must not make that same
|
|
153
|
+
* collapse (#3057 B2) and reads `_stateLockBodyStatus` directly instead.
|
|
154
|
+
*/
|
|
155
|
+
function _stateLockBodyPid(lockPath) {
|
|
156
|
+
const status = _stateLockBodyStatus(lockPath);
|
|
157
|
+
return status.kind === 'pid' ? status.pid : null;
|
|
145
158
|
}
|
|
146
159
|
// Monotonic sequence for unique stale-steal rename targets (no crypto dependency).
|
|
147
160
|
let _stateStealSeq = 0;
|
|
@@ -160,7 +173,8 @@ const STOP_H2_H3 = (lv) => lv === 2 || lv === 3;
|
|
|
160
173
|
const STOP_H2_ONLY = (lv) => lv === 2;
|
|
161
174
|
function cmdStateLoad(cwd, raw) {
|
|
162
175
|
const config = loadConfig(cwd);
|
|
163
|
-
const
|
|
176
|
+
const paths = planningPaths(cwd);
|
|
177
|
+
const planDir = paths.planning;
|
|
164
178
|
const stateRaw = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planDir, 'STATE.md')) || '';
|
|
165
179
|
const configExists = node_fs_1.default.existsSync(node_path_1.default.join(planDir, 'config.json'));
|
|
166
180
|
const roadmapExists = node_fs_1.default.existsSync(node_path_1.default.join(planDir, 'ROADMAP.md'));
|
|
@@ -173,10 +187,12 @@ function cmdStateLoad(cwd, raw) {
|
|
|
173
187
|
config_exists: configExists,
|
|
174
188
|
// #2376: absolute (anchored on cwd), not orchestrator-cwd-relative — a
|
|
175
189
|
// spawned subagent's own cwd may differ from the orchestrator's.
|
|
176
|
-
// debug.md has
|
|
177
|
-
// `state load
|
|
178
|
-
//
|
|
179
|
-
|
|
190
|
+
// #3149: debug.md now has its own `init.debug` entry point and reads this
|
|
191
|
+
// field from there, not from `state load`. This stays on the state.load
|
|
192
|
+
// bundle regardless: it is a shipped query surface with its own test anchor
|
|
193
|
+
// (tests/state.test.cjs), so narrowing it would break unseen consumers for
|
|
194
|
+
// no gain (Hyrum's Law). Both emit the SAME `planningPaths(cwd).debug`.
|
|
195
|
+
debug_dir: (0, shell_command_projection_cjs_1.toPosixPath)(paths.debug),
|
|
180
196
|
};
|
|
181
197
|
// For --raw, output a condensed key=value format
|
|
182
198
|
if (raw) {
|
|
@@ -276,7 +292,7 @@ function cmdStatePatch(cwd, patches, raw) {
|
|
|
276
292
|
// and the resync-progress decision stay in this adapter.
|
|
277
293
|
let results = { updated: [], failed: [] };
|
|
278
294
|
readModifyWriteStateMd(statePath, (content) => {
|
|
279
|
-
const result = transitionCore(content, { kind: 'patch', patches }, { clock: clock_cjs_1.realClock
|
|
295
|
+
const result = transitionCore(content, { kind: 'patch', patches }, { clock: clock_cjs_1.realClock });
|
|
280
296
|
results = result.data ?? results;
|
|
281
297
|
return result.content;
|
|
282
298
|
}, cwd, { resync: shouldResync });
|
|
@@ -308,7 +324,7 @@ function cmdStateUpdate(cwd, field, value) {
|
|
|
308
324
|
// Preserve curated progress for body-only updates, but allow fields that
|
|
309
325
|
// directly project into progress.* frontmatter to rebuild after mutation.
|
|
310
326
|
readModifyWriteStateMd(statePath, (content) => {
|
|
311
|
-
const result = transitionCore(content, { kind: 'update', field: field, value: value }, { clock: clock_cjs_1.realClock
|
|
327
|
+
const result = transitionCore(content, { kind: 'update', field: field, value: value }, { clock: clock_cjs_1.realClock });
|
|
312
328
|
updated = result.data?.updated === true;
|
|
313
329
|
return result.content;
|
|
314
330
|
}, cwd, { resync: shouldResync });
|
|
@@ -359,7 +375,6 @@ function cmdStateAdvancePlan(cwd, raw) {
|
|
|
359
375
|
const intent = { kind: 'advancePlan' };
|
|
360
376
|
const deps = {
|
|
361
377
|
clock: clock_cjs_1.realClock,
|
|
362
|
-
progressProvider: () => null,
|
|
363
378
|
sourcePath: statePath,
|
|
364
379
|
};
|
|
365
380
|
let resultData;
|
|
@@ -1152,6 +1167,29 @@ function matchSessionSection(body) {
|
|
|
1152
1167
|
?? (0, markdown_sectionizer_cjs_1.collectSection)(body, isSessionContinuity, { levelBounded: true });
|
|
1153
1168
|
return section ? section.body : null;
|
|
1154
1169
|
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Match the "Current Position" section body from a STATE.md body. #2956: this
|
|
1172
|
+
* is the Phase analogue of matchSessionSection. `Phase` canonically lives under
|
|
1173
|
+
* `## Current Position` (gsd-core/templates/state.md), so — like Stopped At /
|
|
1174
|
+
* Paused At under `## Session` — it must be extracted from THAT section, not
|
|
1175
|
+
* from the first `Phase:` / `**Phase:**` line anywhere in the body. Without the
|
|
1176
|
+
* scope, a historical `Phase:` line in an archive section silently overwrites
|
|
1177
|
+
* `current_phase` on every write, and because `current_phase` is routing input
|
|
1178
|
+
* for gsd-progress / --next the rewind routes work to the wrong phase.
|
|
1179
|
+
*
|
|
1180
|
+
* Level-flexible: the canonical template uses an h2 `## Current Position`, the
|
|
1181
|
+
* bootstrap template an h3 `### Current Position` (templates/state.md). Both
|
|
1182
|
+
* must match — mirroring how matchSessionSection recognises `## Session` and
|
|
1183
|
+
* `## Session Continuity`. Exact 'current position' text match (case-insensitive)
|
|
1184
|
+
* excludes unrelated headings. Built on the same `collectSection` seam as
|
|
1185
|
+
* matchSessionSection, so it inherits that seam's CRLF tolerance (#2444 fix).
|
|
1186
|
+
* Returns the section body, or null (caller falls back to full-body search).
|
|
1187
|
+
*/
|
|
1188
|
+
function matchCurrentPositionSection(body) {
|
|
1189
|
+
const isCurrentPosition = (h) => (h.level === 2 || h.level === 3) && h.text.trim().toLowerCase() === 'current position';
|
|
1190
|
+
const section = (0, markdown_sectionizer_cjs_1.collectSection)(body, isCurrentPosition, { levelBounded: true });
|
|
1191
|
+
return section ? section.body : null;
|
|
1192
|
+
}
|
|
1155
1193
|
/**
|
|
1156
1194
|
* #2567: prevent a stale archive "Last activity:" line from overwriting a
|
|
1157
1195
|
* newer frontmatter value. `stateExtractField` matches the first body
|
|
@@ -1183,6 +1221,14 @@ function preferNewerLastActivity(existingFm, derivedFm) {
|
|
|
1183
1221
|
derivedFm['last_activity_desc'] = existingFm['last_activity_desc'];
|
|
1184
1222
|
}
|
|
1185
1223
|
}
|
|
1224
|
+
else if (derDate === exDate) {
|
|
1225
|
+
// #3052: same-date — frontmatter is authoritative for this date, so
|
|
1226
|
+
// preserve its last_activity_desc rather than letting the derived body
|
|
1227
|
+
// prose (which may be stale) overwrite it.
|
|
1228
|
+
if (existingFm['last_activity_desc'] !== undefined) {
|
|
1229
|
+
derivedFm['last_activity_desc'] = existingFm['last_activity_desc'];
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1186
1232
|
}
|
|
1187
1233
|
function parseProsePhaseField(value) {
|
|
1188
1234
|
// #2121 Phase 2 (#2125): delegate to the canonical anchored parser so this
|
|
@@ -1234,7 +1280,14 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
1234
1280
|
return null;
|
|
1235
1281
|
};
|
|
1236
1282
|
// Extract basic fields — frontmatter keys take precedence over body
|
|
1237
|
-
|
|
1283
|
+
// #2956: scope `Phase` extraction to ## Current Position so a historical
|
|
1284
|
+
// Phase: / **Phase:** line in an archive section cannot overwrite the current
|
|
1285
|
+
// value. Phase canonically lives in ## Current Position (templates/state.md),
|
|
1286
|
+
// so it is scopeable exactly like Stopped At under ## Session. Fall back to
|
|
1287
|
+
// full-body search only when no ## Current Position section exists, so files
|
|
1288
|
+
// with no section heading keep their current behaviour.
|
|
1289
|
+
const currentPositionScope = matchCurrentPositionSection(body) ?? body;
|
|
1290
|
+
const prosePhase = parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(currentPositionScope, 'Phase'));
|
|
1238
1291
|
const currentPhase = fmScalar('current_phase') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Current Phase') ?? prosePhase.phase;
|
|
1239
1292
|
const currentPhaseName = fmScalar('current_phase_name') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Current Phase Name') ?? prosePhase.name;
|
|
1240
1293
|
const totalPhasesRaw = fmScalar('total_phases') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Total Phases');
|
|
@@ -1246,7 +1299,12 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
1246
1299
|
const proseLastActivity = parseProseLastActivityField(rawLastActivity);
|
|
1247
1300
|
const lastActivity = fmScalar('last_activity') ?? proseLastActivity.date ?? rawLastActivity;
|
|
1248
1301
|
const lastActivityDesc = fmScalar('last_activity_desc') ?? (0, state_document_cjs_1.stateExtractField)(body, 'Last Activity Description') ?? proseLastActivity.description;
|
|
1249
|
-
|
|
1302
|
+
// #2956: Paused At canonically lives in ## Session (see the comment above
|
|
1303
|
+
// preferNewerLastActivity and the write seam in buildStateFrontmatter). The
|
|
1304
|
+
// write seam already scopes it to ## Session; this read seam must agree, so a
|
|
1305
|
+
// stale "Paused At:" in a Session Continuity Archive cannot win here either.
|
|
1306
|
+
const sessionScope = matchSessionSection(body) ?? body;
|
|
1307
|
+
const pausedAt = fmScalar('paused_at') ?? (0, state_document_cjs_1.stateExtractField)(sessionScope, 'Paused At');
|
|
1250
1308
|
// Parse numeric fields
|
|
1251
1309
|
const totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
|
|
1252
1310
|
const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
|
|
@@ -1324,25 +1382,11 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
1324
1382
|
output(result, raw, undefined);
|
|
1325
1383
|
}
|
|
1326
1384
|
// ─── State Frontmatter Sync ──────────────────────────────────────────────────
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
* each map to one key. For a directory, extract its phase token first.
|
|
1333
|
-
*
|
|
1334
|
-
* Stripping the project-code prefix is GSD's canonical phase identity (a
|
|
1335
|
-
* project_code is a display prefix; normalizePhaseName / phaseTokenMatches treat
|
|
1336
|
-
* `CK-01` and `01` as the same phase, which is what lets a prefixed dir match a
|
|
1337
|
-
* bare ROADMAP token). A consistent project uses one scheme, so a bare numeric
|
|
1338
|
-
* and a same-suffix project-code phase never coexist in one milestone.
|
|
1339
|
-
*/
|
|
1340
|
-
function phaseKeyFromToken(token) {
|
|
1341
|
-
return normalizePhaseName(token).toUpperCase();
|
|
1342
|
-
}
|
|
1343
|
-
function phaseKeyFromDir(dir) {
|
|
1344
|
-
return phaseKeyFromToken(extractPhaseToken(dir));
|
|
1345
|
-
}
|
|
1385
|
+
// `phaseKeyFromToken` / `phaseKeyFromDir` — the canonical key for matching a
|
|
1386
|
+
// ROADMAP phase token against an on-disk phase directory — moved to the
|
|
1387
|
+
// phase-id owner module in #2562 so every consumer derives BOTH sides of a
|
|
1388
|
+
// phase comparison from the same function (see phase-id.cts). Imported at the
|
|
1389
|
+
// top of this file; call sites below are unchanged.
|
|
1346
1390
|
/**
|
|
1347
1391
|
* Extract the set of retired/folded phase keys from a ROADMAP milestone scope
|
|
1348
1392
|
* (#1514). A retired phase is struck through with GFM strikethrough,
|
|
@@ -1386,8 +1430,15 @@ function extractRetiredPhaseNumbers(scope) {
|
|
|
1386
1430
|
* a YAML frontmatter object. Allows hooks and scripts to read state
|
|
1387
1431
|
* reliably via `state json` instead of fragile regex parsing.
|
|
1388
1432
|
*/
|
|
1389
|
-
function buildStateFrontmatter(bodyContent, cwd) {
|
|
1390
|
-
|
|
1433
|
+
function buildStateFrontmatter(bodyContent, cwd, storedMilestone) {
|
|
1434
|
+
// #2956: scope `Phase` extraction to ## Current Position (mirrors the read
|
|
1435
|
+
// path in cmdStateSnapshot and the Stopped At / Paused At ## Session scoping
|
|
1436
|
+
// below). Phase canonically lives in ## Current Position (templates/state.md);
|
|
1437
|
+
// without the scope, a historical Phase: / **Phase:** line in an archive
|
|
1438
|
+
// section overwrites current_phase here, and the next read surfaces it. Fall
|
|
1439
|
+
// back to full-body search when no ## Current Position section exists.
|
|
1440
|
+
const currentPositionScope = matchCurrentPositionSection(bodyContent) ?? bodyContent;
|
|
1441
|
+
const prosePhase = parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(currentPositionScope, 'Phase'));
|
|
1391
1442
|
const currentPhase = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Current Phase') ?? prosePhase.phase;
|
|
1392
1443
|
const currentPhaseName = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Current Phase Name') ?? prosePhase.name;
|
|
1393
1444
|
const currentPlan = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Current Plan');
|
|
@@ -1453,7 +1504,10 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1453
1504
|
}
|
|
1454
1505
|
}
|
|
1455
1506
|
catch { /* fall through: no roadmap scope → no retired exclusion */ }
|
|
1456
|
-
|
|
1507
|
+
// #3017: scope the milestone filter to the STORED milestone when available,
|
|
1508
|
+
// so a state.* write doesn't auto-derive (and mis-bind) to a different
|
|
1509
|
+
// milestone's heading and clobber the stored value + progress counts.
|
|
1510
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd, storedMilestone ?? undefined);
|
|
1457
1511
|
const allMatchingDirs = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true })
|
|
1458
1512
|
.filter(e => e.isDirectory()).map(e => e.name)
|
|
1459
1513
|
.filter(isDirInMilestone);
|
|
@@ -1633,7 +1687,11 @@ function syncStateFrontmatter(content, cwd, authoritativeFm) {
|
|
|
1633
1687
|
// derivable here without widening the signature (#1882).
|
|
1634
1688
|
const existingFm = extractFrontmatter(content, cwd ? planningPaths(cwd).state : undefined);
|
|
1635
1689
|
const body = stripFrontmatter(content);
|
|
1636
|
-
|
|
1690
|
+
// #3017: pass the stored milestone from the existing frontmatter so
|
|
1691
|
+
// buildStateFrontmatter scopes its disk scan to the correct milestone
|
|
1692
|
+
// instead of auto-deriving (and potentially mis-binding).
|
|
1693
|
+
const storedMilestone = typeof existingFm['milestone'] === 'string' ? existingFm['milestone'] : null;
|
|
1694
|
+
const derivedFm = buildStateFrontmatter(body, cwd, storedMilestone);
|
|
1637
1695
|
// Preserve existing frontmatter status when body-derived status is 'unknown'.
|
|
1638
1696
|
// This prevents a missing Status: field in the body from overwriting a
|
|
1639
1697
|
// previously valid status (e.g., 'executing' → 'unknown').
|
|
@@ -1849,17 +1907,22 @@ function acquireStateLock(statePath, clock) {
|
|
|
1849
1907
|
if (err.code !== 'EEXIST')
|
|
1850
1908
|
throw err; // propagate — silent bypass causes lost updates
|
|
1851
1909
|
// Liveness-gated steal (audit M1) + steal-safety (PR #1532 review). The steal
|
|
1852
|
-
// decision is
|
|
1910
|
+
// decision is four-way on the lock body (#3057 B2 added the fourth):
|
|
1853
1911
|
// - VERIFIED-LIVE holder (parseable pid that signals alive): NEVER stolen until
|
|
1854
1912
|
// its age crosses the absolute deadman ceiling (the pid-reuse backstop) —
|
|
1855
1913
|
// nuking a slow-but-live writer's lock causes lost updates (#3711 / #500/#905/
|
|
1856
1914
|
// #1230 family).
|
|
1857
1915
|
// - COMPLETE DEAD pid (parseable pid, not alive): stolen PROMPTLY regardless of
|
|
1858
1916
|
// age — a crashed holder left a full body.
|
|
1859
|
-
// -
|
|
1860
|
-
//
|
|
1861
|
-
//
|
|
1862
|
-
//
|
|
1917
|
+
// - UNREADABLE body (I/O fault reading the file): NOT the same as empty — we
|
|
1918
|
+
// have no evidence this is a fresh create window, only that we could not read
|
|
1919
|
+
// it. Held to the SAME conservative ceiling as a verified-live holder rather
|
|
1920
|
+
// than the short fresh-create floor, so a transient read fault can never rob
|
|
1921
|
+
// an active holder the way stealing at 1s would.
|
|
1922
|
+
// - EMPTY / unparseable body (body WAS read, and holds no valid pid): liveness is
|
|
1923
|
+
// unknowable. While FRESH (age <= freshCreateFloorMs) it is a lock still
|
|
1924
|
+
// mid-creation (O_EXCL done, pid not yet written) and is NOT stolen (window a);
|
|
1925
|
+
// only once aged past the floor is it a genuine orphan and stealable.
|
|
1863
1926
|
// The steal itself is an ATOMIC rename-then-recreate (only one racer can rename the
|
|
1864
1927
|
// inode) guarded by an identity re-confirm, so a racer that recreates a fresh lock
|
|
1865
1928
|
// in the decision→steal gap never has its replacement deleted (window b). Mirrors
|
|
@@ -1867,7 +1930,8 @@ function acquireStateLock(statePath, clock) {
|
|
|
1867
1930
|
try {
|
|
1868
1931
|
const stat = node_fs_1.default.statSync(lockPath);
|
|
1869
1932
|
const ageMs = clock.now() - stat.mtimeMs;
|
|
1870
|
-
const
|
|
1933
|
+
const bodyStatus = _stateLockBodyStatus(lockPath);
|
|
1934
|
+
const bodyPid = bodyStatus.kind === 'pid' ? bodyStatus.pid : null;
|
|
1871
1935
|
const holderLive = bodyPid !== null && _stateLockIsPidAlive(bodyPid);
|
|
1872
1936
|
let steal;
|
|
1873
1937
|
if (holderLive) {
|
|
@@ -1876,6 +1940,9 @@ function acquireStateLock(statePath, clock) {
|
|
|
1876
1940
|
else if (bodyPid !== null) {
|
|
1877
1941
|
steal = true; // complete dead pid → prompt steal
|
|
1878
1942
|
}
|
|
1943
|
+
else if (bodyStatus.kind === 'unreadable') {
|
|
1944
|
+
steal = ageMs > deadmanCeilingMs; // I/O fault ≠ known-fresh — do not grant the short floor
|
|
1945
|
+
}
|
|
1879
1946
|
else {
|
|
1880
1947
|
steal = ageMs > freshCreateFloorMs; // empty/garbage → protect the create window
|
|
1881
1948
|
}
|
|
@@ -2064,7 +2131,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
2064
2131
|
// content already returns the mutated string, and callers that detect a
|
|
2065
2132
|
// no-op explicitly return the original content unchanged.
|
|
2066
2133
|
if (modified === content) {
|
|
2067
|
-
return;
|
|
2134
|
+
return false;
|
|
2068
2135
|
}
|
|
2069
2136
|
let synced = syncStateFrontmatter(modified, cwd, options?.authoritativeFm);
|
|
2070
2137
|
// Post-transform body source fields used for the delta comparison (#1230).
|
|
@@ -2115,6 +2182,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
2115
2182
|
synced = `---\n${yamlStr}\n---\n\n${body}`;
|
|
2116
2183
|
}
|
|
2117
2184
|
(0, shell_command_projection_cjs_1.platformWriteSync)(statePath, synced);
|
|
2185
|
+
return true;
|
|
2118
2186
|
}
|
|
2119
2187
|
finally {
|
|
2120
2188
|
releaseStateLock(lockPath);
|
|
@@ -2193,7 +2261,6 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
2193
2261
|
};
|
|
2194
2262
|
const deps = {
|
|
2195
2263
|
clock: clock_cjs_1.realClock,
|
|
2196
|
-
progressProvider: () => null, // beginPhase doesn't consult disk progress; syncStateFrontmatter's scan is authoritative
|
|
2197
2264
|
sourcePath: statePath,
|
|
2198
2265
|
};
|
|
2199
2266
|
// #2736: the transition holds the exact display name; without this the
|
|
@@ -2462,7 +2529,6 @@ function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
|
|
2462
2529
|
};
|
|
2463
2530
|
const deps = {
|
|
2464
2531
|
clock: clock_cjs_1.realClock,
|
|
2465
|
-
progressProvider: () => null,
|
|
2466
2532
|
sourcePath: statePath,
|
|
2467
2533
|
};
|
|
2468
2534
|
let updated = [];
|
|
@@ -2496,7 +2562,7 @@ function cmdStateMilestoneSwitch(cwd, version, name, raw) {
|
|
|
2496
2562
|
// milestoneSwitch rebuilds frontmatter directly and must not run the
|
|
2497
2563
|
// steady-state syncStateFrontmatter post-sync.
|
|
2498
2564
|
const intent = { kind: 'milestoneSwitch', version, name: resolvedName };
|
|
2499
|
-
const deps = { clock: clock_cjs_1.realClock,
|
|
2565
|
+
const deps = { clock: clock_cjs_1.realClock, sourcePath: statePath };
|
|
2500
2566
|
const lockPath = acquireStateLock(statePath);
|
|
2501
2567
|
try {
|
|
2502
2568
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(statePath) || '';
|
|
@@ -2719,7 +2785,7 @@ function cmdStateSync(cwd, options, raw) {
|
|
|
2719
2785
|
const p = (0, state_document_cjs_1.computeProgressPercent)(totalDiskSummaries, totalDiskPlans, diskCompletedPhases, syncTotalPhases);
|
|
2720
2786
|
percent = p !== null ? p : 0;
|
|
2721
2787
|
}
|
|
2722
|
-
const syncResult = transitionCore(modified, { kind: 'sync', totalPlansInPhase: highestIncompletePhase ? highestIncompletePhaseplanCount : null, percent }, { clock: clock_cjs_1.realClock
|
|
2788
|
+
const syncResult = transitionCore(modified, { kind: 'sync', totalPlansInPhase: highestIncompletePhase ? highestIncompletePhaseplanCount : null, percent }, { clock: clock_cjs_1.realClock });
|
|
2723
2789
|
modified = syncResult.content;
|
|
2724
2790
|
const coreChanges = syncResult.data?.changes ?? [];
|
|
2725
2791
|
changes.push(...coreChanges);
|
|
@@ -2787,7 +2853,7 @@ function cmdStatePrune(cwd, options, raw) {
|
|
|
2787
2853
|
// This adapter owns currentPhase derivation (#1760 `Phase`/`Current Phase`
|
|
2788
2854
|
// fallback above), dry-run, and STATE-ARCHIVE.md writes.
|
|
2789
2855
|
const runPruneCore = (content) => {
|
|
2790
|
-
const result = transitionCore(content, { kind: 'prune', cutoff }, { clock: clock_cjs_1.realClock
|
|
2856
|
+
const result = transitionCore(content, { kind: 'prune', cutoff }, { clock: clock_cjs_1.realClock });
|
|
2791
2857
|
return {
|
|
2792
2858
|
newContent: result.content,
|
|
2793
2859
|
archivedSections: (result.data?.archivedSections) ?? [],
|
|
@@ -2866,11 +2932,20 @@ function cmdStateRebuild(cwd, options, raw) {
|
|
|
2866
2932
|
// is the same canonical source `buildStateFrontmatter` consults; the Leaky-
|
|
2867
2933
|
// Abstractions guard in `rebuildCore` (ADR-1817 §1) keeps the pure core
|
|
2868
2934
|
// testable without this dep — here we provide it.
|
|
2935
|
+
//
|
|
2936
|
+
// #3057 B1: a missing `.planning/phases/` directory is genuinely "nothing
|
|
2937
|
+
// to reconcile" (`ok:true, phases: []`) — but a `readdirSync`/`statSync`
|
|
2938
|
+
// THROW on a directory that DOES exist (permission fault, corrupted
|
|
2939
|
+
// mount, etc.) is a real scan failure (`ok:false`). The old implementation
|
|
2940
|
+
// returned `null` for both, so `state rebuild` could report success while
|
|
2941
|
+
// by-phase-table reconciliation silently never ran. Per-entry stat
|
|
2942
|
+
// failures (an individual phase dir vanishing mid-scan) still `continue`
|
|
2943
|
+
// past that one entry — that is not a whole-scan failure.
|
|
2869
2944
|
const phaseInventoryProvider = () => {
|
|
2870
2945
|
try {
|
|
2871
2946
|
const phasesDir = node_path_1.default.join(planningPaths(cwd).planning, 'phases');
|
|
2872
2947
|
if (!node_fs_1.default.existsSync(phasesDir) || !node_fs_1.default.statSync(phasesDir).isDirectory())
|
|
2873
|
-
return
|
|
2948
|
+
return { ok: true, phases: [] };
|
|
2874
2949
|
const entries = node_fs_1.default.readdirSync(phasesDir);
|
|
2875
2950
|
const records = [];
|
|
2876
2951
|
for (const entry of entries) {
|
|
@@ -2893,14 +2968,13 @@ function cmdStateRebuild(cwd, options, raw) {
|
|
|
2893
2968
|
const summaryCount = files.filter(f => /-SUMMARY\.md$/i.test(f)).length;
|
|
2894
2969
|
records.push({ number: m[1], name: m[2], planCount, summaryCount });
|
|
2895
2970
|
}
|
|
2896
|
-
return records;
|
|
2971
|
+
return { ok: true, phases: records };
|
|
2897
2972
|
}
|
|
2898
|
-
catch {
|
|
2899
|
-
return
|
|
2973
|
+
catch (err) {
|
|
2974
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
2900
2975
|
}
|
|
2901
2976
|
};
|
|
2902
2977
|
const deps = {
|
|
2903
|
-
progressProvider: () => null,
|
|
2904
2978
|
clock: clock_cjs_1.realClock,
|
|
2905
2979
|
phaseInventoryProvider,
|
|
2906
2980
|
// Without this, `state rebuild --dry-run` reported a truncated STATE.md anonymously: the
|
|
@@ -2919,18 +2993,25 @@ function cmdStateRebuild(cwd, options, raw) {
|
|
|
2919
2993
|
process.stderr.write(`[rebuild] ${JSON.stringify(entry)}\n`);
|
|
2920
2994
|
}
|
|
2921
2995
|
};
|
|
2996
|
+
const scanFailureNote = (reason) => 'Nothing rebuilt: the phase-inventory disk scan failed, so by-phase-table reconciliation did not run' +
|
|
2997
|
+
(reason ? ` (${reason})` : '');
|
|
2922
2998
|
if (dryRun) {
|
|
2923
2999
|
const content = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
2924
3000
|
const result = runRebuild(content);
|
|
2925
3001
|
const data = (result.data ?? {});
|
|
2926
3002
|
emitVerboseLog(data.log);
|
|
2927
3003
|
const mutated = data.mutated === true;
|
|
3004
|
+
const scanFailed = data.phase_inventory_scan_failed === true;
|
|
2928
3005
|
emit({
|
|
2929
3006
|
rebuilt: false,
|
|
2930
3007
|
dry_run: true,
|
|
2931
3008
|
mutations: Array.isArray(data.log) ? data.log.length : 0,
|
|
2932
3009
|
mutated,
|
|
2933
|
-
|
|
3010
|
+
phase_inventory_scan_failed: scanFailed,
|
|
3011
|
+
phase_inventory_scan_reason: scanFailed ? data.phase_inventory_scan_reason : undefined,
|
|
3012
|
+
note: mutated
|
|
3013
|
+
? 'Run without --dry-run to apply changes'
|
|
3014
|
+
: scanFailed ? scanFailureNote(data.phase_inventory_scan_reason) : 'Nothing to rebuild',
|
|
2934
3015
|
}, raw, mutated ? 'true' : 'false');
|
|
2935
3016
|
return;
|
|
2936
3017
|
}
|
|
@@ -2939,18 +3020,26 @@ function cmdStateRebuild(cwd, options, raw) {
|
|
|
2939
3020
|
// to STATE.md by rebuildCore itself, per ADR-1817 §3).
|
|
2940
3021
|
let capturedLog = [];
|
|
2941
3022
|
let capturedMutated = false;
|
|
3023
|
+
let capturedScanFailed = false;
|
|
3024
|
+
let capturedScanReason;
|
|
2942
3025
|
readModifyWriteStateMd(statePath, (content) => {
|
|
2943
3026
|
const result = runRebuild(content);
|
|
2944
3027
|
const data = (result.data ?? {});
|
|
2945
3028
|
capturedLog = Array.isArray(data.log) ? data.log : [];
|
|
2946
3029
|
capturedMutated = data.mutated === true;
|
|
3030
|
+
capturedScanFailed = data.phase_inventory_scan_failed === true;
|
|
3031
|
+
capturedScanReason = data.phase_inventory_scan_reason;
|
|
2947
3032
|
return result.content;
|
|
2948
3033
|
}, cwd);
|
|
2949
3034
|
emitVerboseLog(capturedLog);
|
|
2950
3035
|
emit({
|
|
2951
3036
|
rebuilt: capturedMutated,
|
|
2952
3037
|
mutations: capturedLog.length,
|
|
2953
|
-
|
|
3038
|
+
phase_inventory_scan_failed: capturedScanFailed,
|
|
3039
|
+
phase_inventory_scan_reason: capturedScanFailed ? capturedScanReason : undefined,
|
|
3040
|
+
note: capturedMutated
|
|
3041
|
+
? 'STATE.md rebuilt; see ## Rebuild Log section for the audit trail'
|
|
3042
|
+
: capturedScanFailed ? scanFailureNote(capturedScanReason) : 'Nothing to rebuild',
|
|
2954
3043
|
}, raw, capturedMutated ? 'true' : 'false');
|
|
2955
3044
|
}
|
|
2956
3045
|
/**
|
|
@@ -50,6 +50,8 @@ const { findInstallSourceRoot } = runtimeArtifactLayout;
|
|
|
50
50
|
const runtimeArtifactConversion = require("./runtime-artifact-conversion.cjs");
|
|
51
51
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
52
52
|
const runtimeArtifactInstallPlan = require("./runtime-artifact-install-plan.cjs");
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
54
|
+
const retiredArtifactCleanup = require("./retired-artifact-cleanup.cjs");
|
|
53
55
|
const { assertDestWithinConfigHome } = runtimeArtifactInstallPlan;
|
|
54
56
|
const SURFACE_FILE_NAME = '.gsd-surface.json';
|
|
55
57
|
/**
|
|
@@ -302,6 +304,9 @@ function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry,
|
|
|
302
304
|
}
|
|
303
305
|
const skillManifest = normalizeSkillManifest(layout.configDir, manifest);
|
|
304
306
|
const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap, registry);
|
|
307
|
+
// Profile toggles must converge retired surfaces too. Once a kind disappears
|
|
308
|
+
// from artifactLayout there is no normal sync pass left to prune it (#2644).
|
|
309
|
+
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(layout.runtime, layout.configDir);
|
|
305
310
|
// #1575: agents kind now mirrors createRuntimeArtifactInstallPlan — build
|
|
306
311
|
// agentCtx (pathPrefix + attribution) and pass it to kind.stage() so
|
|
307
312
|
// stageAgentsForRuntimeWithConverter applies the full inline-loop pipeline
|
|
@@ -360,7 +365,13 @@ function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry,
|
|
|
360
365
|
tempDirsToClean.push(rewritten);
|
|
361
366
|
}
|
|
362
367
|
}
|
|
363
|
-
|
|
368
|
+
// #2911: honor kind.home as a FALLBACK-preferred override (e.g. Codex
|
|
369
|
+
// skills -> $HOME/.agents), never a blanket replacement — kinds without
|
|
370
|
+
// a `home` must keep resolving against layout.configDir. This must stay
|
|
371
|
+
// in lockstep with _copyStaged's root selection in src/install-engine.cts;
|
|
372
|
+
// the parity test in tests/runtime-artifact-layout-surface.test.cjs
|
|
373
|
+
// enforces that the two writers never diverge again.
|
|
374
|
+
const dest = assertDestWithinConfigHome(kind.home ?? layout.configDir, kind.destSubpath);
|
|
364
375
|
_syncGsdDir(staged, dest, kind, skillManifest, layout.runtime);
|
|
365
376
|
}
|
|
366
377
|
}
|
|
@@ -186,6 +186,8 @@ function evaluateUatPassed(phaseFullDir, opts) {
|
|
|
186
186
|
blockers,
|
|
187
187
|
no_uat_artifacts,
|
|
188
188
|
policy: { require_verification: requireVerification },
|
|
189
|
+
// readVerificationStatus was never reached on this early-return path.
|
|
190
|
+
verification_stale_check_indeterminate: false,
|
|
189
191
|
};
|
|
190
192
|
}
|
|
191
193
|
// Filter UAT and VERIFICATION files using the same filter as cmdPhaseComplete
|
|
@@ -263,8 +265,15 @@ function evaluateUatPassed(phaseFullDir, opts) {
|
|
|
263
265
|
// (handled by the requireVerification policy check below if needed)
|
|
264
266
|
}
|
|
265
267
|
// ── Policy: requireVerification ───────────────────────────────────────────
|
|
268
|
+
// #3057 B3: routing here is UNCHANGED — an indeterminate staleness check
|
|
269
|
+
// still falls through to the same `verificationStatus !== 'passed'` branch
|
|
270
|
+
// it always did (the pre-existing fail-open contract). `verificationStaleCheckIndeterminate`
|
|
271
|
+
// only records the fact for the report below; it never itself gates `blockers`.
|
|
272
|
+
let verificationStaleCheckIndeterminate = false;
|
|
266
273
|
if (requireVerification) {
|
|
267
|
-
const
|
|
274
|
+
const verificationResult = readVerificationStatus(phaseFullDir);
|
|
275
|
+
const verificationStatus = verificationResult.status;
|
|
276
|
+
verificationStaleCheckIndeterminate = verificationResult.staleCheckIndeterminate === true;
|
|
268
277
|
if (verificationStatus === 'stale') {
|
|
269
278
|
blockers.push('policy: verification status=stale');
|
|
270
279
|
}
|
|
@@ -288,6 +297,7 @@ function evaluateUatPassed(phaseFullDir, opts) {
|
|
|
288
297
|
policy: {
|
|
289
298
|
require_verification: requireVerification,
|
|
290
299
|
},
|
|
300
|
+
verification_stale_check_indeterminate: verificationStaleCheckIndeterminate,
|
|
291
301
|
};
|
|
292
302
|
}
|
|
293
303
|
module.exports = {
|