@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
|
@@ -23,12 +23,19 @@ const stateMod = require("./state.cjs");
|
|
|
23
23
|
const modelProfilesMod = require("./model-profiles.cjs");
|
|
24
24
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
|
|
25
25
|
const planScanMod = require("./plan-scan.cjs");
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- verification.cjs is an export= CommonJS module
|
|
27
|
+
const verificationMod = require("./verification.cjs");
|
|
26
28
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
|
|
27
29
|
const coreUtilsMod = require("./core-utils.cjs");
|
|
28
30
|
const { findOrphanSummaries, findUnsummarizedPlans } = coreUtilsMod;
|
|
29
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-scope.cjs is an export= CommonJS module
|
|
30
32
|
const planningScopeMod = require("./planning-scope.cjs");
|
|
31
33
|
const { SCOPE } = planningScopeMod;
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- worktree-safety.cjs is an export= CommonJS module
|
|
35
|
+
const worktreeSafetyMod = require("./worktree-safety.cjs");
|
|
36
|
+
// Single owner of git C-quoted-path decoding (see #4081 note at the
|
|
37
|
+
// codebase-drift --name-status parse loop).
|
|
38
|
+
const { decodeGitQuotedPath } = worktreeSafetyMod;
|
|
32
39
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
33
40
|
const security_cjs_1 = require("./security.cjs");
|
|
34
41
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
@@ -57,6 +64,7 @@ const { SEVERITY: HEALTH_SEVERITY, REMEDY_ACTION, REMEDY_RISK, evaluateRules, ev
|
|
|
57
64
|
const planningSnapshotMod = require("./planning-snapshot.cjs");
|
|
58
65
|
const { buildPlanningSnapshot } = planningSnapshotMod;
|
|
59
66
|
const { planningDir } = planningWorkspace;
|
|
67
|
+
const { defaultPhaseCleanCommitTimesMs } = verificationMod;
|
|
60
68
|
const { extractFrontmatter, parseMustHavesBlock } = frontmatterMod;
|
|
61
69
|
const { readStateHeadFreshness } = stateMod;
|
|
62
70
|
/**
|
|
@@ -694,6 +702,189 @@ function scanFileWideNegativeGateConflict(content) {
|
|
|
694
702
|
// This detector is warn-only: it never sets valid=false.
|
|
695
703
|
return { warnings, valid: true };
|
|
696
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* Issue #4024 — quantitative-criteria trap-shape scanner, the third plan-discipline
|
|
707
|
+
* gate in this family (after #429's comment-echo gate and #968's file-wide
|
|
708
|
+
* negative-gate conflict detector). #429/#968 judge `grep -c … == 0` shapes; this
|
|
709
|
+
* scanner judges the OTHER quantitative criterion shapes that are provably traps
|
|
710
|
+
* at HEAD, so a plan whose criteria are unsatisfiable-or-vacuous before any
|
|
711
|
+
* executor touches them no longer passes `verify plan-structure` clean.
|
|
712
|
+
*
|
|
713
|
+
* Scope: criteria text only — `<acceptance_criteria>`, `<automated>`, and
|
|
714
|
+
* `<verify>` blocks. Prose elsewhere in the plan (e.g. an explanatory
|
|
715
|
+
* `<action>`) is not judged (fail-open; see the negative-space section of the
|
|
716
|
+
* #4024 diagnosis).
|
|
717
|
+
*
|
|
718
|
+
* Ban list, each rule with a corrected arm asserted in tests (a rule that fires
|
|
719
|
+
* on its own fix is a refusal, not a rule):
|
|
720
|
+
* R1 (error) exact count out of `grep -c` with N ≥ 2 — `grep -c` counts LINES,
|
|
721
|
+
* not matches. Hedged counts (`>=`, "at least"), `== 0` (the #429/#968
|
|
722
|
+
* family) and the `== 1` presence idiom stay clean.
|
|
723
|
+
* R2 (error) bulk observed-failing claims — "all N tests … observed failing" /
|
|
724
|
+
* "tests N through M … each … observed failing". A test asserting a
|
|
725
|
+
* non-change cannot go red before the change exists. Per-test
|
|
726
|
+
* "discriminates" wording and subset claims stay clean.
|
|
727
|
+
* R3 (error) `$VAR` unquoted in command position — zsh does not word-split an
|
|
728
|
+
* unquoted expansion, the command exits 127, and any arm reading its
|
|
729
|
+
* status passes vacuously.
|
|
730
|
+
* R4 (warn) a fallible command (`git …`) in a non-final pipeline stage — the
|
|
731
|
+
* pipeline reports the LAST stage's status, so the failure is swallowed.
|
|
732
|
+
* Warn-only: the shape is ambiguous unless the criterion reads the status.
|
|
733
|
+
* R5 (error) `wc` output compared by string equality (`… | wc -l | grep -x 0`)
|
|
734
|
+
* — BSD `wc` pads its output, so the comparison never matches.
|
|
735
|
+
* R6 (error) `git diff`/`git log` with a relative `HEAD~N` anchor — it names
|
|
736
|
+
* whatever commit happened to land (another session's). Bare `git diff`
|
|
737
|
+
* with no range is a WARNING: a committed change produces no output and
|
|
738
|
+
* the check passes, but an uncommitted-tree check is a legitimate idiom.
|
|
739
|
+
*
|
|
740
|
+
* Legitimate exit: `<!-- plan-criteria-allow: R# - reason -->` (house style of
|
|
741
|
+
* `planner-discipline-allow`); the reason must be non-empty. Fail open: no
|
|
742
|
+
* criteria zones → no findings; every parse is text-only, nothing executes.
|
|
743
|
+
*/
|
|
744
|
+
function scanQuantitativeCriteria(content) {
|
|
745
|
+
const errors = [];
|
|
746
|
+
const warnings = [];
|
|
747
|
+
// Criteria zones: <acceptance_criteria>, <automated>, <verify>. A <verify>
|
|
748
|
+
// block contains its <automated> child, so the same segment may be harvested
|
|
749
|
+
// twice — dedupe findings by message below.
|
|
750
|
+
const zones = [];
|
|
751
|
+
for (const tag of ['acceptance_criteria', 'automated', 'verify']) {
|
|
752
|
+
for (const block of (0, markdown_sectionizer_cjs_1.extractTaggedBlocks)(content || '', tag)) {
|
|
753
|
+
zones.push({ text: block, prose: tag === 'acceptance_criteria' });
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (zones.length === 0)
|
|
757
|
+
return { errors, warnings };
|
|
758
|
+
// Allow markers may sit anywhere in the plan (same as the sibling scanners).
|
|
759
|
+
// The reason must be non-empty: `R1 - -->` is not an auditable exit.
|
|
760
|
+
const allow = new Set();
|
|
761
|
+
const allowRe = /<!--\s*plan-criteria-allow:\s*(R[1-6])\s+-\s+([^>]*\S)\s*-->/g;
|
|
762
|
+
let am;
|
|
763
|
+
while ((am = allowRe.exec(content || '')) !== null)
|
|
764
|
+
allow.add(am[1]);
|
|
765
|
+
const record = (bucket, rule, message, seen) => {
|
|
766
|
+
if (allow.has(rule) || seen.has(message))
|
|
767
|
+
return;
|
|
768
|
+
seen.add(message);
|
|
769
|
+
bucket.push(message);
|
|
770
|
+
};
|
|
771
|
+
// A count-grep invocation (grep with -c / --count), per the #429 idiom.
|
|
772
|
+
const countGrepRe = /grep((?:\s+-{1,2}[A-Za-z][A-Za-z-]*)+)\s+(?:'[^']*'|"[^"]*"|[^\s|>&;]+)/g;
|
|
773
|
+
const optsHaveCount = (opts) => /(?:^|\s)-[A-Za-z]*c[A-Za-z]*(?=\s|$)/.test(opts) || /--count\b/.test(opts);
|
|
774
|
+
const hasCountGrep = (s) => {
|
|
775
|
+
countGrepRe.lastIndex = 0;
|
|
776
|
+
let m;
|
|
777
|
+
while ((m = countGrepRe.exec(s)) !== null) {
|
|
778
|
+
if (optsHaveCount(m[1]))
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
return false;
|
|
782
|
+
};
|
|
783
|
+
const seenErr = new Set();
|
|
784
|
+
const seenWarn = new Set();
|
|
785
|
+
for (const zone of zones) {
|
|
786
|
+
// Same normalization as the sibling scanners (#3611): newline join,
|
|
787
|
+
// backslash continuation join, entity-amp decode.
|
|
788
|
+
const text = decodeEntityAmps(zone.text
|
|
789
|
+
.replace(/\r\n/g, '\n')
|
|
790
|
+
.replace(/\r/g, '\n')
|
|
791
|
+
.replace(/\\\n/g, ' '));
|
|
792
|
+
// R2 — bulk observed-failing prose (acceptance_criteria only). The gap
|
|
793
|
+
// [^\n.]{0,120} keeps the subject and the claim adjacent within one
|
|
794
|
+
// sentence, so the corrected arm (a subset claim, or "each test
|
|
795
|
+
// discriminates") never fires.
|
|
796
|
+
if (zone.prose) {
|
|
797
|
+
const bulkRedRes = [
|
|
798
|
+
/\ball\s+(?:\d+|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\s+tests\b[^\n.]{0,120}?observed\s+fail/i,
|
|
799
|
+
/\btests\s+\d+\s+through\s+\d+\b[^\n.]{0,120}?observed\s+fail/i,
|
|
800
|
+
];
|
|
801
|
+
for (const re of bulkRedRes) {
|
|
802
|
+
if (re.test(text)) {
|
|
803
|
+
record(errors, 'R2', '[plan-criteria R2] A bulk "all N tests / tests N through M were observed failing" criterion is ' +
|
|
804
|
+
'unsatisfiable whenever any of the N tests asserts a non-change, which cannot go red before the ' +
|
|
805
|
+
'change exists. Say "each test discriminates" (fails before the change, passes after), and attach a ' +
|
|
806
|
+
'mutation record for any test that cannot be seen red.', seenErr);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
for (const seg of text.split('\n').flatMap(splitShellSegments)) {
|
|
811
|
+
// R1 — exact count out of `grep -c` (N ≥ 2). Spaced `==`/`-eq` (the
|
|
812
|
+
// #429 zeroCmp idiom, excluding assignments and >=/<=/!=), or the prose
|
|
813
|
+
// "returns exactly N".
|
|
814
|
+
if (hasCountGrep(seg)) {
|
|
815
|
+
const exact = [];
|
|
816
|
+
let m;
|
|
817
|
+
const shellCmp = /\s==?\s*(\d+)\b/g;
|
|
818
|
+
while ((m = shellCmp.exec(seg)) !== null)
|
|
819
|
+
exact.push(parseInt(m[1], 10));
|
|
820
|
+
const eqCmp = /-eq\s+(\d+)\b/g;
|
|
821
|
+
while ((m = eqCmp.exec(seg)) !== null)
|
|
822
|
+
exact.push(parseInt(m[1], 10));
|
|
823
|
+
const proseCmp = /\breturns?\s+exactly\s+(\d+)\b/gi;
|
|
824
|
+
while ((m = proseCmp.exec(seg)) !== null)
|
|
825
|
+
exact.push(parseInt(m[1], 10));
|
|
826
|
+
if (exact.some((n) => n >= 2)) {
|
|
827
|
+
record(errors, 'R1', '[plan-criteria R1] An exact count out of `grep -c` is a trap: `grep -c` counts LINES, not matches, ' +
|
|
828
|
+
'so the criterion can be false at HEAD before any executor touches it. Use `>= 1`, `== 0`, or ' +
|
|
829
|
+
'`grep -n` and read the line numbers.', seenErr);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// R3 — unquoted $VAR in command position (zsh does not word-split; the
|
|
833
|
+
// command exits 127 and any arm reading its status passes vacuously).
|
|
834
|
+
// The anchor tolerates a markdown list bullet and/or an inline-code
|
|
835
|
+
// backtick before the command — criteria are prose lines like
|
|
836
|
+
// "- `$NOKEY npx tsx x.ts` exits 0" — but nothing else: the expansion
|
|
837
|
+
// must still be the FIRST token of the command itself.
|
|
838
|
+
if (/^(?:[-*]\s*)?\u0060?\$[A-Z_][A-Z0-9_]*\s/.test(seg)) {
|
|
839
|
+
record(errors, 'R3', '[plan-criteria R3] `$VAR` unquoted in command position does not word-split under zsh — the shell ' +
|
|
840
|
+
'looks for a command literally named by the whole expansion and exits 127, so any arm reading its ' +
|
|
841
|
+
'status passes vacuously. Write the prefix inline or use a shell function.', seenErr);
|
|
842
|
+
}
|
|
843
|
+
// R4 — a fallible `git …` in a non-final pipeline stage: the pipeline
|
|
844
|
+
// reports the LAST stage's status, so git's failure is swallowed.
|
|
845
|
+
// Warn-only: without a status assertion the shape is ambiguous.
|
|
846
|
+
// Not a markdown table: the negated-pipe class matches a SHELL pipeline
|
|
847
|
+
// stage boundary (git before the next `|`), the same shape the
|
|
848
|
+
// #429/#968 scanners use; there is no table row to parse.
|
|
849
|
+
// allow-adhoc-markdown: shell pipeline stage boundary, not a table cell (#4024)
|
|
850
|
+
if (/\bgit\s+[a-z][^\n|]*\|/.test(seg)) {
|
|
851
|
+
record(warnings, 'R4', '[plan-criteria R4] A fallible `git` in a non-final pipeline stage is swallowed — the pipeline reports ' +
|
|
852
|
+
'the last stage\'s status, so a broken command reads as clean. Capture the status first.', seenWarn);
|
|
853
|
+
}
|
|
854
|
+
// R5 — `wc` output compared by string equality: BSD `wc` pads its output
|
|
855
|
+
// (seven spaces then the number), so `grep -x 0` can never match.
|
|
856
|
+
if (/\bwc\b/.test(seg) && /\bgrep\s+(?:-{1,2}[A-Za-z]*x[A-Za-z]*|--line-regexp)\s+['"]?\d+['"]?/.test(seg)) {
|
|
857
|
+
record(errors, 'R5', '[plan-criteria R5] `wc` output compared by string equality is unsatisfiable on BSD `wc`, which pads ' +
|
|
858
|
+
'its output before the number. Compare numerically: `test "$n" -eq 0`, or `tr -d \' \'` first.', seenErr);
|
|
859
|
+
}
|
|
860
|
+
// R6 — relative HEAD~N anchors name whatever commit happened to land;
|
|
861
|
+
// a bare `git diff` with no range makes a committed change invisible.
|
|
862
|
+
if (/\bgit\s+(?:diff|log)\b[^\n|;&]*\bHEAD~\d+/.test(seg)) {
|
|
863
|
+
record(errors, 'R6', '[plan-criteria R6] `git diff`/`git log` with a relative `HEAD~N` anchor names whatever commit ' +
|
|
864
|
+
'happened to land last — in a repository with parallel sessions that is another session\'s commit. ' +
|
|
865
|
+
'Pin an explicit range: `<sha>^..<sha>`.', seenErr);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
// The argument span ends at the closing backtick of an inline-code
|
|
869
|
+
// span ("`git diff` shows ..."), at a redirection, or at a chain
|
|
870
|
+
// operator — trailing prose must never read as diff arguments.
|
|
871
|
+
const bareDiff = /\bgit\s+diff\b([^\u0060\n|;&<>]*)/.exec(seg);
|
|
872
|
+
if (bareDiff) {
|
|
873
|
+
const rest = bareDiff[1].trim();
|
|
874
|
+
// Bare or flags-only (no rev, range, or path): ambiguous between an
|
|
875
|
+
// uncommitted-tree check (legitimate) and a committed-change check
|
|
876
|
+
// (vacuously clean) — warn, never error.
|
|
877
|
+
if (rest === '' || /^-{1,2}[A-Za-z-]+$/.test(rest)) {
|
|
878
|
+
record(warnings, 'R6', '[plan-criteria R6] Bare `git diff` with no revision range produces no output for a COMMITTED ' +
|
|
879
|
+
'change, so the criterion passes vacuously. Pin an explicit range (`<sha>^..<sha>`) unless the ' +
|
|
880
|
+
'check is intentionally about the uncommitted tree.', seenWarn);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return { errors, warnings };
|
|
887
|
+
}
|
|
697
888
|
/**
|
|
698
889
|
* Single pass over `<task ...>…</task>` blocks. The body pattern is
|
|
699
890
|
* ReDoS-safe stop-at-next-open (mirrors `taggedBlockPattern` in
|
|
@@ -904,6 +1095,12 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
|
904
1095
|
warnings.push(...echoScan.warnings);
|
|
905
1096
|
const conflictScan = scanFileWideNegativeGateConflict(content);
|
|
906
1097
|
warnings.push(...conflictScan.warnings);
|
|
1098
|
+
// #4024: quantitative-criteria trap shapes (exact grep -c counts, bulk
|
|
1099
|
+
// observed-failing claims, $VAR command position, swallowed pipeline
|
|
1100
|
+
// stages, wc string-equality, relative git anchors).
|
|
1101
|
+
const quantScan = scanQuantitativeCriteria(content);
|
|
1102
|
+
errors.push(...quantScan.errors);
|
|
1103
|
+
warnings.push(...quantScan.warnings);
|
|
907
1104
|
output({
|
|
908
1105
|
valid: errors.length === 0,
|
|
909
1106
|
errors,
|
|
@@ -1080,12 +1277,19 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
|
|
|
1080
1277
|
results.push(check);
|
|
1081
1278
|
}
|
|
1082
1279
|
const passed = results.filter((r) => r['passed']).length;
|
|
1280
|
+
// Positive-evidence floor (#3956): a non-empty artifacts block whose items are
|
|
1281
|
+
// all bare strings / path-less objects is item-by-item skipped, leaving results
|
|
1282
|
+
// empty; `passed === results.length` would then be `0 === 0` → a vacuous GREEN
|
|
1283
|
+
// over zero checks. Require at least one checked artifact, mirroring the
|
|
1284
|
+
// no-vacuous-pass rule at src/uat-predicate.cts. The fully-empty block is still
|
|
1285
|
+
// caught earlier by the `artifacts.length === 0` guard and returns its error.
|
|
1286
|
+
const allPassed = results.length > 0 && passed === results.length;
|
|
1083
1287
|
output({
|
|
1084
|
-
all_passed:
|
|
1288
|
+
all_passed: allPassed,
|
|
1085
1289
|
passed,
|
|
1086
1290
|
total: results.length,
|
|
1087
1291
|
artifacts: results,
|
|
1088
|
-
}, raw,
|
|
1292
|
+
}, raw, allPassed ? 'valid' : 'invalid');
|
|
1089
1293
|
}
|
|
1090
1294
|
/**
|
|
1091
1295
|
* Returns a Set of file paths (relative to cwd) that are promised by plans in
|
|
@@ -1299,7 +1503,17 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
|
|
|
1299
1503
|
// A pending link (from: file promised by a same-or-later-wave plan) is not a
|
|
1300
1504
|
// hard failure — it should not count against the all_verified gate (#1202).
|
|
1301
1505
|
const hardFailed = results.filter((r) => !r['verified'] && !r['pending']).length;
|
|
1302
|
-
|
|
1506
|
+
// Positive-evidence floor (#3956): an all-bare-string key_links block skips
|
|
1507
|
+
// every item (only `typeof link === 'string'` items are continue-skipped
|
|
1508
|
+
// above), leaving results empty; `hardFailed === 0` would then be a vacuous
|
|
1509
|
+
// GREEN over zero checks. Require at least one checked link. (A `from:`-less
|
|
1510
|
+
// object is NOT skipped, unlike a path-less object on the artifacts side — it
|
|
1511
|
+
// falls through to a `verified: false` result and hard-fails, so it was never
|
|
1512
|
+
// part of the vacuous-pass surface; only the all-bare-string case is.) A
|
|
1513
|
+
// pending link IS pushed to results (with pending: true), so an all-pending
|
|
1514
|
+
// block still satisfies results.length > 0 and its #1202 non-hard-failing
|
|
1515
|
+
// semantics are unchanged — the floor only rejects the zero-result case.
|
|
1516
|
+
const allVerified = results.length > 0 && hardFailed === 0;
|
|
1303
1517
|
output({
|
|
1304
1518
|
all_verified: allVerified,
|
|
1305
1519
|
verified,
|
|
@@ -1593,6 +1807,121 @@ function cmdValidateAgents(cwd, raw) {
|
|
|
1593
1807
|
sandbox_posture: sandboxPosture,
|
|
1594
1808
|
}, raw);
|
|
1595
1809
|
}
|
|
1810
|
+
// ─── Context drift (#3348) ───────────────────────────────────────────────────
|
|
1811
|
+
/**
|
|
1812
|
+
* Resolve a phase directory under `phasesDir` from a user-supplied `phaseArg`,
|
|
1813
|
+
* via the canonical phase-directory matcher (phase-id.cjs::matchPhaseDirs) rather
|
|
1814
|
+
* than a naive substring test — a bare `.includes(phaseArg)` lets a non-existent
|
|
1815
|
+
* phase silently match a different phase whose directory name merely contains the
|
|
1816
|
+
* requested token (e.g. "1" matching "11-expansion"). Falls back to an exact
|
|
1817
|
+
* directory-name match. Returns null if neither resolves. (#1571, #2528)
|
|
1818
|
+
*/
|
|
1819
|
+
function resolvePhaseDirByToken(phasesDir, phaseArg) {
|
|
1820
|
+
const normalizedPhase = normalizePhaseName(phaseArg);
|
|
1821
|
+
const dirEntries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
1822
|
+
const dirNames = dirEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1823
|
+
const matched = matchPhaseDirs(dirNames, normalizedPhase).matches[0];
|
|
1824
|
+
if (matched)
|
|
1825
|
+
return node_path_1.default.join(phasesDir, matched);
|
|
1826
|
+
const check = (0, security_cjs_1.validatePath)(phaseArg, phasesDir);
|
|
1827
|
+
if (check.safe && node_fs_1.default.existsSync(check.resolved))
|
|
1828
|
+
return check.resolved;
|
|
1829
|
+
return null;
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Pure comparator: which of `entries` have an effective last-changed time
|
|
1833
|
+
* STRICTLY BEFORE `contextEffectiveMs` (CONTEXT.md's own effective time)? Strict
|
|
1834
|
+
* `<` is "stale" (matches findStaleVerificationSummary's own strict `>` convention
|
|
1835
|
+
* for "newer than" elsewhere in this codebase — an artifact committed in the SAME
|
|
1836
|
+
* commit/second as CONTEXT.md is in sync, not stale).
|
|
1837
|
+
*/
|
|
1838
|
+
function computeContextDrift(contextEffectiveMs, entries) {
|
|
1839
|
+
return entries.filter((e) => e.effectiveMs < contextEffectiveMs).map((e) => e.file);
|
|
1840
|
+
}
|
|
1841
|
+
function buildContextDriftMessage(staleArtifacts, phaseArg) {
|
|
1842
|
+
const parts = [`CONTEXT.md decisions are newer than: ${staleArtifacts.join(', ')}.`];
|
|
1843
|
+
if (staleArtifacts.some((f) => f.endsWith('-RESEARCH.md'))) {
|
|
1844
|
+
parts.push(`Regenerate research: /gsd:plan-phase ${phaseArg} --research.`);
|
|
1845
|
+
}
|
|
1846
|
+
if (staleArtifacts.some((f) => f.endsWith('-PATTERNS.md'))) {
|
|
1847
|
+
parts.push('Regenerate patterns: delete the PATTERNS.md file, then re-run /gsd:plan-phase.');
|
|
1848
|
+
}
|
|
1849
|
+
if (staleArtifacts.some((f) => f.endsWith('-VALIDATION.md') || (f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md')))) {
|
|
1850
|
+
parts.push('Regenerate or manually reconcile VALIDATION.md / SPEC.md against the current decisions.');
|
|
1851
|
+
}
|
|
1852
|
+
parts.push('Do not hand-inject the newer decisions into a prompt as a substitute for regenerating — that carries the staleness forward.');
|
|
1853
|
+
return parts.join(' ');
|
|
1854
|
+
}
|
|
1855
|
+
function cmdVerifyContextDrift(cwd, phaseArg, raw) {
|
|
1856
|
+
if (!phaseArg) {
|
|
1857
|
+
error('Usage: verify context-drift <phase>');
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const pDir = planningDir(cwd);
|
|
1861
|
+
const phasesDir = node_path_1.default.join(pDir, 'phases');
|
|
1862
|
+
const emitSkip = (reason, message = '') => {
|
|
1863
|
+
output({ block: false, skipped: true, reason, stale_artifacts: [], message }, raw);
|
|
1864
|
+
};
|
|
1865
|
+
if (!node_fs_1.default.existsSync(phasesDir)) {
|
|
1866
|
+
emitSkip('phase-not-found', `Phase directory not found: ${phaseArg}`);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
// Same phase-directory resolution rule cmdVerifySchemaDrift uses (#1571, #2528):
|
|
1870
|
+
// matchPhaseDirs, never a naive substring test.
|
|
1871
|
+
const phaseDir = resolvePhaseDirByToken(phasesDir, phaseArg);
|
|
1872
|
+
if (!phaseDir) {
|
|
1873
|
+
emitSkip('phase-not-found', `Phase directory not found: ${phaseArg}`);
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
let phaseFiles;
|
|
1877
|
+
try {
|
|
1878
|
+
phaseFiles = node_fs_1.default.readdirSync(phaseDir).slice().sort();
|
|
1879
|
+
}
|
|
1880
|
+
catch {
|
|
1881
|
+
emitSkip('phase-not-found', `Phase directory not found: ${phaseArg}`);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const contextFile = phaseFiles.find((f) => f.endsWith('-CONTEXT.md'));
|
|
1885
|
+
if (!contextFile) {
|
|
1886
|
+
emitSkip('no-context-md');
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
const researchFile = phaseFiles.find((f) => f.endsWith('-RESEARCH.md'));
|
|
1890
|
+
const patternsFile = phaseFiles.find((f) => f.endsWith('-PATTERNS.md'));
|
|
1891
|
+
const validationFile = phaseFiles.find((f) => f.endsWith('-VALIDATION.md'));
|
|
1892
|
+
const specFile = phaseFiles.find((f) => f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md'));
|
|
1893
|
+
const upstreamFiles = [researchFile, patternsFile, validationFile, specFile].filter((f) => !!f);
|
|
1894
|
+
if (upstreamFiles.length === 0) {
|
|
1895
|
+
emitSkip('no-upstream-artifacts');
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
const allFiles = [contextFile, ...upstreamFiles];
|
|
1899
|
+
const cleanCommitMs = defaultPhaseCleanCommitTimesMs(phaseDir, allFiles);
|
|
1900
|
+
const effectiveTimeMs = (file) => cleanCommitMs.has(file)
|
|
1901
|
+
? cleanCommitMs.get(file)
|
|
1902
|
+
: node_fs_1.default.statSync(node_path_1.default.join(phaseDir, file)).mtimeMs;
|
|
1903
|
+
const contextMs = effectiveTimeMs(contextFile);
|
|
1904
|
+
const driftEntries = upstreamFiles.map((f) => ({ file: f, effectiveMs: effectiveTimeMs(f) }));
|
|
1905
|
+
const staleArtifacts = computeContextDrift(contextMs, driftEntries);
|
|
1906
|
+
let wf;
|
|
1907
|
+
try {
|
|
1908
|
+
const rawCfg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(pDir, 'config.json'), 'utf-8'));
|
|
1909
|
+
wf = rawCfg['workflow'];
|
|
1910
|
+
}
|
|
1911
|
+
catch {
|
|
1912
|
+
wf = undefined;
|
|
1913
|
+
}
|
|
1914
|
+
const action = wf?.context_drift_action === 'block' ? 'block' : 'warn';
|
|
1915
|
+
const block = staleArtifacts.length > 0 && action === 'block';
|
|
1916
|
+
const message = staleArtifacts.length > 0 ? buildContextDriftMessage(staleArtifacts, phaseArg) : '';
|
|
1917
|
+
output({
|
|
1918
|
+
block,
|
|
1919
|
+
skipped: false,
|
|
1920
|
+
stale_artifacts: staleArtifacts,
|
|
1921
|
+
action,
|
|
1922
|
+
message,
|
|
1923
|
+
}, raw);
|
|
1924
|
+
}
|
|
1596
1925
|
function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
|
|
1597
1926
|
if (!phaseArg) {
|
|
1598
1927
|
error('Usage: verify schema-drift <phase> [--skip]');
|
|
@@ -1611,18 +1940,7 @@ function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
|
|
|
1611
1940
|
// matching "11-expansion"), making the drift gate inspect the wrong phase.
|
|
1612
1941
|
// This shares the one selection rule with find-phase / verify
|
|
1613
1942
|
// phase-completeness rather than restating it. (#1571, #2528)
|
|
1614
|
-
|
|
1615
|
-
const normalizedPhase = normalizePhaseName(phaseArg);
|
|
1616
|
-
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
1617
|
-
const dirNames = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1618
|
-
const drift = matchPhaseDirs(dirNames, normalizedPhase).matches[0];
|
|
1619
|
-
if (drift)
|
|
1620
|
-
phaseDir = node_path_1.default.join(phasesDir, drift);
|
|
1621
|
-
if (!phaseDir) {
|
|
1622
|
-
const exact = node_path_1.default.join(phasesDir, phaseArg);
|
|
1623
|
-
if (node_fs_1.default.existsSync(exact))
|
|
1624
|
-
phaseDir = exact;
|
|
1625
|
-
}
|
|
1943
|
+
const phaseDir = resolvePhaseDirByToken(phasesDir, phaseArg);
|
|
1626
1944
|
if (!phaseDir) {
|
|
1627
1945
|
output({ block: false, drift_detected: false, blocking: false, message: `Phase directory not found: ${phaseArg}` }, raw);
|
|
1628
1946
|
return;
|
|
@@ -1748,7 +2066,16 @@ function cmdVerifyCodebaseDrift(cwd, raw) {
|
|
|
1748
2066
|
if (!m)
|
|
1749
2067
|
continue;
|
|
1750
2068
|
const status = m[1];
|
|
1751
|
-
|
|
2069
|
+
// execGit sets no core.quotepath config, so git's default `true` applies:
|
|
2070
|
+
// any path containing non-ASCII bytes (or `"`, `\`, control bytes) is
|
|
2071
|
+
// C-quoted — `"docs/\350\256\276…/overview.md"`. Capturing that verbatim
|
|
2072
|
+
// garbles affected_paths/elements and makes isPathMapped compare the
|
|
2073
|
+
// quoted prefix (`"docs`) against STRUCTURE.md, misclassifying DOCUMENTED
|
|
2074
|
+
// directories as new_dir (#4081). Decode with the single owner of the
|
|
2075
|
+
// git C-quote seam (worktree-safety.cjs); a non-quoted value — the plain
|
|
2076
|
+
// ASCII common case — passes through untouched. Both capture groups are
|
|
2077
|
+
// decoded: R/C lines carry old AND new paths, either may be quoted.
|
|
2078
|
+
const file = decodeGitQuotedPath(m[3] || m[2]);
|
|
1752
2079
|
if (status === 'A' || status === 'R' || status === 'C')
|
|
1753
2080
|
added.push(file);
|
|
1754
2081
|
else if (status === 'M')
|
|
@@ -1811,6 +2138,7 @@ function cmdVerifyCodebaseDrift(cwd, raw) {
|
|
|
1811
2138
|
module.exports = {
|
|
1812
2139
|
scanNegativeGrepCommentEcho,
|
|
1813
2140
|
scanFileWideNegativeGateConflict,
|
|
2141
|
+
scanQuantitativeCriteria,
|
|
1814
2142
|
cmdVerifySummary,
|
|
1815
2143
|
verifySummaryCore,
|
|
1816
2144
|
cmdVerifyPlanStructure,
|
|
@@ -1824,5 +2152,7 @@ module.exports = {
|
|
|
1824
2152
|
cmdValidateAgents,
|
|
1825
2153
|
cmdVerifySchemaDrift,
|
|
1826
2154
|
cmdVerifyCodebaseDrift,
|
|
2155
|
+
computeContextDrift,
|
|
2156
|
+
cmdVerifyContextDrift,
|
|
1827
2157
|
STATE_HEAD_ADVISORY_COMMITS,
|
|
1828
2158
|
};
|
|
@@ -63,7 +63,19 @@ function countRoadmapPhases(roadmapPath, fallbackCount, cwd, ws, versionOverride
|
|
|
63
63
|
return fallbackCount;
|
|
64
64
|
if (!cwd)
|
|
65
65
|
return fallbackCount;
|
|
66
|
-
|
|
66
|
+
// #2761 B1 (trek-e review): `undefined`, NOT `null`. The third argument
|
|
67
|
+
// discriminates "not resolved yet — resolve it from this workstream's
|
|
68
|
+
// config" (`undefined`) from "resolved, and it is not bracket" (`null`).
|
|
69
|
+
// This site meant the former and spelled the latter, so a workstream that
|
|
70
|
+
// opted into `phase_id_convention: "bracket"` had its ROADMAP scanned with
|
|
71
|
+
// the LEGACY grammar: the heading set came back empty, `phaseCount` was 0,
|
|
72
|
+
// and the count silently fell back to the on-disk directory count — the
|
|
73
|
+
// very degrade the #3185 rewrite of this function existed to remove.
|
|
74
|
+
// Measured on a bracket workstream whose milestone declares 3 phases:
|
|
75
|
+
// `null` -> 0 (fallback), `undefined` -> 3. A non-bracket workstream
|
|
76
|
+
// resolves to a value that compiles the same base grammar `null` did, so
|
|
77
|
+
// its count is unchanged (verified against a legacy control).
|
|
78
|
+
const filter = getMilestonePhaseFilter(cwd, versionOverride ?? null, undefined, ws ?? null);
|
|
67
79
|
// A pass-all degrade (phaseCount 0) means the window declared no phases —
|
|
68
80
|
// fall back rather than reporting a confident zero.
|
|
69
81
|
return filter.phaseCount > 0 ? filter.phaseCount : fallbackCount;
|
|
@@ -437,7 +449,13 @@ function inspectWorkstream(cwd, name, options = {}) {
|
|
|
437
449
|
// filtering. Consulted only when it is genuinely scoped to a single milestone
|
|
438
450
|
// (`versionScoped`); the unversioned whole-roadmap shape spans the project's
|
|
439
451
|
// lifetime and would re-admit prior-milestone phases — the very defect here.
|
|
440
|
-
|
|
452
|
+
//
|
|
453
|
+
// #2761 B1 (trek-e review): `undefined`, not `null` — same discriminator fix
|
|
454
|
+
// as `countRoadmapPhases` above. This site iterates workstreams by NAME, so
|
|
455
|
+
// it is exactly the caller that cannot set `GSD_WORKSTREAM`; spelling `null`
|
|
456
|
+
// pinned every workstream to the legacy grammar and made `headingScoped`
|
|
457
|
+
// unreachable (`phaseCount > 0` never held) on a bracket workstream.
|
|
458
|
+
const headingFilter = getMilestonePhaseFilter(cwd, currentVersion, undefined, name);
|
|
441
459
|
const headingScoped = headingFilter.versionScoped && headingFilter.phaseCount > 0;
|
|
442
460
|
// Phase keys the ROADMAP attributes to some OTHER milestone. A row carrying an
|
|
443
461
|
// explicit version that is not the current one is a positive claim by a prior
|
|
@@ -509,6 +509,11 @@ const SUMMARY_ARTIFACT_SUFFIX = 'SUMMARY.md';
|
|
|
509
509
|
* (a trailing lone backslash, or an octal escape with fewer than three
|
|
510
510
|
* digits) never throws — it degrades to treating the character literally, so
|
|
511
511
|
* a single bad path can never take down the whole cleanup wave.
|
|
512
|
+
*
|
|
513
|
+
* Exported (#4081) so the codebase-drift gate's `--name-status` parser can
|
|
514
|
+
* decode the identical C-quoted form before classifying paths — the gate's
|
|
515
|
+
* `execGit` call sets no `core.quotepath` config, so it receives the same
|
|
516
|
+
* quoting and must decode it with the same single owner of this seam.
|
|
512
517
|
*/
|
|
513
518
|
function decodeGitQuotedPath(raw) {
|
|
514
519
|
if (raw.length < 2 || !raw.startsWith('"') || !raw.endsWith('"'))
|
|
@@ -1986,6 +1991,9 @@ function pruneOrphanedWorktrees(repoRoot, deps = {}) {
|
|
|
1986
1991
|
return [];
|
|
1987
1992
|
}
|
|
1988
1993
|
module.exports = {
|
|
1994
|
+
// Re-exported for the codebase-drift gate's --name-status parser (#4081):
|
|
1995
|
+
// single owner of git C-quoted-path decoding.
|
|
1996
|
+
decodeGitQuotedPath,
|
|
1989
1997
|
resolveWorktreeContext,
|
|
1990
1998
|
resolveWorktreeLinkage,
|
|
1991
1999
|
parseWorktreePorcelain,
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"git.branching_strategy",
|
|
46
46
|
"git.base_branch",
|
|
47
47
|
"git.protected_branches",
|
|
48
|
+
"git.allow_default_branch_commits",
|
|
48
49
|
"git.create_tag",
|
|
49
50
|
"git.phase_branch_template",
|
|
50
51
|
"git.milestone_branch_template",
|
|
@@ -53,6 +54,8 @@
|
|
|
53
54
|
"planning.pr_strict",
|
|
54
55
|
"planning.search_gitignored",
|
|
55
56
|
"planning.sub_repos",
|
|
57
|
+
"planning.chunked_parallel",
|
|
58
|
+
"agent_tools",
|
|
56
59
|
"review.default_reviewers",
|
|
57
60
|
"review.max_prompt_tokens",
|
|
58
61
|
"review.max_prompt_tokens_per_reviewer",
|
|
@@ -118,6 +121,11 @@
|
|
|
118
121
|
"workflow._auto_chain_active"
|
|
119
122
|
],
|
|
120
123
|
"dynamicKeyPatterns": [
|
|
124
|
+
{
|
|
125
|
+
"topLevel": "agent_tools",
|
|
126
|
+
"source": "^agent_tools\\.(\\*|[a-zA-Z0-9_-]+)$",
|
|
127
|
+
"description": "agent_tools.<selector>"
|
|
128
|
+
},
|
|
121
129
|
{
|
|
122
130
|
"topLevel": "agent_skills",
|
|
123
131
|
"source": "^agent_skills\\.[a-zA-Z0-9_-]+$",
|
|
@@ -184,9 +184,72 @@ const REASON = Object.freeze({
|
|
|
184
184
|
FAIL_USER_LINES_MISSING: 'fail_user_lines_missing',
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
/**
|
|
188
|
+
* #4086: resolve where a backed-up file's INSTALLED counterpart lives.
|
|
189
|
+
* Primary is the config-dir-relative join (the manifest key's native form).
|
|
190
|
+
* For skills/ keys of a runtime whose skills kind declares a `home` override
|
|
191
|
+
* (codex global → $HOME/.agents/skills), the file lives OUTSIDE configDir, so
|
|
192
|
+
* when the primary path is absent we fall back to the runtime's ACTUAL skills
|
|
193
|
+
* root — derived from the same `resolveRuntimeArtifactLayout` seam the
|
|
194
|
+
* installer writes through. Returns the primary path unchanged whenever the
|
|
195
|
+
* fallback is impossible (no manifest, no runtime, no override, no file).
|
|
196
|
+
*/
|
|
197
|
+
function resolveInstalledPath(configDir, relPath, skillsRedirect) {
|
|
198
|
+
const primary = path.join(configDir, relPath);
|
|
199
|
+
if (!skillsRedirect) return primary;
|
|
200
|
+
const hashKey = relPath.replace(/\\/g, '/');
|
|
201
|
+
if (!hashKey.startsWith(skillsRedirect.prefix)) return primary;
|
|
202
|
+
if (fs.existsSync(primary)) return primary;
|
|
203
|
+
const alt = path.join(skillsRedirect.root, hashKey.slice(skillsRedirect.prefix.length));
|
|
204
|
+
return fs.existsSync(alt) ? alt : primary;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* #4086: build the skills-root fallback descriptor for a config dir from its
|
|
209
|
+
* own gsd-file-manifest.json (runtime + scope) and the runtime-artifact
|
|
210
|
+
* layout seam. Returns null whenever any step is unavailable — the verifier
|
|
211
|
+
* then behaves exactly as before (config-dir-relative only). Lazy + guarded
|
|
212
|
+
* require: runtime-artifact-layout.cjs is a built lib; a layout-unaware
|
|
213
|
+
* invocation must never crash the gate.
|
|
214
|
+
*/
|
|
215
|
+
function resolveSkillsRedirect(configDir) {
|
|
216
|
+
try {
|
|
217
|
+
const manifest = JSON.parse(
|
|
218
|
+
fs.readFileSync(path.join(configDir, 'gsd-file-manifest.json'), 'utf8'),
|
|
219
|
+
);
|
|
220
|
+
if (!manifest || typeof manifest.runtime !== 'string' || !manifest.runtime) return null;
|
|
221
|
+
const scope = manifest.scope === 'local' ? 'local' : 'global';
|
|
222
|
+
const { resolveRuntimeArtifactLayout } = require('./lib/runtime-artifact-layout.cjs');
|
|
223
|
+
const layout = resolveRuntimeArtifactLayout(manifest.runtime, configDir, scope);
|
|
224
|
+
const kind = layout.kinds.find((k) => k.kind === 'skills');
|
|
225
|
+
if (!kind) return null;
|
|
226
|
+
const root = path.resolve(path.join(kind.home || configDir, kind.destSubpath));
|
|
227
|
+
const resolvedConfig = path.resolve(configDir);
|
|
228
|
+
if (root === resolvedConfig || root.startsWith(resolvedConfig + path.sep)) return null;
|
|
229
|
+
// Same descriptor-driven manifest prefix writeManifest uses for skills
|
|
230
|
+
// keys (hermes nests under 'skills/gsd/', everyone else 'skills/'), read
|
|
231
|
+
// from the same shipped registry the installer's _resolveHostBehaviors
|
|
232
|
+
// consults — never a re-derived literal (generative-fix-divergence guard).
|
|
233
|
+
let prefix = 'skills/';
|
|
234
|
+
try {
|
|
235
|
+
const { runtimes: registryRuntimes } = require('./lib/capability-registry.cjs');
|
|
236
|
+
const declared = registryRuntimes
|
|
237
|
+
&& registryRuntimes[manifest.runtime]
|
|
238
|
+
&& registryRuntimes[manifest.runtime].runtime
|
|
239
|
+
&& registryRuntimes[manifest.runtime].runtime.hostBehaviors;
|
|
240
|
+
if (declared && typeof declared.skillsManifestPrefix === 'string') {
|
|
241
|
+
prefix = declared.skillsManifestPrefix;
|
|
242
|
+
}
|
|
243
|
+
} catch { /* keep default prefix */ }
|
|
244
|
+
return { root, prefix };
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function verifyFile({ relPath, patchesDir, configDir, pristineDir, pristineHashes, skillsRedirect }) {
|
|
188
251
|
const backupPath = path.join(patchesDir, relPath);
|
|
189
|
-
const installedPath =
|
|
252
|
+
const installedPath = resolveInstalledPath(configDir, relPath, skillsRedirect);
|
|
190
253
|
const result = { file: relPath, status: 'ok', missing: [], reason: null };
|
|
191
254
|
|
|
192
255
|
if (!fs.existsSync(backupPath) || !fs.statSync(backupPath).isFile()) {
|
|
@@ -336,6 +399,9 @@ function main() {
|
|
|
336
399
|
// Bug #3657: read pristine_hashes from backup-meta.json once and share
|
|
337
400
|
// across all per-file verifications so each can detect drift independently.
|
|
338
401
|
const pristineHashes = readPristineHashes(opts.patchesDir);
|
|
402
|
+
// #4086: skills-root fallback for runtimes whose skills kind declares a
|
|
403
|
+
// `home` override outside configDir (codex global → $HOME/.agents/skills).
|
|
404
|
+
const skillsRedirect = resolveSkillsRedirect(opts.configDir);
|
|
339
405
|
const results = files.map((relPath) =>
|
|
340
406
|
verifyFile({
|
|
341
407
|
relPath,
|
|
@@ -343,6 +409,7 @@ function main() {
|
|
|
343
409
|
configDir: opts.configDir,
|
|
344
410
|
pristineDir: opts.pristineDir,
|
|
345
411
|
pristineHashes,
|
|
412
|
+
skillsRedirect,
|
|
346
413
|
}),
|
|
347
414
|
);
|
|
348
415
|
|
|
@@ -396,4 +463,4 @@ if (require.main === module) {
|
|
|
396
463
|
runMain(main);
|
|
397
464
|
}
|
|
398
465
|
|
|
399
|
-
module.exports = { computeUserAddedLines, isSignificantLine, verifyFile, walk, REASON, readPristineHashes, sha256 };
|
|
466
|
+
module.exports = { computeUserAddedLines, isSignificantLine, verifyFile, walk, REASON, readPristineHashes, sha256, resolveInstalledPath, resolveSkillsRedirect };
|