@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
|
@@ -219,6 +219,11 @@ function _isNonTerminal(status) {
|
|
|
219
219
|
* - Same `job_id` for the same `plan_id` -> allowed (status progression).
|
|
220
220
|
* - A prior job for the same `plan_id` that is already terminal -> allowed
|
|
221
221
|
* (the duplicate guard only protects against re-dispatching live work).
|
|
222
|
+
* - If a SIBLING manifest (not the target) cannot be read or parsed, the
|
|
223
|
+
* duplicate scan cannot be completed and may be hiding a live duplicate
|
|
224
|
+
* -> refuse (`scan_incomplete`), naming the offending file's path so an
|
|
225
|
+
* operator can quarantine or repair it. Never silently skip a sibling the
|
|
226
|
+
* scan could not inspect.
|
|
222
227
|
*/
|
|
223
228
|
function writeManifest(manifest, planningDir, opts = {}) {
|
|
224
229
|
const fs = opts.fs ?? node_fs_1.default;
|
|
@@ -240,18 +245,28 @@ function writeManifest(manifest, planningDir, opts = {}) {
|
|
|
240
245
|
try {
|
|
241
246
|
raw = String(fs.readFileSync(p));
|
|
242
247
|
}
|
|
243
|
-
catch {
|
|
244
|
-
|
|
248
|
+
catch (e) {
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
kind: 'scan_incomplete',
|
|
252
|
+
message: `duplicate scan incomplete: failed to READ sibling manifest ${p} (${e.message}); quarantine or repair this file before retrying`,
|
|
253
|
+
offendingPath: p,
|
|
254
|
+
};
|
|
245
255
|
}
|
|
246
256
|
let existing;
|
|
247
257
|
try {
|
|
248
258
|
existing = JSON.parse(raw);
|
|
249
259
|
}
|
|
250
|
-
catch {
|
|
260
|
+
catch (e) {
|
|
251
261
|
if (p === target) {
|
|
252
262
|
return { ok: false, kind: 'malformed_existing', message: `target manifest ${p} is not valid JSON` };
|
|
253
263
|
}
|
|
254
|
-
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
kind: 'scan_incomplete',
|
|
267
|
+
message: `duplicate scan incomplete: failed to PARSE sibling manifest ${p} (${e.message}); quarantine or repair this file before retrying`,
|
|
268
|
+
offendingPath: p,
|
|
269
|
+
};
|
|
255
270
|
}
|
|
256
271
|
const samePlan = existing.plan_id === manifest.plan_id;
|
|
257
272
|
const sameJob = existing.job_id === manifest.job_id;
|
|
@@ -195,6 +195,18 @@ function parseYamlRegion(yaml) {
|
|
|
195
195
|
* hold only an in-memory string; those dedup on a content digest instead.
|
|
196
196
|
*/
|
|
197
197
|
function extractFrontmatter(content, sourcePath) {
|
|
198
|
+
// #2977: tolerate a single leading UTF-8 BOM (\uFEFF), which Windows tooling
|
|
199
|
+
// (PowerShell `>`/`Out-File` on PS 5.1, several editors) writes by default. Without this
|
|
200
|
+
// strip, the byte-0 `startsWith('---')` fence check below fails on the BOM and the whole
|
|
201
|
+
// parse collapses to {} — every frontmatter field silently disappears, and the engine
|
|
202
|
+
// proceeds as though the file had no frontmatter at all. The BOM is a single codepoint;
|
|
203
|
+
// stripping it here restores byte-0 alignment so the rest of the function is unchanged.
|
|
204
|
+
// Scope: BOM only. Arbitrary non-BOM content before the fence (leading whitespace/blank
|
|
205
|
+
// line/comment) is a separate product-intent decision (tolerate vs diagnose) left to a
|
|
206
|
+
// future change — this fix does not broaden the byte-0 fence rule beyond the BOM.
|
|
207
|
+
if (content.charCodeAt(0) === 0xFEFF) {
|
|
208
|
+
content = content.slice(1);
|
|
209
|
+
}
|
|
198
210
|
// Match frontmatter only at byte 0 — a `---` block later in the document
|
|
199
211
|
// body (YAML examples, horizontal rules) must never be treated as frontmatter.
|
|
200
212
|
const headerEnd = content.startsWith('---\r\n') ? 5 : content.startsWith('---\n') ? 4 : -1;
|
|
@@ -607,29 +619,64 @@ function parseMustHavesBlock(content, blockName) {
|
|
|
607
619
|
return items;
|
|
608
620
|
}
|
|
609
621
|
// ─── Frontmatter CRUD commands ────────────────────────────────────────────────
|
|
622
|
+
// Shared base for 'plan' and 'plan-gap-closure' below — a plain array reference (not
|
|
623
|
+
// FRONTMATTER_SCHEMAS.plan.required) because the object literal that defines
|
|
624
|
+
// FRONTMATTER_SCHEMAS cannot refer to itself mid-initialization (TDZ).
|
|
625
|
+
const PLAN_REQUIRED_FIELDS = ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'];
|
|
626
|
+
// `requiredValues` is optional per schema: when a field name is a key here, the
|
|
627
|
+
// field must be PRESENT AND strictly equal (===) to the given value to satisfy
|
|
628
|
+
// the schema — presence alone is not enough. Every other required field (no
|
|
629
|
+
// entry in requiredValues) keeps the original presence-only contract.
|
|
610
630
|
const FRONTMATTER_SCHEMAS = {
|
|
611
|
-
plan: { required:
|
|
631
|
+
plan: { required: PLAN_REQUIRED_FIELDS },
|
|
632
|
+
// #2847: gap-closure plans carry every 'plan' field PLUS gap_closure — the flag
|
|
633
|
+
// execute-phase --gaps-only filters on. A separate schema (not a change to
|
|
634
|
+
// 'plan') so standard/reviews-mode plans stay unaffected: they validate against
|
|
635
|
+
// 'plan' and are never required to declare or be checked for gap_closure.
|
|
636
|
+
// Derived from PLAN_REQUIRED_FIELDS (never hand-duplicated) so the two can't drift.
|
|
637
|
+
//
|
|
638
|
+
// requiredValues.gap_closure = true (not just presence): --gaps-only filters
|
|
639
|
+
// strictly on gap_closure === true (execute-phase.md, partial-wave.md), so a
|
|
640
|
+
// plan carrying `gap_closure: false` would pass a presence-only check and
|
|
641
|
+
// still be silently skipped at execute time — the exact symptom #2847
|
|
642
|
+
// reports, one value away. Presence-only was flagged in review as a live
|
|
643
|
+
// reproduction of the bug this schema exists to close.
|
|
644
|
+
'plan-gap-closure': {
|
|
645
|
+
required: [...PLAN_REQUIRED_FIELDS, 'gap_closure'],
|
|
646
|
+
// extractFrontmatter parses every scalar as a string (FrontmatterValue has
|
|
647
|
+
// no boolean member — `gap_closure: true` in YAML becomes the JS string
|
|
648
|
+
// "true", not the boolean true), so the required value is the string here.
|
|
649
|
+
requiredValues: { gap_closure: 'true' },
|
|
650
|
+
},
|
|
612
651
|
summary: { required: ['phase', 'plan', 'subsystem', 'tags', 'duration', 'completed'] },
|
|
613
652
|
verification: { required: ['phase', 'verified', 'status', 'score'] },
|
|
614
653
|
};
|
|
615
654
|
/**
|
|
616
|
-
* Strip
|
|
655
|
+
* Strip frontmatter blocks from the start of `content`.
|
|
656
|
+
*
|
|
657
|
+
* Handles CRLF line endings and, by default, multiple stacked blocks
|
|
658
|
+
* (corruption recovery): greedily strips consecutive `---...---` blocks
|
|
659
|
+
* separated by optional whitespace, so a doubled/tripled frontmatter header
|
|
660
|
+
* (e.g. from a botched merge) is fully removed, not just the first block.
|
|
617
661
|
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
662
|
+
* Pass `{ once: true }` to stop after the first block. Callers whose input is
|
|
663
|
+
* an arbitrary user-authored document — rather than a GSD artefact with a
|
|
664
|
+
* known doubling failure mode — need this: a body that opens with a
|
|
665
|
+
* thematic-break-delimited section is lexically indistinguishable from a
|
|
666
|
+
* second frontmatter block, and the greedy loop deletes it silently (#2703).
|
|
622
667
|
*
|
|
623
668
|
* Canonical home for this primitive (#2143 audit dedup): previously
|
|
624
669
|
* duplicated byte-identically in both `state.cts` and `state-transition.cts`.
|
|
625
670
|
*/
|
|
626
|
-
function stripFrontmatter(content) {
|
|
671
|
+
function stripFrontmatter(content, opts = {}) {
|
|
627
672
|
let result = content;
|
|
628
673
|
while (true) {
|
|
629
674
|
const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
|
|
630
675
|
if (stripped === result)
|
|
631
676
|
break;
|
|
632
677
|
result = stripped;
|
|
678
|
+
if (opts.once)
|
|
679
|
+
break;
|
|
633
680
|
}
|
|
634
681
|
return result;
|
|
635
682
|
}
|
|
@@ -750,10 +797,19 @@ function cmdFrontmatterValidate(cwd, filePath, schemaName, raw) {
|
|
|
750
797
|
if (filePath.includes('\0')) {
|
|
751
798
|
error('file path contains null bytes');
|
|
752
799
|
}
|
|
753
|
-
|
|
754
|
-
|
|
800
|
+
// Guard against prototype-chain keys (__proto__, constructor, toString, hasOwnProperty,
|
|
801
|
+
// valueOf, ...): a bare FRONTMATTER_SCHEMAS[schemaName] lookup resolves those to
|
|
802
|
+
// Object.prototype members instead of undefined, so a `!schema` check on the raw
|
|
803
|
+
// lookup never fires and the code crashes later on `schema.required.filter` with an
|
|
804
|
+
// uncaught TypeError instead of the intended "Unknown schema" message. Confirmed live
|
|
805
|
+
// with --schema __proto__. Now that --schema is agent-bound (agents/gsd-planner.md's
|
|
806
|
+
// $SCHEMA), this is reachable from prompt state, not just an unreachable literal.
|
|
807
|
+
// Checked and rejected BEFORE the lookup (rather than `?? undefined`-ing the lookup
|
|
808
|
+
// itself) so `schema`'s inferred type stays non-optional and needs no assertion below.
|
|
809
|
+
if (!Object.prototype.hasOwnProperty.call(FRONTMATTER_SCHEMAS, schemaName)) {
|
|
755
810
|
error(`Unknown schema: ${schemaName}. Available: ${Object.keys(FRONTMATTER_SCHEMAS).join(', ')}`);
|
|
756
811
|
}
|
|
812
|
+
const schema = FRONTMATTER_SCHEMAS[schemaName];
|
|
757
813
|
const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
|
|
758
814
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
|
|
759
815
|
if (!content) {
|
|
@@ -771,9 +827,25 @@ function cmdFrontmatterValidate(cwd, filePath, schemaName, raw) {
|
|
|
771
827
|
// Pass the resolved path so a truncated file is named in the diagnostic and deduplicated
|
|
772
828
|
// per file rather than per content digest (#1882, ADR-1411 wiring clause).
|
|
773
829
|
const fm = extractFrontmatter(content, fullPath);
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
830
|
+
const requiredValues = schema.requiredValues || {};
|
|
831
|
+
// A field satisfies the schema when it is present AND — for fields with a
|
|
832
|
+
// requiredValues entry — strictly equal to that value. Absent and
|
|
833
|
+
// wrong-value both surface as `missing` (existing `missing`/`present`
|
|
834
|
+
// partition of `required` is unchanged — no consumer reads `present` to
|
|
835
|
+
// mean "physically exists regardless of value," confirmed by searching
|
|
836
|
+
// every caller before choosing this). But #2847 review: folding silently
|
|
837
|
+
// made "missing" misleading for a WRONG-valued field the plan author can
|
|
838
|
+
// plainly see in the file (e.g. `gap_closure: True`) — nothing told them
|
|
839
|
+
// the field is present but the VALUE is wrong, so they could loop trying
|
|
840
|
+
// to add a field that is already there. `invalidValue` names exactly that
|
|
841
|
+
// subset (present, but not the required value) so the message stays
|
|
842
|
+
// actionable without changing what `missing`/`present` mean.
|
|
843
|
+
const wrongValue = (f) => fm[f] !== undefined && Object.prototype.hasOwnProperty.call(requiredValues, f) && fm[f] !== requiredValues[f];
|
|
844
|
+
const satisfies = (f) => fm[f] !== undefined && !wrongValue(f);
|
|
845
|
+
const missing = schema.required.filter(f => !satisfies(f));
|
|
846
|
+
const present = schema.required.filter(f => satisfies(f));
|
|
847
|
+
const invalidValue = schema.required.filter(wrongValue);
|
|
848
|
+
output({ valid: missing.length === 0, missing, present, invalidValue, schema: schemaName }, raw, missing.length === 0 ? 'valid' : 'invalid');
|
|
777
849
|
}
|
|
778
850
|
module.exports = {
|
|
779
851
|
extractFrontmatter,
|
|
@@ -18,11 +18,10 @@
|
|
|
18
18
|
* non-zero check-command exit, which the workflow's two-step gate contract treats
|
|
19
19
|
* as a step-1 command failure (routed per the gate's `onError`).
|
|
20
20
|
*
|
|
21
|
-
* Built-in
|
|
22
|
-
* `sh -c` subprocess
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* for the full sandbox contract.
|
|
21
|
+
* Built-in kinds: `command-exit-zero` runs a declared command in a bounded
|
|
22
|
+
* `sh -c` subprocess; `artifact-frontmatter-equals` compares a declared value
|
|
23
|
+
* with frontmatter read through injected artifact dependencies. See ADR-2008
|
|
24
|
+
* for the full contracts.
|
|
26
25
|
*
|
|
27
26
|
* This is a leaf pure module: no fs, no child_process, no config — the subprocess
|
|
28
27
|
* seam is injected so the evaluator is trivially testable without spawning.
|
|
@@ -35,7 +34,7 @@ const COMMAND_MAX_OUTPUT_CHARS = 2000;
|
|
|
35
34
|
/** Hard cap on the declared command length (defense-in-depth against ARGV overflow / abuse). */
|
|
36
35
|
const COMMAND_MAX_LENGTH = 4096;
|
|
37
36
|
/** Predicate kinds this evaluator recognises (extensible — add to KIND_TABLE). */
|
|
38
|
-
const EVALUATOR_KINDS = Object.freeze(['command-exit-zero']);
|
|
37
|
+
const EVALUATOR_KINDS = Object.freeze(['command-exit-zero', 'artifact-frontmatter-equals']);
|
|
39
38
|
/** Placeholders interpolated into a declared command, in addition to sh's own vars. */
|
|
40
39
|
const INTERPOLATION_VAR_NAMES = Object.freeze(['PHASE_NUMBER', 'PHASE_DIR', 'PHASE_REQ_IDS']);
|
|
41
40
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -102,9 +101,53 @@ function evaluateCommandExitZero(predicate, ctx, deps) {
|
|
|
102
101
|
details: { kind: 'command-exit-zero', exitCode: res.exitCode, signal: res.signal },
|
|
103
102
|
};
|
|
104
103
|
}
|
|
104
|
+
// ─── Kind: artifact-frontmatter-equals ──────────────────────────────────────────
|
|
105
|
+
function evaluateArtifactFrontmatterEquals(predicate, ctx, deps) {
|
|
106
|
+
const artifactSuffix = predicate['artifact'];
|
|
107
|
+
if (!isNonEmptyString(artifactSuffix)) {
|
|
108
|
+
throw new Error('artifact-frontmatter-equals predicate requires a non-empty string "artifact"');
|
|
109
|
+
}
|
|
110
|
+
const field = predicate['field'];
|
|
111
|
+
if (!isNonEmptyString(field)) {
|
|
112
|
+
throw new Error('artifact-frontmatter-equals predicate requires a non-empty string "field"');
|
|
113
|
+
}
|
|
114
|
+
const expectedValue = predicate['equals'];
|
|
115
|
+
if (expectedValue === undefined) {
|
|
116
|
+
throw new Error('artifact-frontmatter-equals predicate requires an "equals" key');
|
|
117
|
+
}
|
|
118
|
+
const targetDir = isNonEmptyString(ctx.phaseDir) ? ctx.phaseDir : ctx.cwd;
|
|
119
|
+
const filePath = deps.findPhaseArtifact(targetDir, artifactSuffix);
|
|
120
|
+
if (!filePath) {
|
|
121
|
+
return {
|
|
122
|
+
block: true,
|
|
123
|
+
message: `Artifact matching ${artifactSuffix} not found in ${targetDir}`,
|
|
124
|
+
details: { kind: 'artifact-frontmatter-equals', artifactNotFound: true },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const fm = deps.readFrontmatter(filePath);
|
|
128
|
+
const actualValue = fm[field];
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
130
|
+
const expectedStr = typeof expectedValue === 'object' ? JSON.stringify(expectedValue) : String(expectedValue);
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
132
|
+
const actualStr = typeof actualValue === 'object' ? JSON.stringify(actualValue) : String(actualValue);
|
|
133
|
+
const matches = actualValue === expectedValue || (actualValue !== undefined && actualValue !== null && actualStr === expectedStr);
|
|
134
|
+
if (matches) {
|
|
135
|
+
return {
|
|
136
|
+
block: false,
|
|
137
|
+
message: `Frontmatter field "${field}" matches expected value (${expectedStr})`,
|
|
138
|
+
details: { kind: 'artifact-frontmatter-equals', match: true },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
block: true,
|
|
143
|
+
message: `Frontmatter field "${field}" in ${artifactSuffix} is ${actualStr}, expected ${expectedStr}`,
|
|
144
|
+
details: { kind: 'artifact-frontmatter-equals', match: false, actual: actualValue, expected: expectedValue },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
105
147
|
// ─── Kind dispatch table ──────────────────────────────────────────────────────
|
|
106
148
|
const KIND_TABLE = {
|
|
107
149
|
'command-exit-zero': evaluateCommandExitZero,
|
|
150
|
+
'artifact-frontmatter-equals': evaluateArtifactFrontmatterEquals,
|
|
108
151
|
};
|
|
109
152
|
// ─── Public entry point ───────────────────────────────────────────────────────
|
|
110
153
|
/**
|
|
@@ -133,6 +176,14 @@ function evaluatePredicate(predicate, context, deps) {
|
|
|
133
176
|
if (typeof kind !== 'string' || kind.length === 0) {
|
|
134
177
|
throw new Error('predicate.kind must be a non-empty string');
|
|
135
178
|
}
|
|
179
|
+
if (kind === 'artifact-frontmatter-equals') {
|
|
180
|
+
if (typeof d.findPhaseArtifact !== 'function') {
|
|
181
|
+
throw new Error('predicate deps require a "findPhaseArtifact" function');
|
|
182
|
+
}
|
|
183
|
+
if (typeof d.readFrontmatter !== 'function') {
|
|
184
|
+
throw new Error('predicate deps require a "readFrontmatter" function');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
136
187
|
const handler = KIND_TABLE[kind];
|
|
137
188
|
if (!handler) {
|
|
138
189
|
throw new Error(`Unknown predicate kind: "${kind}". Known kinds: ${EVALUATOR_KINDS.join(', ')}`);
|
|
@@ -18,7 +18,14 @@
|
|
|
18
18
|
* 5. "main" (last-resort default)
|
|
19
19
|
*
|
|
20
20
|
* Every git subprocess is bounded with a timeout (≤ 30 s); on timeout/error
|
|
21
|
-
* the resolver degrades gracefully to the next tier — it never throws.
|
|
21
|
+
* the resolver degrades gracefully to the next tier — it never throws. Tier 5
|
|
22
|
+
* is reachable two ways that `resolveBaseBranch()` alone cannot tell apart: a
|
|
23
|
+
* repository that genuinely has no candidate branch (every git query on tiers
|
|
24
|
+
* 2-4 completed and cleanly answered "nothing"), or a total resolution
|
|
25
|
+
* failure (some query timed out / could not run). `resolveBaseBranchDiagnostics()`
|
|
26
|
+
* distinguishes the two via `verified`; `cmdGitBaseBranch` surfaces the
|
|
27
|
+
* unverified case as a stderr diagnostic without changing its stdout contract
|
|
28
|
+
* (#3057 B4).
|
|
22
29
|
*
|
|
23
30
|
* Pure/testable: all I/O is injectable via the `deps` argument so unit
|
|
24
31
|
* tests can run without touching the real filesystem or spawning real git.
|
|
@@ -31,6 +38,7 @@ exports.readConfigBaseBranch = readConfigBaseBranch;
|
|
|
31
38
|
exports.trySymbolicRef = trySymbolicRef;
|
|
32
39
|
exports.tryRemoteShow = tryRemoteShow;
|
|
33
40
|
exports.tryLocalBranch = tryLocalBranch;
|
|
41
|
+
exports.resolveBaseBranchDiagnostics = resolveBaseBranchDiagnostics;
|
|
34
42
|
exports.resolveBaseBranch = resolveBaseBranch;
|
|
35
43
|
exports.gitWorktreeInfoInternal = gitWorktreeInfoInternal;
|
|
36
44
|
exports.cmdGitBaseBranch = cmdGitBaseBranch;
|
|
@@ -115,7 +123,8 @@ function tryRemoteShow(cwd, execGit) {
|
|
|
115
123
|
const branch = m[1];
|
|
116
124
|
// git emits "(unknown)" when the remote is offline but the local cache
|
|
117
125
|
// resolved it; treat that as non-authoritative and fall through.
|
|
118
|
-
|
|
126
|
+
// No `!branch ||` guard: m[1] comes from the `(\S+)` capture group above, so it is never empty.
|
|
127
|
+
if (branch === '(unknown)')
|
|
119
128
|
return null;
|
|
120
129
|
return branch;
|
|
121
130
|
}
|
|
@@ -153,41 +162,70 @@ function tryLocalBranch(cwd, execGit) {
|
|
|
153
162
|
}
|
|
154
163
|
}
|
|
155
164
|
/**
|
|
156
|
-
* Resolve the default/base branch for the repository at `cwd
|
|
165
|
+
* Resolve the default/base branch for the repository at `cwd`, along with
|
|
166
|
+
* whether the tier-5 last-resort default (if reached) was verified.
|
|
157
167
|
*
|
|
158
168
|
* Consults the full precedence ladder and always returns a non-empty string.
|
|
159
169
|
* Never throws.
|
|
160
170
|
*/
|
|
161
|
-
function
|
|
162
|
-
const
|
|
171
|
+
function resolveBaseBranchDiagnostics(cwd, deps) {
|
|
172
|
+
const rawExecGit = deps?.execGit ?? shell_command_projection_cjs_1.execGit;
|
|
173
|
+
// A genuine execGit failure (timeout, or the call could not even spawn —
|
|
174
|
+
// e.g. git missing, surfaced as exitCode 127 with `error` set) is distinct
|
|
175
|
+
// from git completing and cleanly reporting a negative answer (non-zero
|
|
176
|
+
// exit with no useful output, or exit 0 with empty stdout). Only the former
|
|
177
|
+
// means a tier's answer was never actually obtained. Wrapping execGit here
|
|
178
|
+
// observes every tier's calls uniformly without changing trySymbolicRef /
|
|
179
|
+
// tryRemoteShow / tryLocalBranch's own return contracts.
|
|
180
|
+
let anyGitFailure = false;
|
|
181
|
+
const execGit = (args, opts) => {
|
|
182
|
+
const r = rawExecGit(args, opts);
|
|
183
|
+
if (r.timedOut || r.error)
|
|
184
|
+
anyGitFailure = true;
|
|
185
|
+
return r;
|
|
186
|
+
};
|
|
163
187
|
// Derive .planning dir relative to cwd (mirrors planningDir() in planning-workspace.cjs)
|
|
164
188
|
const planningDir = node_path_1.default.join(cwd, '.planning');
|
|
165
189
|
// 1. Config override
|
|
166
190
|
const configured = readConfigBaseBranch(planningDir, deps);
|
|
167
191
|
if (configured)
|
|
168
|
-
return configured;
|
|
192
|
+
return { branch: configured, verified: true };
|
|
169
193
|
// 2. symbolic-ref (fast, no network)
|
|
170
194
|
const symref = trySymbolicRef(cwd, execGit);
|
|
171
195
|
if (symref)
|
|
172
|
-
return symref;
|
|
196
|
+
return { branch: symref, verified: true };
|
|
173
197
|
// 3. git remote show origin (authoritative when origin/HEAD unset)
|
|
174
198
|
const remoteShow = tryRemoteShow(cwd, execGit);
|
|
175
199
|
if (remoteShow)
|
|
176
|
-
return remoteShow;
|
|
200
|
+
return { branch: remoteShow, verified: true };
|
|
177
201
|
// 4. Local branch existence
|
|
178
202
|
const local = tryLocalBranch(cwd, execGit);
|
|
179
203
|
if (local)
|
|
180
|
-
return local;
|
|
181
|
-
// 5. Last-resort default
|
|
182
|
-
|
|
204
|
+
return { branch: local, verified: true };
|
|
205
|
+
// 5. Last-resort default. `verified:false` when at least one tier-2/3/4
|
|
206
|
+
// execGit call timed out or failed to run — the default was never actually
|
|
207
|
+
// checked against this repository, it is just what's left after git could
|
|
208
|
+
// not answer (#3057 B4).
|
|
209
|
+
return { branch: 'main', verified: !anyGitFailure };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Resolve the default/base branch for the repository at `cwd`.
|
|
213
|
+
*
|
|
214
|
+
* Consults the full precedence ladder and always returns a non-empty string.
|
|
215
|
+
* Never throws. See {@link resolveBaseBranchDiagnostics} for a caller that
|
|
216
|
+
* needs to distinguish a verified answer from an unverified fallback.
|
|
217
|
+
*/
|
|
218
|
+
function resolveBaseBranch(cwd, deps) {
|
|
219
|
+
return resolveBaseBranchDiagnostics(cwd, deps).branch;
|
|
183
220
|
}
|
|
184
221
|
/**
|
|
185
222
|
* Detect whether `cwd` sits inside a git worktree, and if so, return the
|
|
186
223
|
* absolute path of the worktree root.
|
|
187
224
|
*/
|
|
188
|
-
function gitWorktreeInfoInternal(cwd) {
|
|
225
|
+
function gitWorktreeInfoInternal(cwd, deps) {
|
|
226
|
+
const execGit = deps?.execGit ?? shell_command_projection_cjs_1.execGit;
|
|
189
227
|
try {
|
|
190
|
-
const insideResult =
|
|
228
|
+
const insideResult = execGit(['rev-parse', '--is-inside-work-tree'], { cwd, timeout: 5000 });
|
|
191
229
|
if (insideResult.exitCode !== 0) {
|
|
192
230
|
return { inside: false, worktreeRoot: null };
|
|
193
231
|
}
|
|
@@ -195,7 +233,7 @@ function gitWorktreeInfoInternal(cwd) {
|
|
|
195
233
|
if (insideStdout !== 'true') {
|
|
196
234
|
return { inside: false, worktreeRoot: null };
|
|
197
235
|
}
|
|
198
|
-
const rootResult =
|
|
236
|
+
const rootResult = execGit(['rev-parse', '--show-toplevel'], { cwd, timeout: 5000 });
|
|
199
237
|
if (rootResult.exitCode !== 0) {
|
|
200
238
|
return { inside: true, worktreeRoot: null };
|
|
201
239
|
}
|
|
@@ -213,7 +251,12 @@ function gitWorktreeInfoInternal(cwd) {
|
|
|
213
251
|
* Called by workflows via `gsd_run query git.base-branch`.
|
|
214
252
|
*/
|
|
215
253
|
function cmdGitBaseBranch(cwd, _args, deps) {
|
|
216
|
-
const branch =
|
|
254
|
+
const { branch, verified } = resolveBaseBranchDiagnostics(cwd, deps);
|
|
255
|
+
if (!verified) {
|
|
256
|
+
const writeDiagnostic = deps?.writeDiagnostic ?? ((s) => process.stderr.write(s));
|
|
257
|
+
writeDiagnostic(`⚠ git-base-branch: defaulted to 'main' WITHOUT verifying against this repository — ` +
|
|
258
|
+
`a git query timed out or could not run. See #3057.\n`);
|
|
259
|
+
}
|
|
217
260
|
const write = deps?.write ?? ((s) => process.stdout.write(s));
|
|
218
261
|
write(branch + '\n');
|
|
219
262
|
return branch;
|