@opengsd/gsd-core 1.12.0 → 1.13.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 +1 -1
- package/.opencode/plugins/gsd-core.js +12 -0
- package/agents/gsd-executor.md +63 -35
- package/agents/gsd-plan-checker.md +76 -57
- package/agents/gsd-planner.md +14 -0
- package/agents/gsd-ui-checker.md +19 -3
- package/agents/gsd-ui-researcher.md +29 -0
- package/agents/gsd-verifier.md +23 -1
- package/bin/install.js +239 -67
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +195 -50
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-registry.cjs +514 -114
- package/gsd-core/bin/lib/capability-state.cjs +7 -1
- package/gsd-core/bin/lib/capability-validator.cjs +120 -4
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +85 -2
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/commands.cjs +337 -13
- package/gsd-core/bin/lib/config-loader.cjs +3 -0
- package/gsd-core/bin/lib/core-utils.cjs +34 -7
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +14 -1
- package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +137 -23
- package/gsd-core/bin/lib/gap-checker.cjs +22 -13
- package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- package/gsd-core/bin/lib/host-integration.cjs +57 -5
- package/gsd-core/bin/lib/init-command-router.cjs +14 -0
- package/gsd-core/bin/lib/init.cjs +132 -15
- package/gsd-core/bin/lib/install-engine.cjs +184 -12
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
- package/gsd-core/bin/lib/install-profiles.cjs +22 -14
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/io.cjs +35 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +123 -0
- package/gsd-core/bin/lib/milestone.cjs +22 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-id.cjs +251 -9
- package/gsd-core/bin/lib/phase.cjs +774 -35
- package/gsd-core/bin/lib/plan-document.cjs +10 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +147 -20
- package/gsd-core/bin/lib/planning-workspace.cjs +103 -28
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-parser.cjs +499 -26
- package/gsd-core/bin/lib/roadmap.cjs +187 -58
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +233 -33
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +286 -108
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -43
- package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
- package/gsd-core/bin/lib/smart-entry.cjs +7 -9
- package/gsd-core/bin/lib/state-document.cjs +30 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +23 -13
- package/gsd-core/bin/lib/state-transition.cjs +333 -44
- package/gsd-core/bin/lib/state.cjs +684 -125
- package/gsd-core/bin/lib/surface.cjs +23 -8
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/uat.cjs +1419 -515
- package/gsd-core/bin/lib/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +273 -12
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +346 -16
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +8 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +3 -3
- package/gsd-core/references/edge-probe.md +17 -13
- package/gsd-core/references/execute-mvp-tdd.md +18 -16
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/mvp-concepts.md +2 -2
- package/gsd-core/references/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- package/gsd-core/references/planner-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +75 -2
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/tdd.md +14 -9
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/templates/phase-prompt.md +4 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/add-backlog.md +2 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +2 -0
- package/gsd-core/workflows/audit-uat.md +2 -0
- package/gsd-core/workflows/autonomous.md +2 -0
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +15 -13
- package/gsd-core/workflows/code-review-fix.md +2 -0
- package/gsd-core/workflows/code-review.md +73 -31
- package/gsd-core/workflows/complete-milestone.md +13 -4
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +43 -13
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +2 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +17 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +32 -14
- package/gsd-core/workflows/execute-plan.md +8 -8
- package/gsd-core/workflows/explore.md +2 -0
- package/gsd-core/workflows/extract-learnings.md +2 -0
- package/gsd-core/workflows/fast.md +6 -0
- package/gsd-core/workflows/forensics.md +2 -0
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +3 -3
- package/gsd-core/workflows/inbox.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +2 -0
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +2 -0
- package/gsd-core/workflows/list-workspaces.md +2 -0
- package/gsd-core/workflows/manager.md +3 -3
- package/gsd-core/workflows/map-codebase.md +2 -0
- package/gsd-core/workflows/milestone-summary.md +2 -0
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +1 -1
- package/gsd-core/workflows/new-project.md +5 -3
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -0
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +19 -4
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
- package/gsd-core/workflows/plan-phase.md +130 -12
- package/gsd-core/workflows/plan-review-convergence.md +102 -10
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/pr-branch.md +11 -3
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +25 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
- package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +13 -3
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +2 -0
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +6 -2
- package/gsd-core/workflows/review.md +215 -10
- package/gsd-core/workflows/scan.md +2 -0
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/settings-integrations.md +9 -8
- package/gsd-core/workflows/settings.md +1 -1
- package/gsd-core/workflows/ship.md +10 -10
- package/gsd-core/workflows/sketch-wrap-up.md +2 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/spec-phase.md +24 -19
- package/gsd-core/workflows/spike-wrap-up.md +2 -0
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +2 -0
- package/gsd-core/workflows/sync-skills.md +12 -4
- package/gsd-core/workflows/thread.md +2 -0
- package/gsd-core/workflows/transition.md +2 -0
- package/gsd-core/workflows/ui-phase.md +26 -5
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +2 -0
- package/gsd-core/workflows/undo.md +1 -1
- package/gsd-core/workflows/update.md +41 -38
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +49 -3
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-context-monitor.js +283 -12
- package/hooks/dist/gsd-node-runner.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +30 -5
- package/hooks/dist/gsd-read-guard.js +2 -0
- package/hooks/dist/gsd-read-injection-scanner.js +5 -5
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-statusline.js +7 -3
- package/hooks/dist/gsd-validate-commit.sh +444 -7
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/git-cmd.js +210 -1
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-context-monitor.js +283 -12
- package/hooks/gsd-node-runner.sh +1 -0
- package/hooks/gsd-prompt-guard.js +30 -5
- package/hooks/gsd-read-guard.js +2 -0
- package/hooks/gsd-read-injection-scanner.js +5 -5
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-statusline.js +7 -3
- package/hooks/gsd-validate-commit.sh +444 -7
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/hooks.json +6 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +5 -5
- package/scripts/build-hooks.js +11 -4
- package/scripts/ci-test-scope.cjs +7 -0
- package/scripts/docs-guard-registry.cjs +10 -0
- package/scripts/gen-loop-host-contract.cjs +67 -15
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +5 -0
- package/scripts/lint-phase-enumeration-drift.cjs +24 -6
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-response-language-coverage.cjs +524 -0
- package/scripts/lint-test-file-count.allowlist.json +3 -1
- package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +8 -0
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/vscode/package.json +1 -1
|
@@ -1311,6 +1311,128 @@ function reconcileCursorHooksJson(hooksJsonPath, managedEntries) {
|
|
|
1311
1311
|
}
|
|
1312
1312
|
return { changed: changed, wrote: shouldWrite, path: hooksJsonPath };
|
|
1313
1313
|
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Stage the `hooks/lib/` helpers a set of staged hook scripts require, walking
|
|
1316
|
+
* the require graph TRANSITIVELY to a fixed point.
|
|
1317
|
+
*
|
|
1318
|
+
* Extracted from writeCursorHooksJson (#3911 review, commit 704859e9c) so the
|
|
1319
|
+
* reduced Codex hook bundle can share one implementation instead of growing a
|
|
1320
|
+
* second, divergent copy (#4087 / #4098). Both reduced bundles hand-pick which
|
|
1321
|
+
* hook SCRIPTS they ship, and neither can hand-pick their helpers correctly for
|
|
1322
|
+
* long: `hooks/lib/hook-exit.js` requires `./cli-exit.js`, which requires
|
|
1323
|
+
* `./exit-code-registry.js` — with NO `./lib/` prefix, because from inside
|
|
1324
|
+
* `lib/` the sibling is already local. A single-pass scan for the `./lib/…`
|
|
1325
|
+
* spelling used FROM a hook script stages hook-exit.js and stops, and the
|
|
1326
|
+
* installed hook then dies on MODULE_NOT_FOUND at load, before its own
|
|
1327
|
+
* try/catch, on every event it is registered for.
|
|
1328
|
+
*
|
|
1329
|
+
* Scans CONTENT rather than paths so a caller can seed from whatever it staged,
|
|
1330
|
+
* transformed or not, without this helper knowing the caller's layout.
|
|
1331
|
+
*
|
|
1332
|
+
* @returns the lib filenames actually staged, in staging order.
|
|
1333
|
+
*/
|
|
1334
|
+
function stageTransitiveHookLibs(opts) {
|
|
1335
|
+
const { seedSources, srcLibDir, destLibDir, runtimeLabel, transform } = opts;
|
|
1336
|
+
const requiredLibFiles = new Set();
|
|
1337
|
+
const scannedLibFiles = new Set();
|
|
1338
|
+
const staged = [];
|
|
1339
|
+
// `./X` means DIFFERENT things depending on where the scanned file lives, and
|
|
1340
|
+
// conflating them stages the wrong file. From a hook SCRIPT in hooks/, a bare
|
|
1341
|
+
// `./X` is a sibling hook-level artifact — Codex's gsd-check-update-worker.js
|
|
1342
|
+
// requires `./managed-hooks-registry.cjs`, which lives in hooks/, not
|
|
1343
|
+
// hooks/lib/ — so only the explicit `./lib/X` spelling is a lib requirement.
|
|
1344
|
+
// From inside a LIB file, the sibling is already local, so `./X` IS a lib
|
|
1345
|
+
// requirement (hook-exit.js -> ./cli-exit.js -> ./exit-code-registry.js); that
|
|
1346
|
+
// is the case 704859e9c added and it must keep working. Cursor never exposed
|
|
1347
|
+
// the difference because none of its staged scripts has a bare sibling
|
|
1348
|
+
// require; Codex's does, and the fail-loud guard below caught it immediately
|
|
1349
|
+
// by demanding managed-hooks-registry.cjs out of hooks/dist/lib.
|
|
1350
|
+
// Fresh per call: a module-level /g regex carries lastIndex across calls and
|
|
1351
|
+
// would silently skip matches on the second install in one process.
|
|
1352
|
+
const seedRequireRe = /require\(\s*['"]\.\/lib\/([A-Za-z0-9._-]+)['"]\s*\)/g;
|
|
1353
|
+
const libRequireRe = /require\(\s*['"]\.\/(?:lib\/)?([A-Za-z0-9._-]+)['"]\s*\)/g;
|
|
1354
|
+
// A NESTED helper path is outside the flat layout hooks/lib/ has and the
|
|
1355
|
+
// build emits, and the character classes above cannot express it — so it
|
|
1356
|
+
// would be a SILENT miss, staging nothing and shipping a hook that dies at
|
|
1357
|
+
// load. Detected separately and refused loudly instead: a silent miss is the
|
|
1358
|
+
// failure mode this whole function exists to remove (review of #4087).
|
|
1359
|
+
const nestedRequireRe = /require\(\s*['"]\.\/lib\/[A-Za-z0-9._-]+\/[^'"]*['"]\s*\)/;
|
|
1360
|
+
const scanForLibRequires = (source, fromLib) => {
|
|
1361
|
+
if (nestedRequireRe.test(source)) {
|
|
1362
|
+
throw new Error(`A staged ${runtimeLabel} hook requires a NESTED hooks/lib path. hooks/lib/ is flat and `
|
|
1363
|
+
+ 'this stager only resolves flat helper names, so the nested helper would never be '
|
|
1364
|
+
+ 'staged and the hook would throw MODULE_NOT_FOUND at load. Flatten the helper or '
|
|
1365
|
+
+ 'extend this stager deliberately.');
|
|
1366
|
+
}
|
|
1367
|
+
const re = fromLib ? libRequireRe : seedRequireRe;
|
|
1368
|
+
re.lastIndex = 0;
|
|
1369
|
+
let m;
|
|
1370
|
+
while ((m = re.exec(source)) !== null) {
|
|
1371
|
+
const candidate = m[1];
|
|
1372
|
+
// A capture with no alphanumeric character is not a module name — it is
|
|
1373
|
+
// prose. This scan reads whole file text, comments included, and
|
|
1374
|
+
// hooks/lib/injection-patterns.js's own header documents this mechanism
|
|
1375
|
+
// with the literal string `require('./lib/...')`, which captures `...`
|
|
1376
|
+
// and would send the resolver hunting for `hooks/lib/...` and fail the
|
|
1377
|
+
// install (measured; that helper is not staged for either reduced bundle
|
|
1378
|
+
// today, so it is latent rather than live).
|
|
1379
|
+
//
|
|
1380
|
+
// KNOWN LIMIT, disclosed rather than papered over: this does NOT make the
|
|
1381
|
+
// scan comment-aware. A comment naming a REAL helper — `require(
|
|
1382
|
+
// './lib/git-cmd.js')` in prose — still registers it and would over-stage
|
|
1383
|
+
// that helper. Closing that needs a comment-stripping pass; the
|
|
1384
|
+
// line-based stripper in scripts/lint-hooks-runtime-build-seam.cjs is the
|
|
1385
|
+
// precedent (its header explains why the naive two-regex strip corrupts
|
|
1386
|
+
// these very files), but promoting a lint-script helper into installer
|
|
1387
|
+
// runtime code is a larger change than this fix.
|
|
1388
|
+
if (!/[A-Za-z0-9]/.test(candidate))
|
|
1389
|
+
continue;
|
|
1390
|
+
requiredLibFiles.add(candidate);
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
for (const source of seedSources)
|
|
1394
|
+
scanForLibRequires(source, false);
|
|
1395
|
+
if (requiredLibFiles.size === 0)
|
|
1396
|
+
return staged;
|
|
1397
|
+
node_fs_1.default.mkdirSync(destLibDir, { recursive: true });
|
|
1398
|
+
// Iterate to a fixed point: staging a lib file can add MORE required lib
|
|
1399
|
+
// files (its own requires), which must themselves be staged and scanned.
|
|
1400
|
+
let libFile = [...requiredLibFiles].find((f) => !scannedLibFiles.has(f));
|
|
1401
|
+
while (libFile !== undefined) {
|
|
1402
|
+
scannedLibFiles.add(libFile);
|
|
1403
|
+
// Node's own extension resolution: `require('./lib/x')` is a valid, working
|
|
1404
|
+
// CommonJS spelling today, and matching only the extension-bearing form
|
|
1405
|
+
// resolved `x` literally, found nothing, and failed the install on a
|
|
1406
|
+
// legitimate require (review of #4087). Try the bare name first so an
|
|
1407
|
+
// extension-bearing capture still wins, then .js/.cjs.
|
|
1408
|
+
let resolvedName;
|
|
1409
|
+
for (const candidate of [libFile, `${libFile}.js`, `${libFile}.cjs`]) {
|
|
1410
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(srcLibDir, candidate))) {
|
|
1411
|
+
resolvedName = candidate;
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
const libSrc = node_path_1.default.join(srcLibDir, resolvedName ?? libFile);
|
|
1416
|
+
if (resolvedName === undefined) {
|
|
1417
|
+
// FAIL LOUD. Skipping here would ship hook scripts whose top-level
|
|
1418
|
+
// require() throws before their own try/catch, wedging every session —
|
|
1419
|
+
// and the install would still exit 0, so nobody would know until a user
|
|
1420
|
+
// hit it. A missing helper source is a packaging bug; surface it.
|
|
1421
|
+
throw new Error(`hooks/lib/${libFile} is required by a staged ${runtimeLabel} hook but is missing from ${srcLibDir}. `
|
|
1422
|
+
+ 'Installing would ship a hook that throws MODULE_NOT_FOUND at load.');
|
|
1423
|
+
}
|
|
1424
|
+
let libContent = node_fs_1.default.readFileSync(libSrc, 'utf8');
|
|
1425
|
+
if (transform)
|
|
1426
|
+
libContent = transform(libContent);
|
|
1427
|
+
// Written under its RESOLVED name so an extensionless require still lands a
|
|
1428
|
+
// file Node can resolve at the destination.
|
|
1429
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(destLibDir, resolvedName), libContent);
|
|
1430
|
+
staged.push(resolvedName);
|
|
1431
|
+
scanForLibRequires(libContent, true);
|
|
1432
|
+
libFile = [...requiredLibFiles].find((f) => !scannedLibFiles.has(f));
|
|
1433
|
+
}
|
|
1434
|
+
return staged;
|
|
1435
|
+
}
|
|
1314
1436
|
function writeCursorHooksJson(targetDir, src, opts) {
|
|
1315
1437
|
opts = opts || {};
|
|
1316
1438
|
const hooksDir = node_path_1.default.join(targetDir, 'hooks');
|
|
@@ -1348,43 +1470,13 @@ function writeCursorHooksJson(targetDir, src, opts) {
|
|
|
1348
1470
|
// "./lib/X" requires, and every lib file staged is itself scanned for further
|
|
1349
1471
|
// "./lib/X" OR bare "./X" (sibling-within-lib) requires, so the requirement
|
|
1350
1472
|
// graph is derived to a fixed point instead of one hand-tuned level deep.
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
requiredLibFiles.add(m[1]);
|
|
1359
|
-
}
|
|
1360
|
-
for (const script of installedScripts) {
|
|
1361
|
-
scanForLibRequires(node_fs_1.default.readFileSync(node_path_1.default.join(hooksDir, script), 'utf8'));
|
|
1362
|
-
}
|
|
1363
|
-
if (requiredLibFiles.size > 0) {
|
|
1364
|
-
const srcLibDir = node_path_1.default.join(srcHooksDir, 'lib');
|
|
1365
|
-
const destLibDir = node_path_1.default.join(hooksDir, 'lib');
|
|
1366
|
-
node_fs_1.default.mkdirSync(destLibDir, { recursive: true });
|
|
1367
|
-
// Iterate to a fixed point: staging a lib file can add MORE required lib
|
|
1368
|
-
// files (its own requires), which must themselves be staged and scanned.
|
|
1369
|
-
let libFile = [...requiredLibFiles].find((f) => !scannedLibFiles.has(f));
|
|
1370
|
-
while (libFile !== undefined) {
|
|
1371
|
-
scannedLibFiles.add(libFile);
|
|
1372
|
-
const libSrc = node_path_1.default.join(srcLibDir, libFile);
|
|
1373
|
-
if (!node_fs_1.default.existsSync(libSrc)) {
|
|
1374
|
-
// FAIL LOUD. Skipping here would ship hook scripts whose top-level
|
|
1375
|
-
// require() throws before their own try/catch, wedging every session —
|
|
1376
|
-
// and the install would still exit 0, so nobody would know until a user
|
|
1377
|
-
// hit it. A missing helper source is a packaging bug; surface it.
|
|
1378
|
-
throw new Error(`hooks/lib/${libFile} is required by a staged Cursor hook but is missing from ${srcLibDir}. `
|
|
1379
|
-
+ 'Installing would ship a hook that throws MODULE_NOT_FOUND at load.');
|
|
1380
|
-
}
|
|
1381
|
-
let libContent = node_fs_1.default.readFileSync(libSrc, 'utf8');
|
|
1382
|
-
libContent = libContent.replace(/gsd:/gi, 'gsd-');
|
|
1383
|
-
node_fs_1.default.writeFileSync(node_path_1.default.join(destLibDir, libFile), libContent);
|
|
1384
|
-
scanForLibRequires(libContent);
|
|
1385
|
-
libFile = [...requiredLibFiles].find((f) => !scannedLibFiles.has(f));
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1473
|
+
stageTransitiveHookLibs({
|
|
1474
|
+
seedSources: [...installedScripts].map((script) => node_fs_1.default.readFileSync(node_path_1.default.join(hooksDir, script), 'utf8')),
|
|
1475
|
+
srcLibDir: node_path_1.default.join(srcHooksDir, 'lib'),
|
|
1476
|
+
destLibDir: node_path_1.default.join(hooksDir, 'lib'),
|
|
1477
|
+
runtimeLabel: 'Cursor',
|
|
1478
|
+
transform: (content) => content.replace(/gsd:/gi, 'gsd-'),
|
|
1479
|
+
});
|
|
1388
1480
|
// #2717: write the CommonJS marker into hooks/ alongside the staged .js
|
|
1389
1481
|
// scripts. Cursor sets skipSharedHooksInstall, so it never reaches
|
|
1390
1482
|
// installSharedHooksBundle (the only other writer of this marker); without
|
|
@@ -1587,6 +1679,26 @@ function writeWindsurfHooksJson(targetDir, src, opts) {
|
|
|
1587
1679
|
installedScripts.add(script);
|
|
1588
1680
|
}
|
|
1589
1681
|
}
|
|
1682
|
+
// Stage the hooks/lib/ helpers these scripts require (#4087 review). Windsurf
|
|
1683
|
+
// sets hostBehaviors.skipSharedHooksInstall, so like Cursor it never reaches
|
|
1684
|
+
// installSharedHooksBundle — the only other stager of hooks/lib — and it was
|
|
1685
|
+
// staging neither. Both Cascade guards require helpers at module load:
|
|
1686
|
+
// gsd-windsurf-pre-write.js requires ./lib/hook-exit.js and ./lib/git-probe.js,
|
|
1687
|
+
// gsd-windsurf-pre-command.js requires ./lib/hook-exit.js. Measured against a
|
|
1688
|
+
// real `--windsurf --global` install before this call existed: the installer
|
|
1689
|
+
// exited 0, hooks/ held only the two scripts, and running either one exited 1
|
|
1690
|
+
// with "Cannot find module './lib/hook-exit.js'" — the same failure #4087
|
|
1691
|
+
// reports for Codex, on every pre_write_code / pre_run_command event.
|
|
1692
|
+
//
|
|
1693
|
+
// The transform matches the one applied to the scripts above: a helper must be
|
|
1694
|
+
// rewritten the same way as its caller or the two disagree on the spelling.
|
|
1695
|
+
stageTransitiveHookLibs({
|
|
1696
|
+
seedSources: [...installedScripts].map((script) => node_fs_1.default.readFileSync(node_path_1.default.join(hooksDir, script), 'utf8')),
|
|
1697
|
+
srcLibDir: node_path_1.default.join(srcHooksDir, 'lib'),
|
|
1698
|
+
destLibDir: node_path_1.default.join(hooksDir, 'lib'),
|
|
1699
|
+
runtimeLabel: 'Windsurf',
|
|
1700
|
+
transform: (content) => content.replace(/gsd:/gi, 'gsd-'),
|
|
1701
|
+
});
|
|
1590
1702
|
// #2717: write the CommonJS marker into hooks/ alongside the staged .js
|
|
1591
1703
|
// scripts. Windsurf sets skipSharedHooksInstall, so it never reaches
|
|
1592
1704
|
// installSharedHooksBundle (the only other writer of this marker); without
|
|
@@ -1737,6 +1849,37 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1737
1849
|
if (!settings.hooks.SessionStart) {
|
|
1738
1850
|
settings.hooks.SessionStart = [];
|
|
1739
1851
|
}
|
|
1852
|
+
// #3981: Claude Code treats a timed-out hook as NON-blocking — the tool
|
|
1853
|
+
// call continues through the normal permission flow. The blocking
|
|
1854
|
+
// PreToolUse guards therefore need a budget a host stall cannot exceed,
|
|
1855
|
+
// not one sized to the hook's own ~0.1 s runtime. Observed stalls reached
|
|
1856
|
+
// 84.3 s; 120 s is the top of the issue's prescribed 60–120 range and
|
|
1857
|
+
// returns every observed verdict. Registration below uses this constant,
|
|
1858
|
+
// and the migration pass right here raises existing managed entries.
|
|
1859
|
+
const BLOCKING_GUARD_TIMEOUT_S = 120;
|
|
1860
|
+
const blockingGuardNames = [
|
|
1861
|
+
'gsd-prompt-guard',
|
|
1862
|
+
'gsd-workflow-guard',
|
|
1863
|
+
'gsd-worktree-path-guard',
|
|
1864
|
+
'gsd-agent-isolation-guard',
|
|
1865
|
+
'gsd-write-guard',
|
|
1866
|
+
'gsd-secret-read-guard',
|
|
1867
|
+
'gsd-validate-commit',
|
|
1868
|
+
];
|
|
1869
|
+
for (const entries of Object.values(settings.hooks)) {
|
|
1870
|
+
if (!Array.isArray(entries))
|
|
1871
|
+
continue;
|
|
1872
|
+
for (const entry of entries) {
|
|
1873
|
+
if (!entry || !Array.isArray(entry.hooks))
|
|
1874
|
+
continue;
|
|
1875
|
+
for (const h of entry.hooks) {
|
|
1876
|
+
if (blockingGuardNames.some((name) => referencesHook(h, name)) &&
|
|
1877
|
+
h.timeout === 5) {
|
|
1878
|
+
h.timeout = BLOCKING_GUARD_TIMEOUT_S;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1740
1883
|
const hasGsdUpdateHook = settings.hooks.SessionStart.some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-check-update')));
|
|
1741
1884
|
// Guard: only register if the hook file was actually installed (#1754).
|
|
1742
1885
|
// When hooks/dist/ is missing from the npm package (as in v1.32.0), the
|
|
@@ -1817,7 +1960,7 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1817
1960
|
{
|
|
1818
1961
|
type: 'command',
|
|
1819
1962
|
command: promptGuardCommand,
|
|
1820
|
-
timeout:
|
|
1963
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
1821
1964
|
}
|
|
1822
1965
|
]
|
|
1823
1966
|
});
|
|
@@ -1894,7 +2037,7 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1894
2037
|
{
|
|
1895
2038
|
type: 'command',
|
|
1896
2039
|
command: workflowGuardCommand,
|
|
1897
|
-
timeout:
|
|
2040
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
1898
2041
|
}
|
|
1899
2042
|
]
|
|
1900
2043
|
});
|
|
@@ -1919,7 +2062,7 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1919
2062
|
{
|
|
1920
2063
|
type: 'command',
|
|
1921
2064
|
command: worktreePathGuardCommand,
|
|
1922
|
-
timeout:
|
|
2065
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
1923
2066
|
}
|
|
1924
2067
|
]
|
|
1925
2068
|
});
|
|
@@ -1949,7 +2092,7 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1949
2092
|
{
|
|
1950
2093
|
type: 'command',
|
|
1951
2094
|
command: agentIsolationGuardCommand,
|
|
1952
|
-
timeout:
|
|
2095
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
1953
2096
|
}
|
|
1954
2097
|
]
|
|
1955
2098
|
});
|
|
@@ -1976,7 +2119,7 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1976
2119
|
{
|
|
1977
2120
|
type: 'command',
|
|
1978
2121
|
command: writeGuardCommand,
|
|
1979
|
-
timeout:
|
|
2122
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
1980
2123
|
}
|
|
1981
2124
|
]
|
|
1982
2125
|
});
|
|
@@ -1985,6 +2128,33 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
1985
2128
|
else if (!hasWriteGuardHook && !node_fs_1.default.existsSync(writeGuardFile)) {
|
|
1986
2129
|
console.warn(` ${yellow}⚠${reset} Skipped write guard hook — gsd-write-guard.js not found at target`);
|
|
1987
2130
|
}
|
|
2131
|
+
// Configure PreToolUse hook for secret-file read protection (#4221).
|
|
2132
|
+
// Hard-blocks Read/Grep/Bash reads of .env, .env.<suffix> and .secrets.
|
|
2133
|
+
// Replaces the Read(.env*) permission deny rules the installer used to
|
|
2134
|
+
// write (#768): on Claude Code >= 2.1.259 ANY Read() deny rule makes every
|
|
2135
|
+
// `cd DIR && grep …` compound prompt for approval, even in auto mode; a
|
|
2136
|
+
// hook denial is not a permission rule and never arms that check.
|
|
2137
|
+
const secretReadGuardCommand = isGlobal
|
|
2138
|
+
? buildHookCommand(targetDir, 'gsd-secret-read-guard.js', hookOpts)
|
|
2139
|
+
: localCmd('gsd-secret-read-guard.js');
|
|
2140
|
+
const hasSecretReadGuardHook = settings.hooks[preToolEvent].some((entry) => entry.hooks && entry.hooks.some((h) => referencesHook(h, 'gsd-secret-read-guard')));
|
|
2141
|
+
const secretReadGuardFile = node_path_1.default.join(targetDir, 'hooks', 'gsd-secret-read-guard.js');
|
|
2142
|
+
if (!hasSecretReadGuardHook && node_fs_1.default.existsSync(secretReadGuardFile) && secretReadGuardCommand) {
|
|
2143
|
+
settings.hooks[preToolEvent].push({
|
|
2144
|
+
matcher: 'Read|Grep|Bash',
|
|
2145
|
+
hooks: [
|
|
2146
|
+
{
|
|
2147
|
+
type: 'command',
|
|
2148
|
+
command: secretReadGuardCommand,
|
|
2149
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
2150
|
+
}
|
|
2151
|
+
]
|
|
2152
|
+
});
|
|
2153
|
+
console.log(` ${green}✓${reset} Configured secret read guard hook (.env / .secrets read protection)`);
|
|
2154
|
+
}
|
|
2155
|
+
else if (!hasSecretReadGuardHook && !node_fs_1.default.existsSync(secretReadGuardFile)) {
|
|
2156
|
+
console.warn(` ${yellow}⚠${reset} Skipped secret read guard hook — gsd-secret-read-guard.js not found at target`);
|
|
2157
|
+
}
|
|
1988
2158
|
// Configure commit validation hook (Conventional Commits enforcement, opt-in)
|
|
1989
2159
|
const validateCommitCommand = isGlobal
|
|
1990
2160
|
? buildHookCommand(targetDir, 'gsd-validate-commit.sh', hookOpts)
|
|
@@ -2001,7 +2171,7 @@ function applySettingsJsonHooks(settings, opts) {
|
|
|
2001
2171
|
{
|
|
2002
2172
|
type: 'command',
|
|
2003
2173
|
command: validateCommitCommand,
|
|
2004
|
-
timeout:
|
|
2174
|
+
timeout: BLOCKING_GUARD_TIMEOUT_S
|
|
2005
2175
|
}
|
|
2006
2176
|
]
|
|
2007
2177
|
});
|
|
@@ -2330,6 +2500,7 @@ function buildKimiHooksTomlBlock(targetDir, opts) {
|
|
|
2330
2500
|
{ event: 'PreToolUse', command: cmd('gsd-read-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
|
|
2331
2501
|
{ event: 'PreToolUse', command: cmd('gsd-worktree-path-guard.js'), matcher: 'WriteFile|StrReplaceFile', timeout: 5 },
|
|
2332
2502
|
{ event: 'PreToolUse', command: cmd('gsd-write-guard.js'), matcher: 'WriteFile', timeout: 5 },
|
|
2503
|
+
{ event: 'PreToolUse', command: cmd('gsd-secret-read-guard.js'), matcher: 'ReadFile|Grep|Shell', timeout: 5 },
|
|
2333
2504
|
{ event: 'PreToolUse', command: cmd('gsd-workflow-guard.js'), matcher: 'Shell|WriteFile|StrReplaceFile', timeout: 5 },
|
|
2334
2505
|
{ event: 'PreToolUse', command: cmd('gsd-validate-commit.sh'), matcher: 'Shell', timeout: 5 },
|
|
2335
2506
|
// PostToolUse
|
|
@@ -2532,6 +2703,7 @@ module.exports = {
|
|
|
2532
2703
|
KIMI_HOOKS_TOML_MARKER_BEGIN,
|
|
2533
2704
|
KIMI_HOOKS_TOML_MARKER_END,
|
|
2534
2705
|
// Shared
|
|
2706
|
+
stageTransitiveHookLibs,
|
|
2535
2707
|
buildHookCommand,
|
|
2536
2708
|
applySettingsJsonHooks,
|
|
2537
2709
|
referencesHook,
|
|
@@ -242,6 +242,8 @@ const MANAGED_HOOK_BASENAMES_BY_SURFACE = {
|
|
|
242
242
|
'gsd-write-guard.js',
|
|
243
243
|
'gsd-agent-isolation-guard.js',
|
|
244
244
|
'gsd-worktree-path-guard.js',
|
|
245
|
+
// #4221: secret-file read guard (Read|Grep|Bash).
|
|
246
|
+
'gsd-secret-read-guard.js',
|
|
245
247
|
]),
|
|
246
248
|
'codex-toml': new Set([
|
|
247
249
|
'gsd-check-update.js',
|
|
@@ -269,6 +271,8 @@ const MANAGED_HOOK_COMMAND_BASENAMES_BY_SURFACE = {
|
|
|
269
271
|
'gsd-write-guard.js',
|
|
270
272
|
'gsd-agent-isolation-guard.js',
|
|
271
273
|
'gsd-worktree-path-guard.js',
|
|
274
|
+
// #4221: secret-file read guard (Read|Grep|Bash).
|
|
275
|
+
'gsd-secret-read-guard.js',
|
|
272
276
|
]),
|
|
273
277
|
'codex-toml': new Set([
|
|
274
278
|
'gsd-check-update.js',
|
|
@@ -64,7 +64,7 @@ const stateDocument = require("./state-document.cjs");
|
|
|
64
64
|
const { stateFieldValue } = stateDocument;
|
|
65
65
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
66
66
|
const phaseId = require("./phase-id.cjs");
|
|
67
|
-
const { comparePhaseNum, extractPhaseToken, matchPhaseDirs, normalizePhaseName, stripProjectCodePrefix } = phaseId;
|
|
67
|
+
const { comparePhaseNum, extractPhaseToken, matchPhaseDirs, normalizePhaseName, parsePhaseFromProse, stripProjectCodePrefix } = phaseId;
|
|
68
68
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
69
69
|
const stateMod = require("./state.cjs");
|
|
70
70
|
const { readStateHeadFreshness } = stateMod;
|
|
@@ -233,10 +233,7 @@ function readGitSignals(cwd) {
|
|
|
233
233
|
}
|
|
234
234
|
/** Leading numeric phase token from a STATE.md scalar or body `Phase:` value. */
|
|
235
235
|
function phaseTokenFromState(raw) {
|
|
236
|
-
|
|
237
|
-
return null;
|
|
238
|
-
const match = raw.trim().match(/^(\d+(?:[A-Z])?(?:\.\d+)*)/i);
|
|
239
|
-
return match ? match[1] : null;
|
|
236
|
+
return parsePhaseFromProse(raw).phase;
|
|
240
237
|
}
|
|
241
238
|
/**
|
|
242
239
|
* Detect whether the current phase's verify report indicates failure. The
|
|
@@ -429,7 +426,7 @@ function detectSignals(cwd, now = Date.now) {
|
|
|
429
426
|
const stateHeadRaw = stateFieldValue(fm, body, 'state_head', 'State Head').value;
|
|
430
427
|
const freshness = readStateHeadFreshness(cwd, stateHeadRaw);
|
|
431
428
|
return {
|
|
432
|
-
current_phase:
|
|
429
|
+
current_phase: phaseTokenFromState(currentPhaseRaw),
|
|
433
430
|
total_phases: parseIntOrNull(totalPhasesRaw),
|
|
434
431
|
status: (statusRaw || '').toLowerCase(),
|
|
435
432
|
progress: parseIntOrNull(progressRaw),
|
|
@@ -472,11 +469,12 @@ function isComplete(s) {
|
|
|
472
469
|
return false;
|
|
473
470
|
}
|
|
474
471
|
else {
|
|
475
|
-
// Legacy path: STATE.md comparison.
|
|
476
|
-
//
|
|
472
|
+
// Legacy path: STATE.md comparison. It only fires when ROADMAP.md is
|
|
473
|
+
// absent or has no Progress table, and uses canonical phase-id ordering
|
|
474
|
+
// so dotted phase ids are never coerced to JavaScript numbers.
|
|
477
475
|
if (s.total_phases === null || s.current_phase === null)
|
|
478
476
|
return false;
|
|
479
|
-
if (s.current_phase
|
|
477
|
+
if (comparePhaseNum(s.current_phase, s.total_phases) < 0)
|
|
480
478
|
return false;
|
|
481
479
|
}
|
|
482
480
|
// Status regex: require milestone-level completion language. The pre-fix
|
|
@@ -512,17 +512,42 @@ function stateCurrentPositionSlice(body) {
|
|
|
512
512
|
const section = (0, markdown_sectionizer_cjs_1.collectSection)(body, isCurrentPosition, { levelBounded: true });
|
|
513
513
|
return section ? section.body : null;
|
|
514
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* Join a matched `**Field:**`/`Field:` label prefix to its new value, inserting a
|
|
517
|
+
* single space when the prefix does not already end in same-line whitespace.
|
|
518
|
+
*
|
|
519
|
+
* On an empty field the same-line `[ \t]*` gap (below) captures nothing, so the
|
|
520
|
+
* bare `prefix + value` would glue the value to the label (`**Status:**value`);
|
|
521
|
+
* this inserts the missing separator. A non-empty field whose label-to-value
|
|
522
|
+
* separator is ordinary space/tab keeps that separator in the prefix, so `[ \t]$`
|
|
523
|
+
* is true and the output stays byte-identical to prior behaviour. The one
|
|
524
|
+
* exception is a non-empty field written with NO separator at all (a hand-edited
|
|
525
|
+
* `**Status:**value`): the narrowed gap captures nothing, `[ \t]$` is false, and a
|
|
526
|
+
* single space is inserted — an intentional normalization, not byte-identical, and
|
|
527
|
+
* with no GSD-template trigger. An empty new value inserts no separator, avoiding a
|
|
528
|
+
* dangling trailing space. See #4010.
|
|
529
|
+
*/
|
|
530
|
+
function joinFieldReplacement(prefix, newValue) {
|
|
531
|
+
const value = `${newValue}`;
|
|
532
|
+
const needsSeparator = value.length > 0 && !/[ \t]$/.test(prefix);
|
|
533
|
+
return `${prefix}${needsSeparator ? ' ' : ''}${value}`;
|
|
534
|
+
}
|
|
515
535
|
function stateReplaceField(content, fieldName, newValue) {
|
|
516
536
|
const escaped = (0, pattern_cjs_1.escapeRegex)(fieldName);
|
|
517
537
|
// Bold inline format: **FieldName:** value
|
|
518
|
-
|
|
538
|
+
// The label-to-value gap is same-line whitespace only (`[ \t]*`, mirroring the
|
|
539
|
+
// read side at stateExtractField). `\s*` here matched `\n`, so on an empty field
|
|
540
|
+
// `(.*)` captured the following line and the rebuild discarded it — the #4010
|
|
541
|
+
// data-loss. ADR-3180 §7.7 makes stateExtractField the same-line-confined owner;
|
|
542
|
+
// this aligns the writer to it.
|
|
543
|
+
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*[ \\t]*)(.*)`, 'i');
|
|
519
544
|
if (boldPattern.test(content)) {
|
|
520
|
-
return content.replace(boldPattern, (_match, prefix) =>
|
|
545
|
+
return content.replace(boldPattern, (_match, prefix) => joinFieldReplacement(prefix, newValue));
|
|
521
546
|
}
|
|
522
|
-
// Plain line-start format: FieldName: value
|
|
523
|
-
const plainPattern = new RegExp(`(^${escaped}
|
|
547
|
+
// Plain line-start format: FieldName: value (same same-line confinement as above)
|
|
548
|
+
const plainPattern = new RegExp(`(^${escaped}:[ \\t]*)(.*)`, 'im');
|
|
524
549
|
if (plainPattern.test(content)) {
|
|
525
|
-
return content.replace(plainPattern, (_match, prefix) =>
|
|
550
|
+
return content.replace(plainPattern, (_match, prefix) => joinFieldReplacement(prefix, newValue));
|
|
526
551
|
}
|
|
527
552
|
// Pipe-table format: | FieldName | value |
|
|
528
553
|
// Preserve the surrounding pipe/whitespace structure; only swap the value cell.
|
|
@@ -128,19 +128,29 @@ exports.STATE_FIELD_SCHEMA = Object.freeze(Object.assign(Object.create(null), {
|
|
|
128
128
|
// (verified: it calls `stateExtractField(bodyContent, 'Current Plan')`
|
|
129
129
|
// only), so that shape is out of scope for this row regardless.
|
|
130
130
|
//
|
|
131
|
-
// #3784
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
131
|
+
// #3784/#3791 WIDENED THIS ROW. The paragraph above describes the
|
|
132
|
+
// pre-#3791 parser and is kept as the record of what the shape was
|
|
133
|
+
// before, because row 25 exists to stop exactly that reading from
|
|
134
|
+
// being re-asserted by accident.
|
|
135
|
+
//
|
|
136
|
+
// `advancePlanCore` now accepts the hybrid shape, so the declared set
|
|
137
|
+
// is `['N', 'N of M']`. Two properties of the widening matter to a
|
|
138
|
+
// future reader:
|
|
139
|
+
//
|
|
140
|
+
// - It is ANCHORED. The parser matches `/^(\d+)\s+of\s+(\d+)\s*$/`
|
|
141
|
+
// against the whole value, so `4 — blocked on review of 2 PRs`
|
|
142
|
+
// is REJECTED rather than yielding a total of 2 out of prose.
|
|
143
|
+
// Declaring `'N of M'` is a claim about that grammar, not about
|
|
144
|
+
// "contains the word of".
|
|
145
|
+
// - `'N/M'` stays UNDECLARED and must keep failing. Row 23 probes
|
|
146
|
+
// the undeclared remainder of `SHAPE_EXAMPLES`, so it needs at
|
|
147
|
+
// least one member outside the declared set to stay non-vacuous.
|
|
148
|
+
//
|
|
149
|
+
// Widening this row without widening the parser (or the reverse) goes
|
|
150
|
+
// RED on rows 23/24/25. That coupling is the forcing function, and it
|
|
151
|
+
// is the reason this row is data rather than a predicate: §8.8's
|
|
152
|
+
// "parsers are checked, not generated".
|
|
153
|
+
acceptedShapes: Object.freeze(['N', 'N of M']),
|
|
144
154
|
emitted: 'when-present',
|
|
145
155
|
},
|
|
146
156
|
// Status / lifecycle (body-derived; #1230 delta heuristic applies)
|