@opengsd/gsd-core 1.13.0 → 1.14.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/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +458 -0
- package/agents/gsd-code-fixer.md +5 -5
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +345 -0
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/bin/install.js +206 -68
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +1 -0
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +338 -125
- package/gsd-core/bin/lib/adr-parser.cjs +1 -1
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +39 -22
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +79 -67
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -1
- package/gsd-core/bin/lib/check-command-router.cjs +113 -36
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/commands.cjs +650 -72
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +153 -38
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +137 -34
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/init.cjs +409 -47
- package/gsd-core/bin/lib/install-engine.cjs +16 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +19 -8
- package/gsd-core/bin/lib/model-resolver.cjs +101 -10
- package/gsd-core/bin/lib/phase-command-router.cjs +7 -1
- package/gsd-core/bin/lib/phase-id.cjs +161 -22
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase.cjs +167 -63
- package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +50 -1
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +56 -15
- package/gsd-core/bin/lib/roadmap.cjs +108 -14
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +27 -10
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +12 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +193 -4
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +142 -28
- package/gsd-core/bin/lib/state.cjs +223 -27
- package/gsd-core/bin/lib/surface.cjs +60 -2
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/uat.cjs +1 -1
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +47 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
- package/gsd-core/bin/lib/verify.cjs +188 -23
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +13 -7
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +12 -3
- package/gsd-core/references/planning-config.md +3 -0
- package/gsd-core/references/tdd.md +5 -2
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/worktree-path-safety.md +112 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/workflows/add-todo.md +3 -2
- package/gsd-core/workflows/autonomous.md +13 -10
- package/gsd-core/workflows/check-todos.md +4 -2
- package/gsd-core/workflows/cleanup.md +3 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +7 -0
- package/gsd-core/workflows/code-review-fix.md +3 -3
- package/gsd-core/workflows/code-review.md +156 -30
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +39 -262
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +14 -155
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +18 -3
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase.md +53 -152
- package/gsd-core/workflows/execute-plan.md +20 -7
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/map-codebase.md +50 -3
- package/gsd-core/workflows/new-milestone.md +54 -12
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project.md +32 -202
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase.md +22 -181
- package/gsd-core/workflows/pr-branch.md +19 -7
- package/gsd-core/workflows/quick.md +8 -1
- package/gsd-core/workflows/reapply-patches.md +77 -3
- package/gsd-core/workflows/settings.md +18 -5
- package/gsd-core/workflows/update.md +7 -5
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work.md +20 -180
- package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +44 -18
- package/hooks/dist/gsd-statusline.js +11 -7
- package/hooks/dist/gsd-validate-commit.sh +34 -4
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +42 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +44 -18
- package/hooks/gsd-statusline.js +11 -7
- package/hooks/gsd-validate-commit.sh +34 -4
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +10 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/check-contract-drift.cjs +4 -1
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +7 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +19 -2
- package/scripts/lint-phase-id-drift.cjs +338 -13
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +85 -0
- package/scripts/prompt-injection-scan.sh +14 -0
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +1 -0
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -33,6 +33,8 @@ No Pass/Fail buttons. No severity questions. Just: "Here's what should happen. D
|
|
|
33
33
|
|
|
34
34
|
<process>
|
|
35
35
|
|
|
36
|
+
**Compact Content Gate.** Read and follow `gsd-core/references/compact-content-gate.md` now — it states the `workflow.compact_content` check and the resolution rule this spine defers to. When it directs a Read, read `gsd-core/workflows/verify-work/detail/elaboration.md` in full before continuing past this point; its content elaborates on the resume/reconcile steps and the full gap-closure sub-flow below.
|
|
37
|
+
|
|
36
38
|
<step name="initialize" priority="first">
|
|
37
39
|
If $ARGUMENTS contains a phase number, load context:
|
|
38
40
|
|
|
@@ -489,50 +491,20 @@ If no more tests → Go to `complete_session`
|
|
|
489
491
|
</step>
|
|
490
492
|
|
|
491
493
|
<step name="reconcile_gaps">
|
|
492
|
-
**Reconcile diagnosed gaps against completed gap-closure plans (#1921):**
|
|
493
|
-
|
|
494
|
-
When verify-work resumes after `/gsd:execute-phase --gaps-only`, the UAT `## Gaps` entries still read `status: failed` even though their fix plans have executed. Without reconciliation verify-work re-diagnoses them as fresh blockers and spawns new gap plans — losing the verification state. This step closes the loop.
|
|
495
|
-
|
|
496
|
-
Read the UAT `## Gaps` section and the phase dir `*-PLAN.md` frontmatter. For each gap with `status: failed`:
|
|
497
|
-
1. Find a `*-PLAN.md` whose frontmatter `gap_ids` includes the gap's `gap_id` (`G-{phase}-{N}`).
|
|
498
|
-
2. If such a plan exists AND has a matching `*-SUMMARY.md` in the phase dir (the plan was executed by `--gaps-only`), the gap is **resolved** — update its YAML in place:
|
|
499
|
-
```yaml
|
|
500
|
-
- gap_id: G-{phase}-{N}
|
|
501
|
-
status: resolved # was: failed
|
|
502
|
-
resolved_by: {plan basename}
|
|
503
|
-
resolved_at: {today}
|
|
504
|
-
```
|
|
505
|
-
3. If no plan references the `gap_id`, or the plan has no SUMMARY, leave the gap `status: failed` (still open).
|
|
494
|
+
**Reconcile diagnosed gaps against completed gap-closure plans (#1921):** when verify-work resumes after `/gsd:execute-phase --gaps-only`, UAT `## Gaps` entries still read `status: failed` even though their fix plans already executed — without reconciliation they'd be re-diagnosed as fresh blockers. For each `status: failed` gap with a `*-PLAN.md` whose `gap_ids` names it AND a matching `*-SUMMARY.md`, mark it `resolved` (with `resolved_by`/`resolved_at`) in place; otherwise leave it `failed`. Resolved gaps are never re-diagnosed or re-planned; a later regression gets a fresh `gap_id`, not a reopened old one.
|
|
506
495
|
|
|
507
|
-
|
|
508
|
-
```
|
|
509
|
-
Reconciled gap-closure state: {resolved_count} gap(s) resolved by executed plans, {open_count} still open.
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
Resolved gaps are NOT re-diagnosed and do NOT spawn new gap plans. If the user later reports the same behavior as still broken, treat it as a new issue (a regression) with a fresh `gap_id`.
|
|
496
|
+
Exact YAML shape and the announcement line: `gsd-core/workflows/verify-work/detail/elaboration.md` § 1.
|
|
513
497
|
</step>
|
|
514
498
|
|
|
515
499
|
<step name="resume_from_file">
|
|
516
|
-
**Resume testing from UAT file:**
|
|
517
|
-
|
|
518
|
-
**First run `reconcile_gaps`** (above) so gaps already fixed by `/gsd:execute-phase --gaps-only` are marked `resolved` before testing resumes (#1921).
|
|
519
|
-
|
|
520
|
-
Read the full UAT file.
|
|
500
|
+
**Resume testing from UAT file:** first run `reconcile_gaps` (above), then read the full UAT file.
|
|
521
501
|
|
|
522
502
|
Find first test with `result: [pending]`.
|
|
523
503
|
If no `[pending]` test found → go to `complete_session`.
|
|
524
504
|
|
|
525
|
-
|
|
526
|
-
```
|
|
527
|
-
Resuming: Phase {phase} UAT
|
|
528
|
-
Progress: {passed + issues + skipped}/{total}
|
|
529
|
-
Issues found so far: {issues count}
|
|
505
|
+
Otherwise announce progress and continue from that test at `present_test`.
|
|
530
506
|
|
|
531
|
-
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
Update Current Test section with the pending test.
|
|
535
|
-
Proceed to `present_test`.
|
|
507
|
+
Exact resume-announcement wording: `gsd-core/workflows/verify-work/detail/elaboration.md` § 2.
|
|
536
508
|
</step>
|
|
537
509
|
|
|
538
510
|
<step name="complete_session">
|
|
@@ -726,138 +698,29 @@ SECURITY: File paths in output are constructed from validated path components on
|
|
|
726
698
|
</step>
|
|
727
699
|
|
|
728
700
|
<step name="diagnose_issues">
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
```
|
|
732
|
-
---
|
|
733
|
-
|
|
734
|
-
{N} issues found. Diagnosing root causes...
|
|
735
|
-
|
|
736
|
-
Spawning parallel debug agents to investigate each issue.
|
|
737
|
-
```
|
|
738
|
-
|
|
739
|
-
- Load diagnose-issues workflow
|
|
740
|
-
- Follow @~/.claude/gsd-core/workflows/diagnose-issues.md
|
|
741
|
-
- Spawn parallel debug agents for each issue
|
|
742
|
-
- Collect root causes
|
|
743
|
-
- Update UAT.md with root causes
|
|
744
|
-
- Proceed to `plan_gap_closure`
|
|
745
|
-
|
|
746
|
-
Diagnosis runs automatically - no user prompt. Parallel agents investigate simultaneously, so overhead is minimal and fixes are more accurate.
|
|
701
|
+
When UAT testing found issues, this sub-flow (diagnose_issues -> plan_gap_closure -> verify_gap_plans -> revision_loop) runs before present_ready; a session with zero issues never reaches it. Spawn parallel debug agents (one per issue, via diagnose-issues.md) to find root causes with no user prompt, then update UAT.md and proceed to plan_gap_closure.
|
|
747
702
|
</step>
|
|
748
703
|
|
|
749
704
|
<step name="plan_gap_closure">
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
Display:
|
|
753
|
-
```
|
|
754
|
-
### GSD ► PLANNING FIXES
|
|
755
|
-
|
|
756
|
-
◆ Spawning planner for gap closure... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
|
|
757
|
-
```
|
|
758
|
-
|
|
759
|
-
Spawn gsd-planner in --gaps mode:
|
|
760
|
-
|
|
761
|
-
<!-- #2517 model-omit-on-inherit -->
|
|
762
|
-
|
|
763
|
-
> **Model omission (#2517).** Omit the `model` parameter entirely when the value it would carry (`planner_model`, `checker_model`) is `"inherit"` or empty. An empty value 404s on runtimes without native tier aliases — the default on non-Claude runtimes. Omitting it inherits the orchestrator's model. See @gsd-core/references/model-profile-resolution.md.
|
|
764
|
-
|
|
765
|
-
````
|
|
766
|
-
Agent(
|
|
767
|
-
prompt="""
|
|
768
|
-
<planning_context>
|
|
769
|
-
|
|
770
|
-
**Phase:** {phase_number}
|
|
771
|
-
**Mode:** gap_closure
|
|
772
|
-
|
|
773
|
-
<required_reading>
|
|
774
|
-
- {phase_dir}/{phase_num}-UAT.md (UAT with diagnoses)
|
|
775
|
-
- {state_path} (Project State)
|
|
776
|
-
- {roadmap_path} (Roadmap)
|
|
777
|
-
</required_reading>
|
|
778
|
-
|
|
779
|
-
${AGENT_SKILLS_PLANNER}
|
|
780
|
-
|
|
781
|
-
</planning_context>
|
|
782
|
-
|
|
783
|
-
<downstream_consumer>
|
|
784
|
-
Output consumed by /gsd:execute-phase
|
|
785
|
-
Plans must be executable prompts.
|
|
786
|
-
|
|
787
|
-
<!-- #2508 runtime-aware-dispatch -->
|
|
788
|
-
|
|
789
|
-
> **Runtime-aware dispatch (#2508 Phase 4).** GSD workflows dispatch specialized subagents by role. Before dispatching on a built-in-only runtime (kimi-code — three built-ins only), resolve the role to a built-in via `gsd_run query resolve-dispatch-type --requested <role> --raw`. On named-dispatch runtimes (Claude/OpenCode/…) the role is returned unchanged; on kimi-code it maps to `coder`/`explore`/`plan` by role-suffix. The persona rides `${AGENT_SKILLS_<ROLE>}` (Phase 3) regardless. See @gsd-core/references/runtime-aware-dispatch.md.
|
|
790
|
-
|
|
791
|
-
**Gap linkage (#1921):** each created `*-PLAN.md` MUST list the UAT gap ids it addresses in its frontmatter:
|
|
792
|
-
```yaml
|
|
793
|
-
---
|
|
794
|
-
gap_closure: true
|
|
795
|
-
gap_ids: [G-{phase}-{N}, ...] # the ## Gaps gap_id values this plan fixes
|
|
796
|
-
---
|
|
797
|
-
```
|
|
798
|
-
This lets `/gsd:verify-work` reconcile resolved gaps on resume (a gap whose plan has a matching `*-SUMMARY.md` is marked `status: resolved`, not re-diagnosed as a fresh blocker).
|
|
799
|
-
</downstream_consumer>
|
|
800
|
-
""",
|
|
801
|
-
subagent_type="gsd-planner",
|
|
802
|
-
model="{planner_model}",
|
|
803
|
-
description="Plan gap fixes for Phase {phase}"
|
|
804
|
-
)
|
|
805
|
-
````
|
|
705
|
+
Spawn gsd-planner in --gaps mode against the UAT (with diagnoses), `{state_path}` (Project State), and `{roadmap_path}` (Roadmap). Each created PLAN.md MUST carry `gap_closure: true` and `gap_ids: [...]` in its frontmatter (#1921) so a later verify-work resume can reconcile it.
|
|
806
706
|
|
|
707
|
+
<!-- gsd:protected -->
|
|
807
708
|
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
|
|
808
709
|
|
|
809
|
-
|
|
810
|
-
- **PLANNING COMPLETE:** Proceed to `verify_gap_plans`
|
|
811
|
-
- **PLANNING INCONCLUSIVE:** Report and offer manual intervention
|
|
710
|
+
PLANNING COMPLETE proceeds to verify_gap_plans; PLANNING INCONCLUSIVE reports and offers manual intervention.
|
|
812
711
|
</step>
|
|
813
712
|
|
|
814
713
|
<step name="verify_gap_plans">
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
Display:
|
|
818
|
-
```
|
|
819
|
-
### GSD ► VERIFYING FIX PLANS
|
|
820
|
-
|
|
821
|
-
◆ Spawning plan checker... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
Initialize: `iteration_count = 1`
|
|
825
|
-
|
|
826
|
-
Spawn gsd-plan-checker:
|
|
827
|
-
|
|
828
|
-
```
|
|
829
|
-
Agent(
|
|
830
|
-
prompt="""
|
|
831
|
-
<verification_context>
|
|
832
|
-
|
|
833
|
-
**Phase:** {phase_number}
|
|
834
|
-
**Phase Goal:** Close diagnosed gaps from UAT
|
|
835
|
-
|
|
836
|
-
<required_reading>
|
|
837
|
-
- {phase_dir}/*-PLAN.md (Plans to verify)
|
|
838
|
-
</required_reading>
|
|
839
|
-
|
|
840
|
-
${AGENT_SKILLS_CHECKER}
|
|
841
|
-
|
|
842
|
-
</verification_context>
|
|
843
|
-
|
|
844
|
-
<expected_output>
|
|
845
|
-
Return one of:
|
|
846
|
-
- ## VERIFICATION PASSED — all checks pass
|
|
847
|
-
- ## ISSUES FOUND — structured issue list
|
|
848
|
-
</expected_output>
|
|
849
|
-
""",
|
|
850
|
-
subagent_type="gsd-plan-checker",
|
|
851
|
-
model="{checker_model}",
|
|
852
|
-
description="Verify Phase {phase} fix plans"
|
|
853
|
-
)
|
|
854
|
-
```
|
|
714
|
+
Spawn gsd-plan-checker against the fix plans (iteration_count starts at 1), model="{checker_model}" (omit on inherit/empty, #2517).
|
|
855
715
|
|
|
716
|
+
<!-- gsd:protected -->
|
|
856
717
|
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
|
|
857
718
|
|
|
858
719
|
On return:
|
|
859
720
|
- **VERIFICATION PASSED:** Proceed to `present_ready`
|
|
860
721
|
- **ISSUES FOUND:** Count BLOCKER + WARNING entries in the YAML issues block; an entry whose severity is missing or unrecognized counts as a BLOCKER (fail closed). If zero — every entry is explicitly INFO — display `ℹ advisory — {dimension}: {description}` per entry and proceed to `present_ready`; INFO is advisory and never enters the loop (#3724). Otherwise proceed to `revision_loop`
|
|
722
|
+
|
|
723
|
+
Exact Agent() prompt fields: `gsd-core/workflows/verify-work/detail/elaboration.md` § 2.
|
|
861
724
|
</step>
|
|
862
725
|
|
|
863
726
|
<step name="revision_loop">
|
|
@@ -869,26 +732,6 @@ Display: `Sending back to planner for revision... (iteration {N}/3)`
|
|
|
869
732
|
|
|
870
733
|
Spawn gsd-planner with revision context:
|
|
871
734
|
|
|
872
|
-
```
|
|
873
|
-
Agent(
|
|
874
|
-
prompt="""
|
|
875
|
-
<revision_context>
|
|
876
|
-
|
|
877
|
-
**Phase:** {phase_number}
|
|
878
|
-
**Mode:** revision
|
|
879
|
-
|
|
880
|
-
<required_reading>
|
|
881
|
-
- {phase_dir}/*-PLAN.md (Existing plans)
|
|
882
|
-
</required_reading>
|
|
883
|
-
|
|
884
|
-
${AGENT_SKILLS_PLANNER}
|
|
885
|
-
|
|
886
|
-
**Checker issues:**
|
|
887
|
-
{structured_issues_from_checker}
|
|
888
|
-
|
|
889
|
-
</revision_context>
|
|
890
|
-
|
|
891
|
-
<instructions>
|
|
892
735
|
Read existing PLAN.md files. Make targeted updates to address checker issues.
|
|
893
736
|
|
|
894
737
|
`required_property` + evidence + severity BIND. `fix_hint` is ONE non-binding example route: a
|
|
@@ -900,14 +743,8 @@ the alternatives rather than applying or working around it. Full contract:
|
|
|
900
743
|
`gsd-core/references/planner-revision.md`, which you load in revision mode.
|
|
901
744
|
|
|
902
745
|
Do NOT replan from scratch unless issues are fundamental.
|
|
903
|
-
</instructions>
|
|
904
|
-
""",
|
|
905
|
-
subagent_type="gsd-planner",
|
|
906
|
-
model="{planner_model}",
|
|
907
|
-
description="Revise Phase {phase} plans"
|
|
908
|
-
)
|
|
909
|
-
```
|
|
910
746
|
|
|
747
|
+
<!-- gsd:protected -->
|
|
911
748
|
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
|
|
912
749
|
|
|
913
750
|
**If the planner returns `## REVISION_CONFLICT`:** do NOT increment `iteration_count` and do NOT
|
|
@@ -938,9 +775,12 @@ Offer options:
|
|
|
938
775
|
2. Provide guidance (user gives direction, retry)
|
|
939
776
|
3. Abandon (exit, user runs /gsd:plan-phase manually)
|
|
940
777
|
|
|
941
|
-
|
|
778
|
+
Then wait for the user to pick one.
|
|
779
|
+
|
|
780
|
+
Exact Agent() prompt fields (revision_context, required_reading): `gsd-core/workflows/verify-work/detail/elaboration.md` § 3.
|
|
942
781
|
</step>
|
|
943
782
|
|
|
783
|
+
|
|
944
784
|
<step name="present_ready">
|
|
945
785
|
**Present completion and next steps:**
|
|
946
786
|
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
const fs = require('fs');
|
|
64
64
|
const path = require('path');
|
|
65
65
|
const os = require('os');
|
|
66
|
-
const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch } = require('./lib/isolation-sentinel.js');
|
|
67
|
-
const { REASON_CODE } = require('./lib/isolation-deny-reason.js');
|
|
66
|
+
const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch, buildSentinelDiscard } = require('./lib/isolation-sentinel.js');
|
|
67
|
+
const { REASON_CODE, describeSentinelDiscard } = require('./lib/isolation-deny-reason.js');
|
|
68
68
|
const { HOOK_ON_CRASH, allow, deny, crash } = require('./lib/hook-exit.js');
|
|
69
69
|
|
|
70
70
|
// Required at module top, alongside the other ./lib requires — NOT behind
|
|
@@ -385,16 +385,20 @@ function resolveIsolationState(cwd, { clock = Date, dispatchIds = null } = {}) {
|
|
|
385
385
|
projectExists = false;
|
|
386
386
|
}
|
|
387
387
|
if (!projectExists) {
|
|
388
|
-
return { gsdProject: false, isolation: null, harnessFlag: null, error: null };
|
|
388
|
+
return { gsdProject: false, isolation: null, harnessFlag: null, error: null, sentinelDiscarded: null };
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
const sentinel = readSentinel(cwd, { clock });
|
|
392
|
-
|
|
392
|
+
// Hoisted so the "sentinel was present/fresh but did not apply" case below
|
|
393
|
+
// (#4594 row 15 — Postel's-Law finding) can distinguish itself from
|
|
394
|
+
// "absent"/"stale" without re-deriving applicability.
|
|
395
|
+
const applies = sentinelAppliesToDispatch(sentinel, dispatchIds);
|
|
396
|
+
if (sentinel.present && !sentinel.stale && applies) {
|
|
393
397
|
if (sentinel.isolation !== 'harness-worktree') {
|
|
394
|
-
return { gsdProject: true, isolation: sentinel.isolation, harnessFlag: null, error: null };
|
|
398
|
+
return { gsdProject: true, isolation: sentinel.isolation, harnessFlag: null, error: null, sentinelDiscarded: null };
|
|
395
399
|
}
|
|
396
400
|
if (sentinel.harnessFlag) {
|
|
397
|
-
return { gsdProject: true, isolation: 'harness-worktree', harnessFlag: sentinel.harnessFlag, error: null };
|
|
401
|
+
return { gsdProject: true, isolation: 'harness-worktree', harnessFlag: sentinel.harnessFlag, error: null, sentinelDiscarded: null };
|
|
398
402
|
}
|
|
399
403
|
// #3045 BLOCKER 2 fix: the sentinel already PROVED this dispatch requires
|
|
400
404
|
// isolation (it resolved harness-worktree) but carries no usable flag —
|
|
@@ -423,6 +427,7 @@ function resolveIsolationState(cwd, { clock = Date, dispatchIds = null } = {}) {
|
|
|
423
427
|
'dispatch-isolation sentinel resolved "harness-worktree" but recorded no harness_flag — ' +
|
|
424
428
|
'cannot verify what parameter the dispatch must carry.'
|
|
425
429
|
),
|
|
430
|
+
sentinelDiscarded: null,
|
|
426
431
|
};
|
|
427
432
|
}
|
|
428
433
|
|
|
@@ -430,11 +435,19 @@ function resolveIsolationState(cwd, { clock = Date, dispatchIds = null } = {}) {
|
|
|
430
435
|
// conservative fallback (#3045 finding — must still cover fail-closed case
|
|
431
436
|
// (a): a project that opted out of worktrees entirely via
|
|
432
437
|
// workflow.use_worktrees).
|
|
438
|
+
//
|
|
439
|
+
// #4594 row 15: a PRESENT, FRESH sentinel that simply does not apply to
|
|
440
|
+
// THIS dispatch (identifiers disagree) is a distinct case from "absent" or
|
|
441
|
+
// "stale" — record what was discarded so evaluateDispatch can name it in a
|
|
442
|
+
// block reason instead of silently falling through to a registry-resolution
|
|
443
|
+
// message that never mentions the sentinel existed.
|
|
444
|
+
const sentinelDiscarded = buildSentinelDiscard(sentinel, dispatchIds);
|
|
445
|
+
|
|
433
446
|
try {
|
|
434
447
|
const { isolation, harnessFlag } = resolveRegistryIsolation(cwd, configPath);
|
|
435
|
-
return { gsdProject: true, isolation, harnessFlag, error: null };
|
|
448
|
+
return { gsdProject: true, isolation, harnessFlag, error: null, sentinelDiscarded };
|
|
436
449
|
} catch (err) {
|
|
437
|
-
return { gsdProject: true, isolation: null, harnessFlag: null, error: err };
|
|
450
|
+
return { gsdProject: true, isolation: null, harnessFlag: null, error: err, sentinelDiscarded };
|
|
438
451
|
}
|
|
439
452
|
}
|
|
440
453
|
|
|
@@ -464,10 +477,16 @@ function evaluateDispatch(data, { clock = Date } = {}) {
|
|
|
464
477
|
}
|
|
465
478
|
|
|
466
479
|
const cwd = data.cwd || process.cwd();
|
|
467
|
-
// #3045 SECURITY F2: best-effort plan/phase extraction from this
|
|
468
|
-
// dispatch's own
|
|
469
|
-
//
|
|
470
|
-
|
|
480
|
+
// #3045 SECURITY F2 / #4594: best-effort plan/phase extraction from this
|
|
481
|
+
// dispatch's own text, so a fresh sentinel that disagrees with THIS
|
|
482
|
+
// dispatch is treated as inapplicable rather than trusted. PROMPT FIRST:
|
|
483
|
+
// `description` is short, model-authored free text that only carries usable
|
|
484
|
+
// identity when the model happens to reproduce the dispatch template
|
|
485
|
+
// verbatim, while the canonical `[gsd:dispatch phase="…" plan="…"]` marker
|
|
486
|
+
// (or, failing that, the prose fallback) lives in the prompt body itself —
|
|
487
|
+
// `description` is kept only as a fallback for a marker/prose match that
|
|
488
|
+
// exists solely in it.
|
|
489
|
+
const dispatchIds = extractDispatchIdentifiers(toolInput.prompt, toolInput.description);
|
|
471
490
|
const state = resolveIsolationState(cwd, { clock, dispatchIds });
|
|
472
491
|
|
|
473
492
|
if (!state.gsdProject) return { action: 'allow' };
|
|
@@ -493,7 +512,8 @@ function evaluateDispatch(data, { clock = Date } = {}) {
|
|
|
493
512
|
`required — a guard that cannot verify must not answer "safe" (#3050). Retry once the ` +
|
|
494
513
|
`project configuration is readable.`;
|
|
495
514
|
const reasonCode = isBuildFailure ? REASON_CODE.RUNTIME_BUILD_FAILED : REASON_CODE.CONFIG_UNREADABLE;
|
|
496
|
-
|
|
515
|
+
const fullReason = state.sentinelDiscarded ? reason + describeSentinelDiscard(state.sentinelDiscarded) : reason;
|
|
516
|
+
return { action: 'block', reason: fullReason, reasonCode, sentinelDiscarded: state.sentinelDiscarded };
|
|
497
517
|
}
|
|
498
518
|
|
|
499
519
|
if (state.isolation !== 'harness-worktree') return { action: 'allow' };
|
|
@@ -503,13 +523,14 @@ function evaluateDispatch(data, { clock = Date } = {}) {
|
|
|
503
523
|
|
|
504
524
|
if (toolInput[parsed.param] === parsed.value) return { action: 'allow' };
|
|
505
525
|
|
|
506
|
-
|
|
526
|
+
let reason =
|
|
507
527
|
`Agent isolation guard: this project's dispatch isolation resolves to "harness-worktree", ` +
|
|
508
528
|
`but the Agent() dispatch for subagent_type="${subagentType}" is missing ` +
|
|
509
529
|
`${parsed.param}="${parsed.value}". Add ${parsed.param}="${parsed.value}" to the Agent() ` +
|
|
510
530
|
`call so the executor runs in an isolated worktree instead of the primary checkout ` +
|
|
511
531
|
`(gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md).`;
|
|
512
|
-
|
|
532
|
+
if (state.sentinelDiscarded) reason += describeSentinelDiscard(state.sentinelDiscarded);
|
|
533
|
+
return { action: 'block', reason, reasonCode: REASON_CODE.HARNESS_FLAG_MISSING, sentinelDiscarded: state.sentinelDiscarded };
|
|
513
534
|
}
|
|
514
535
|
|
|
515
536
|
/* istanbul ignore next -- stdin adapter, exercised via spawnSync in tests */
|
|
@@ -524,7 +545,12 @@ function main() {
|
|
|
524
545
|
const data = JSON.parse(input);
|
|
525
546
|
const decision = evaluateDispatch(data);
|
|
526
547
|
if (decision.action === 'block') {
|
|
527
|
-
const out = {
|
|
548
|
+
const out = {
|
|
549
|
+
decision: 'block',
|
|
550
|
+
reason: decision.reason,
|
|
551
|
+
reason_code: decision.reasonCode,
|
|
552
|
+
sentinel_discarded: decision.sentinelDiscarded ?? null,
|
|
553
|
+
};
|
|
528
554
|
// Kimi feeds stderr (not stdout) back to the model on exit 2.
|
|
529
555
|
deny(out, decision.reason);
|
|
530
556
|
}
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
// Thresholds:
|
|
15
15
|
// WARNING (remaining <= 35%): Agent should wrap up current task
|
|
16
16
|
// CRITICAL (remaining <= 25%): Agent should stop immediately and save state
|
|
17
|
+
// Both fire-points are overridable per project via .planning/config.json
|
|
18
|
+
// (hooks.context_warning_threshold / hooks.context_critical_threshold, #4285);
|
|
19
|
+
// the values above are the defaults used when the keys are absent or unusable.
|
|
17
20
|
//
|
|
18
21
|
// Debounce: 5 tool uses between warnings to avoid spam
|
|
19
22
|
// Severity escalation bypasses debounce (WARNING -> CRITICAL fires immediately)
|
|
@@ -30,8 +33,8 @@ const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
|
|
|
30
33
|
// context warning is far cheaper than stalling the agent's work (#3911).
|
|
31
34
|
const ON_CRASH = HOOK_ON_CRASH.ALLOW;
|
|
32
35
|
|
|
33
|
-
const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
|
|
34
|
-
const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
|
|
36
|
+
const WARNING_THRESHOLD = 35; // remaining_percentage <= 35% (default, see resolveThresholds)
|
|
37
|
+
const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25% (default, see resolveThresholds)
|
|
35
38
|
const STALE_SECONDS = 60; // ignore metrics older than 60s
|
|
36
39
|
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
|
|
37
40
|
// How long after a PreCompact readings stay suspect. The watermark records the
|
|
@@ -57,6 +60,41 @@ const COMPACT_GRACE_SECONDS = 60;
|
|
|
57
60
|
// watermark this far ahead pushes first recovery from +61 to +66 (measured).
|
|
58
61
|
const WATERMARK_SKEW_SECONDS = 5;
|
|
59
62
|
|
|
63
|
+
// Resolve the two fire-points from the project's `.planning/config.json`
|
|
64
|
+
// (#4285). The constants above are the DEFAULTS; a project overrides either one
|
|
65
|
+
// through `hooks.context_warning_threshold` / `hooks.context_critical_threshold`,
|
|
66
|
+
// which is what keeps a tuned fire-point alive across updates — this file is in
|
|
67
|
+
// the MANAGED registry, so an edit to the constants is re-staged away by the
|
|
68
|
+
// next install.
|
|
69
|
+
//
|
|
70
|
+
// TOTAL and never-throwing: this hook must not block the tool call it rides in
|
|
71
|
+
// on, so every unusable input degrades to the default instead of raising.
|
|
72
|
+
// Unusable is decided by Number.isFinite, which is type-strict (the string
|
|
73
|
+
// "30" and true are both rejected, unlike the global isFinite), plus the 0-100
|
|
74
|
+
// domain of the remaining_percentage these are compared against.
|
|
75
|
+
//
|
|
76
|
+
// The PAIR is validated too, and falls back TOGETHER. `critical >= warning` has
|
|
77
|
+
// no coherent reading — critical fires deeper into the window than warning —
|
|
78
|
+
// and honouring one side of an inconsistent pair silently picks which of the
|
|
79
|
+
// operator's two numbers to discard. This also rejects a single override that
|
|
80
|
+
// contradicts the OTHER key's default (warning 20 with critical absent, i.e.
|
|
81
|
+
// 25); the resulting pair is the same nonsense either way. Set-time validation
|
|
82
|
+
// cannot stand in for this check: `config-set` writes one key per call, so
|
|
83
|
+
// tuning both (warning first, then critical) is transiently inconsistent on
|
|
84
|
+
// disk, and refusing it there would block a legitimate configuration.
|
|
85
|
+
function resolveThresholds(hooks) {
|
|
86
|
+
const defaults = { warning: WARNING_THRESHOLD, critical: CRITICAL_THRESHOLD };
|
|
87
|
+
if (!hooks || typeof hooks !== 'object') return defaults;
|
|
88
|
+
|
|
89
|
+
const usable = (value, fallback) =>
|
|
90
|
+
(Number.isFinite(value) && value >= 0 && value <= 100) ? value : fallback;
|
|
91
|
+
|
|
92
|
+
const warning = usable(hooks.context_warning_threshold, WARNING_THRESHOLD);
|
|
93
|
+
const critical = usable(hooks.context_critical_threshold, CRITICAL_THRESHOLD);
|
|
94
|
+
|
|
95
|
+
return critical < warning ? { warning, critical } : defaults;
|
|
96
|
+
}
|
|
97
|
+
|
|
60
98
|
// One DEFINITION of what counts as a lifecycle event name, shared by the #3709
|
|
61
99
|
// PreCompact reset and the #2289 output-envelope allowlist. Two call sites, one
|
|
62
100
|
// rule — so the two cannot drift into disagreeing about what "no event name" is.
|
|
@@ -164,14 +202,11 @@ function writeSentinel(target, payload) {
|
|
|
164
202
|
}
|
|
165
203
|
|
|
166
204
|
let input = '';
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
process.stdin.setEncoding('utf8');
|
|
173
|
-
process.stdin.on('data', chunk => input += chunk);
|
|
174
|
-
process.stdin.on('end', () => {
|
|
205
|
+
// Assigned by main(); the handler below clears it. Declared out here rather
|
|
206
|
+
// than inside main() because the handler closes over it.
|
|
207
|
+
let stdinTimeout = null;
|
|
208
|
+
|
|
209
|
+
const handleStdinEnd = () => {
|
|
175
210
|
clearTimeout(stdinTimeout);
|
|
176
211
|
try {
|
|
177
212
|
const data = JSON.parse(input);
|
|
@@ -266,18 +301,26 @@ process.stdin.on('end', () => {
|
|
|
266
301
|
allow(undefined);
|
|
267
302
|
}
|
|
268
303
|
|
|
269
|
-
// Check if context warnings are disabled via config
|
|
304
|
+
// Check if context warnings are disabled via config, and resolve the two
|
|
305
|
+
// fire-points from the same read (#4285 — one config read, not two).
|
|
270
306
|
// Collapsed existsSync+readFileSync into a single read guarded by try/catch
|
|
271
307
|
// (ENOENT or parse error → use defaults, same as old "planningDir absent" branch).
|
|
272
308
|
const cwd = data.cwd || process.cwd();
|
|
309
|
+
let thresholds = { warning: WARNING_THRESHOLD, critical: CRITICAL_THRESHOLD };
|
|
273
310
|
try {
|
|
274
311
|
const configPath = path.join(cwd, '.planning', 'config.json');
|
|
275
312
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
276
313
|
if (config.hooks?.context_warnings === false) {
|
|
277
314
|
allow(undefined);
|
|
278
315
|
}
|
|
316
|
+
// After the disable check, not before: a disabled monitor exits above and
|
|
317
|
+
// never reaches a threshold, so resolving first would only add work to the
|
|
318
|
+
// path that does nothing. allow() exits the process (it does not throw),
|
|
319
|
+
// so this line is unreachable when warnings are off.
|
|
320
|
+
thresholds = resolveThresholds(config.hooks);
|
|
279
321
|
} catch (e) {
|
|
280
|
-
// Missing or unparseable config → proceed with defaults (context warnings
|
|
322
|
+
// Missing or unparseable config → proceed with defaults (context warnings
|
|
323
|
+
// enabled, thresholds at the constants above, which `thresholds` already holds)
|
|
281
324
|
}
|
|
282
325
|
|
|
283
326
|
// If no metrics file, this is a subagent or fresh session -- exit silently.
|
|
@@ -352,7 +395,7 @@ process.stdin.on('end', () => {
|
|
|
352
395
|
const usedPct = metrics.used_pct;
|
|
353
396
|
|
|
354
397
|
// No warning needed
|
|
355
|
-
if (remaining >
|
|
398
|
+
if (remaining > thresholds.warning) {
|
|
356
399
|
allow(undefined);
|
|
357
400
|
}
|
|
358
401
|
|
|
@@ -389,7 +432,7 @@ process.stdin.on('end', () => {
|
|
|
389
432
|
|
|
390
433
|
warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
|
|
391
434
|
|
|
392
|
-
const isCritical = remaining <=
|
|
435
|
+
const isCritical = remaining <= thresholds.critical;
|
|
393
436
|
const currentLevel = isCritical ? 'critical' : 'warning';
|
|
394
437
|
|
|
395
438
|
// Emit immediately on first warning, then debounce subsequent ones
|
|
@@ -491,4 +534,34 @@ process.stdin.on('end', () => {
|
|
|
491
534
|
// exit(0) fail-open behavior exactly (#3911).
|
|
492
535
|
crash(ON_CRASH, undefined);
|
|
493
536
|
}
|
|
494
|
-
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// The stdin adapter is the only side-effecting statement in this file, so it is
|
|
540
|
+
// the only thing that must not run on `require()`. Gating it lets a test import
|
|
541
|
+
// `resolveThresholds` and drive it directly — the repo's own conclusion for a
|
|
542
|
+
// seam like this (CONTEXT-INDEX, on the ROADMAP Requirements parser: a closure
|
|
543
|
+
// reachable only by spawning the CLI is one "no fast-check property can do").
|
|
544
|
+
// A spawn-per-case property test is not the same test: it would exercise the
|
|
545
|
+
// resolver at whatever pairs survive to an observable severity, not over its
|
|
546
|
+
// whole numeric domain.
|
|
547
|
+
/* istanbul ignore next -- stdin adapter, exercised via spawnSync in tests */
|
|
548
|
+
function main() {
|
|
549
|
+
// Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
|
|
550
|
+
// Windows/Git Bash, or slow Claude Code piping during large outputs),
|
|
551
|
+
// exit silently instead of hanging until Claude Code kills the process
|
|
552
|
+
// and reports "hook error". See #775, #1162.
|
|
553
|
+
stdinTimeout = setTimeout(() => allow(undefined), 10000);
|
|
554
|
+
process.stdin.setEncoding('utf8');
|
|
555
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
556
|
+
process.stdin.on('end', handleStdinEnd);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (require.main === module) {
|
|
560
|
+
main();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Exported for the #4285 property test only. The two constants ride along so a
|
|
564
|
+
// test asserts the fallback pair against the SOURCE of truth rather than
|
|
565
|
+
// re-hardcoding 35/25 — a test carrying its own copy of the defaults would stay
|
|
566
|
+
// green if the constants were edited.
|
|
567
|
+
module.exports = { resolveThresholds, WARNING_THRESHOLD, CRITICAL_THRESHOLD };
|
|
@@ -62,8 +62,8 @@ const { allow } = require('./lib/hook-exit.js');
|
|
|
62
62
|
// hooks/lib/cursor-workspace.js. Staged next to these scripts by
|
|
63
63
|
// writeCursorHooksJson so the require always resolves post-install.
|
|
64
64
|
const { resolveStatePath } = require('./lib/cursor-workspace.js');
|
|
65
|
-
const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch } = require('./lib/isolation-sentinel.js');
|
|
66
|
-
const { REASON_CODE } = require('./lib/isolation-deny-reason.js');
|
|
65
|
+
const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch, buildSentinelDiscard } = require('./lib/isolation-sentinel.js');
|
|
66
|
+
const { REASON_CODE, describeSentinelDiscard } = require('./lib/isolation-deny-reason.js');
|
|
67
67
|
// #3582: gsd-core/bin/lib/*.cjs (runtime-homes.cjs, worktree-safety.cjs,
|
|
68
68
|
// runtime-name-policy.cjs, capability-registry.cjs — required below, inside
|
|
69
69
|
// resolveIsolationEvidence and resolveFallbackIsolation) are tsc build
|
|
@@ -491,18 +491,25 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
|
|
|
491
491
|
`Refusing to allow this subagent to spawn until the runtime library is built — a guard ` +
|
|
492
492
|
`that cannot verify must not answer "safe" (#3050).`,
|
|
493
493
|
reasonCode: REASON_CODE.RUNTIME_BUILD_FAILED,
|
|
494
|
+
sentinelDiscarded: null,
|
|
494
495
|
};
|
|
495
496
|
}
|
|
496
497
|
|
|
498
|
+
// #3045 BLOCKER fix: a fresh sentinel is authoritative for THIS dispatch's
|
|
499
|
+
// actual resolved isolation — see the doc comment above.
|
|
500
|
+
// #3045 SECURITY F2: a fresh sentinel that names a DIFFERENT plan/phase
|
|
501
|
+
// than this dispatch is not applicable to it — fall through to the
|
|
502
|
+
// conservative fallback exactly as a stale sentinel would.
|
|
503
|
+
// Hoisted (readSentinel never throws) so the "present, fresh, but did not
|
|
504
|
+
// apply" case (#4594 row 15) can be reported on every deny path below
|
|
505
|
+
// instead of silently discarded.
|
|
506
|
+
const sentinel = readSentinel(root, { clock });
|
|
507
|
+
const applies = sentinelAppliesToDispatch(sentinel, dispatchIds);
|
|
508
|
+
const sentinelDiscarded = buildSentinelDiscard(sentinel, dispatchIds);
|
|
509
|
+
|
|
497
510
|
let declaredIsolation;
|
|
498
511
|
try {
|
|
499
|
-
|
|
500
|
-
// dispatch's actual resolved isolation — see the doc comment above.
|
|
501
|
-
// #3045 SECURITY F2: a fresh sentinel that names a DIFFERENT
|
|
502
|
-
// plan/phase than this dispatch is not applicable to it — fall through
|
|
503
|
-
// to the conservative fallback exactly as a stale sentinel would.
|
|
504
|
-
const sentinel = readSentinel(root, { clock });
|
|
505
|
-
declaredIsolation = (sentinel.present && !sentinel.stale && sentinelAppliesToDispatch(sentinel, dispatchIds))
|
|
512
|
+
declaredIsolation = (sentinel.present && !sentinel.stale && applies)
|
|
506
513
|
? sentinel.isolation
|
|
507
514
|
: resolveFallbackIsolation(root, configPath);
|
|
508
515
|
} catch {
|
|
@@ -513,8 +520,10 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
|
|
|
513
520
|
`dispatch-isolation configuration ('.planning/config.json' exists under "${root}"). ` +
|
|
514
521
|
`Refusing to allow this subagent to spawn without being able to verify whether ` +
|
|
515
522
|
`isolation is required — a guard that cannot verify must not answer "safe" (#3050). ` +
|
|
516
|
-
`Retry once the project configuration is readable
|
|
523
|
+
`Retry once the project configuration is readable.` +
|
|
524
|
+
(sentinelDiscarded ? describeSentinelDiscard(sentinelDiscarded) : ''),
|
|
517
525
|
reasonCode: REASON_CODE.CONFIG_UNREADABLE,
|
|
526
|
+
sentinelDiscarded,
|
|
518
527
|
};
|
|
519
528
|
}
|
|
520
529
|
|
|
@@ -530,8 +539,10 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
|
|
|
530
539
|
`GSD subagent isolation guard: this project's dispatch isolation resolves to ` +
|
|
531
540
|
`"harness-worktree", but the subagentStart payload for this dispatch carries no usable ` +
|
|
532
541
|
`subagent_type. Refusing to allow it to spawn without being able to confirm whether it ` +
|
|
533
|
-
`is a GSD executor — a guard that cannot verify must not answer "safe" (#3050)
|
|
542
|
+
`is a GSD executor — a guard that cannot verify must not answer "safe" (#3050).` +
|
|
543
|
+
(sentinelDiscarded ? describeSentinelDiscard(sentinelDiscarded) : ''),
|
|
534
544
|
reasonCode: REASON_CODE.NO_SUBAGENT_TYPE,
|
|
545
|
+
sentinelDiscarded,
|
|
535
546
|
};
|
|
536
547
|
}
|
|
537
548
|
|
|
@@ -547,8 +558,10 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
|
|
|
547
558
|
`"harness-worktree", but whether "${root}" is running in an isolated Cursor worktree ` +
|
|
548
559
|
`could not be determined (git did not respond). Refusing to allow subagent_type=` +
|
|
549
560
|
`"${subagentType}" to spawn without being able to verify isolation — a guard that ` +
|
|
550
|
-
`cannot verify must not answer "safe" (#3050). Retry once git is responsive
|
|
561
|
+
`cannot verify must not answer "safe" (#3050). Retry once git is responsive.` +
|
|
562
|
+
(sentinelDiscarded ? describeSentinelDiscard(sentinelDiscarded) : ''),
|
|
551
563
|
reasonCode: REASON_CODE.CANNOT_DETERMINE_ISOLATION,
|
|
564
|
+
sentinelDiscarded,
|
|
552
565
|
};
|
|
553
566
|
}
|
|
554
567
|
|
|
@@ -560,8 +573,10 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
|
|
|
560
573
|
`which is not an isolated Cursor worktree — it would edit the user's primary checkout ` +
|
|
561
574
|
`directly, with no consent and no warning. Start an isolated session first (the ` +
|
|
562
575
|
`"--worktree" CLI flag or the "/worktree" chat command; Cursor manages these worktrees ` +
|
|
563
|
-
`under "~/.cursor/worktrees/") and retry
|
|
576
|
+
`under "~/.cursor/worktrees/") and retry.` +
|
|
577
|
+
(sentinelDiscarded ? describeSentinelDiscard(sentinelDiscarded) : ''),
|
|
564
578
|
reasonCode: REASON_CODE.NOT_ISOLATED_WORKTREE,
|
|
579
|
+
sentinelDiscarded,
|
|
565
580
|
};
|
|
566
581
|
}
|
|
567
582
|
|
|
@@ -611,7 +626,12 @@ function main() {
|
|
|
611
626
|
decision = { action: 'allow' };
|
|
612
627
|
}
|
|
613
628
|
if (decision.action === 'deny') {
|
|
614
|
-
const out = {
|
|
629
|
+
const out = {
|
|
630
|
+
permission: 'deny',
|
|
631
|
+
user_message: decision.reason,
|
|
632
|
+
reason_code: decision.reasonCode,
|
|
633
|
+
sentinel_discarded: decision.sentinelDiscarded ?? null,
|
|
634
|
+
};
|
|
615
635
|
if (additionalContext !== null) out.additional_context = additionalContext;
|
|
616
636
|
process.stdout.write(JSON.stringify(out));
|
|
617
637
|
return;
|