@opengsd/gsd-core 1.20.0 → 1.21.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 +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +265 -161
- package/commands/gsd/autonomous.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/gsd-core/bin/gsd-tools.cjs +554 -823
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +99 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +20 -18
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +20 -28
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-id.cjs +26 -9
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +170 -45
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +103 -46
- package/gsd-core/bin/lib/state.cjs +448 -63
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +168 -13
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +4 -3
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +9 -6
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +168 -13
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/lint.cjs +28 -3
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +11 -2
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -19,19 +19,35 @@ guessing one.
|
|
|
19
19
|
**Run a count/match gate before it ships (#4927).** A gate you author — or one a `RESEARCH.md`
|
|
20
20
|
hands you — that counts or matches text (`grep -c`, `grep -q`, `rg`, `test -n "$(grep …)"`) is a
|
|
21
21
|
claim about the tree, and two defects are invisible to reading and take one command to expose. You
|
|
22
|
-
have a shell when you author the plan, so run each such gate against the **current tree** and
|
|
23
|
-
|
|
22
|
+
have a shell when you author the plan, so run each such gate against the **current tree** and record
|
|
23
|
+
what it printed, and what it must print after the change, as a **baseline notation** next to the
|
|
24
|
+
gate (#4309):
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
[HEAD=<value> EXPECT<op><value> [@<sha>] [GUARD MUTATION: <ref>] [UNPINNED]]
|
|
28
|
+
value ∈ <integer> | absent | present | error:<exit-code> op ∈ == != >= > <= <
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
In an `<acceptance_criteria>` line write it inline: ``- `grep -c "## Quickstart" README.md` -ge 1 [HEAD=0 EXPECT>=1 @<sha>]``.
|
|
32
|
+
In an `<automated>` command put it in a trailing shell comment: `grep -c "## Quickstart" README.md # [HEAD=0 EXPECT>=1]`.
|
|
33
|
+
`HEAD` is the value the command printed, whichever counting flag produced it. A missing file is
|
|
34
|
+
`error:2` (what `grep` exits with), never `0`. `@<sha>` is the commit you measured at; write
|
|
35
|
+
`UNPINNED` instead when you did not record one. `verify plan-structure` reads the notation and
|
|
36
|
+
rejects one whose comparison already holds (B1). Do not write the old free-form
|
|
37
|
+
`# baseline at HEAD: N` comment: nothing can check it, and `verify plan-structure` warns on it (B2).
|
|
24
38
|
|
|
25
39
|
- **It already passes at HEAD.** `grep -c "Agent" .planning/PROJECT.md` returned `6` before any work
|
|
26
|
-
existed, because the word sat in an unrelated row
|
|
27
|
-
or not the work happens. Fix the gate: anchor it to the
|
|
28
|
-
creates, or
|
|
40
|
+
existed, because the word sat in an unrelated row. `[HEAD=6 EXPECT>=1]` is already true, so the
|
|
41
|
+
gate measures nothing and passes whether or not the work happens. Fix the gate: anchor it to the
|
|
42
|
+
new heading, key, or line the task creates, or tighten `EXPECT` to the delta (`[HEAD=6 EXPECT>=7]`).
|
|
43
|
+
Only a gate that is meant to hold before and after (a guard against regression) may stay
|
|
44
|
+
already-true, and then it must name the mutation that turns it red: `GUARD MUTATION: <ref>`.
|
|
29
45
|
- **Its literal can never match the deliverable.** `grep` is case-sensitive, so `grep -c "quickstart"`
|
|
30
46
|
fails on a correct `## Quickstart`. Match the real casing of the text the task writes, or use
|
|
31
47
|
`-i` when casing is not part of the contract.
|
|
32
48
|
|
|
33
49
|
A gate that is meant to prove a *negative* is checked the other way: it must pass after the work
|
|
34
|
-
and its baseline must show the thing was there to remove. Only read-only commands are safe to run
|
|
50
|
+
and its baseline must show the thing was there to remove (`[HEAD=3 EXPECT==0]`). Only read-only commands are safe to run
|
|
35
51
|
this way — run `grep`/`rg`/`test` gates; do not execute a gate that builds, installs, or writes.
|
|
36
52
|
|
|
37
53
|
**Root-relative, never absolute (#4767).** The paths in your prompt's `<required_reading>` are
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<purpose>
|
|
4
4
|
|
|
5
|
-
Drive milestone phases autonomously — all remaining phases, a range via `--from N`/`--to N`, or a single phase via `--only N`. For each incomplete phase: discuss → plan → execute using Skill() flat invocations. When `--converge` or `--cross-ai` is set, route the planning step through plan-review convergence before execution. Pauses only for explicit user decisions (grey
|
|
5
|
+
Drive milestone phases autonomously — all remaining phases, a range via `--from N`/`--to N`, or a single phase via `--only N`. For each incomplete phase: discuss → plan → execute using Skill() flat invocations. When `--converge` or `--cross-ai` is set, route the planning step through plan-review convergence before execution. Pauses only for explicit user decisions (grey areas that need the user, blockers, validation requests). Re-reads ROADMAP.md after each phase to catch dynamically inserted phases.
|
|
6
6
|
|
|
7
7
|
</purpose>
|
|
8
8
|
|
|
@@ -311,7 +311,7 @@ The discuss step in `--auto` mode MUST NOT loop. If CONTEXT.md already exists af
|
|
|
311
311
|
Skill(skill="gsd-discuss-phase", args="${PHASE_NUM}")
|
|
312
312
|
```
|
|
313
313
|
|
|
314
|
-
**If `INTERACTIVE` is NOT set:** Execute the smart_discuss step for this phase (
|
|
314
|
+
**If `INTERACTIVE` is NOT set:** Execute the smart_discuss step for this phase (classified grey-area questions).
|
|
315
315
|
|
|
316
316
|
After discuss completes (either mode), verify context was written:
|
|
317
317
|
|
|
@@ -573,7 +573,7 @@ Display the review result summary and score from UI-REVIEW.md if produced. Conti
|
|
|
573
573
|
|
|
574
574
|
> Full instructions are in `gsd-core/references/autonomous-smart-discuss.md`. Read that file now and follow it exactly.
|
|
575
575
|
|
|
576
|
-
Smart discuss is an autonomous-optimized variant of `gsd-discuss-phase`. It
|
|
576
|
+
Smart discuss is an autonomous-optimized variant of `gsd-discuss-phase`. It auto-decides reversible technical questions (logged), asks the rest (preference questions neutrally), and writes CONTEXT.md and DISCUSSION-LOG.md.
|
|
577
577
|
|
|
578
578
|
**Inputs:** `PHASE_NUM` from execute_phase.
|
|
579
579
|
|
|
@@ -802,7 +802,7 @@ When any phase operation fails or a blocker is detected, present 3 options via A
|
|
|
802
802
|
|
|
803
803
|
<success_criteria>
|
|
804
804
|
- [ ] All incomplete phases executed in order (smart discuss → ui-phase → plan → execute → ui-review each)
|
|
805
|
-
- [ ] Smart discuss
|
|
805
|
+
- [ ] Smart discuss auto-decides reversible technical questions and asks the rest
|
|
806
806
|
- [ ] Progress banners displayed between phases
|
|
807
807
|
- [ ] Execute-phase invoked with --no-transition (autonomous manages transitions)
|
|
808
808
|
- [ ] Post-execution verification reads VERIFICATION.md and routes on status
|
|
@@ -86,21 +86,19 @@ FIX_REPORT_PATH="${PHASE_DIR}/${PADDED_PHASE}-REVIEW-FIX.md"
|
|
|
86
86
|
</step>
|
|
87
87
|
|
|
88
88
|
<step name="check_config_gate">
|
|
89
|
-
Check if code review is active via the capability
|
|
89
|
+
Check if code review is active via `workflow.code_review` (the capability's on/off toggle — independent of `workflow.code_review_point`, the loop-point selector; a manual invocation must work regardless of which automatic point is currently configured):
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
|
-
|
|
92
|
+
CODE_REVIEW_ENABLED=$(gsd_run query config-get workflow.code_review --raw 2>/dev/null || echo "true")
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
If no active code-review step hook exists:
|
|
95
|
+
If `CODE_REVIEW_ENABLED` is not `"true"`:
|
|
98
96
|
```
|
|
99
97
|
Code review fix skipped (code-review capability inactive)
|
|
100
98
|
```
|
|
101
99
|
Exit workflow.
|
|
102
100
|
|
|
103
|
-
Default is active
|
|
101
|
+
Default is active (`workflow.code_review` schema default is `true`) — only skip when explicitly disabled. This check runs AFTER phase validation so invalid phase errors are shown first.
|
|
104
102
|
|
|
105
103
|
Note: This reuses the code-review capability activation rather than introducing a separate code-review-fix capability. Rationale: fixes are meaningless without review, so a single activation boundary makes sense. If independent control is needed later, a separate key can be added in v2.
|
|
106
104
|
</step>
|
|
@@ -666,7 +664,7 @@ echo "════════════════════════
|
|
|
666
664
|
|
|
667
665
|
<success_criteria>
|
|
668
666
|
- [ ] Phase validated before config gate check
|
|
669
|
-
- [ ] Capability gate checked (
|
|
667
|
+
- [ ] Capability gate checked (`workflow.code_review` config key)
|
|
670
668
|
- [ ] REVIEW.md existence verified (error if missing)
|
|
671
669
|
- [ ] REVIEW.md status checked (skip if clean/skipped)
|
|
672
670
|
- [ ] Agent spawned with correct config (review_path, fix_scope, fix_report_path)
|
|
@@ -335,12 +335,6 @@ All subsequent commits go to this branch. User handles merging.
|
|
|
335
335
|
From init JSON: `phase_dir`, `plan_count`, `incomplete_count`.
|
|
336
336
|
|
|
337
337
|
Report: "Found {plan_count} plans in {phase_dir} ({incomplete_count} incomplete)"
|
|
338
|
-
|
|
339
|
-
**Update STATE.md for phase start:**
|
|
340
|
-
```bash
|
|
341
|
-
gsd_run query state.begin-phase --phase "${PHASE_NUMBER}" --name "${PHASE_NAME}" --plans "${PLAN_COUNT}"
|
|
342
|
-
```
|
|
343
|
-
This updates Status, Last Activity, Current focus, Current Position, and plan counts in STATE.md so frontmatter and body text reflect the active phase immediately.
|
|
344
338
|
</step>
|
|
345
339
|
|
|
346
340
|
<step name="discover_and_group_plans">
|
|
@@ -432,6 +426,12 @@ Report:
|
|
|
432
426
|
| 1 | 01-01, 01-02 | {from plan objectives, 3-8 words} |
|
|
433
427
|
| 2 | 01-03 | ... |
|
|
434
428
|
```
|
|
429
|
+
|
|
430
|
+
**Update STATE.md for phase start — only now that at least one plan will be dispatched (#5843).** Every all-filtered arm above has already exited or jumped to `aggregate_results` / `update_roadmap`; reaching here means the post-filter list is non-empty. A run that dispatches nothing must leave STATE.md Current Position, Status and Last activity unchanged.
|
|
431
|
+
```bash
|
|
432
|
+
gsd_run query state.begin-phase --phase "${PHASE_NUMBER}" --name "${PHASE_NAME}" --plans "${PLAN_COUNT}"
|
|
433
|
+
```
|
|
434
|
+
This updates Status, Last Activity, Current focus, Current Position, and plan counts in STATE.md so frontmatter and body text reflect the active phase immediately.
|
|
435
435
|
</step>
|
|
436
436
|
|
|
437
437
|
<step name="cross_ai_delegation">
|
|
@@ -1184,7 +1184,21 @@ Route on `$STATUS`: if `passed`, proceed to update_roadmap. Otherwise keep the p
|
|
|
1184
1184
|
|
|
1185
1185
|
**Step A: Persist human verification items as UAT file.**
|
|
1186
1186
|
|
|
1187
|
-
|
|
1187
|
+
Merge or skip — never blindly create (#5845). A `{phase_num}-UAT.md` may already hold a human's recorded results (from `/gsd:verify-work`, or from an earlier run before gap-closure) that cannot be re-derived. Check first:
|
|
1188
|
+
|
|
1189
|
+
```bash
|
|
1190
|
+
UAT_FILE="{phase_dir}/{phase_num}-UAT.md"
|
|
1191
|
+
if test -f "$UAT_FILE"; then echo "UAT_EXISTS"; else echo "UAT_ABSENT"; fi
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
**If `UAT_EXISTS`:** read the file and merge in place. Never reset, overwrite, or replace it, and do not rename it aside (`phase uat-passed` reads `{phase_num}-UAT.round1.md` in the phase directory as a live blocker).
|
|
1195
|
+
- Keep every recorded result exactly as written, including `result:` values other than `[pending]`, `source:`/`coverage_id:` lines, `## Gaps`, and `## Deferred Follow-Ups`. Keep `status` and `started` unchanged (`/gsd:verify-work` resume recomputes the status from the rows).
|
|
1196
|
+
- For each `human_verification` item in VERIFICATION.md, compare its description to the existing `### N. {description}` test entries. Items not already present (matched by description, case-insensitive with whitespace collapsed) are appended under `## Tests` as the next numbered entry with `expected:` from VERIFICATION.md and `result: [pending]`. Items already present are left untouched.
|
|
1197
|
+
- Recompute the `## Summary` counts (`total`, `passed`, `issues`, `pending`, `skipped`, `blocked`) from the merged test list; set `updated` to now.
|
|
1198
|
+
- If nothing to append (every item already present), do not write the file and do not commit; continue to Step B.
|
|
1199
|
+
- Otherwise commit the merged file with the command below.
|
|
1200
|
+
|
|
1201
|
+
**If `UAT_ABSENT`:** create `{phase_dir}/{phase_num}-UAT.md` using UAT template format:
|
|
1188
1202
|
|
|
1189
1203
|
```markdown
|
|
1190
1204
|
---
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Plan:Pre Generic Step and Gate Dispatch (#5982)
|
|
2
|
+
|
|
3
|
+
Implements @gsd-core/references/loop-hook-dispatch.md for `plan:pre`. Read `activeHooks` from `PLAN_PRE_HOOKS_JSON` in-context. This runs in **manual and auto-chain mode alike**, before the planner is spawned — a resolved hook is dispatched, never silently dropped.
|
|
4
|
+
|
|
5
|
+
**Dedicated sections.** These `capId`s own a `plan:pre` step or gate that a named section of `plan-phase.md` already runs with its own mode-specific behavior. Skip their `step` and `gate` entries here so none runs twice; dispatch **every other** active `step` and `gate`, first-party or third-party, in array order. (Contributions are injected at §5.55 and §8, not here.)
|
|
6
|
+
|
|
7
|
+
| capId | Section | What that section owns |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `research` | §5 | the research step, behind §5's research decision |
|
|
10
|
+
| `drift` | §4.6 | the context-drift gate (§4.6) and the codebase-drift gate (§5.65), both non-blocking |
|
|
11
|
+
| `ui` | §5.6 | the `ui-phase` step and the `ui.plan-gate` gate (Branches 2–6) |
|
|
12
|
+
| `ai-integration` | §5.6 | the AI-integration step (pipeline: skill; manual: recommendation) |
|
|
13
|
+
| `pattern-mapper` | §7.8 | the pattern-mapper agent step, which needs `RESEARCH_PATH` |
|
|
14
|
+
| `intel` | §7.9 | the `intel api-surface` command step, which sets `API_SURFACE_PATH` |
|
|
15
|
+
|
|
16
|
+
If no active `step` or `gate` remains after that, `PLAN_PRE_PRODUCTS` stays empty; return to §5.6. Otherwise make sure the phase values the dispatch lines carry are set — §1 already parsed them from `INIT`, so each is only filled when empty:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _gsd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') return 0;; *) return 1;; esac; }; _gsd_homes() { set -- "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}" "$HOME/.gemini/antigravity-ide" "$HOME/.gemini/antigravity-cli" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}" "${CLINE_CONFIG_DIR:-$HOME/.cline}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}" "${CODEX_HOME:-$HOME/.codex}" "${COPILOT_CONFIG_DIR:-${COPILOT_HOME:-$HOME/.copilot}}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}" "${HERMES_HOME:-$HOME/.hermes}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}" "${KIMI_CONFIG_DIR:-$HOME/.config/agents}" "$HOME/.agents" "${KIMI_CODE_HOME:-$HOME/.kimi-code}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}" "${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}" "${TRAE_CONFIG_DIR:-$HOME/.trae}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}" "${ZCODE_CONFIG_DIR:-$HOME/.zcode}" "${GROK_AGENTS_HOME:-$HOME/.agents}"; for _h; do _gsd_at "$_h/gsd-core/bin/${_GSD_SHIM_NAME}" && return 0; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif _gsd_homes; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; [ -n "$_G" ] && _gsd_id_ok "$_G"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and no identity-proving gsd_run is on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; _gsd_id_ok gsd_run && GSD_IDENTITY_STATUS=ok; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
|
|
20
|
+
IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
|
|
21
|
+
$ARGUMENTS
|
|
22
|
+
GSD_ARGS_END
|
|
23
|
+
GSD_ARGS=${GSD_ARGS%$'\n'}
|
|
24
|
+
GSD_WS=$(echo " $GSD_ARGS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
|
|
25
|
+
PHASE_NUMBER="${PHASE_NUMBER:-$(gsd_run query init.plan-phase ${GSD_WS:+--ws=${GSD_WS##* }} "$PHASE" --pick phase_number 2>/dev/null)}"
|
|
26
|
+
PHASE_DIR="${PHASE_DIR:-$(gsd_run query init.plan-phase ${GSD_WS:+--ws=${GSD_WS##* }} "$PHASE" --pick phase_dir 2>/dev/null)}"
|
|
27
|
+
PHASE_DIR="${PHASE_DIR:-$(gsd_run query init.plan-phase ${GSD_WS:+--ws=${GSD_WS##* }} "$PHASE" --pick expected_phase_dir 2>/dev/null)}"
|
|
28
|
+
PHASE_REQ_IDS="${PHASE_REQ_IDS:-$(gsd_run query init.plan-phase ${GSD_WS:+--ws=${GSD_WS##* }} "$PHASE" --pick phase_req_ids 2>/dev/null)}"
|
|
29
|
+
PHASE_REQ_IDS="${PHASE_REQ_IDS:-TBD}"
|
|
30
|
+
AUTO_CHAIN=$(gsd_run query check auto-mode --pick auto_chain_active 2>/dev/null)
|
|
31
|
+
AUTO_CHAIN="${AUTO_CHAIN:-false}"
|
|
32
|
+
echo "PHASE_DIR=${PHASE_DIR}"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Steps
|
|
36
|
+
|
|
37
|
+
For each remaining active entry where `kind == "step"` (exactly one of `ref.skill`, `ref.agent`, `ref.command` is set):
|
|
38
|
+
|
|
39
|
+
- **`ref.skill`** → `Skill(skill="gsd-${ref.skill}", args="${PHASE} ${GSD_WS}")`, appending `--auto` to `args` only when `AUTO_CHAIN` is `true`. Prepend `gsd-` to `ref.skill`.
|
|
40
|
+
- **`ref.agent`** → print `◆ Spawning ${ref.agent}... (runs in a subagent — no output until it returns; expected, not a freeze)`, then `Agent(prompt=filled_hook_fragment, subagent_type=ref.agent, model="{researcher_model}")` with the hook's `fragment.inline` as the prompt body, phase fields filled. Omit `model=` when `researcher_model` is `"inherit"` or empty (#2517).
|
|
41
|
+
- **`ref.command`** → validate it IN-CONTEXT before any shell use: the value must match `^[a-z][a-z0-9-]*( [a-z][a-z0-9-]*)*$`. Check it yourself — never by pasting it into a shell command to be tested there. A value that fails is a malformed manifest: record a warning naming the `capId`, skip it, continue. Only a value that passed is run:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# shellcheck disable=SC2086 # the validated command is a family and subcommand: splitting it into two words is intended
|
|
45
|
+
gsd_run ${ref.command} --phase "${PHASE_NUMBER}" --raw
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Wait for each result before the next hook. A step is advisory: on error record a warning naming the `capId` and continue, unless `onError == "halt"` — then surface the error and stop the plan-phase workflow.
|
|
49
|
+
|
|
50
|
+
## Gates
|
|
51
|
+
|
|
52
|
+
For each remaining active entry where `kind == "gate"`, in array order. ⚠ **Validate `check` before shell use** (third-party manifest input; `loop-hook-dispatch.md` § `gate`): replace every `.` in `check.query` with `-`, check that normalized value against `^[a-z][a-z0-9]*(-[a-z0-9]+)*( [a-z][a-z0-9]*(-[a-z0-9]+)*)*$` in-context, and write the dot-normalized query wherever `${hook.check.query}` appears below. Serialize `check.predicate` to compact one-line JSON and pass it as a **single argv element** through the quoted heredoc below — never re-quote it into a shell string, where an apostrophe would close the literal. A malformed check is routed per `onError` in step 1, never run.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# named-query gate (the validated query may be several words; splitting it is intended):
|
|
56
|
+
# shellcheck disable=SC2086
|
|
57
|
+
GATE_RESULT=$(gsd_run check ${hook.check.query} "${PHASE_DIR}" "${PHASE_REQ_IDS}" --raw) && CHECK_EXIT=0 || CHECK_EXIT=$?
|
|
58
|
+
```
|
|
59
|
+
OR, for a `predicate` gate (ADR-2008):
|
|
60
|
+
```bash
|
|
61
|
+
IFS= read -rd '' PREDICATE_JSON <<'GSD_PREDICATE_END' || true
|
|
62
|
+
<hook.check.predicate as compact one-line JSON>
|
|
63
|
+
GSD_PREDICATE_END
|
|
64
|
+
PREDICATE_JSON=${PREDICATE_JSON%$'\n'}
|
|
65
|
+
GATE_RESULT=$(gsd_run check predicate --predicate "$PREDICATE_JSON" --phase-dir "${PHASE_DIR}" --phase-number "${PHASE_NUMBER}" --phase-req-ids "${PHASE_REQ_IDS}" --raw) && CHECK_EXIT=0 || CHECK_EXIT=$?
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A gate carrying neither — including an `agentVerdict` check, which has no runner at `plan:pre` — cannot be evaluated here: record a warning naming the `capId` and treat it as a check-command failure in step 1, **never** as a silent pass.
|
|
69
|
+
|
|
70
|
+
**Step 1 — did the check command fail?** (malformed check, non-zero `CHECK_EXIT` including `69` UNAVAILABLE, empty output, or unparseable JSON — its `block` is not a verdict):
|
|
71
|
+
- `onError == "halt"` → surface `⚠ plan:pre gate check failed ({hook.capId})` with the error and stop the plan-phase workflow.
|
|
72
|
+
- otherwise → record a warning naming the `capId` and continue to the next gate.
|
|
73
|
+
|
|
74
|
+
**Step 2 — read `GATE_RESULT.block`:**
|
|
75
|
+
- `blocking == true` and `block == true` → output `⚠ Planning blocked by capability gate ({hook.capId}): {message}` (`check.message`, plus the result's own `message` when it has one) and **EXIT the plan-phase workflow. Do not spawn the planner.**
|
|
76
|
+
- `blocking == false` → advisory: surface the message when `block` is `true`, and continue.
|
|
77
|
+
- `block == false` → continue silently.
|
|
78
|
+
|
|
79
|
+
## Products for the planner (#5960)
|
|
80
|
+
|
|
81
|
+
A step dispatched above hands its declared `produces` files to the planner, so a capability's plan:pre output (for example MemPalace's `MEMORY-RECALL.md`) is read, and only when its step was active this run. Validate each `produces` entry IN-CONTEXT against `^[A-Za-z0-9][A-Za-z0-9._-]*$` (a bare file name: no `/`, no leading `.`); record a warning naming the `capId` and drop a name that fails. Then put the remaining names of the dispatched steps, space-separated, in place of the placeholder:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Plan:pre products (#5960)
|
|
85
|
+
IFS= read -rd '' _PHASE_DIR_IN <<'GSD_PHASE_DIR_END' || true
|
|
86
|
+
<PHASE_DIR from the phase-value block above>
|
|
87
|
+
GSD_PHASE_DIR_END
|
|
88
|
+
PHASE_DIR="${PHASE_DIR:-${_PHASE_DIR_IN%$'\n'}}"
|
|
89
|
+
PLAN_PRE_PRODUCES='<validated produces names of the steps dispatched above, space-separated>'
|
|
90
|
+
[ -n "${PLAN_PRE_PRODUCES}" ] && [ -z "${PHASE_DIR}" ] && echo "WARNING: plan:pre products not collected — PHASE_DIR is empty" >&2
|
|
91
|
+
PLAN_PRE_PRODUCTS=""
|
|
92
|
+
# shellcheck disable=SC2046 # validated bare file names: splitting the substitution is intended
|
|
93
|
+
for _f in $(printf '%s' "$PLAN_PRE_PRODUCES"); do
|
|
94
|
+
if [ -n "${PHASE_DIR}" ] && [ -f "${PHASE_DIR}/${_f}" ]; then PLAN_PRE_PRODUCTS="${PLAN_PRE_PRODUCTS:+${PLAN_PRE_PRODUCTS}, }${PHASE_DIR}/${_f}"; fi
|
|
95
|
+
done
|
|
96
|
+
echo "PLAN_PRE_PRODUCTS=${PLAN_PRE_PRODUCTS}"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`PLAN_PRE_PRODUCTS` is listed in the §8 planner `<required_reading>`; it stays empty when no dispatched step wrote its file. A dedicated-section capability's output (RESEARCH.md, PATTERNS.md, UI-SPEC.md, ...) reaches the planner through its own section, not this list. Shell variables do not survive between tool calls, so fill the `PHASE_DIR` value the phase-value block printed into the quoted heredoc as well.
|
|
100
|
+
|
|
101
|
+
When every remaining hook has been handled, return to §5.6.
|
|
@@ -99,7 +99,7 @@ CONTEXT_WINDOW=$(gsd_run query config-get context_window --raw 2>/dev/null || ec
|
|
|
99
99
|
MVP_MODE_CFG=$(gsd_run query config-get workflow.mvp_mode --raw 2>/dev/null || echo "false")
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
When the tdd capability's `workflow.tdd_mode` is active (resolved via the plan:pre render-hooks), the planner agent is instructed to apply `type: tdd` to eligible tasks using heuristics from `gsd-core/references/tdd.md`. The TDD guidance is injected via the tdd capability's contribution hook at §
|
|
102
|
+
When the tdd capability's `workflow.tdd_mode` is active (resolved via the plan:pre render-hooks), the planner agent is instructed to apply `type: tdd` to eligible tasks using heuristics from `gsd-core/references/tdd.md`. The TDD guidance is injected via the tdd capability's contribution hook at §8; no inline config-get is needed.
|
|
103
103
|
|
|
104
104
|
When `CONTEXT_WINDOW >= 500000`, the planner prompt includes the 3 most recent prior-phase CONTEXT.md/SUMMARY.md files plus any phases in the current phase's `Depends on:` field (explicit deps load regardless of recency).
|
|
105
105
|
|
|
@@ -357,9 +357,9 @@ If `section_manifest` is `null` or `"research-only-modifiers"` is in its `includ
|
|
|
357
357
|
|
|
358
358
|
### 5.1. Standard Research Decision
|
|
359
359
|
|
|
360
|
-
**Skip if** `RESEARCH_ONLY=true` (the research-only mode in 5.0 already determined the path: spawn or exit). Without this guard, an LLM following the workflow could fall through into "use existing,
|
|
360
|
+
**Skip if** `RESEARCH_ONLY=true` (the research-only mode in 5.0 already determined the path: spawn or exit). Without this guard, an LLM following the workflow could fall through into "use existing, continue to step 5.5" → planner spawn, violating the research-only contract. **CR #3045 finding: this gate makes the early-exit unreachable from any non-research-only branch.**
|
|
361
361
|
|
|
362
|
-
**If `has_research` is true (from init) AND no `--research` flag:** Use existing,
|
|
362
|
+
**If `has_research` is true (from init) AND no `--research` flag:** Use existing, continue to step 5.5.
|
|
363
363
|
|
|
364
364
|
**If RESEARCH.md missing OR `--research` flag:**
|
|
365
365
|
|
|
@@ -391,7 +391,7 @@ AskUserQuestion([
|
|
|
391
391
|
])
|
|
392
392
|
```
|
|
393
393
|
|
|
394
|
-
If user selects "Skip research":
|
|
394
|
+
If user selects "Skip research": continue to step 5.5.
|
|
395
395
|
|
|
396
396
|
**If `--auto` and `research_enabled` is false:** Skip research silently (preserves automated behavior).
|
|
397
397
|
|
|
@@ -437,7 +437,7 @@ Agent(
|
|
|
437
437
|
|
|
438
438
|
### Handle Researcher Return
|
|
439
439
|
|
|
440
|
-
- **`## RESEARCH COMPLETE`:** Display confirmation, continue to step
|
|
440
|
+
- **`## RESEARCH COMPLETE`:** Display confirmation, continue to step 5.5
|
|
441
441
|
- **`## RESEARCH BLOCKED`:** Display blocker, offer: 1) Provide context, 2) Skip research, 3) Abort
|
|
442
442
|
|
|
443
443
|
<!-- gsd:section id="research-only-early-exit" when="flag:--research-phase" -->
|
|
@@ -448,14 +448,14 @@ If `section_manifest` is `null` or `"research-only-early-exit"` is in its `inclu
|
|
|
448
448
|
|
|
449
449
|
Skip if `nyquist_validation_enabled` is false OR `research_enabled` is false.
|
|
450
450
|
|
|
451
|
-
If `research_enabled` is false and `nyquist_validation_enabled` is true: warn "Nyquist validation enabled but research disabled — VALIDATION.md cannot be created without RESEARCH.md. Plans will lack validation requirements (Dimension 8)." Continue to step
|
|
451
|
+
If `research_enabled` is false and `nyquist_validation_enabled` is true: warn "Nyquist validation enabled but research disabled — VALIDATION.md cannot be created without RESEARCH.md. Plans will lack validation requirements (Dimension 8)." Continue to step 5.55.
|
|
452
452
|
|
|
453
453
|
**But Nyquist is not applicable for this run** when all of the following are true:
|
|
454
454
|
- `research_enabled` is false
|
|
455
455
|
- `has_research` is false
|
|
456
456
|
- no `--research` flag was provided
|
|
457
457
|
|
|
458
|
-
In that case: **skip validation-strategy creation entirely**. Do **not** expect `RESEARCH.md` or `VALIDATION.md` for this run, and continue to
|
|
458
|
+
In that case: **skip validation-strategy creation entirely**. Do **not** expect `RESEARCH.md` or `VALIDATION.md` for this run, and continue to step 5.55.
|
|
459
459
|
|
|
460
460
|
```bash
|
|
461
461
|
grep -l "## Validation Architecture" "${PHASE_DIR}"/*-RESEARCH.md 2>/dev/null || true
|
|
@@ -506,7 +506,7 @@ Continue to step 5.6. Security config is passed to the planner in step 8.
|
|
|
506
506
|
|
|
507
507
|
## 5.6. Plan:Pre Capability Dispatch and UI Design Contract Gate
|
|
508
508
|
|
|
509
|
-
> Capability-driven dispatch. Resolves active `plan:pre` hooks via the capability registry; each hook's `when` condition is evaluated by the registry — no inline config-get needed. This section
|
|
509
|
+
> Capability-driven dispatch. Resolves active `plan:pre` hooks via the capability registry; each hook's `when` condition is evaluated by the registry — no inline config-get needed. This section runs the generic step/gate dispatch, the `ai-integration` preflight, and the UI gate whose deterministic check comes from `check.query`.
|
|
510
510
|
>
|
|
511
511
|
> **Config semantics (cutover fix):** `workflow.ui_phase` gates UI-SPEC *generation* (step); `workflow.ui_safety_gate` gates the *planning block* (gate). Both-on = identical to OLD §5.6. Intended change: `{ui_phase:true, ui_safety_gate:false}` now auto-generates in pipelines but does NOT block manual planning (each key controls exactly what its description says).
|
|
512
512
|
|
|
@@ -517,12 +517,9 @@ HOOKS_JSON="$PLAN_PRE_HOOKS_JSON"
|
|
|
517
517
|
|
|
518
518
|
Read the `activeHooks` array directly from `PLAN_PRE_HOOKS_JSON` / `HOOKS_JSON` (in-context — do NOT invoke a shell pipeline).
|
|
519
519
|
|
|
520
|
-
**Branch 1 — all plan:pre hooks inactive (`activeHooks` is empty or absent):** Skip to step 6.
|
|
520
|
+
**Branch 1 — all plan:pre hooks inactive (`activeHooks` is empty or absent):** Skip to step 6; `PLAN_PRE_PRODUCTS` stays empty.
|
|
521
521
|
|
|
522
|
-
**Generic step hook
|
|
523
|
-
- If `ref.skill` is set, dispatch with `Skill(skill="gsd-${ref.skill}", args="${PHASE} --auto ${GSD_WS}")` when pipeline mode allows auto-chaining. Prepend `gsd-` to `ref.skill` — `ui-phase` → `gsd-ui-phase`.
|
|
524
|
-
- If `ref.agent` is set, dispatch with `Agent(prompt=filled_hook_fragment, subagent_type=ref.agent, model="{researcher_model}")`. Use the hook's `fragment.inline` as the prompt body and fill phase fields before spawning.
|
|
525
|
-
- The `research` hook is handled by §5.1's research decision. The `pattern-mapper` hook is handled by §7.8 after `RESEARCH_PATH` is known. Future plan:pre agent hooks use the same `ref.agent` fragment contract.
|
|
522
|
+
**Generic dispatch (#5982):** dispatch every active `kind == "step"` hook (`ref.skill`, `ref.agent`, `ref.command`) and evaluate every active `kind == "gate"` hook (`check.query`, `check.predicate`) per @gsd-core/references/loop-hook-dispatch.md, in manual and auto-chain mode: read and execute `gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md` — except the capabilities its table assigns to a dedicated section (research, drift, ui, ai-integration, pattern-mapper, intel). A blocking gate that blocks exits the workflow.
|
|
526
523
|
|
|
527
524
|
**AI integration capability:** If the active `ai-integration` step hook is present, `AI_SPEC_PATH` is empty, and the phase goal contains AI keywords (`agent`, `llm`, `rag`, `chatbot`, `embedding`, `langchain`, `llamaindex`, `crewai`, `langgraph`, `openai`, `anthropic`, `vector`, `llm eval`), then:
|
|
528
525
|
- In pipeline / `--auto` mode, invoke the hook's `ref.skill` via `Skill(skill="gsd-${ref.skill}", args="${PHASE} --auto ${GSD_WS}")`.
|
|
@@ -536,7 +533,7 @@ GATE=$(gsd_run check ui-plan-gate "${PHASE}" --raw) && GATE_EXIT=0 || GATE_EXIT=
|
|
|
536
533
|
|
|
537
534
|
A non-zero `GATE_EXIT` is a command failure, including `69` (`UNAVAILABLE`: the gate could not read its evidence, so its `frontend: false` is not an answer): surface it and stop; never fall through to Branch 2. Otherwise read `frontend`, `hasUiSpec`, and `block` from `GATE`.
|
|
538
535
|
|
|
539
|
-
**Branch 2 — no frontend indicators (`frontend` is `false`):** Skip silently to step
|
|
536
|
+
**Branch 2 — no frontend indicators (`frontend` is `false`):** Skip silently to step 5.65.
|
|
540
537
|
|
|
541
538
|
**Branch 3 — UI-SPEC already exists (`hasUiSpec` is `true`):**
|
|
542
539
|
|
|
@@ -545,9 +542,9 @@ UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
|
|
|
545
542
|
UI_SPEC_PATH="${UI_SPEC_FILE}"
|
|
546
543
|
```
|
|
547
544
|
|
|
548
|
-
Display: `Using UI design contract: ${UI_SPEC_PATH}`. Continue to step
|
|
545
|
+
Display: `Using UI design contract: ${UI_SPEC_PATH}`. Continue to step 5.65.
|
|
549
546
|
|
|
550
|
-
**Branch 4 — `--skip-ui` in `$ARGUMENTS`:** Skip silently to step
|
|
547
|
+
**Branch 4 — `--skip-ui` in `$ARGUMENTS`:** Skip silently to step 5.65.
|
|
551
548
|
|
|
552
549
|
**Branches 5 & 6 — frontend detected, UI-SPEC missing, no `--skip-ui`.**
|
|
553
550
|
|
|
@@ -558,7 +555,7 @@ AUTO_CHAIN=$(gsd_run query check auto-mode --pick auto_chain_active 2>/dev/null)
|
|
|
558
555
|
AUTO_CHAIN="${AUTO_CHAIN:-false}"
|
|
559
556
|
```
|
|
560
557
|
|
|
561
|
-
**Branch 5 — `AUTO_CHAIN` is `true` (pipeline / `--auto`):** Fire each active UI **step** hook — runs independently of whether a gate is active (covers `{ui_phase:true,ui_safety_gate:false}`). For each entry in `activeHooks` (in array order) where `kind == "step"` and `ref.skill` is set:
|
|
558
|
+
**Branch 5 — `AUTO_CHAIN` is `true` (pipeline / `--auto`):** Fire each active UI **step** hook — runs independently of whether a gate is active (covers `{ui_phase:true,ui_safety_gate:false}`). For each entry in `activeHooks` (in array order) where `kind == "step"`, `capId == "ui"` and `ref.skill` is set:
|
|
562
559
|
|
|
563
560
|
```
|
|
564
561
|
Skill(skill="gsd-${ref.skill}", args="${PHASE} --auto ${GSD_WS}")
|
|
@@ -571,9 +568,9 @@ UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
|
|
|
571
568
|
UI_SPEC_PATH="${UI_SPEC_FILE}"
|
|
572
569
|
```
|
|
573
570
|
|
|
574
|
-
Continue to step
|
|
571
|
+
Continue to step 5.65.
|
|
575
572
|
|
|
576
|
-
**Branch 6 — `AUTO_CHAIN` is `false` (manual):
|
|
573
|
+
**Branch 6 — `AUTO_CHAIN` is `false` (manual): UI gate handling.** For each entry in `activeHooks` where `kind == "gate"`, `capId == "ui"` and `blocking` is `true`: if `block:true` (from `GATE`), output the block below and **EXIT the plan-phase workflow**. If no active blocking gate (e.g. `workflow.ui_safety_gate` is off), continue to step 5.65 — no block.
|
|
577
574
|
|
|
578
575
|
Output this markdown directly (not as a code block):
|
|
579
576
|
|
|
@@ -637,6 +634,7 @@ SKETCH_FINDINGS_PATH=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/nul
|
|
|
637
634
|
# on non-AI phases; gating it there silently starves the planner of the SPEC (#550 review).
|
|
638
635
|
# init resolves the plain phase SPEC (never the -AI-SPEC.md / -UI-SPEC.md variants) through the one shared predicate.
|
|
639
636
|
PHASE_DIR_FOR_SPEC=$(_gsd_field "$INIT" phase_dir)
|
|
637
|
+
PHASE_DIR_FOR_SPEC="${PHASE_DIR_FOR_SPEC:-$(_gsd_field "$INIT" expected_phase_dir)}" # §2 may create it after INIT (#5960)
|
|
640
638
|
SPEC_PATH=$(_gsd_field "$INIT" spec_path)
|
|
641
639
|
SPEC_FILE="${SPEC_PATH}"
|
|
642
640
|
# Resolve the phase UI-SPEC separately (the glob above excludes -UI-SPEC.md); it carries the
|
|
@@ -780,6 +778,7 @@ Planner prompt:
|
|
|
780
778
|
- {SPIKE_FINDINGS_PATH} (Spike Findings — validated patterns, constraints, landmines from experiments, if exists)
|
|
781
779
|
- {SKETCH_FINDINGS_PATH} (Sketch Findings — validated design decisions, CSS patterns, visual direction, if exists)
|
|
782
780
|
- {API_SURFACE_PATH} (API Surface — HINT ONLY, when intel capability is active; see <intel_surface_hint> below)
|
|
781
|
+
- {PLAN_PRE_PRODUCTS} (plan:pre capability outputs from this run, e.g. MemPalace's MEMORY-RECALL.md — prior decisions, patterns, surprises; if any)
|
|
783
782
|
${CONTEXT_WINDOW >= 500000 ? `
|
|
784
783
|
**Cross-phase context (1M model enrichment):**
|
|
785
784
|
- CONTEXT.md files from the 3 most recent completed phases (locked decisions — maintain consistency)
|
|
@@ -74,6 +74,7 @@ const os = require('os');
|
|
|
74
74
|
const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch, agedNoneGovernsDispatch, buildSentinelDiscard } = require('./lib/isolation-sentinel.js');
|
|
75
75
|
const { REASON_CODE, describeSentinelDiscard } = require('./lib/isolation-deny-reason.js');
|
|
76
76
|
const { HOOK_ON_CRASH, allow, deny, crash } = require('./lib/hook-exit.js');
|
|
77
|
+
const { readConfigFileBounded } = require('./lib/config-read.js');
|
|
77
78
|
|
|
78
79
|
// Required at module top, alongside the other ./lib requires — NOT behind
|
|
79
80
|
// ensureRuntimeBuild() below. Terminating on a parse/timeout failure must
|
|
@@ -208,7 +209,7 @@ function resolveRuntimeIdentity(cwd, configPath, resolveRuntimeNameFromCandidate
|
|
|
208
209
|
// A throw here (corrupt JSON, EISDIR, permission error) propagates to the
|
|
209
210
|
// caller's catch as a resolution failure — this function only decides
|
|
210
211
|
// "confident vs not", never resolution failure.
|
|
211
|
-
const raw =
|
|
212
|
+
const raw = readConfigFileBounded(configPath, { required: true });
|
|
212
213
|
const parsed = JSON.parse(raw);
|
|
213
214
|
if (parsed && typeof parsed === 'object' && 'runtime' in parsed) {
|
|
214
215
|
const configRuntime = resolveRuntimeNameFromCandidates(parsed.runtime);
|
|
@@ -331,7 +332,7 @@ function resolveRegistryIsolation(cwd, configPath) {
|
|
|
331
332
|
}
|
|
332
333
|
if (!ladderAnswered) {
|
|
333
334
|
try {
|
|
334
|
-
const raw =
|
|
335
|
+
const raw = readConfigFileBounded(configPath, { required: true });
|
|
335
336
|
const parsedCfg = JSON.parse(raw);
|
|
336
337
|
if (parsedCfg && typeof parsedCfg === 'object' && parsedCfg.workflow &&
|
|
337
338
|
typeof parsedCfg.workflow === 'object' && parsedCfg.workflow.use_worktrees === false) {
|
|
@@ -422,7 +423,7 @@ function resolveIsolationState(cwd, { clock = Date, dispatchIds = null } = {}) {
|
|
|
422
423
|
return { gsdProject: false, isolation: null, harnessFlag: null, error: null, sentinelDiscarded: null };
|
|
423
424
|
}
|
|
424
425
|
|
|
425
|
-
const sentinel = readSentinel(cwd, { clock });
|
|
426
|
+
const sentinel = readSentinel(cwd, { clock, dispatchIds });
|
|
426
427
|
// Hoisted so the "sentinel was present/fresh but did not apply" case below
|
|
427
428
|
// (#4594 row 15 — Postel's-Law finding) can distinguish itself from
|
|
428
429
|
// "absent"/"stale" without re-deriving applicability.
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
// Enabled for all Claude Code installs. This hook is always-on — it is a
|
|
19
19
|
// no-op when .planning/config.json is absent (ENOENT → exit 0).
|
|
20
20
|
|
|
21
|
-
const fs = require('fs');
|
|
22
21
|
const path = require('path');
|
|
23
22
|
const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
|
|
23
|
+
const { readConfigFileBounded } = require('./lib/config-read.js');
|
|
24
24
|
|
|
25
25
|
// This hook only injects an advisory config-reload summary; a crash mid-parse
|
|
26
26
|
// must not block the session or the FileChanged event that triggered it — the
|
|
@@ -70,7 +70,7 @@ process.stdin.on('end', () => {
|
|
|
70
70
|
// Read the updated config file
|
|
71
71
|
let config;
|
|
72
72
|
try {
|
|
73
|
-
const raw =
|
|
73
|
+
const raw = readConfigFileBounded(filePath, { required: true });
|
|
74
74
|
config = JSON.parse(raw);
|
|
75
75
|
} catch (e) {
|
|
76
76
|
if (e && e.code === 'ENOENT') allow(undefined);
|
|
@@ -28,6 +28,7 @@ const os = require('os');
|
|
|
28
28
|
const path = require('path');
|
|
29
29
|
const { spawn } = require('child_process');
|
|
30
30
|
const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
|
|
31
|
+
const { readConfigFileBounded } = require('./lib/config-read.js');
|
|
31
32
|
|
|
32
33
|
// This hook only injects an advisory context-usage warning; it never blocks
|
|
33
34
|
// the tool call it rides in on. A crash here (e.g. a malformed bridge file)
|
|
@@ -334,7 +335,7 @@ const handleStdinEnd = () => {
|
|
|
334
335
|
let thresholds = { warning: WARNING_THRESHOLD, critical: CRITICAL_THRESHOLD };
|
|
335
336
|
try {
|
|
336
337
|
const configPath = path.join(cwd, '.planning', 'config.json');
|
|
337
|
-
const config = JSON.parse(
|
|
338
|
+
const config = JSON.parse(readConfigFileBounded(configPath, { required: true }));
|
|
338
339
|
if (config.hooks?.context_warnings === false) {
|
|
339
340
|
allow(undefined);
|
|
340
341
|
}
|
|
@@ -65,6 +65,7 @@ const fs = require('fs');
|
|
|
65
65
|
const path = require('path');
|
|
66
66
|
const os = require('os');
|
|
67
67
|
const { allow } = require('./lib/hook-exit.js');
|
|
68
|
+
const { readConfigFileBounded } = require('./lib/config-read.js');
|
|
68
69
|
|
|
69
70
|
// Workspace resolution is shared across the Cursor hooks (#2587) — see
|
|
70
71
|
// hooks/lib/cursor-workspace.js. Staged next to these scripts by
|
|
@@ -133,12 +134,12 @@ function realpathOrNull(p, realpathFn) {
|
|
|
133
134
|
* i.e. `~/.cursor/worktrees` by default — `getGlobalConfigDir('cursor')`
|
|
134
135
|
* honors the `CURSOR_CONFIG_DIR` env override and `~` expansion for free).
|
|
135
136
|
* That is made NECESSARY AND SUFFICIENT below. Do NOT reinstate
|
|
136
|
-
*
|
|
137
|
+
* a linked-worktree result of `resolveProjectContext` as an alternative
|
|
137
138
|
* OR'd proof of isolation — that is precisely the bypass finding 3 closed;
|
|
138
139
|
* a future "simplification" that merges it back in re-opens unconsented
|
|
139
140
|
* writes to the human's active checkout.
|
|
140
141
|
*
|
|
141
|
-
* `
|
|
142
|
+
* `resolveProjectContext` is still called, but ONLY as a diagnostic: its provenance
|
|
142
143
|
* distinguishes "confidently not isolated" from "git could not answer
|
|
143
144
|
* (timeout) — cannot determine" so the eventual deny reason stays
|
|
144
145
|
* actionable. Its result never flips `isolated`.
|
|
@@ -195,8 +196,10 @@ function resolveIsolationEvidence(root, { realpath = fs.realpathSync } = {}) {
|
|
|
195
196
|
let linkageReason = null;
|
|
196
197
|
try {
|
|
197
198
|
// Sibling data/policy module, staged alongside this hook at install time.
|
|
198
|
-
const {
|
|
199
|
-
|
|
199
|
+
const { resolveProjectContext } = require('../gsd-core/bin/lib/project-context.cjs');
|
|
200
|
+
const gitProvenance = resolveProjectContext(root).provenance;
|
|
201
|
+
// A degraded context (git timed out or could not run) is "cannot determine".
|
|
202
|
+
linkageReason = gitProvenance.degraded ? 'git_timed_out' : gitProvenance.reason;
|
|
200
203
|
} catch {
|
|
201
204
|
linkageReason = null;
|
|
202
205
|
}
|
|
@@ -421,7 +424,7 @@ function resolveFallbackIsolation(root, configPath) {
|
|
|
421
424
|
const { runtimes } = require('../gsd-core/bin/lib/capability-registry.cjs');
|
|
422
425
|
|
|
423
426
|
let runtimeId = resolveRuntimeNameFromCandidates(process.env.GSD_RUNTIME);
|
|
424
|
-
const rawConfig =
|
|
427
|
+
const rawConfig = readConfigFileBounded(configPath, { required: true });
|
|
425
428
|
const parsedConfig = JSON.parse(rawConfig);
|
|
426
429
|
if (!runtimeId && parsedConfig && typeof parsedConfig === 'object' && 'runtime' in parsedConfig) {
|
|
427
430
|
runtimeId = resolveRuntimeNameFromCandidates(parsedConfig.runtime) || null;
|
|
@@ -512,7 +515,7 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
|
|
|
512
515
|
// Hoisted (readSentinel never throws) so the "present, fresh, but did not
|
|
513
516
|
// apply" case (#4594 row 15) can be reported on every deny path below
|
|
514
517
|
// instead of silently discarded.
|
|
515
|
-
const sentinel = readSentinel(root, { clock });
|
|
518
|
+
const sentinel = readSentinel(root, { clock, dispatchIds });
|
|
516
519
|
const applies = sentinelAppliesToDispatch(sentinel, dispatchIds);
|
|
517
520
|
const sentinelDiscarded = buildSentinelDiscard(sentinel, dispatchIds);
|
|
518
521
|
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
// hook source files — these legitimately contain injection-like strings.
|
|
23
23
|
|
|
24
24
|
const path = require('path');
|
|
25
|
-
const fs = require('fs');
|
|
26
25
|
const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
|
|
26
|
+
const { readConfigFileBounded } = require('./lib/config-read.js');
|
|
27
27
|
|
|
28
28
|
// This is a PostToolUse advisory scanner over content the tool call already
|
|
29
29
|
// returned; a crash while scanning must not retroactively block that Read/
|
|
@@ -343,7 +343,7 @@ process.stdin.on('end', () => {
|
|
|
343
343
|
try {
|
|
344
344
|
const cfgBase = data.cwd || process.cwd();
|
|
345
345
|
const cfgPath = path.join(cfgBase, '.planning', 'config.json');
|
|
346
|
-
const cfg = JSON.parse(
|
|
346
|
+
const cfg = JSON.parse(readConfigFileBounded(cfgPath, { required: true }));
|
|
347
347
|
blocking = cfg.security?.injection_blocking === true;
|
|
348
348
|
} catch { /* no config ⇒ advisory */ }
|
|
349
349
|
}
|