@opengsd/gsd-core 1.14.0 → 1.15.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/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +8 -7
- package/agents/gsd-planner.md +12 -8
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +642 -95
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/mempalace-capture.md +7 -3
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/review.md +2 -3
- package/gsd-core/bin/gsd-tools.cjs +335 -22
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/audit.cjs +81 -13
- package/gsd-core/bin/lib/capability-registry.cjs +82 -187
- package/gsd-core/bin/lib/capability-validator.cjs +0 -1
- package/gsd-core/bin/lib/check-command-router.cjs +101 -14
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +175 -42
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +33 -7
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +207 -41
- package/gsd-core/bin/lib/install-engine.cjs +13 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/milestone.cjs +18 -5
- package/gsd-core/bin/lib/model-resolver.cjs +159 -50
- package/gsd-core/bin/lib/phase-command-router.cjs +9 -1
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +109 -7
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +227 -26
- package/gsd-core/bin/lib/plan-document.cjs +49 -1
- package/gsd-core/bin/lib/planning-document.cjs +459 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +18 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +8 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +163 -3
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +251 -31
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +283 -31
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -33
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +42 -0
- package/gsd-core/bin/lib/surface.cjs +17 -1
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
- package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
- package/gsd-core/bin/lib/uat.cjs +8 -0
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/verification.cjs +268 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify.cjs +132 -25
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +11 -2
- package/gsd-core/references/tdd.md +27 -4
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +321 -0
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +2 -2
- package/gsd-core/workflows/add-todo.md +3 -3
- package/gsd-core/workflows/ai-integration-phase.md +11 -3
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +16 -6
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +63 -46
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +1 -1
- package/gsd-core/workflows/complete-milestone.md +2 -2
- package/gsd-core/workflows/debug.md +3 -3
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update.md +3 -3
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +10 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +3 -3
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +36 -26
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +3 -3
- package/gsd-core/workflows/extract-learnings.md +2 -1
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +2 -2
- package/gsd-core/workflows/help/modes/full.compact.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/import.md +2 -2
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +2 -2
- package/gsd-core/workflows/map-codebase.md +2 -2
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +2 -2
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +7 -7
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +24 -7
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +113 -13
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +1 -1
- package/gsd-core/workflows/quick.md +21 -9
- package/gsd-core/workflows/reapply-patches.md +9 -3
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +31 -16
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -2
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +4 -4
- package/gsd-core/workflows/ship.md +3 -2
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +44 -8
- package/gsd-core/workflows/ui-review.md +18 -4
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +7 -7
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +81 -16
- package/hooks/dist/gsd-agent-isolation-guard.js +24 -0
- package/hooks/dist/gsd-secret-read-guard.js +27 -1
- package/hooks/dist/gsd-statusline.js +70 -13
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-agent-isolation-guard.js +24 -0
- package/hooks/gsd-secret-read-guard.js +27 -1
- package/hooks/gsd-statusline.js +70 -13
- package/hooks/gsd-validate-commit.sh +63 -4
- package/package.json +3 -2
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/docs-guard-registry.cjs +28 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +14 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +28 -1
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +290 -5
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +28 -9
- package/scripts/lint-workflow-shellcheck-baseline.json +15 -0
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +7 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-review/SKILL.md +2 -3
- package/vscode/package.json +1 -1
|
@@ -337,6 +337,30 @@ function resolveRegistryIsolation(cwd, configPath) {
|
|
|
337
337
|
if (!useWorktrees) isolation = 'none';
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
// #4734: a harness worktree can never be created in a directory that is not
|
|
341
|
+
// a git repository (or a repository with no commits) — git's definitive
|
|
342
|
+
// exit-128 answer on `rev-parse HEAD`. Demanding the isolation flag there
|
|
343
|
+
// blocked every flag-less dispatch the moment the sentinel went stale, on a
|
|
344
|
+
// root where no worktree can exist. Degrade to 'none' exactly like the
|
|
345
|
+
// use_worktrees opt-out above; the classification is owned by
|
|
346
|
+
// `classifyGitHead` (worktree-base-ref.cjs) — the same single owner the
|
|
347
|
+
// base-check's degrade decision uses — and ambiguous or failed resolutions
|
|
348
|
+
// keep the conservative (enforce) default, matching that check's own
|
|
349
|
+
// fail-closed treatment of the same classes.
|
|
350
|
+
if (isolation === 'harness-worktree') {
|
|
351
|
+
try {
|
|
352
|
+
ensureRuntimeBuild();
|
|
353
|
+
const { classifyGitHead } = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
|
|
354
|
+
if (classifyGitHead({ cwd }).status === 'definitive-absence') {
|
|
355
|
+
isolation = 'none';
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
// Unbuilt or unreadable runtime library — keep the conservative
|
|
359
|
+
// (enforce) default rather than silently disabling the guard, same
|
|
360
|
+
// posture as the ladder fallback above.
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
340
364
|
return { isolation, harnessFlag };
|
|
341
365
|
}
|
|
342
366
|
|
|
@@ -86,7 +86,12 @@
|
|
|
86
86
|
// on plain commands, without arming the compound-`cd` prompt". Writes to
|
|
87
87
|
// secret files are out of scope (Write/Edit were never gated). Commands
|
|
88
88
|
// over 1 MiB are denied outright (`command-too-large`) rather than
|
|
89
|
-
// scanned partially or waved through.
|
|
89
|
+
// scanned partially or waved through. (#4639 adds one more, by design: the
|
|
90
|
+
// value of `--env-file` under a container runtime is exempt, so the
|
|
91
|
+
// container's own command can print the interpolated environment
|
|
92
|
+
// (`alpine printenv`, `docker compose config`) — the same exposure class
|
|
93
|
+
// as the pre-existing volume-mount gap (`-v .env:/s`); the flag's value
|
|
94
|
+
// itself is a name, never contents.)
|
|
90
95
|
//
|
|
91
96
|
// Triggers on: Read, Grep, Bash tool calls (Kimi: ReadFile, Grep, Shell)
|
|
92
97
|
// Action: BLOCK (decision: 'block', exit 2) — codes secret-read |
|
|
@@ -127,6 +132,15 @@ const NON_READING_COMMANDS = new Set([
|
|
|
127
132
|
'basename', 'dirname', 'realpath', 'file', 'echo', 'printf',
|
|
128
133
|
]);
|
|
129
134
|
|
|
135
|
+
// #4639: container runtimes take `--env-file <file>` — the runtime opens the
|
|
136
|
+
// file itself, interpolates it into the container environment, and returns
|
|
137
|
+
// nothing to the agent, so the flag's VALUE is a name, never contents. The
|
|
138
|
+
// same category NON_READING_COMMANDS encodes, expressed as a flag value. Only
|
|
139
|
+
// the flag's value under these runtimes is exempt; every other operand in the
|
|
140
|
+
// segment is still checked, so the carve-out cannot launder a read.
|
|
141
|
+
const CONTAINER_RUNTIMES = new Set(['docker', 'docker-compose', 'podman', 'nerdctl']);
|
|
142
|
+
const ENV_FILE_FLAG_RE = /^--env-file(=|$)/;
|
|
143
|
+
|
|
130
144
|
// Shell interpreters that run a script from `-c`, a file operand, or stdin
|
|
131
145
|
// (heredoc / here-string / piped `echo`|`printf`). `su` is here for its `-c`
|
|
132
146
|
// form (`su [user] -c 'cmd'`); a bare `su user` resolves to file mode, which
|
|
@@ -840,7 +854,19 @@ function findSecretRead(command, depth) {
|
|
|
840
854
|
|
|
841
855
|
if (NON_READING_COMMANDS.has(base)) continue;
|
|
842
856
|
|
|
857
|
+
// #4639: exempt ONLY the value of `--env-file`, and only when the
|
|
858
|
+
// segment's command word is a container runtime. A bare `--env-file`
|
|
859
|
+
// consumes the next operand; `--env-file=<value>` is a single word.
|
|
860
|
+
const envFileExempt = CONTAINER_RUNTIMES.has(base);
|
|
861
|
+
let skipNext = false;
|
|
843
862
|
for (const w of operands) {
|
|
863
|
+
if (envFileExempt) {
|
|
864
|
+
if (skipNext) { skipNext = false; continue; }
|
|
865
|
+
if (ENV_FILE_FLAG_RE.test(w.text)) {
|
|
866
|
+
if (!w.text.includes('=')) skipNext = true; // bare flag consumes the next operand
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
844
870
|
if (namesSecret(normalizeOperand(w.text))) return w.text;
|
|
845
871
|
}
|
|
846
872
|
}
|
package/hooks/gsd-statusline.js
CHANGED
|
@@ -42,6 +42,10 @@ if (require.main === module) {
|
|
|
42
42
|
const { isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs');
|
|
43
43
|
const { PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs');
|
|
44
44
|
const { normalizeStateStatus } = require('../gsd-core/bin/lib/state-document.cjs');
|
|
45
|
+
const {
|
|
46
|
+
renderBracketPhaseDisplay,
|
|
47
|
+
renderBracketMilestoneDisplay,
|
|
48
|
+
} = require('../gsd-core/bin/lib/phase-id-display.cjs');
|
|
45
49
|
// #2850: reuse the existing workstream resolution seams rather than
|
|
46
50
|
// re-implementing CLI>env>store precedence or path construction inline.
|
|
47
51
|
// peekActiveWorkstream is the read-only sibling of the store-tier lookup
|
|
@@ -359,16 +363,26 @@ function renderProgressBar(percent) {
|
|
|
359
363
|
* Progress bar is opt-in: appended to the milestone segment only when
|
|
360
364
|
* progress.percent is present in frontmatter; absent → empty string.
|
|
361
365
|
*/
|
|
362
|
-
function formatGsdState(s) {
|
|
366
|
+
function formatGsdState(s, opts = {}) {
|
|
363
367
|
// #2850: workstream mode with nothing resolvable — an observable signal,
|
|
364
368
|
// never silent emptiness (distinguishes from "GSD isn't installed here").
|
|
365
369
|
if (s.noActiveWorkstream) return NO_ACTIVE_WORKSTREAM_LABEL;
|
|
366
370
|
|
|
367
371
|
const parts = [];
|
|
372
|
+
const bracket = opts.convention === 'bracket';
|
|
373
|
+
const bracketMilestone = bracket
|
|
374
|
+
? renderBracketMilestoneDisplay(s.milestone, opts.projectCode)
|
|
375
|
+
: null;
|
|
376
|
+
const phaseLabel = (phase) => {
|
|
377
|
+
const display = bracket
|
|
378
|
+
? renderBracketPhaseDisplay(s.milestone, phase, opts.projectCode)
|
|
379
|
+
: null;
|
|
380
|
+
return display ?? `Phase ${phase}`;
|
|
381
|
+
};
|
|
368
382
|
|
|
369
383
|
// Milestone segment: version + name + (opt-in) progress bar
|
|
370
384
|
if (s.milestone || s.milestoneName) {
|
|
371
|
-
const ver = s.milestone
|
|
385
|
+
const ver = bracketMilestone ?? s.milestone ?? '';
|
|
372
386
|
const name = (s.milestoneName && s.milestoneName !== 'milestone') ? s.milestoneName : '';
|
|
373
387
|
const bar = renderProgressBar(s.percent);
|
|
374
388
|
const pieces = [ver, name, bar].filter(Boolean);
|
|
@@ -384,7 +398,8 @@ function formatGsdState(s) {
|
|
|
384
398
|
// stage = whichever lifecycle status was written by the orchestrator
|
|
385
399
|
// (discussing / planning / executing / verifying)
|
|
386
400
|
const stage = s.status || '';
|
|
387
|
-
|
|
401
|
+
const phase = phaseLabel(s.activePhase);
|
|
402
|
+
parts.push(stage ? `${phase} ${stage}` : phase);
|
|
388
403
|
} else if (s.nextAction && phasesStr) {
|
|
389
404
|
// Scene 2: idle + a recommended next command is visible to the user.
|
|
390
405
|
// Surfaces "what to run next" without the user opening STATE.md.
|
|
@@ -402,9 +417,16 @@ function formatGsdState(s) {
|
|
|
402
417
|
// earlier so no existing project's status-line changes shape.
|
|
403
418
|
if (s.status) parts.push(s.status);
|
|
404
419
|
if (s.phaseNum && s.phaseTotal) {
|
|
420
|
+
const bracketPhase = bracket
|
|
421
|
+
? renderBracketPhaseDisplay(s.milestone, s.phaseNum, opts.projectCode)
|
|
422
|
+
: null;
|
|
405
423
|
const phase = s.phaseName
|
|
406
|
-
?
|
|
407
|
-
|
|
424
|
+
? bracketPhase
|
|
425
|
+
? `${bracketPhase} ${s.phaseName} (${s.phaseNum}/${s.phaseTotal})`
|
|
426
|
+
: `${s.phaseName} (${s.phaseNum}/${s.phaseTotal})`
|
|
427
|
+
: bracketPhase
|
|
428
|
+
? `${bracketPhase} (${s.phaseNum}/${s.phaseTotal})`
|
|
429
|
+
: `ph ${s.phaseNum}/${s.phaseTotal}`;
|
|
408
430
|
parts.push(phase);
|
|
409
431
|
}
|
|
410
432
|
}
|
|
@@ -490,17 +512,24 @@ function shortGsdStatus(status) {
|
|
|
490
512
|
* default format — and collapses narrative statuses via shortGsdStatus().
|
|
491
513
|
* The default "full" format is untouched.
|
|
492
514
|
*/
|
|
493
|
-
function formatGsdStateCompact(s) {
|
|
515
|
+
function formatGsdStateCompact(s, opts = {}) {
|
|
494
516
|
// #2850: mirrors formatGsdState's observable "nothing resolvable" signal.
|
|
495
517
|
if (s.noActiveWorkstream) return NO_ACTIVE_WORKSTREAM_LABEL;
|
|
496
518
|
|
|
497
519
|
const parts = [];
|
|
520
|
+
const bracket = opts.convention === 'bracket';
|
|
521
|
+
const bracketMilestone = bracket
|
|
522
|
+
? renderBracketMilestoneDisplay(s.milestone, opts.projectCode)
|
|
523
|
+
: null;
|
|
498
524
|
|
|
499
|
-
if (s.milestone) parts.push(s.milestone);
|
|
525
|
+
if (s.milestone) parts.push(bracketMilestone ?? s.milestone);
|
|
500
526
|
|
|
501
527
|
const phaseId = s.activePhase || s.phaseNum;
|
|
502
528
|
if (phaseId) {
|
|
503
|
-
|
|
529
|
+
const bracketPhase = bracket
|
|
530
|
+
? renderBracketPhaseDisplay(s.milestone, phaseId, opts.projectCode)
|
|
531
|
+
: null;
|
|
532
|
+
parts.push(bracketPhase ?? (s.phaseTotal ? `P${phaseId}/${s.phaseTotal}` : `P${phaseId}`));
|
|
504
533
|
}
|
|
505
534
|
|
|
506
535
|
// Scene exclusivity mirrors formatGsdState's if/else chain: an in-flight
|
|
@@ -747,7 +776,7 @@ function formatStateFreshness(fresh) {
|
|
|
747
776
|
* entry points — this collapses both onto one resolver.
|
|
748
777
|
*
|
|
749
778
|
* @param {object} cfg — parsed .planning/config.json (readGsdConfig())
|
|
750
|
-
* @returns {{ showLastCommand: boolean, position: 'end'|'front', stateFormat: 'full'|'compact', showGit: boolean, showStateFreshness: boolean }}
|
|
779
|
+
* @returns {{ showLastCommand: boolean, position: 'end'|'front', stateFormat: 'full'|'compact', showGit: boolean, showStateFreshness: boolean, convention: string|null, projectCode: string|null }}
|
|
751
780
|
*/
|
|
752
781
|
function resolveStatuslineOptions(cfg) {
|
|
753
782
|
const showLastCommand = getConfigValue(cfg, 'statusline.show_last_command') === true;
|
|
@@ -759,7 +788,22 @@ function resolveStatuslineOptions(cfg) {
|
|
|
759
788
|
const stateFormat = getConfigValue(cfg, 'statusline.state_format') === 'compact' ? 'compact' : 'full';
|
|
760
789
|
const showGit = getConfigValue(cfg, 'statusline.show_git') === true;
|
|
761
790
|
const showStateFreshness = getConfigValue(cfg, 'statusline.show_state_freshness') === true;
|
|
762
|
-
|
|
791
|
+
const convention = getConfigValue(cfg, 'phase_id_convention') === 'bracket'
|
|
792
|
+
? 'bracket'
|
|
793
|
+
: null;
|
|
794
|
+
const projectCodeValue = getConfigValue(cfg, 'project_code');
|
|
795
|
+
const projectCode = typeof projectCodeValue === 'string' && projectCodeValue !== ''
|
|
796
|
+
? projectCodeValue
|
|
797
|
+
: null;
|
|
798
|
+
return {
|
|
799
|
+
showLastCommand,
|
|
800
|
+
position,
|
|
801
|
+
stateFormat,
|
|
802
|
+
showGit,
|
|
803
|
+
showStateFreshness,
|
|
804
|
+
convention,
|
|
805
|
+
projectCode,
|
|
806
|
+
};
|
|
763
807
|
}
|
|
764
808
|
|
|
765
809
|
// --- stdin ------------------------------------------------------------------
|
|
@@ -934,7 +978,9 @@ function runStatusline() {
|
|
|
934
978
|
// freshness git spawn here would spend a subprocess on discarded output.
|
|
935
979
|
if (!task) {
|
|
936
980
|
const state = readGsdState(dir, { stateFreshness: options.showStateFreshness }) || {};
|
|
937
|
-
gsdStateStr = options.stateFormat === 'compact'
|
|
981
|
+
gsdStateStr = options.stateFormat === 'compact'
|
|
982
|
+
? formatGsdStateCompact(state, options)
|
|
983
|
+
: formatGsdState(state, options);
|
|
938
984
|
}
|
|
939
985
|
|
|
940
986
|
// Output
|
|
@@ -1042,6 +1088,7 @@ module.exports = {
|
|
|
1042
1088
|
STATE_HEAD_ADVISORY_COMMITS, isValidStateHeadStamp,
|
|
1043
1089
|
readStateHeadCommits, parseRevListCounts, deriveStateFreshness,
|
|
1044
1090
|
formatStateFreshness, resolveStatuslineOptions,
|
|
1091
|
+
renderBracketPhaseDisplay, renderBracketMilestoneDisplay,
|
|
1045
1092
|
};
|
|
1046
1093
|
|
|
1047
1094
|
/**
|
|
@@ -1059,7 +1106,15 @@ function renderStatusline(data) {
|
|
|
1059
1106
|
// key from reaching only one of them.
|
|
1060
1107
|
let lastCmdSuffix = '';
|
|
1061
1108
|
let gitSuffix = '';
|
|
1062
|
-
let options = {
|
|
1109
|
+
let options = {
|
|
1110
|
+
showLastCommand: false,
|
|
1111
|
+
position: 'end',
|
|
1112
|
+
stateFormat: 'full',
|
|
1113
|
+
showGit: false,
|
|
1114
|
+
showStateFreshness: false,
|
|
1115
|
+
convention: null,
|
|
1116
|
+
projectCode: null,
|
|
1117
|
+
};
|
|
1063
1118
|
try {
|
|
1064
1119
|
const cfg = readGsdConfig(dir);
|
|
1065
1120
|
options = resolveStatuslineOptions(cfg);
|
|
@@ -1075,7 +1130,9 @@ function renderStatusline(data) {
|
|
|
1075
1130
|
} catch (e) { /* swallow */ }
|
|
1076
1131
|
|
|
1077
1132
|
const state = readGsdState(dir, { stateFreshness: options.showStateFreshness }) || {};
|
|
1078
|
-
const gsdStateStr = options.stateFormat === 'compact'
|
|
1133
|
+
const gsdStateStr = options.stateFormat === 'compact'
|
|
1134
|
+
? formatGsdStateCompact(state, options)
|
|
1135
|
+
: formatGsdState(state, options);
|
|
1079
1136
|
const middle = gsdStateStr ? `\x1b[2m${gsdStateStr}\x1b[0m` : null;
|
|
1080
1137
|
return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, gitSuffix, position: options.position });
|
|
1081
1138
|
}
|
|
@@ -16,6 +16,21 @@ set -euo pipefail
|
|
|
16
16
|
# Idempotent and failure-proof by construction: unset vars expand to "" (a
|
|
17
17
|
# no-op rm -f target), and `|| true` guarantees the trap itself never changes
|
|
18
18
|
# the script's exit status.
|
|
19
|
+
# Subprocess exit statuses, pre-initialised so they can never be inherited from
|
|
20
|
+
# the ambient environment. Each is captured as `... || VAR=$?`, which assigns
|
|
21
|
+
# ONLY on the failure branch; on success the variable keeps whatever it already
|
|
22
|
+
# held, and `${VAR:-0}` defaults only when unset or empty. So an EXPORTED
|
|
23
|
+
# CONFIG_STATUS / CMD_STATUS / CLASSIFY_STATUS — from a CI wrapper, a .envrc, or
|
|
24
|
+
# another hook — survived into the success path and was read as "the subprocess
|
|
25
|
+
# failed". Measured: `CLASSIFY_STATUS=3 git commit -m "nope: bad"` printed
|
|
26
|
+
# "validator disabled for this call" and exited 0, silently accepting a
|
|
27
|
+
# non-conforming commit. Same for CONFIG_STATUS and CMD_STATUS. Found by the
|
|
28
|
+
# security review of #4429; the gate is fail-open by design on a genuine
|
|
29
|
+
# subprocess failure (#3838), which is exactly what made this bypass quiet.
|
|
30
|
+
CONFIG_STATUS=0
|
|
31
|
+
CMD_STATUS=0
|
|
32
|
+
CLASSIFY_STATUS=0
|
|
33
|
+
|
|
19
34
|
ENABLED_ERR=""
|
|
20
35
|
CMD_ERR=""
|
|
21
36
|
CLASSIFY_ERR=""
|
|
@@ -61,6 +76,12 @@ if [ -f .planning/config.json ]; then
|
|
|
61
76
|
process.exit(3);
|
|
62
77
|
}
|
|
63
78
|
" 2>"$ENABLED_ERR") || CONFIG_STATUS=$?
|
|
79
|
+
# Pre-initialised, NOT left to `${...:-0}` alone: the capture below only
|
|
80
|
+
# assigns on the `||` branch, so on SUCCESS the variable keeps whatever it
|
|
81
|
+
# already held — and an EXPORTED variable of this name is inherited from the
|
|
82
|
+
# ambient environment. `${VAR:-0}` defaults only when unset/empty, so
|
|
83
|
+
# `CONFIG_STATUS=3 git commit …` made this hook print "validator disabled" and exit 0,
|
|
84
|
+
# silently accepting a non-conforming commit. Found by review of #4429.
|
|
64
85
|
CONFIG_STATUS=${CONFIG_STATUS:-0}
|
|
65
86
|
if [ "$CONFIG_STATUS" != "0" ]; then
|
|
66
87
|
# Could not determine the opt-in flag at all (node missing, JSON parse
|
|
@@ -103,6 +124,12 @@ CMD=$(echo "$INPUT" | node -e "
|
|
|
103
124
|
}
|
|
104
125
|
});
|
|
105
126
|
" 2>"$CMD_ERR") || CMD_STATUS=$?
|
|
127
|
+
# Pre-initialised, NOT left to `${...:-0}` alone: the capture below only
|
|
128
|
+
# assigns on the `||` branch, so on SUCCESS the variable keeps whatever it
|
|
129
|
+
# already held — and an EXPORTED variable of this name is inherited from the
|
|
130
|
+
# ambient environment. `${VAR:-0}` defaults only when unset/empty, so
|
|
131
|
+
# `CMD_STATUS=3 git commit …` made this hook print "validator disabled" and exit 0,
|
|
132
|
+
# silently accepting a non-conforming commit. Found by review of #4429.
|
|
106
133
|
CMD_STATUS=${CMD_STATUS:-0}
|
|
107
134
|
if [ "$CMD_STATUS" != "0" ]; then
|
|
108
135
|
# Could not extract tool_input.command at all (node missing, malformed
|
|
@@ -127,6 +154,12 @@ GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" node -e "
|
|
|
127
154
|
process.exit(3);
|
|
128
155
|
}
|
|
129
156
|
" "$CMD" 2>"$CLASSIFY_ERR" || CLASSIFY_STATUS=$?
|
|
157
|
+
# Pre-initialised, NOT left to `${...:-0}` alone: the capture below only
|
|
158
|
+
# assigns on the `||` branch, so on SUCCESS the variable keeps whatever it
|
|
159
|
+
# already held — and an EXPORTED variable of this name is inherited from the
|
|
160
|
+
# ambient environment. `${VAR:-0}` defaults only when unset/empty, so
|
|
161
|
+
# `CLASSIFY_STATUS=3 git commit …` made this hook print "validator disabled" and exit 0,
|
|
162
|
+
# silently accepting a non-conforming commit. Found by review of #4429.
|
|
130
163
|
CLASSIFY_STATUS=${CLASSIFY_STATUS:-0}
|
|
131
164
|
if [ "$CLASSIFY_STATUS" != "0" ] && [ "$CLASSIFY_STATUS" != "1" ]; then
|
|
132
165
|
# 0 = is a git commit (validate below); 1 = genuinely not a git commit
|
|
@@ -557,7 +590,7 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
|
|
|
557
590
|
fi
|
|
558
591
|
# Single source of truth for the accepted commit-type list (#3811): the
|
|
559
592
|
# 10 built-ins plus whatever passed the safe-token filter above. Both the
|
|
560
|
-
#
|
|
593
|
+
# membership test and the human-readable error text below are derived
|
|
561
594
|
# from this ONE array — no hand-synced second copy.
|
|
562
595
|
#
|
|
563
596
|
# The `"${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}"` form (not
|
|
@@ -568,7 +601,6 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
|
|
|
568
601
|
# /bin/bash 3.2.57 on macOS. The `${arr[@]+word}` form is the
|
|
569
602
|
# nounset-safe idiom for "expand if set, empty otherwise" on empty arrays.
|
|
570
603
|
COMMIT_TYPES=("${BUILTIN_COMMIT_TYPES[@]}" "${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}")
|
|
571
|
-
COMMIT_TYPE_ALT=$(IFS='|'; echo "${COMMIT_TYPES[*]}")
|
|
572
604
|
COMMIT_TYPE_LIST=$(printf '%s, ' "${COMMIT_TYPES[@]}")
|
|
573
605
|
COMMIT_TYPE_LIST="${COMMIT_TYPE_LIST%, }"
|
|
574
606
|
# Typed `valid_types` array (#3811 review finding): CONTRIBUTING.md bans
|
|
@@ -580,8 +612,35 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
|
|
|
580
612
|
# or `\`.
|
|
581
613
|
COMMIT_TYPES_JSON=$(printf '"%s",' "${COMMIT_TYPES[@]}")
|
|
582
614
|
COMMIT_TYPES_JSON="[${COMMIT_TYPES_JSON%,}]"
|
|
583
|
-
# Validate Conventional Commits format
|
|
584
|
-
|
|
615
|
+
# Validate Conventional Commits format.
|
|
616
|
+
#
|
|
617
|
+
# #4429: do NOT build `^(type1|type2|...)` out of COMMIT_TYPES. That
|
|
618
|
+
# alternation grows with the CONFIGURED list, and how large a pattern can be
|
|
619
|
+
# compiled is a property of the platform's regex engine. bash 3.2.57 / BSD
|
|
620
|
+
# libc (macOS, this file's stated target) caps it at 64 KiB - bisected: a
|
|
621
|
+
# 65504-byte alternation compiles, 65515 fails. bash 5.2 / glibc has no
|
|
622
|
+
# reachable cap, so this half never bit Linux. Past a cap `[[ =~ ]]`
|
|
623
|
+
# returns 2, and `if !` cannot tell a COMPILE ERROR from "the subject does
|
|
624
|
+
# not conform" - so a valid `feat(auth): ...` was blocked with
|
|
625
|
+
# CONVENTIONAL_COMMITS_VIOLATION while `feat` sat in its own valid_types.
|
|
626
|
+
#
|
|
627
|
+
# Match the SHAPE with a fixed-size pattern, then test membership against
|
|
628
|
+
# the array. The regex no longer depends on how many types are configured,
|
|
629
|
+
# and the loop adds no subprocess or pipe (the #4429 hazard this file
|
|
630
|
+
# already avoids elsewhere). The character class is exactly the safe-token
|
|
631
|
+
# filter `^[a-z][a-z0-9-]*$` applied above, so it captures every type that
|
|
632
|
+
# can legally reach COMMIT_TYPES and no token that cannot.
|
|
633
|
+
SUBJECT_TYPE=''
|
|
634
|
+
if [[ "$SUBJECT" =~ ^([a-z][a-z0-9-]*)(\(.+\))?:[[:space:]].+ ]]; then
|
|
635
|
+
SUBJECT_TYPE="${BASH_REMATCH[1]}"
|
|
636
|
+
fi
|
|
637
|
+
COMMIT_TYPE_OK=0
|
|
638
|
+
if [ -n "$SUBJECT_TYPE" ]; then
|
|
639
|
+
for _known_type in "${COMMIT_TYPES[@]}"; do
|
|
640
|
+
if [ "$_known_type" = "$SUBJECT_TYPE" ]; then COMMIT_TYPE_OK=1; break; fi
|
|
641
|
+
done
|
|
642
|
+
fi
|
|
643
|
+
if [ "$COMMIT_TYPE_OK" -ne 1 ]; then
|
|
585
644
|
# Emit typed `code` and `valid_types` fields alongside `reason` (#2974,
|
|
586
645
|
# #3811). Tests assert on the stable code string and the typed array;
|
|
587
646
|
# the reason is the human-readable copy, never grepped by tests.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/gsd-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
|
5
5
|
"main": ".opencode/plugins/gsd-core.js",
|
|
6
6
|
"bin": {
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
"lint:frontmatter-scalar-broad-grep": "node scripts/lint-frontmatter-scalar-broad-grep.cjs",
|
|
125
125
|
"lint:removed-but-needed": "node scripts/lint-removed-but-needed.cjs",
|
|
126
126
|
"lint:response-language": "node scripts/lint-response-language-coverage.cjs",
|
|
127
|
-
"lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/lint-portable-timeout.cjs && node scripts/lint-portable-grep.cjs && node scripts/lint-allowed-tools-parity.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs && node scripts/lint-fix-has-regression-tests.cjs && node scripts/lint-example-parser-parity.cjs && node scripts/lint-docs-command-form.cjs && node scripts/lint-plan-count-drift.cjs && node scripts/lint-milestone-window-drift.cjs && node scripts/lint-phase-enumeration-drift.cjs && node scripts/lint-planning-prompt-drift.cjs && node scripts/lint-unreachable-guard-drift.cjs && node scripts/lint-completion-ratio-drift.cjs && node scripts/lint-slug-derivation-drift.cjs && node scripts/lint-state-field-drift.cjs && node scripts/lint-state-write-path-drift.cjs && node scripts/lint-completion-predicate-drift.cjs && node scripts/lint-planning-snapshot-bypass-drift.cjs && node scripts/lint-health-diagnostic-rule-table.cjs && node scripts/lint-planning-artifact-writer-drift.cjs && node scripts/lint-frontmatter-scalar-broad-grep.cjs && node scripts/lint-removed-but-needed.cjs && node scripts/lint-no-adhoc-regex-escape.cjs && node scripts/lint-vendored-deps.cjs && node scripts/lint-docs-guard-registration.cjs && node scripts/lint-source-test-name-collision.cjs && npm run lint:hooks-runtime-build-seam && node scripts/check-contract-drift.cjs && node scripts/lint-mutation-test-derivation-drift.cjs && node scripts/lint-seam-enforcement.cjs && node scripts/lint-workflow-shellcheck.cjs && npm run lint:response-language",
|
|
127
|
+
"lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && npm run lint:retired-runtime-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/lint-portable-timeout.cjs && node scripts/lint-portable-grep.cjs && node scripts/lint-allowed-tools-parity.cjs && node scripts/lint-phase-arg-assignment.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs && node scripts/lint-pr-branch-pattern-drift.cjs && node scripts/lint-fix-has-regression-tests.cjs && node scripts/lint-example-parser-parity.cjs && node scripts/lint-docs-command-form.cjs && node scripts/lint-plan-count-drift.cjs && node scripts/lint-milestone-window-drift.cjs && node scripts/lint-phase-enumeration-drift.cjs && node scripts/lint-planning-prompt-drift.cjs && node scripts/lint-unreachable-guard-drift.cjs && node scripts/lint-completion-ratio-drift.cjs && node scripts/lint-slug-derivation-drift.cjs && node scripts/lint-state-field-drift.cjs && node scripts/lint-state-write-path-drift.cjs && node scripts/lint-completion-predicate-drift.cjs && node scripts/lint-planning-snapshot-bypass-drift.cjs && node scripts/lint-health-diagnostic-rule-table.cjs && node scripts/lint-planning-artifact-writer-drift.cjs && node scripts/lint-frontmatter-scalar-broad-grep.cjs && node scripts/lint-removed-but-needed.cjs && node scripts/lint-no-adhoc-regex-escape.cjs && node scripts/lint-vendored-deps.cjs && node scripts/lint-docs-guard-registration.cjs && node scripts/lint-source-test-name-collision.cjs && npm run lint:hooks-runtime-build-seam && node scripts/check-contract-drift.cjs && node scripts/lint-mutation-test-derivation-drift.cjs && node scripts/lint-seam-enforcement.cjs && node scripts/lint-workflow-shellcheck.cjs && npm run lint:response-language",
|
|
128
128
|
"lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
|
|
129
129
|
"lint:regression-names": "node scripts/lint-regression-test-names.cjs",
|
|
130
130
|
"lint:descriptions": "node scripts/lint-descriptions.cjs",
|
|
@@ -136,6 +136,7 @@
|
|
|
136
136
|
"lint:docs": "node scripts/lint-docs-required.cjs",
|
|
137
137
|
"lint:qa-smells": "node scripts/qa-smell-ratchet.cjs",
|
|
138
138
|
"lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs",
|
|
139
|
+
"lint:retired-runtime-name": "node scripts/lint-retired-runtime-name.cjs",
|
|
139
140
|
"lint:seam-enforcement": "node scripts/lint-seam-enforcement.cjs",
|
|
140
141
|
"lint:docs-command-form": "node scripts/lint-docs-command-form.cjs",
|
|
141
142
|
"lint:hooks-runtime-build-seam": "node scripts/lint-hooks-runtime-build-seam.cjs",
|
package/scripts/build-hooks.js
CHANGED
|
@@ -11,16 +11,25 @@ const path = require('path');
|
|
|
11
11
|
const vm = require('vm');
|
|
12
12
|
|
|
13
13
|
const HOOKS_DIR = path.join(__dirname, '..', 'hooks');
|
|
14
|
+
const REPO_ROOT = path.join(HOOKS_DIR, '..');
|
|
14
15
|
const DIST_DIR = path.join(HOOKS_DIR, 'dist');
|
|
15
16
|
// Per-process staging directory for atomic writes. Using process.pid in the
|
|
16
17
|
// name eliminates all contention between concurrent builders: each process
|
|
17
18
|
// owns its own staging dir and never races with another builder's cleanup.
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
19
|
+
// A SIBLING of hooks/ (not inside it) — still on the same filesystem as
|
|
20
|
+
// DIST_DIR (both live under the repo checkout), so rename(2) stays atomic —
|
|
21
|
+
// but this way nothing that walks or copies hooks/ as a whole can ever
|
|
22
|
+
// observe this directory being created/populated/deleted concurrently.
|
|
23
|
+
// tests/gsd-validate-commit-sigpipe.test.cjs's fixture setup does exactly
|
|
24
|
+
// that (fs.cpSync(HOOKS_DIR, ..., {recursive: true})), and when its walk
|
|
25
|
+
// entered a staging dir that a concurrent build-hooks.js run deleted
|
|
26
|
+
// mid-iteration, Node's native recursive-copy (std::filesystem under the
|
|
27
|
+
// hood) threw an uncaught C++ exception and aborted the whole process with
|
|
28
|
+
// SIGABRT — not a catchable JS error. Moving the staging dir out of hooks/
|
|
29
|
+
// closes that race for every current and future hooks/-tree walker, not
|
|
30
|
+
// just that one call site (which also gained its own defensive filter).
|
|
31
|
+
// The parent pattern .dist-staging-*/ (repo root) is gitignored.
|
|
32
|
+
const STAGE_DIR = path.join(REPO_ROOT, `.dist-staging-${process.pid}`);
|
|
24
33
|
|
|
25
34
|
// Hooks to copy (pure Node.js, no bundling needed)
|
|
26
35
|
const HOOKS_TO_COPY = [
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
* 7. Reverse direction: a workflow/command matching a quoted `## TOKEN`
|
|
28
28
|
* no agent declares or emits is dispatch-on-phantom
|
|
29
29
|
* (unmatched_consumer_token) — F9's shape from the consumer side.
|
|
30
|
+
* 8. Every reference pointer an agent file carries either resolves to
|
|
31
|
+
* exactly the regular file it names, contained under
|
|
32
|
+
* gsd-core/references/, or is reported (unresolved_reference_include) —
|
|
33
|
+
* a pointer the follower declines is never silently dropped (#4930).
|
|
30
34
|
*
|
|
31
35
|
* Exit 0 = clean. Exit 1 = violations (with diagnostics on stderr).
|
|
32
36
|
*/
|
|
@@ -35,6 +39,9 @@
|
|
|
35
39
|
|
|
36
40
|
const fs = require('fs');
|
|
37
41
|
const path = require('path');
|
|
42
|
+
// The repo's ONE containment decision (ADR-4650, src/security.cts) — realpath-resolved, so an
|
|
43
|
+
// intermediate component referring outside the root is refused before this loop reads the file.
|
|
44
|
+
const { tryWithinRoot } = require('../gsd-core/bin/lib/security.cjs');
|
|
38
45
|
|
|
39
46
|
function resolveRoot(argv) {
|
|
40
47
|
const idx = argv.indexOf('--root');
|
|
@@ -92,8 +99,9 @@ function toRepoRelative(absPath) {
|
|
|
92
99
|
/**
|
|
93
100
|
* referenceIncludes(content)
|
|
94
101
|
*
|
|
95
|
-
* Plain scan for `@~/.claude/gsd-core/references/*.md` tokens
|
|
96
|
-
*
|
|
102
|
+
* Plain scan for `@~/.claude/gsd-core/references/*.md` tokens (and the bare
|
|
103
|
+
* `@gsd-core/references/*.md` spelling, #4841) anywhere in an agent file's
|
|
104
|
+
* content -- inside an `<execution_context>` block (already
|
|
97
105
|
* covered structurally by `executionContextRefs` in command-contract-helpers,
|
|
98
106
|
* but a raw regex over the whole string picks those up too) and, just as
|
|
99
107
|
* importantly, OUTSIDE one: agents frequently point at a reference doc from
|
|
@@ -105,17 +113,94 @@ function toRepoRelative(absPath) {
|
|
|
105
113
|
* lives in `gsd-core/references/planner-guidance.md`, which the agent only
|
|
106
114
|
* `@`-includes -- so the producer scan below must follow these includes to
|
|
107
115
|
* see markers an agent's contract legitimately delegates to a reference doc.
|
|
108
|
-
* Returns
|
|
116
|
+
* Returns `{ includes, declined }`: `includes` are ROOT-relative paths
|
|
117
|
+
* (`gsd-core/references/foo.md`), de-duplicated; `declined` are the pointers
|
|
118
|
+
* this scan refused to follow, as written, each with its reason (#4930).
|
|
109
119
|
*/
|
|
110
120
|
function referenceIncludes(content) {
|
|
111
121
|
const seen = new Set();
|
|
112
|
-
const
|
|
122
|
+
const declined = new Map();
|
|
123
|
+
// Both spellings the agent corpus has carried: the installed-path form the
|
|
124
|
+
// installer rewrites per profile, and the bare repo-relative `@gsd-core/…`
|
|
125
|
+
// form (#4841) that it does not. The bare form is now refused in agents/ by
|
|
126
|
+
// tests/shipped-reference-cites.test.cjs; it is followed here so a pointer
|
|
127
|
+
// that slips past that gate is still scanned rather than silently dropped.
|
|
128
|
+
//
|
|
129
|
+
// THE TOKEN IS CAPTURED WHOLE AND THE NAME GRAMMAR IS ANCHORED AT BOTH ENDS.
|
|
130
|
+
// Nested names are allowed (`few-shot-examples/verifier.md`); every segment
|
|
131
|
+
// starts with `[A-Za-z0-9_-]` — narrower than "non-dot", since `+x.md` and
|
|
132
|
+
// `é.md` do not match either — so a `.` or `..` segment is never a name.
|
|
133
|
+
// Anchoring is what makes that a statement about the whole pointer rather
|
|
134
|
+
// than about its first few segments: an earlier form of this pattern ended
|
|
135
|
+
// at `\.md` with no following boundary, so `…/references/tdd.md/xx/yy` was
|
|
136
|
+
// followed as `tdd.md`, and THIS FUNCTION'S CALLER READS THE PATH IT
|
|
137
|
+
// RETURNS — `fs.readFileSync` in main()'s agent loop — so a truncated prefix
|
|
138
|
+
// folded the wrong file's text into the scanned corpus. Matching the whole
|
|
139
|
+
// whitespace-delimited token and requiring it to satisfy the grammar end to
|
|
140
|
+
// end closes that by construction, and for any separator spelling rather
|
|
141
|
+
// than the ones a boundary lookahead happens to enumerate.
|
|
142
|
+
//
|
|
143
|
+
// ANCHORING DOES NOT ESTABLISH CONTAINMENT, and an earlier revision of this
|
|
144
|
+
// comment said it did. The grammar's refusal of `.`/`..` segments covers the
|
|
145
|
+
// TEXTUAL half only: an intermediate component that refers outside the tree
|
|
146
|
+
// is resolved transparently on the way to the file. Containment is decided
|
|
147
|
+
// separately, below, by `tryWithinRoot` — and it has to be, because this
|
|
148
|
+
// loop READS what it resolves.
|
|
149
|
+
//
|
|
150
|
+
// A token that does not parse WHOLE is declined rather than truncated — the
|
|
151
|
+
// conservative half of the same rule — and the decline is RETURNED, not
|
|
152
|
+
// dropped (#4930): main() reports it as unresolved_reference_include. A
|
|
153
|
+
// skipped pointer is a scan this tool did not perform, and the verdict it
|
|
154
|
+
// prints depends on that scan, so the skip has to be visible in the tool's
|
|
155
|
+
// own output. tests/shipped-reference-cites.test.cjs reports the same token
|
|
156
|
+
// for the shipped agents/ tree; this is the half that holds for any --root.
|
|
157
|
+
// FOUR spellings. `@$HOME/.claude/` is a second installed-path form the installer rewrites
|
|
158
|
+
// explicitly (applyAgentPathRewritesInner's `/\$HOME\/\.claude\//g` replace, beside the `~/.claude/`
|
|
159
|
+
// one). The fourth is a FAMILY rather than a string: `--relative-includes` (#4377) makes a local
|
|
160
|
+
// install emit project-relative includes whose prefix is DERIVED from the resolved config dir, so it
|
|
161
|
+
// is matched by SHAPE — one or MORE leading segments before `gsd-core` (a config dir nested under
|
|
162
|
+
// the project root emits `@config/nested/gsd-core/…`), none of them `.` or `..`. The `+` keeps the
|
|
163
|
+
// bare form the bare form by construction, since it needs a segment BEFORE `gsd-core`.
|
|
164
|
+
//
|
|
165
|
+
// THE SHAPE IS NARROWER THAN THE FAMILY, and saying otherwise would be the overstatement this
|
|
166
|
+
// function has already had to retract once. `_computePathPrefix` can emit a prefix this character
|
|
167
|
+
// class does not match — a config dir named `config+nested` or `ümlaut` — and widening the class to
|
|
168
|
+
// arbitrary directory names is what would turn every `@scope/…` token in prose into a pointer. So a
|
|
169
|
+
// prefix outside the class is not followed, which is where this follower was for ALL
|
|
170
|
+
// project-relative spellings before #4841. The gate's own comment states the same bound; the two
|
|
171
|
+
// must not drift apart, because between them they are the only record of it.
|
|
172
|
+
const re = /@(?:(?:~|\$HOME)\/\.claude\/|(?:(?!\.\.?\/)[A-Za-z0-9._-]+\/)+)?gsd-core\/references\/(\S+)/g;
|
|
173
|
+
const name = /^(?:[A-Za-z0-9_-][A-Za-z0-9._-]*\/)*[A-Za-z0-9_-][A-Za-z0-9._-]*\.md$/;
|
|
174
|
+
// Trailing prose punctuation is not part of a filename — a pointer may end a
|
|
175
|
+
// sentence or sit inside backticks, parentheses or bold markers. The class
|
|
176
|
+
// cannot eat into `.md`, which ends at `d`.
|
|
177
|
+
// START-ANCHORED: this tests a CUT SUFFIX, so it must be punctuation END TO END. An unanchored
|
|
178
|
+
// `/…$/` answers true for `/xx?`, which would let the loop cut a separator and call the remainder a
|
|
179
|
+
// name — `tdd.md/xx?` following as `tdd.md`, the defect this whole function was rewritten to close.
|
|
180
|
+
const trailingProseOnly = /^[.,;:!?)\]}>"'`*]+$/;
|
|
113
181
|
let m;
|
|
114
182
|
while ((m = re.exec(content)) !== null) {
|
|
115
|
-
const
|
|
116
|
-
|
|
183
|
+
const raw = m[1];
|
|
184
|
+
// MINIMAL strip, same rule as the gate: shortest trailing run whose removal yields a valid name.
|
|
185
|
+
let candidate = null;
|
|
186
|
+
for (let cut = 0; cut <= raw.length; cut++) {
|
|
187
|
+
const probe = raw.slice(0, raw.length - cut);
|
|
188
|
+
if (cut > 0 && !trailingProseOnly.test(raw.slice(raw.length - cut))) break;
|
|
189
|
+
if (name.test(probe)) { candidate = probe; break; }
|
|
190
|
+
}
|
|
191
|
+
if (candidate === null) {
|
|
192
|
+
declined.set(m[0], 'does not name a reference whole — the text continues past the name, so nothing is followed');
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
// AMBIGUITY, mirrored from the gate. If the UNSTRIPPED token also names something, the strip would
|
|
196
|
+
// pick one of two readings — and this loop READS what it picks, so it declines rather than guess.
|
|
197
|
+
if (candidate !== raw && fs.existsSync(path.join(ROOT, 'gsd-core', 'references', raw))) {
|
|
198
|
+
declined.set(m[0], `is ambiguous — \`${raw}\` itself names something on disk, so stripping it to \`${candidate}\` would pick one of two readings; neither is followed`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
seen.add('gsd-core/references/' + candidate);
|
|
117
202
|
}
|
|
118
|
-
return [...seen];
|
|
203
|
+
return { includes: [...seen], declined: [...declined].map(([pointer, reason]) => ({ pointer, reason })) };
|
|
119
204
|
}
|
|
120
205
|
|
|
121
206
|
function remedyFor(kind) {
|
|
@@ -158,6 +243,7 @@ function main() {
|
|
|
158
243
|
const candidateMarkers = new Map();
|
|
159
244
|
const agentTexts = new Map();
|
|
160
245
|
const unclosedFenceViolations = [];
|
|
246
|
+
const includeViolations = [];
|
|
161
247
|
|
|
162
248
|
// #4407: .compact.md variant siblings are an alternate rendering of their
|
|
163
249
|
// canonical agent's SAME contract, not a distinct one — excluded so they
|
|
@@ -173,12 +259,41 @@ function main() {
|
|
|
173
259
|
// Single pass over the agent's @-included references: each file is read
|
|
174
260
|
// once and feeds BOTH the read-tag fold (agentTexts) and marker
|
|
175
261
|
// extraction (producer/candidate attribution).
|
|
262
|
+
// #4930: every include this loop does NOT fold in is reported, never
|
|
263
|
+
// swallowed. An unfollowed include is text the marker verdict below was
|
|
264
|
+
// computed without, so a decline that leaves no trace in the output makes
|
|
265
|
+
// that verdict unexplainable. (lint-command-contract's @-ref existence rule
|
|
266
|
+
// covers commands' <execution_context> only, not agents.)
|
|
176
267
|
const includeTexts = [];
|
|
177
|
-
|
|
268
|
+
const reportInclude = (pointer, reason) => includeViolations.push({
|
|
269
|
+
kind: 'unresolved_reference_include',
|
|
270
|
+
agent,
|
|
271
|
+
marker: null,
|
|
272
|
+
detail: `${toRepoRelative(abs)}: ${pointer} ${reason}`,
|
|
273
|
+
});
|
|
274
|
+
const { includes, declined } = referenceIncludes(content);
|
|
275
|
+
for (const { pointer, reason } of declined) reportInclude(pointer, reason);
|
|
276
|
+
for (const refRelPath of includes) {
|
|
277
|
+
// CONTAIN BEFORE READ. The name grammar refuses `.`/`..` segments, which covers the TEXTUAL
|
|
278
|
+
// half of containment and nothing else: an intermediate path component that refers outside
|
|
279
|
+
// the tree is resolved transparently on the way to the file, so a textually-clean name can
|
|
280
|
+
// still address something outside `references/`. This loop READS what it resolves and folds
|
|
281
|
+
// the text into the scanned corpus, so the real path is what has to be checked.
|
|
282
|
+
const refsDir = path.join(ROOT, 'gsd-core', 'references');
|
|
283
|
+
const contained = tryWithinRoot(refRelPath.slice('gsd-core/references/'.length), refsDir);
|
|
284
|
+
if (contained === null) {
|
|
285
|
+
reportInclude(refRelPath, 'does not resolve inside gsd-core/references/');
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
178
288
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
289
|
+
// Read the ContainedPath the predicate returned, never a re-joined path (ADR-4650).
|
|
290
|
+
if (!fs.lstatSync(contained).isFile()) {
|
|
291
|
+
reportInclude(refRelPath, 'is not a regular file');
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
includeTexts.push(fs.readFileSync(contained, 'utf-8'));
|
|
295
|
+
} catch (e) {
|
|
296
|
+
reportInclude(refRelPath, e && e.code === 'ENOENT' ? 'does not exist' : `could not be read (${e && e.code})`);
|
|
182
297
|
}
|
|
183
298
|
}
|
|
184
299
|
agentTexts.set(agent, [content, ...includeTexts].join('\n'));
|
|
@@ -237,6 +352,7 @@ function main() {
|
|
|
237
352
|
const allViolations = [
|
|
238
353
|
...parseViolations,
|
|
239
354
|
...unclosedFenceViolations,
|
|
355
|
+
...includeViolations,
|
|
240
356
|
...contractViolationsList,
|
|
241
357
|
...readTagViolationsList,
|
|
242
358
|
...reverseViolationsList,
|