@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
package/hooks/lib/git-cmd.js
CHANGED
|
@@ -180,4 +180,213 @@ function isGitSubcommand(cmd, sub) {
|
|
|
180
180
|
return tokens[subIdx] === sub;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Resolve a `-m` message argument to its SUBJECT — the first line of the commit
|
|
185
|
+
* message — resolving the command-substituted heredoc form to the heredoc
|
|
186
|
+
* BODY's first line.
|
|
187
|
+
*
|
|
188
|
+
* PURE STRING FUNCTION. It deliberately does NOT tokenize or walk the command
|
|
189
|
+
* line: selecting *which* argument is the message stays with the caller, exactly
|
|
190
|
+
* as before, so this cannot change which commands are validated. An earlier
|
|
191
|
+
* revision of this fix did walk tokens and regressed four separate cases —
|
|
192
|
+
* `git commit -- -m WIP` (a pathspec), `git commit --amend && echo -m WIP` (a
|
|
193
|
+
* later command's flag), `-m "" --allow-empty-message` (the scanner drops empty
|
|
194
|
+
* tokens, so the following flag became the subject), and unquoted
|
|
195
|
+
* `git commit -m WIP`. All were allowed upstream and would have started being
|
|
196
|
+
* blocked. Reported in review of #3802.
|
|
197
|
+
*
|
|
198
|
+
* The defect this DOES fix: `gsd-validate-commit.sh` captured the message with
|
|
199
|
+
* `-m[[:space:]]+"([^"]+)"`, and bash `[^"]` matches newlines, so the widely
|
|
200
|
+
* used agent-authored commit idiom
|
|
201
|
+
*
|
|
202
|
+
* git commit -m "$(cat <<'EOF'
|
|
203
|
+
* feat(auth): add login flow
|
|
204
|
+
* EOF
|
|
205
|
+
* )"
|
|
206
|
+
*
|
|
207
|
+
* captured the whole span up to the final quote at `)"`. Taking its first line
|
|
208
|
+
* yielded the literal `$(cat <<'EOF'`, which can never satisfy Conventional
|
|
209
|
+
* Commits, so every heredoc-form commit was blocked regardless of its message.
|
|
210
|
+
*
|
|
211
|
+
* Recognition is anchored at BOTH ends and requires a command substitution, so
|
|
212
|
+
* an ordinary message merely CONTAINING — or ending in — `<<WORD` is not
|
|
213
|
+
* mistaken for an opener. Without the `^\$\(` anchor,
|
|
214
|
+
* `-m "WIP notes <<EOF\nfix: smuggled subject"` resolved to the second line and
|
|
215
|
+
* ALLOWED a non-conforming commit: an enforcement bypass, not just a
|
|
216
|
+
* misclassification (review of #3802).
|
|
217
|
+
*
|
|
218
|
+
* NOT resolved, by design: an UNQUOTED delimiter (`<<EOF`). bash expands `$var`
|
|
219
|
+
* and `$(...)` in that body, so the literal text here is not what git receives.
|
|
220
|
+
* An earlier revision resolved it anyway and called the gap "the same
|
|
221
|
+
* pre-existing limit as expansions in a plain `-m` argument" — that framing was
|
|
222
|
+
* wrong on both halves: on base the whole heredoc form was blocked, so this fix
|
|
223
|
+
* CREATED the path, and it dodged the length gate as well as the format gate.
|
|
224
|
+
* See the expansion guard in the body (review of #3816, round 4).
|
|
225
|
+
*
|
|
226
|
+
* KNOWN LIMIT: the DOUBLE-QUOTED delimiter spelling (`<<"EOF"`) is resolvable
|
|
227
|
+
* here but unreachable through the caller — `gsd-validate-commit.sh`'s
|
|
228
|
+
* double-quoted `-m` capture stops at the first `"`, which in that spelling is
|
|
229
|
+
* the delimiter's own quote, so the resolver only ever sees a truncated opener
|
|
230
|
+
* and the commit stays blocked. Its single-quoted `-m` capture DOES deliver the
|
|
231
|
+
* spelling intact, which is why an earlier revision's claim of unreachability
|
|
232
|
+
* was false; the caller now gates the resolver on the double-quoted arm alone
|
|
233
|
+
* (review of #3816, round 4), so the claim holds again — for that reason, not
|
|
234
|
+
* by luck. That is the pre-fix behaviour for the whole form (fail closed, a
|
|
235
|
+
* false positive on one rare spelling), and widening the bash capture to span
|
|
236
|
+
* inner quotes would change what is captured for EVERY message containing one —
|
|
237
|
+
* a regression class this fix deliberately does not touch (Codex review of
|
|
238
|
+
* #3816). The same capture truncation blocks a `"` in the SUBJECT LINE itself,
|
|
239
|
+
* where it lands inside the line being measured. It does NOT block a `"` on a
|
|
240
|
+
* later body line: the subject is already complete before the truncation point,
|
|
241
|
+
* so that message resolves and is allowed (measured; an earlier revision of this
|
|
242
|
+
* comment and of the changeset claimed a `"` ANYWHERE blocked, which is false —
|
|
243
|
+
* Codex review of #3816, round 4). The truncation guard below is what keeps the
|
|
244
|
+
* unmeasurable half fail-closed. Two more legal-but-unrecognized spellings
|
|
245
|
+
* stay blocked the same fail-closed way: an env-prefixed cat
|
|
246
|
+
* (`$(A=1 cat <<'EOF'`) and an option-terminated cat (`$(cat -- <<'EOF'`) —
|
|
247
|
+
* recognizing either would mean modelling bash prefix words here, cost with
|
|
248
|
+
* no reported user (review of #3816, round 3).
|
|
249
|
+
*
|
|
250
|
+
* @param {string} messageArg - the raw `-m` argument, already selected by the caller
|
|
251
|
+
* @returns {string} the subject to validate
|
|
252
|
+
*/
|
|
253
|
+
function resolveCommitSubject(messageArg) {
|
|
254
|
+
// CRLF-tolerant split. With a bare split('\n') every body line kept its \r,
|
|
255
|
+
// so `body.indexOf(delimiter)` never matched on CRLF input: the truncation
|
|
256
|
+
// guard was inert, an empty CRLF message resolved to 'EOF\r' instead of '',
|
|
257
|
+
// and a real 72-char subject measured 73 (review of #3816). Splitting on
|
|
258
|
+
// /\r?\n/ is the repo-wide remedy for this recurring defect class.
|
|
259
|
+
const lines = String(messageArg == null ? '' : messageArg).split(/\r?\n/);
|
|
260
|
+
// The path prefix is a PATH-CHARACTER class, not \S*: `\S*` accepted
|
|
261
|
+
// `id;/bin/cat`, so `$(id;/bin/cat <<'EOF' ...` was resolved to its heredoc
|
|
262
|
+
// body while bash actually runs `id` first and git's real subject is `id`'s
|
|
263
|
+
// OUTPUT — an enforcement bypass (Codex review of #3816). A prefix carrying
|
|
264
|
+
// any shell metacharacter now fails recognition, which falls back to the
|
|
265
|
+
// opener line and the format gate: fail closed, exactly the pre-fix
|
|
266
|
+
// behaviour for the whole form.
|
|
267
|
+
//
|
|
268
|
+
// Whitespace inside the recognition is ASCII space/tab — [ \t], never \s —
|
|
269
|
+
// because JavaScript \s includes Unicode whitespace bash does NOT split on:
|
|
270
|
+
// `$(<NBSP>/bin/cat <<'EOF'` was recognized here while bash reads
|
|
271
|
+
// `<NBSP>/bin/cat` as the executable NAME, so recognition claimed a
|
|
272
|
+
// substitution that does not run cat (Codex review of #3816, round 2). The
|
|
273
|
+
// same ASCII rule as the blank-line skip below, for the same reason.
|
|
274
|
+
// `cat[ \t]*<<`, not `+`: bash accepts `cat<<'EOF'` with no space (review of
|
|
275
|
+
// #3816, round 3), and recognizing it costs nothing — the token before `<<`
|
|
276
|
+
// is still literally `cat`.
|
|
277
|
+
//
|
|
278
|
+
// The path prefix must be ABSOLUTE (Codex review of #3816, round 4). The old
|
|
279
|
+
// `[\w./-]*\/` also accepted `./cat` and `../evil/cat`, so a relative
|
|
280
|
+
// executable that merely ENDS in `cat` was trusted to echo its stdin: with a
|
|
281
|
+
// planted `../evil/cat` printing `WIP injected`, the resolver validated the
|
|
282
|
+
// heredoc body while git's real subject was `WIP injected` (measured
|
|
283
|
+
// base=2 -> head=0 against a real commit). Requiring `/` up front costs
|
|
284
|
+
// nothing real — `/bin/cat` and a bare `cat` both still resolve.
|
|
285
|
+
//
|
|
286
|
+
// AN ABSOLUTE PATH IS NOT AN IDENTITY EITHER (independent review of #3816,
|
|
287
|
+
// round 8). Round 4 stopped at "must be absolute", so any absolute path
|
|
288
|
+
// ENDING in `/cat` was still trusted to echo its stdin — the very thing the
|
|
289
|
+
// round-4 reasoning rejected one spelling earlier. With an executable at
|
|
290
|
+
// `/some/scratch/dir/cat` printing `WIP injected`, the resolver validated
|
|
291
|
+
// the conforming heredoc body while git's real subject was `WIP injected`
|
|
292
|
+
// (measured on bash 3.2.57 and 5.3.15 against a real commit: hook exit 0,
|
|
293
|
+
// `git cat-file -p` subject `WIP injected`, while the same command through
|
|
294
|
+
// `./cat` was already refused). The prefix is now the canonical system
|
|
295
|
+
// locations, which is the only claim a string can support. `/usr/local/bin`
|
|
296
|
+
// is deliberately excluded: it is user-writable on ordinary machines, which
|
|
297
|
+
// is the plantable case this guard exists for.
|
|
298
|
+
//
|
|
299
|
+
// RESIDUAL, not fixable from a string: a bare `cat` shadowed earlier on PATH
|
|
300
|
+
// has the same effect and is indistinguishable here. It is also not a
|
|
301
|
+
// meaningful boundary — anyone who can plant an executable on PATH can run
|
|
302
|
+
// `git commit` directly — so this hook stays an authoring guard, not a
|
|
303
|
+
// security control.
|
|
304
|
+
//
|
|
305
|
+
// The delimiter alternatives are split so the BARE spelling is its own group:
|
|
306
|
+
// `\\(...)` (backslash-quoted) and `(...)` (bare) were one `\\?(...)` branch,
|
|
307
|
+
// which conflated the only two spellings that differ in bash. See the
|
|
308
|
+
// expansion guard below.
|
|
309
|
+
const opener = /^\$\([ \t]*(?:\/(?:usr\/)?bin\/)?cat[ \t]*<<(-?)[ \t]*(?:'([^']+)'|"([^"]+)"|\\([^\s'"();|&<>\\]+)|([^\s'"();|&<>\\]+))[ \t]*$/
|
|
310
|
+
.exec(lines[0]);
|
|
311
|
+
if (!opener) return lines[0];
|
|
312
|
+
|
|
313
|
+
// EXPANSION GUARD (review of #3816, round 4 — BLOCKER). Only `<<'D'`, `<<"D"`
|
|
314
|
+
// and `<<\D` suppress expansion. A BARE `<<D` is expanded by bash, so the body
|
|
315
|
+
// captured here is NOT the text git receives, and measuring it is an
|
|
316
|
+
// enforcement bypass in both gates at once:
|
|
317
|
+
//
|
|
318
|
+
// -m "$(cat <<EOF\nfeat: $UNSET_VAR\nEOF\n)" git gets `feat:` -> format gate dodged
|
|
319
|
+
// -m "$(cat <<EOF\nfeat: ${LONG}\nEOF\n)" git gets any length -> length gate dodged
|
|
320
|
+
//
|
|
321
|
+
// Both measured base=2 -> head=0 against the real hook. This is NOT the
|
|
322
|
+
// pre-existing plain-`-m` expansion limit an earlier revision claimed it was:
|
|
323
|
+
// on base the whole heredoc form was blocked, so no expansion inside a body
|
|
324
|
+
// ever reached an allow — this fix created the path, and closes it here.
|
|
325
|
+
// Same rule every other guard in this function follows: when the real subject
|
|
326
|
+
// cannot be known, fall back to the opener line, which fails the format gate.
|
|
327
|
+
if (opener[5]) return lines[0];
|
|
328
|
+
|
|
329
|
+
// `<<-` strips leading TABS from every body line, including the terminator.
|
|
330
|
+
const stripTabs = opener[1] === '-';
|
|
331
|
+
const delimiter = opener[2] || opener[3] || opener[4];
|
|
332
|
+
const body = lines.slice(1).map((l) => (stripTabs ? l.replace(/^\t+/, '') : l));
|
|
333
|
+
|
|
334
|
+
// TRUNCATION GUARD. The capture that produced this argument stops at the first
|
|
335
|
+
// `"`, so a message containing one arrives here missing its tail — and its
|
|
336
|
+
// terminator. Resolving anyway would hand the length gate a PREFIX of the real
|
|
337
|
+
// subject and let an over-long message through, an enforcement hole that did
|
|
338
|
+
// not exist before this fix (review of #3802). A body with no terminator is
|
|
339
|
+
// therefore not resolved at all: returning the opener line fails the format
|
|
340
|
+
// gate, which is exactly what this whole form did before the fix. The fix
|
|
341
|
+
// applies where the capture is complete and changes nothing where it is not.
|
|
342
|
+
const end = body.indexOf(delimiter);
|
|
343
|
+
|
|
344
|
+
// POST-TERMINATOR GUARD (review of #3816, round 3 — BLOCKER). Everything
|
|
345
|
+
// after the terminator is still part of the real message once bash
|
|
346
|
+
// substitutes: `-m "$(cat <<'EOF'\nfeat: ok\nEOF\n) <200 a's>"` expands to a
|
|
347
|
+
// single 200+ char subject, but discarding the tail measured 8 and DODGED
|
|
348
|
+
// COMMIT_SUBJECT_TOO_LONG — the same prefix-measurement class the truncation
|
|
349
|
+
// guard below exists for, missed on the other side of the terminator. The
|
|
350
|
+
// canonical idiom's tail is exactly one closing-paren line; anything else
|
|
351
|
+
// means the substitution is composed with more text, so fall back to the
|
|
352
|
+
// opener line — blocked, the pre-fix behaviour for the whole form.
|
|
353
|
+
if (end !== -1) {
|
|
354
|
+
const tail = body.slice(end + 1);
|
|
355
|
+
if (tail.length !== 1 || !/^[ \t]*\)[ \t]*$/.test(tail[0])) return lines[0];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// git's default `cleanup=whitespace` strips leading blank lines, so the subject
|
|
359
|
+
// is the first NON-EMPTY body line, not blindly the first one. Taking lines[1]
|
|
360
|
+
// returned '' for a body that starts blank and falsely blocked a conforming
|
|
361
|
+
// commit — the very defect class #3802 reports (review of #3802).
|
|
362
|
+
const scan = end === -1 ? body : body.slice(0, end);
|
|
363
|
+
// "Blank" is git's ASCII definition — space and tab — never JavaScript's
|
|
364
|
+
// trim(), whose Unicode whitespace class skips lines git KEEPS: a body whose
|
|
365
|
+
// first line is a NBSP resolved to the SECOND line while git's real subject
|
|
366
|
+
// is the NBSP line — an enforcement bypass (Codex review of #3816, verified
|
|
367
|
+
// against `git stripspace`, which preserves the c2a0 bytes). A Unicode-blank
|
|
368
|
+
// first line is now returned as the subject and fails the format gate: the
|
|
369
|
+
// same fail-closed direction git itself takes.
|
|
370
|
+
const idx = scan.findIndex((l) => !/^[ \t]*$/.test(l));
|
|
371
|
+
if (idx === -1) return end === -1 ? lines[0] : '';
|
|
372
|
+
|
|
373
|
+
// Truncation is only fatal to the line it lands IN. A captured line is complete
|
|
374
|
+
// exactly when another line follows it, because the capture kept its newline.
|
|
375
|
+
// So an unterminated body whose subject line is followed by more text is still
|
|
376
|
+
// measurable; only a subject line that runs to the end of a truncated capture
|
|
377
|
+
// is not, and that one falls back to the opener — which fails the format gate,
|
|
378
|
+
// exactly as this whole form did before the fix. Without this, the length gate
|
|
379
|
+
// measured a PREFIX of the real subject and let an over-long message through
|
|
380
|
+
// (review of #3802).
|
|
381
|
+
if (end === -1 && idx >= body.length - 1) return lines[0];
|
|
382
|
+
|
|
383
|
+
// git's `cleanup=whitespace` strips whitespace at BOTH ends of the line, not
|
|
384
|
+
// just leading blanks. Measuring the raw line rejected `feat: <66 x's>` plus
|
|
385
|
+
// three trailing spaces as 75 chars when git's actual subject is a conforming
|
|
386
|
+
// 72 — a still-blocked conforming commit, the very defect #3802 reports
|
|
387
|
+
// (review of #3816). Trailing only, here: leading blank-LINE handling is the
|
|
388
|
+
// findIndex above, and `<<-` leading-tab stripping already happened.
|
|
389
|
+
return scan[idx].replace(/[ \t]+$/, '');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
module.exports = { isGitSubcommand, tokenize, extractBranchArgument, skipToSubcommand, resolveCommitSubject };
|
|
@@ -26,11 +26,29 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
const INJECTION_PATTERNS = Object.freeze([
|
|
29
|
-
|
|
30
|
-
/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// #4016: ONE filler-tolerant imperative-override pattern. It REPLACES the
|
|
30
|
+
// five narrow verb patterns (ignore x2 / disregard / forget / override) that
|
|
31
|
+
// used to sit here: they tolerated no filler between verb and noun, so a
|
|
32
|
+
// planted "forget all of your ..." phrasing (measured in the wild) matched
|
|
33
|
+
// nothing. A single pattern rather than narrow-plus-combined because both
|
|
34
|
+
// consumers count one finding PER PATTERN toward the severity threshold
|
|
35
|
+
// (3 = HIGH, blockable under security.injection_blocking): overlapping
|
|
36
|
+
// patterns made one sentence count twice and pushed a two-phrasing LOW
|
|
37
|
+
// payload to HIGH (PR #4061 review).
|
|
38
|
+
// verbs ignore|disregard|forget|discard|override. `override` carries
|
|
39
|
+
// the old `override system|previous prompt|instructions` family;
|
|
40
|
+
// `discard` is the synonym seen in the same planted phrasings.
|
|
41
|
+
// fillers at least ONE of all|of|the|your|my|<qualifier> must sit between
|
|
42
|
+
// verb and noun (a lookahead, no repetition). Bare "override
|
|
43
|
+
// rules" / "ignore instructions" are ordinary repo prose
|
|
44
|
+
// (measured: 6 hits across docs and source) and stay unmatched.
|
|
45
|
+
// tails `disregard (all) previous` with no noun, and bare `forget` +
|
|
46
|
+
// `instructions`, are kept verbatim because the old narrow
|
|
47
|
+
// patterns accepted them; the superset is pinned by
|
|
48
|
+
// tests/injection-patterns-parity.security.test.cjs.
|
|
49
|
+
// known FP determined linter-doc prose such as "ignore (the) rules" trips
|
|
50
|
+
// a single-pattern LOW advisory; one pattern can never block.
|
|
51
|
+
/(?:ignore|disregard|forget|discard|override)\s+(?=(?:all|of|the|your|my|system|previous|prior|above|earlier)\s)(?:all\s+)?(?:of\s+)?(?:the\s+|your\s+|my\s+)?(?:(?:system|previous|prior|above|earlier)\s+)?(?:instructions|directives|prompts?|rules)|disregard\s+(?:all\s+)?previous|forget\s+instructions/i,
|
|
34
52
|
/you\s+are\s+now\s+(?:a|an|the)\s+/i,
|
|
35
53
|
/act\s+as\s+(?:a|an|the)\s+(?!plan|phase|wave)/i,
|
|
36
54
|
/pretend\s+(?:you(?:'re| are)\s+|to\s+be\s+)/i,
|
|
@@ -42,4 +60,16 @@ const INJECTION_PATTERNS = Object.freeze([
|
|
|
42
60
|
/<<\s*SYS\s*>>/i,
|
|
43
61
|
]);
|
|
44
62
|
|
|
45
|
-
|
|
63
|
+
// Short single-line label for a matched pattern, used for both the rendered
|
|
64
|
+
// advisory prose and the typed `findings[].match` field in BOTH consumer
|
|
65
|
+
// hooks. The raw regex source is not user-facing: the #4016 superset pattern
|
|
66
|
+
// is ~280 characters, and gsd-prompt-guard.js echoed it verbatim into its
|
|
67
|
+
// advisory (PR #4061 review nit). Byte-identical to the transform
|
|
68
|
+
// gsd-read-injection-scanner.js carried inline since #3523, hoisted here so
|
|
69
|
+
// one finding renders the same way in both hooks. Both hooks already require
|
|
70
|
+
// this module, so this adds no new staging dependency.
|
|
71
|
+
function describePattern(pattern) {
|
|
72
|
+
return pattern.source.replace(/\\s\+/g, '-').replace(/[()\\]/g, '').substring(0, 50);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { INJECTION_PATTERNS, describePattern };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/gsd-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.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": {
|
|
@@ -121,7 +121,8 @@
|
|
|
121
121
|
"lint:table-schema-drift": "node scripts/lint-table-schema-drift.cjs",
|
|
122
122
|
"lint:frontmatter-scalar-broad-grep": "node scripts/lint-frontmatter-scalar-broad-grep.cjs",
|
|
123
123
|
"lint:removed-but-needed": "node scripts/lint-removed-but-needed.cjs",
|
|
124
|
-
"lint:
|
|
124
|
+
"lint:response-language": "node scripts/lint-response-language-coverage.cjs",
|
|
125
|
+
"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/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",
|
|
125
126
|
"lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
|
|
126
127
|
"lint:regression-names": "node scripts/lint-regression-test-names.cjs",
|
|
127
128
|
"lint:descriptions": "node scripts/lint-descriptions.cjs",
|
|
@@ -148,13 +149,12 @@
|
|
|
148
149
|
"test:qa": "node scripts/run-tests.cjs --suite qa",
|
|
149
150
|
"test:affected": "node scripts/run-affected-tests.cjs",
|
|
150
151
|
"test:coverage": "c8 --check-coverage --lines 70 --branches 60 --reporter text --include 'gsd-core/bin/lib/**/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs",
|
|
151
|
-
"test:coverage:scripts-floor": "c8 check-coverage --lines 55 --include 'scripts/**/*.cjs' --exclude 'tests/**' --all",
|
|
152
|
+
"test:coverage:scripts-floor": "c8 report --check-coverage --lines 55 --merge-async --include 'scripts/**/*.cjs' --exclude 'tests/**' --all",
|
|
152
153
|
"test:coverage:unit": "c8 --reporter text --reporter json-summary --merge-async --include 'gsd-core/bin/lib/**/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs --suite unit && node scripts/check-coverage-gate.cjs",
|
|
153
|
-
"test:coverage:unit:raw": "c8 --reporter none node scripts/run-tests.cjs --suite unit",
|
|
154
|
+
"test:coverage:unit:raw": "c8 --reporter none --merge-async node scripts/run-tests.cjs --suite unit",
|
|
154
155
|
"test:coverage:report": "c8 report --reporter text --reporter json-summary --merge-async --include 'gsd-core/bin/lib/**/*.cjs' --exclude 'tests/**' --all && node scripts/check-coverage-gate.cjs",
|
|
155
156
|
"test:coverage:all": "npm run test:coverage",
|
|
156
157
|
"test:mutation": "stryker run",
|
|
157
|
-
"test:mutation:since": "stryker run --incremental --since origin/next",
|
|
158
158
|
"gen:features": "node scripts/gen-features.cjs",
|
|
159
159
|
"gen:state-md-docs": "node scripts/gen-state-md-docs.cjs"
|
|
160
160
|
},
|
package/scripts/build-hooks.js
CHANGED
|
@@ -61,6 +61,8 @@ const HOOKS_TO_COPY = [
|
|
|
61
61
|
'gsd-prompt-guard.js',
|
|
62
62
|
'gsd-read-guard.js',
|
|
63
63
|
'gsd-read-injection-scanner.js',
|
|
64
|
+
// Secret-file read guard (#4221) — replaces the installer's Read(.env*) deny rules
|
|
65
|
+
'gsd-secret-read-guard.js',
|
|
64
66
|
'gsd-statusline.js',
|
|
65
67
|
'gsd-update-banner.js',
|
|
66
68
|
'gsd-workflow-guard.js',
|
|
@@ -71,10 +73,15 @@ const HOOKS_TO_COPY = [
|
|
|
71
73
|
'gsd-session-state.sh',
|
|
72
74
|
'gsd-validate-commit.sh',
|
|
73
75
|
'gsd-phase-boundary.sh',
|
|
74
|
-
// Portable node resolver (#3662).
|
|
75
|
-
//
|
|
76
|
-
// <
|
|
77
|
-
//
|
|
76
|
+
// Portable node resolver (#3662). Managed JS hook commands under
|
|
77
|
+
// --portable-hooks route through it (bash <resolver> <baked-node>
|
|
78
|
+
// <script>) so node resolves at hook-fire time in every environment
|
|
79
|
+
// sharing the config root. It IS registered in MANAGED_HOOKS
|
|
80
|
+
// (managed-hooks-registry.cjs) for staleness tracking like every other
|
|
81
|
+
// shipped .sh hook, and install.js stamps its {{GSD_VERSION}} header the
|
|
82
|
+
// same way (#4076 — the prior comment here claimed the opposite on both
|
|
83
|
+
// counts, which is why the header was missing and staleness detection was
|
|
84
|
+
// permanently broken for this file).
|
|
78
85
|
'gsd-node-runner.sh',
|
|
79
86
|
// Graphify auto-update hook (#3347 / PR #3557 / #3579). Opt-in via
|
|
80
87
|
// .planning/config.json graphify.auto_update; off by default.
|
|
@@ -97,6 +97,10 @@ const RULES = [
|
|
|
97
97
|
tests: [
|
|
98
98
|
'tests/run-tests-harness.test.cjs',
|
|
99
99
|
'tests/workflow-shell-pinning.test.cjs',
|
|
100
|
+
// #4220: the run-scoped temp root + computeSweepProtectSet ancestor-walk
|
|
101
|
+
// termination coverage — was previously not re-selected by an edit to
|
|
102
|
+
// scripts/run-tests.cjs, the exact file that shipped the #4020 hang.
|
|
103
|
+
'tests/run-tests-temp-root.test.cjs',
|
|
100
104
|
],
|
|
101
105
|
},
|
|
102
106
|
{
|
|
@@ -302,6 +306,9 @@ const RULES = [
|
|
|
302
306
|
'tests/require-userprofile-with-home.rule.test.cjs',
|
|
303
307
|
'tests/normalize-path-in-content.rule.test.cjs',
|
|
304
308
|
'tests/require-fs-op-fallback.rule.test.cjs',
|
|
309
|
+
// #4244 (origin #4020/#4220 Windows CI hang) — see ADR-1703 amendment.
|
|
310
|
+
'tests/require-full-tmpdir-triad.rule.test.cjs',
|
|
311
|
+
'tests/no-unbounded-dirname-walk.rule.test.cjs',
|
|
305
312
|
],
|
|
306
313
|
},
|
|
307
314
|
{
|
|
@@ -335,6 +335,16 @@ const DOCS_GUARD_TESTS = {
|
|
|
335
335
|
],
|
|
336
336
|
'tests/progress-forensic.test.cjs': ['docs/COMMANDS.md'],
|
|
337
337
|
'tests/repo-layout.test.cjs': ['docs/contributing/bootstrap.md'],
|
|
338
|
+
// Reads REQ-LANG-04 and runs every form it offers an author through the
|
|
339
|
+
// matcher that enforces it, so a reword of the requirement alone is exactly
|
|
340
|
+
// the change this guard must run on (#2529). Both paths are named because
|
|
341
|
+
// #3840 made docs/FEATURES.md a generated projection: the requirement's
|
|
342
|
+
// source is the fragment, and an edit there that is not regenerated would
|
|
343
|
+
// otherwise reach this guard through neither path.
|
|
344
|
+
'tests/response-language-coverage.test.cjs': [
|
|
345
|
+
'docs/FEATURES.md',
|
|
346
|
+
'docs/features/response-language-config.md',
|
|
347
|
+
],
|
|
338
348
|
'tests/reversibility-tagging.test.cjs': ['docs/reference/plan-md.md'],
|
|
339
349
|
'tests/reviewer-docs-parity.test.cjs': [
|
|
340
350
|
'docs/COMMANDS.md',
|
|
@@ -30,7 +30,13 @@ const CONTRACT_PATH = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'loop-host-contr
|
|
|
30
30
|
// The five step workflows in pipeline order
|
|
31
31
|
const STEP_WORKFLOWS = [
|
|
32
32
|
{ file: 'discuss-phase.md', step: 'discuss' },
|
|
33
|
-
{
|
|
33
|
+
{
|
|
34
|
+
file: 'plan-phase.md',
|
|
35
|
+
step: 'plan',
|
|
36
|
+
auxiliaryHosts: [
|
|
37
|
+
{ file: 'quick.md', point: 'plan:pre', kinds: ['contribution'], into: 'planner' },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
34
40
|
{ file: 'execute-phase.md', step: 'execute' },
|
|
35
41
|
{ file: 'verify-work.md', step: 'verify' },
|
|
36
42
|
{ file: 'ship.md', step: 'ship' },
|
|
@@ -295,7 +301,7 @@ function buildContract(workflowsDir) {
|
|
|
295
301
|
const contract = [];
|
|
296
302
|
const allErrors = [];
|
|
297
303
|
|
|
298
|
-
for (const { file, step } of STEP_WORKFLOWS) {
|
|
304
|
+
for (const { file, step, auxiliaryHosts = [] } of STEP_WORKFLOWS) {
|
|
299
305
|
const filePath = path.join(resolvedDir, file);
|
|
300
306
|
let content;
|
|
301
307
|
try {
|
|
@@ -325,6 +331,34 @@ function buildContract(workflowsDir) {
|
|
|
325
331
|
const roleErrors = crossCheckRoles(content, entry.agentRoles, file);
|
|
326
332
|
allErrors.push(...roleErrors);
|
|
327
333
|
|
|
334
|
+
for (const auxiliary of auxiliaryHosts) {
|
|
335
|
+
let auxiliaryContent;
|
|
336
|
+
try {
|
|
337
|
+
auxiliaryContent = fs.readFileSync(path.join(resolvedDir, auxiliary.file), 'utf8');
|
|
338
|
+
} catch (err) {
|
|
339
|
+
allErrors.push('Could not read auxiliary host ' + auxiliary.file + ': ' + String(err.message));
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const wiredPoints = scanWiredPoints(auxiliaryContent);
|
|
344
|
+
if (!wiredPoints.has(auxiliary.point)) {
|
|
345
|
+
allErrors.push(
|
|
346
|
+
auxiliary.file + ': auxiliary host missing expected point "' + auxiliary.point + '"',
|
|
347
|
+
);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const wiredKinds = scanWiredKinds(auxiliaryContent, auxiliary.into).get(auxiliary.point) || new Set();
|
|
352
|
+
for (const kind of auxiliary.kinds) {
|
|
353
|
+
if (!wiredKinds.has(kind)) {
|
|
354
|
+
allErrors.push(
|
|
355
|
+
auxiliary.file + ': auxiliary host point "' + auxiliary.point +
|
|
356
|
+
'" missing expected kind "' + kind + '"',
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
328
362
|
contract.push(entry);
|
|
329
363
|
}
|
|
330
364
|
|
|
@@ -437,7 +471,10 @@ function main() {
|
|
|
437
471
|
* registry generator, conformance gate) must derive from this rather than maintaining
|
|
438
472
|
* a separate hardcoded list.
|
|
439
473
|
*/
|
|
440
|
-
const HOST_LOOP_FILES = STEP_WORKFLOWS.
|
|
474
|
+
const HOST_LOOP_FILES = STEP_WORKFLOWS.flatMap(({ file, auxiliaryHosts = [] }) => [
|
|
475
|
+
'gsd-core/workflows/' + file,
|
|
476
|
+
...auxiliaryHosts.map((host) => 'gsd-core/workflows/' + host.file),
|
|
477
|
+
]);
|
|
441
478
|
|
|
442
479
|
/**
|
|
443
480
|
* Pure function: scan a text string for `loop render-hooks <point>` call sites.
|
|
@@ -475,9 +512,11 @@ const HOOK_KINDS = ['contribution', 'step', 'gate'];
|
|
|
475
512
|
* - A deferral line (one carrying an `@`-included path to the generic
|
|
476
513
|
* contract, e.g. `@gsd-core/references/loop-hook-dispatch.md`) that names a
|
|
477
514
|
* kind (`kind == "step"`) covers that kind; a deferral line with no kind
|
|
478
|
-
* discriminator ("apply each entry") covers every kind
|
|
479
|
-
*
|
|
480
|
-
*
|
|
515
|
+
* discriminator ("apply each entry") covers every kind only when no role
|
|
516
|
+
* target is required. With `expectedInto`, the same segment must explicitly
|
|
517
|
+
* name both the kind and that exact `into` target. A bare §-citation of the
|
|
518
|
+
* reference (validation guidance only, no `@`) covers nothing — plan-phase
|
|
519
|
+
* cites the gate-validation section while dispatching only gates.
|
|
481
520
|
* - Otherwise a kind is covered when some LINE dispatches it unconditionally:
|
|
482
521
|
* a `kind == "<kind>"` discriminator with NO same-line narrowing to one
|
|
483
522
|
* hook (`ref.skill ==`, `ref.agent ==`, `ref.command ==`). A narrowed line
|
|
@@ -491,19 +530,26 @@ const HOOK_KINDS = ['contribution', 'step', 'gate'];
|
|
|
491
530
|
* Pure: same input, same output; CRLF-safe (line splitting tolerates \r).
|
|
492
531
|
*
|
|
493
532
|
* @param {string} region Dispatch text following one call site.
|
|
533
|
+
* @param {string} [expectedInto] Optional role target required in the same segment.
|
|
494
534
|
* @returns {Set<string>}
|
|
495
535
|
*/
|
|
496
|
-
function coveredKindsInRegion(region) {
|
|
536
|
+
function coveredKindsInRegion(region, expectedInto) {
|
|
497
537
|
const covered = new Set();
|
|
498
538
|
// Same-SEGMENT narrowing to ONE hook voids credit: `ref.skill ==`, `capId ==`,
|
|
499
|
-
// and `into ==` each special-case a subset, not the kind generally
|
|
539
|
+
// and `into ==` each special-case a subset, not the kind generally. An
|
|
540
|
+
// auxiliary host may name the one role it actually hosts via `expectedInto`;
|
|
541
|
+
// any other role target still voids credit.
|
|
500
542
|
// (plan-phase's `kind == "contribution" and capId == "security"` is the
|
|
501
543
|
// hand-rolled shape; `into == "planner"` covers only planner-targeted
|
|
502
544
|
// contributions). Segments, not lines: execute-phase legitimately writes
|
|
503
545
|
// "dispatch `kind == "step"` hooks per … . `ref.skill == "code-review"`:" —
|
|
504
546
|
// the deferral is one sentence, the specialization the next; narrowing in a
|
|
505
547
|
// DIFFERENT segment must not void the deferral's credit.
|
|
506
|
-
const
|
|
548
|
+
const identityNarrowingRe = /(?:ref\.(?:skill|agent|command)|capId)\s*={2,3}/;
|
|
549
|
+
const intoNarrowingRe = /into\s*={2,3}/;
|
|
550
|
+
const expectedIntoRe = expectedInto
|
|
551
|
+
? new RegExp(`into\\s*={2,3}\\s*["']${escapeRegExp(expectedInto)}["']`)
|
|
552
|
+
: null;
|
|
507
553
|
// Negated mentions describe an absence, not a dispatch ("Branch 1 — no active
|
|
508
554
|
// step hooks (`activeHooks` has no entry with `kind == "step"`)" — ship.md).
|
|
509
555
|
const negationRe = /\b(?:no|without|absent|lacks?|missing)\b[^.|]*kind\s*={2,3}/;
|
|
@@ -519,17 +565,22 @@ function coveredKindsInRegion(region) {
|
|
|
519
565
|
}
|
|
520
566
|
if (kindDiscriminators.length === 0) {
|
|
521
567
|
// A deferral with no kind discriminator ("apply each entry per …")
|
|
522
|
-
// still covers every kind.
|
|
523
|
-
|
|
568
|
+
// still covers every kind for generic hosts. An auxiliary host with a
|
|
569
|
+
// required role target must state both its kind and target explicitly.
|
|
570
|
+
if (!expectedInto && deferralRe.test(segment)) for (const kind of HOOK_KINDS) covered.add(kind);
|
|
524
571
|
continue;
|
|
525
572
|
}
|
|
526
573
|
if (negationRe.test(segment)) continue;
|
|
574
|
+
const narrowed = identityNarrowingRe.test(segment) ||
|
|
575
|
+
(expectedInto
|
|
576
|
+
? !expectedIntoRe.test(segment)
|
|
577
|
+
: intoNarrowingRe.test(segment));
|
|
527
578
|
if (deferralRe.test(segment)) {
|
|
528
579
|
// Deferral naming kinds ("dispatch `kind == "step"` hooks per …").
|
|
529
|
-
if (!
|
|
580
|
+
if (!narrowed) for (const kind of kindDiscriminators) covered.add(kind);
|
|
530
581
|
continue;
|
|
531
582
|
}
|
|
532
|
-
if (!
|
|
583
|
+
if (!narrowed) for (const kind of kindDiscriminators) covered.add(kind);
|
|
533
584
|
}
|
|
534
585
|
}
|
|
535
586
|
return covered;
|
|
@@ -540,9 +591,10 @@ function coveredKindsInRegion(region) {
|
|
|
540
591
|
* per point, the union of hook kinds its dispatch regions cover (#3606).
|
|
541
592
|
*
|
|
542
593
|
* @param {string} text Content of a workflow file (or any text).
|
|
594
|
+
* @param {string} [expectedInto] Optional role target an auxiliary host must dispatch.
|
|
543
595
|
* @returns {Map<string, Set<string>>} point → covered kinds.
|
|
544
596
|
*/
|
|
545
|
-
function scanWiredKinds(text) {
|
|
597
|
+
function scanWiredKinds(text, expectedInto) {
|
|
546
598
|
const result = new Map();
|
|
547
599
|
const siteRe = CALL_SITE_RE;
|
|
548
600
|
const sites = [];
|
|
@@ -552,7 +604,7 @@ function scanWiredKinds(text) {
|
|
|
552
604
|
for (let i = 0; i < sites.length; i++) {
|
|
553
605
|
const regionEnd = i + 1 < sites.length ? sites[i + 1].start : Math.min(text.length, sites[i].start + REGION_CAP);
|
|
554
606
|
const region = text.slice(sites[i].start, regionEnd);
|
|
555
|
-
const covered = coveredKindsInRegion(region);
|
|
607
|
+
const covered = coveredKindsInRegion(region, expectedInto);
|
|
556
608
|
if (!result.has(sites[i].point)) result.set(sites[i].point, new Set());
|
|
557
609
|
for (const kind of covered) result.get(sites[i].point).add(kind);
|
|
558
610
|
}
|