@opengsd/gsd-core 1.5.0-rc.4 → 1.5.0-rc.5

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.
Files changed (71) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-phase-researcher.md +1 -1
  3. package/agents/gsd-planner.md +4 -0
  4. package/agents/gsd-project-researcher.md +1 -1
  5. package/agents/gsd-verifier.md +45 -13
  6. package/bin/install.js +3 -1
  7. package/gemini-extension.json +1 -1
  8. package/gsd-core/bin/gsd-tools.cjs +49 -48
  9. package/gsd-core/bin/lib/agent-command-router.cjs +2 -2
  10. package/gsd-core/bin/lib/agent-install-check.cjs +143 -0
  11. package/gsd-core/bin/lib/audit-command-router.cjs +4 -4
  12. package/gsd-core/bin/lib/capability-activation.cjs +37 -10
  13. package/gsd-core/bin/lib/capability-registry.cjs +2 -0
  14. package/gsd-core/bin/lib/capability-state.cjs +80 -30
  15. package/gsd-core/bin/lib/capability-writer.cjs +5 -4
  16. package/gsd-core/bin/lib/check-command-router.cjs +50 -51
  17. package/gsd-core/bin/lib/commands.cjs +20 -2
  18. package/gsd-core/bin/lib/config-loader.cjs +3 -4
  19. package/gsd-core/bin/lib/config-schema.cjs +1 -1
  20. package/gsd-core/bin/lib/config-types.cjs +2 -1
  21. package/gsd-core/bin/lib/config.cjs +5 -2
  22. package/gsd-core/bin/lib/docs.cjs +14 -2
  23. package/gsd-core/bin/lib/frontmatter.cjs +2 -2
  24. package/gsd-core/bin/lib/gap-checker.cjs +5 -2
  25. package/gsd-core/bin/lib/git-base-branch.cjs +27 -1
  26. package/gsd-core/bin/lib/graphify-command-router.cjs +6 -8
  27. package/gsd-core/bin/lib/graphify.cjs +7 -31
  28. package/gsd-core/bin/lib/gsd2-import.cjs +2 -2
  29. package/gsd-core/bin/lib/init.cjs +30 -4
  30. package/gsd-core/bin/lib/intel-command-router.cjs +6 -3
  31. package/gsd-core/bin/lib/intel.cjs +28 -34
  32. package/gsd-core/bin/lib/io.cjs +2 -4
  33. package/gsd-core/bin/lib/learnings.cjs +2 -2
  34. package/gsd-core/bin/lib/loop-resolver.cjs +45 -167
  35. package/gsd-core/bin/lib/milestone.cjs +13 -5
  36. package/gsd-core/bin/lib/model-resolver.cjs +3 -4
  37. package/gsd-core/bin/lib/phase-id.cjs +2 -4
  38. package/gsd-core/bin/lib/phase-locator.cjs +3 -6
  39. package/gsd-core/bin/lib/phase.cjs +18 -7
  40. package/gsd-core/bin/lib/probe-core.cjs +33 -11
  41. package/gsd-core/bin/lib/profile-output.cjs +5 -2
  42. package/gsd-core/bin/lib/prohibition-enforcement.cjs +485 -0
  43. package/gsd-core/bin/lib/roadmap-command-router.cjs +2 -2
  44. package/gsd-core/bin/lib/roadmap-parser.cjs +3 -5
  45. package/gsd-core/bin/lib/roadmap.cjs +9 -4
  46. package/gsd-core/bin/lib/state.cjs +11 -2
  47. package/gsd-core/bin/lib/task-command-router.cjs +2 -2
  48. package/gsd-core/bin/lib/template.cjs +11 -2
  49. package/gsd-core/bin/lib/uat.cjs +8 -2
  50. package/gsd-core/bin/lib/verification.cjs +8 -5
  51. package/gsd-core/bin/lib/verify.cjs +311 -4
  52. package/gsd-core/bin/lib/workstream-inventory.cjs +2 -2
  53. package/gsd-core/bin/lib/workstream.cjs +8 -2
  54. package/gsd-core/bin/lib/worktree-safety.cjs +37 -2
  55. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  56. package/gsd-core/references/planner-antipatterns.md +46 -0
  57. package/gsd-core/references/planning-config.md +5 -1
  58. package/gsd-core/references/prohibition-probe.md +51 -2
  59. package/gsd-core/templates/verification-report.md +16 -3
  60. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +9 -0
  61. package/gsd-core/workflows/execute-phase.md +5 -1
  62. package/gsd-core/workflows/quick.md +3 -0
  63. package/gsd-core/workflows/settings-advanced.md +5 -5
  64. package/gsd-core/workflows/settings-integrations.md +5 -5
  65. package/gsd-core/workflows/spec-phase.md +25 -2
  66. package/gsd-core/workflows/verify-phase.md +22 -7
  67. package/package.json +2 -2
  68. package/scripts/gen-capability-registry.cjs +27 -1
  69. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  70. package/scripts/research-profiles.cjs +2 -2
  71. package/gsd-core/bin/lib/core.cjs +0 -345
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gsd-core",
3
3
  "displayName": "GSD Core",
4
- "version": "1.5.0-rc.4",
4
+ "version": "1.5.0-rc.5",
5
5
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
6
6
  "author": {
7
7
  "name": "open-gsd",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gsd-phase-researcher
3
3
  description: Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator.
4
- tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*
4
+ tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*
5
5
  color: cyan
6
6
  # hooks:
7
7
  # PostToolUse:
@@ -198,6 +198,10 @@ Every task has four required fields:
198
198
  Full rules + worked examples: @gsd-core/references/planner-antipatterns.md ("Comment-Text Discipline").
199
199
  </comment_text_discipline>
200
200
 
201
+ <region_scoped_negative_gate>
202
+ **Region-scoped negative gates (WARN, #968):** Region-scope a file-wide negative grep when a sibling task needs that construct elsewhere in the same file; `validate_plan` WARNS. See: @gsd-core/references/planner-antipatterns.md ("Region-Scoped Negative Gates").
203
+ </region_scoped_negative_gate>
204
+
201
205
  **<done>:** Acceptance criteria - measurable state of completion.
202
206
  - Good: "Valid credentials return 200 + JWT cookie, invalid credentials return 401"
203
207
  - Bad: "Authentication is complete"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gsd-project-researcher
3
3
  description: Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators.
4
- tools: Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*
4
+ tools: Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*
5
5
  color: cyan
6
6
  # hooks:
7
7
  # PostToolUse:
@@ -180,21 +180,28 @@ For each truth, determine if codebase enables it.
180
180
 
181
181
  **Verification status:**
182
182
 
183
- - ✓ VERIFIED: All supporting artifacts pass all checks
183
+ - ✓ VERIFIED: All supporting artifacts pass all checks — and, for a behavior-dependent truth, a behavioral test exercises the asserted behavior (see below)
184
+ - ⚠️ PRESENT_BEHAVIOR_UNVERIFIED: Supporting artifacts are present and wired, but the truth asserts runtime behavior that no test exercises — present, not behaviorally proven. Routes to human verification (Step 8) and does NOT count toward the verified score (Step 9).
184
185
  - ✗ FAILED: One or more artifacts missing, stub, or unwired
185
186
  - ? UNCERTAIN: Can't verify programmatically (needs human)
186
187
 
188
+ **Behavior-dependent truths.** A truth is *behavior-dependent* when its correctness hinges on runtime behavior grep/presence checks cannot see — a **state transition** or a **cancellation / cleanup / ordering invariant** (e.g. "cancels the in-flight task and bumps the generation counter", "resets the busy flag on abort", "rolls back on failure"). For these, symbol presence + wiring is *necessary but not sufficient*: the code can be present and wired yet still leak state on the very path the invariant covers.
189
+
187
190
  For each truth:
188
191
 
189
192
  1. Identify supporting artifacts
190
193
  2. Check artifact status (Step 4)
191
194
  3. Check wiring status (Step 5)
192
- 4. **Before marking FAIL:** Check for override (Step 3b)
193
- 5. Determine truth status
195
+ 4. **Before marking FAIL or PRESENT_BEHAVIOR_UNVERIFIED:** Check for override (Step 3b)
196
+ 5. **Classify behavior-dependence.** If the truth asserts a state transition or a cancellation/cleanup/ordering invariant, its status cannot be VERIFIED on presence alone:
197
+ - A pre-existing test exercises the transition/invariant and passes (confirm via Step 7b's single-named-test path) → ✓ VERIFIED.
198
+ - No such test exists, or it can't run without a server/state mutation → ⚠️ PRESENT_BEHAVIOR_UNVERIFIED. Emit a human-verification item (Step 8) and do not count it toward the verified score (Step 9).
199
+ - An accepted override (Step 3b) carries the truth as PASSED (override), exactly as it does for a FAILED truth.
200
+ 6. Determine truth status
194
201
 
195
202
  ## Step 3b: Check Verification Overrides
196
203
 
197
- Before marking any must-have as FAILED, check the VERIFICATION.md frontmatter for an `overrides:` entry that matches this must-have.
204
+ Before marking any must-have as FAILED or ⚠️ PRESENT_BEHAVIOR_UNVERIFIED, check the VERIFICATION.md frontmatter for an `overrides:` entry that matches this must-have.
198
205
 
199
206
  **Override check procedure:**
200
207
 
@@ -204,12 +211,12 @@ Before marking any must-have as FAILED, check the VERIFICATION.md frontmatter fo
204
211
  4. Key technical terms (file paths, component names, API endpoints) have higher weight
205
212
 
206
213
  **If override found:**
207
- - Mark as `PASSED (override)` instead of FAIL
214
+ - Mark as `PASSED (override)` instead of FAIL/PRESENT_BEHAVIOR_UNVERIFIED
208
215
  - Evidence: `Override: {reason} — accepted by {accepted_by} on {accepted_at}`
209
- - Count toward passing score, not failing score
216
+ - Count toward passing score (`verified_truths`), not failing score
210
217
 
211
218
  **If no override found:**
212
- - Mark as FAILED as normal
219
+ - Mark as FAILED (or ⚠️ PRESENT_BEHAVIOR_UNVERIFIED, per Step 3 step 5) as normal
213
220
  - Consider suggesting an override if the failure looks intentional (alternative implementation exists)
214
221
 
215
222
  **Suggesting overrides:** When a must-have FAILs but evidence shows an alternative implementation that achieves the same intent, include an override suggestion in the report:
@@ -461,6 +468,8 @@ Anti-pattern scanning (Step 7) checks for code smells. Behavioral spot-checks go
461
468
 
462
469
  **When to run:** For phases that produce runnable code (APIs, CLI tools, build scripts, data pipelines). Skip for documentation-only or config-only phases.
463
470
 
471
+ **Behavioral evidence for behavior-dependent truths (Step 3).** When a truth asserts a state transition or a cancellation/cleanup/ordering invariant, the single named test below is what upgrades it from ⚠️ PRESENT_BEHAVIOR_UNVERIFIED to ✓ VERIFIED. Run only the one named test that exercises the transition/invariant — never the full suite (per #25/#753). If no such test exists, leave the truth ⚠️ PRESENT_BEHAVIOR_UNVERIFIED and route it to human verification (Step 8); do not mark it VERIFIED on presence.
472
+
464
473
  **How:**
465
474
 
466
475
  1. **Identify checkable behaviors** from must-haves truths. Select 2-4 that can be tested with a single command:
@@ -548,6 +557,8 @@ done
548
557
 
549
558
  **Needs human if uncertain:** Complex wiring grep can't trace, dynamic state behavior, edge cases.
550
559
 
560
+ **Behavior-unverified truths (Step 3):** Every truth left ⚠️ PRESENT_BEHAVIOR_UNVERIFIED is recorded in the `behavior_unverified_items` frontmatter list (emitted whenever the count > 0, regardless of overall status, so it survives a gaps_found phase) and surfaces for human verification; when the overall status is human_needed it also appears in the human_verification section. Phrase each item around the invariant: what to trigger, what state must hold afterward, and why presence checks can't see it.
561
+
551
562
  **Harvest deferred items from PLAN.md (#3309 / `workflow.human_verify_mode = end-of-phase`):** Scan every PLAN file in the phase for `<verify><human-check>` blocks on `auto` tasks. These are verification items the planner deliberately deferred from `checkpoint:human-verify` to end-of-phase to avoid the executor cold-start cost. Each block has the same shape used by the planner:
552
563
 
553
564
  ```xml
@@ -579,18 +590,30 @@ Classify status using this decision tree IN ORDER (most restrictive first):
579
590
  1. IF any truth FAILED, artifact MISSING/STUB, key link NOT_WIRED, or blocker anti-pattern found:
580
591
  → **status: gaps_found**
581
592
 
582
- 2. IF Step 8 produced ANY human verification items (section is non-empty):
593
+ 2. IF Step 8 produced ANY human verification items (section is non-empty) — this includes every ⚠️ PRESENT_BEHAVIOR_UNVERIFIED truth from Step 3:
583
594
  → **status: human_needed**
584
- (Even if all truths are VERIFIED and score is N/N — human items take priority)
595
+ (Even if all other truths are VERIFIED — human items take priority)
585
596
 
586
597
  3. IF all truths VERIFIED, all artifacts pass, all links WIRED, no blockers, AND no human verification items:
587
598
  → **status: passed**
588
599
 
589
- **passed is ONLY valid when the human verification section is empty.** If you identified items requiring human testing in Step 8, status MUST be human_needed.
600
+ **passed is ONLY valid when the human verification section is empty.** If Step 8 produced any items — including any truth left ⚠️ PRESENT_BEHAVIOR_UNVERIFIED — the status is not `passed`: it is `human_needed`, or `gaps_found` when rule 1 also fires (the ordered tree keeps gaps_found's precedence).
601
+
602
+ **A ⚠️ PRESENT_BEHAVIOR_UNVERIFIED truth is never FAILED and never VERIFIED.** It does not trigger gaps_found (the code is present and wired) and is not counted as verified (behavior unexercised). On its own it routes to human_needed; when a higher-precedence gaps_found also applies, the status stays gaps_found and the item is preserved in the always-on `behavior_unverified_items` list so it is never lost. Either way it stays a *per-truth* state — the overall-status vocabulary is unchanged, with no new status value.
590
603
 
591
604
  > **Shared status seam**: the status vocabulary (`passed`, `gaps_found`, `human_needed`) and the per-status routing (next action and next command for each value) are owned by `src/verification.cts` via `gsd_run query verification.status`. This agent is the single emitter of the frontmatter status field; consumers (ship.md, execute-phase.md) read routing from that query instead of re-deriving it.
592
605
 
593
- **Score:** `verified_truths / total_truths`
606
+ **Score (presence- vs behavior-verified split):**
607
+
608
+ - `verified_truths` counts ✓ VERIFIED truths plus PASSED (override) truths (Step 3b). For a behavior-dependent truth, VERIFIED means a behavioral test passed, not just that symbols are present.
609
+ - ⚠️ PRESENT_BEHAVIOR_UNVERIFIED truths are the *only* ones excluded from `verified_truths`; they are reported separately as `behavior_unverified`.
610
+
611
+ ```text
612
+ score: verified_truths / total_truths # e.g. 6/7
613
+ behavior_unverified: P # truths present + wired but behavior not exercised
614
+ ```
615
+
616
+ A headline N/N therefore certifies that every behavior-dependent truth had behavioral evidence — a clean score can no longer be reached on symbol presence alone.
594
617
 
595
618
  ## Step 9b: Filter Deferred Items
596
619
 
@@ -699,6 +722,7 @@ phase: XX-name
699
722
  verified: YYYY-MM-DDTHH:MM:SSZ
700
723
  status: passed | gaps_found | human_needed
701
724
  score: N/M must-haves verified
725
+ behavior_unverified: 0 # Count of ⚠️ PRESENT_BEHAVIOR_UNVERIFIED truths (present + wired, behavior not exercised); each is detailed in behavior_unverified_items below (and in human_verification when status is human_needed)
702
726
  overrides_applied: 0 # Count of PASSED (override) items included in score
703
727
  overrides: # Only if overrides exist — carried forward or newly added
704
728
  - must_have: "Must-have text that was overridden"
@@ -725,6 +749,11 @@ deferred: # Only if deferred items exist (Step 9b)
725
749
  - truth: "Observable truth addressed in a later phase"
726
750
  addressed_in: "Phase N"
727
751
  evidence: "Matching goal or success criteria text"
752
+ behavior_unverified_items: # Only if behavior_unverified > 0 — emitted regardless of overall status, so these survive a gaps_found phase
753
+ - truth: "Observable truth whose state transition or cancellation/cleanup/ordering invariant no test exercises"
754
+ test: "What to trigger"
755
+ expected: "What state must hold afterward"
756
+ why_human: "Why presence checks can't see it"
728
757
  human_verification: # Only if status: human_needed
729
758
  - test: "What to do"
730
759
  expected: "What should happen"
@@ -746,8 +775,9 @@ human_verification: # Only if status: human_needed
746
775
  | --- | ------- | ---------- | -------------- |
747
776
  | 1 | {truth} | ✓ VERIFIED | {evidence} |
748
777
  | 2 | {truth} | ✗ FAILED | {what's wrong} |
778
+ | 3 | {truth} | ⚠️ PRESENT_BEHAVIOR_UNVERIFIED | {present + wired; no test exercises the transition/invariant — see Human Verification} |
749
779
 
750
- **Score:** {N}/{M} truths verified
780
+ **Score:** {N}/{M} truths verified ({P} present, behavior-unverified)
751
781
 
752
782
  ### Deferred Items
753
783
 
@@ -834,7 +864,7 @@ Structured gaps in VERIFICATION.md frontmatter for `/gsd:plan-phase --gaps`.
834
864
 
835
865
  {If human_needed:}
836
866
  ### Human Verification Required
837
- {N} items need human testing:
867
+ {N} items need human testing (including {P} present-but-behavior-unverified truths — code wired, transition/invariant not exercised by a test):
838
868
  1. **{Test name}** — {what to do}
839
869
  - Expected: {what should happen}
840
870
 
@@ -857,6 +887,8 @@ Automated checks passed. Awaiting human verification.
857
887
 
858
888
  **Keep verification fast.** Use grep/file checks, not running the app.
859
889
 
890
+ **Presence is not behavior.** Grep/file checks prove a symbol is present and wired — they do not prove a state transition or a cancellation/cleanup/ordering invariant holds at runtime. For a behavior-dependent truth, require a passing behavioral test (Step 7b's single named test) or mark it ⚠️ PRESENT_BEHAVIOR_UNVERIFIED and route to human verification. Never let symbol presence alone produce a VERIFIED on a behavior-dependent truth.
891
+
860
892
  **DO NOT commit.** Leave committing to the orchestrator.
861
893
 
862
894
  </critical_rules>
package/bin/install.js CHANGED
@@ -265,9 +265,11 @@ const _gsdLibDir = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib');
265
265
  const { MODEL_PROFILES: GSD_MODEL_PROFILES } = require(path.join(_gsdLibDir, 'model-profiles.cjs'));
266
266
  const {
267
267
  RUNTIME_PROFILE_MAP: GSD_RUNTIME_PROFILE_MAP,
268
+ } = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
269
+ const {
268
270
  resolveTierEntry: gsdResolveTierEntry,
269
271
  EFFORT_SET: GSD_EFFORT_SET,
270
- } = require(path.join(_gsdLibDir, 'core.cjs'));
272
+ } = require(path.join(_gsdLibDir, 'model-resolver.cjs'));
271
273
 
272
274
  // #443 — model-catalog and config-defaults.manifest.json exports needed only
273
275
  // by effort-resolution code paths (resolveInstallTimeEffort /
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-core",
3
- "version": "1.5.0-rc.4",
3
+ "version": "1.5.0-rc.5",
4
4
  "description": "GSD Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads gsd's operating context into every Gemini CLI session.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -194,13 +194,14 @@
194
194
  const fs = require('fs');
195
195
  const path = require('path');
196
196
  const { ExitError, runMain } = require('./lib/cli-exit.cjs');
197
- const core = require('./lib/core.cjs');
198
- const { error, ERROR_REASON } = core;
197
+ const io = require('./lib/io.cjs');
198
+ const { error, ERROR_REASON, setJsonErrorMode, output } = io;
199
+ const projectRoot = require('./lib/project-root.cjs');
199
200
  // Resolve findProjectRoot lazily at call time rather than binding it at module
200
- // load. It is a re-export from core.cjs (sourced from project-root.cjs); a
201
- // call-time lookup is robust against any require/load-ordering edge where the
202
- // re-export isn't bound yet when this entrypoint is first required (#604).
203
- const findProjectRoot = (...args) => core.findProjectRoot(...args);
201
+ // load. It is sourced from project-root.cjs; a call-time lookup is robust
202
+ // against any require/load-ordering edge where the export isn't bound yet
203
+ // when this entrypoint is first required (#604).
204
+ const findProjectRoot = (...args) => projectRoot.findProjectRoot(...args);
204
205
  const { getActiveWorkstream } = require('./lib/planning-workspace.cjs');
205
206
  const { resolveActiveWorkstream, applyResolvedWorkstreamEnv } = require('./lib/active-workstream-store.cjs');
206
207
  const state = require('./lib/state.cjs');
@@ -255,7 +256,7 @@ const { getEffectiveAuthority, classifyDriftSeverity } = require('./lib/plan-dri
255
256
  * @param {string} opts.cwd - project dir
256
257
  * @param {boolean} opts.raw - raw output mode
257
258
  * @param {Function} opts.error - error reporter
258
- * @param {Function} opts.output - output emitter (core.output)
259
+ * @param {Function} opts.output - output emitter (output)
259
260
  */
260
261
  function _dispatchNonFamily({ registryCommand, registryArgs, legacyCommand, legacyArgs, cwd, raw, error, output }) {
261
262
  void registryCommand;
@@ -294,7 +295,7 @@ function _dispatchNonFamily({ registryCommand, registryArgs, legacyCommand, lega
294
295
  * @param {string[]} opts.args Remaining args passed to the router
295
296
  * @param {string} opts.cwd Project working directory
296
297
  * @param {boolean} opts.raw Raw output mode flag
297
- * @param {Function} opts.error Error reporter (core.error)
298
+ * @param {Function} opts.error Error reporter (io.error)
298
299
  * @param {object} [opts.registry] Injectable registry (for tests)
299
300
  * @param {Function} [opts.requireModule] Injectable module loader (for tests)
300
301
  * @returns {boolean} true if the command was dispatched, false otherwise
@@ -404,10 +405,10 @@ async function main() {
404
405
  // their plain-text diagnostic.
405
406
  const jsonErrorsIdx = args.indexOf('--json-errors');
406
407
  if (jsonErrorsIdx !== -1) {
407
- core.setJsonErrorMode(true);
408
+ setJsonErrorMode(true);
408
409
  args.splice(jsonErrorsIdx, 1);
409
410
  } else if (process.env.GSD_JSON_ERRORS === '1') {
410
- core.setJsonErrorMode(true);
411
+ setJsonErrorMode(true);
411
412
  }
412
413
 
413
414
  // Optional cwd override for sandboxed subagents running outside project root.
@@ -433,7 +434,7 @@ async function main() {
433
434
  // Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree.
434
435
  // However, in monorepo worktrees where the subdirectory itself owns .planning/,
435
436
  // skip worktree resolution — the CWD is already the correct project root.
436
- const { resolveWorktreeRoot } = require('./lib/core.cjs');
437
+ const { resolveWorktreeRoot } = require('./lib/worktree-safety.cjs');
437
438
  if (!fs.existsSync(path.join(cwd, '.planning'))) {
438
439
  const worktreeRoot = resolveWorktreeRoot(cwd);
439
440
  if (worktreeRoot !== cwd) {
@@ -596,7 +597,7 @@ async function main() {
596
597
  }
597
598
 
598
599
  // Intercept stdout to transparently resolve @file: references (#1891).
599
- // core.cjs output() writes @file:<path> when JSON > 50KB. The --pick path
600
+ // io.cjs output() writes @file:<path> when JSON > 50KB. The --pick path
600
601
  // already resolves this, but the normal path wrote @file: to stdout, forcing
601
602
  // every workflow to have a bash-specific `if [[ "$INIT" == @file:* ]]` check
602
603
  // that breaks on PowerShell and other non-bash shells.
@@ -802,7 +803,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
802
803
  cwd,
803
804
  raw,
804
805
  error,
805
- output: core.output,
806
+ output: output,
806
807
  });
807
808
  if (!handled) phase.cmdFindPhase(cwd, args[1], raw);
808
809
  break;
@@ -895,7 +896,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
895
896
  cwd,
896
897
  raw,
897
898
  error,
898
- output: core.output,
899
+ output: output,
899
900
  });
900
901
  if (handled) break;
901
902
  }
@@ -957,7 +958,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
957
958
  cwd,
958
959
  raw,
959
960
  error,
960
- output: core.output,
961
+ output: output,
961
962
  });
962
963
  if (!handled) commands.cmdGenerateSlug(args[1], raw);
963
964
  break;
@@ -997,7 +998,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
997
998
  cwd,
998
999
  raw,
999
1000
  error,
1000
- output: core.output,
1001
+ output: output,
1001
1002
  });
1002
1003
  if (!handled) config.cmdConfigEnsureSection(cwd, raw);
1003
1004
  break;
@@ -1013,7 +1014,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1013
1014
  cwd,
1014
1015
  raw,
1015
1016
  error,
1016
- output: core.output,
1017
+ output: output,
1017
1018
  });
1018
1019
  if (!handled) config.cmdConfigSet(cwd, args[1], args[2], raw);
1019
1020
  break;
@@ -1029,7 +1030,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1029
1030
  cwd,
1030
1031
  raw,
1031
1032
  error,
1032
- output: core.output,
1033
+ output: output,
1033
1034
  });
1034
1035
  if (!handled) config.cmdConfigSetModelProfile(cwd, args[1], raw);
1035
1036
  break;
@@ -1054,7 +1055,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1054
1055
  cwd,
1055
1056
  raw,
1056
1057
  error,
1057
- output: core.output,
1058
+ output: output,
1058
1059
  });
1059
1060
  if (!handled) config.cmdConfigGet(cwd, args[1], raw, defaultValue);
1060
1061
  break;
@@ -1070,7 +1071,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1070
1071
  cwd,
1071
1072
  raw,
1072
1073
  error,
1073
- output: core.output,
1074
+ output: output,
1074
1075
  });
1075
1076
  if (!handled) config.cmdConfigNewProject(cwd, args[1], raw);
1076
1077
  break;
@@ -1183,7 +1184,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1183
1184
  args,
1184
1185
  cwd,
1185
1186
  raw,
1186
- output: core.output,
1187
+ output: output,
1187
1188
  error,
1188
1189
  });
1189
1190
  break;
@@ -1255,12 +1256,12 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1255
1256
  const configDirIdx = args.indexOf('--config-dir');
1256
1257
  if (configDirEqArg) {
1257
1258
  const value = configDirEqArg.slice('--config-dir='.length).trim();
1258
- if (!value) error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1259
+ if (!value) error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1259
1260
  loopConfigDir = value;
1260
1261
  } else if (configDirIdx !== -1) {
1261
1262
  const value = args[configDirIdx + 1];
1262
1263
  if (!value || value.startsWith('--')) {
1263
- error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1264
+ error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1264
1265
  }
1265
1266
  loopConfigDir = value;
1266
1267
  }
@@ -1270,12 +1271,12 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1270
1271
  const activeCapIdx = args.indexOf('--active-cap');
1271
1272
  if (activeCapEqArg) {
1272
1273
  const value = activeCapEqArg.slice('--active-cap='.length).trim();
1273
- if (!value) error('Missing value for --active-cap (e.g. --active-cap tdd)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1274
+ if (!value) error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1274
1275
  loopActiveCap = value;
1275
1276
  } else if (activeCapIdx !== -1) {
1276
1277
  const value = args[activeCapIdx + 1];
1277
1278
  if (!value || value.startsWith('--')) {
1278
- error('Missing value for --active-cap (e.g. --active-cap tdd)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1279
+ error('Missing value for --active-cap (e.g. --active-cap tdd)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1279
1280
  }
1280
1281
  loopActiveCap = value;
1281
1282
  }
@@ -1286,7 +1287,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1286
1287
  } else {
1287
1288
  error(
1288
1289
  `Unknown loop subcommand: ${loopSubcommand}. Available: render-hooks`,
1289
- core.ERROR_REASON ? core.ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
1290
+ ERROR_REASON ? ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
1290
1291
  );
1291
1292
  }
1292
1293
  break;
@@ -1307,7 +1308,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1307
1308
  const configDirVal = args[configDirIdx + 1];
1308
1309
  // Validate that --config-dir has a following non-flag value.
1309
1310
  if (!configDirVal || configDirVal.startsWith('--')) {
1310
- error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1311
+ error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1311
1312
  }
1312
1313
  configDir = configDirVal;
1313
1314
  }
@@ -1317,7 +1318,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1317
1318
  // capability set <id> [--on|--off|--enable|--disable] [--gate <key>=<bool>]... [--config-dir <dir>] [--runtime <r>] [--scope <s>]
1318
1319
  const capId = args[2];
1319
1320
  if (!capId || capId.startsWith('--')) {
1320
- error('Missing capability id for: capability set <id>', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1321
+ error('Missing capability id for: capability set <id>', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1321
1322
  }
1322
1323
  // Parse --config-dir
1323
1324
  const setConfigDirIdx = args.indexOf('--config-dir');
@@ -1325,7 +1326,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1325
1326
  if (setConfigDirIdx !== -1) {
1326
1327
  const setConfigDirVal = args[setConfigDirIdx + 1];
1327
1328
  if (!setConfigDirVal || setConfigDirVal.startsWith('--')) {
1328
- error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1329
+ error('Missing value for --config-dir', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1329
1330
  }
1330
1331
  setConfigDir = setConfigDirVal;
1331
1332
  }
@@ -1334,7 +1335,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1334
1335
  const hasOn = args.includes('--on') || args.includes('--enable');
1335
1336
  const hasOff = args.includes('--off') || args.includes('--disable');
1336
1337
  if (hasOn && hasOff) {
1337
- error('Conflicting flags: --on/--enable and --off/--disable cannot both be present', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1338
+ error('Conflicting flags: --on/--enable and --off/--disable cannot both be present', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1338
1339
  }
1339
1340
  let setEnabled;
1340
1341
  if (hasOn) {
@@ -1348,16 +1349,16 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1348
1349
  if (args[gi] === '--gate') {
1349
1350
  const gateVal = args[gi + 1];
1350
1351
  if (!gateVal || gateVal.startsWith('--')) {
1351
- error('Missing value for --gate (expected <key>=<true|false>)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1352
+ error('Missing value for --gate (expected <key>=<true|false>)', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1352
1353
  }
1353
1354
  const eqIdx = gateVal.indexOf('=');
1354
1355
  if (eqIdx === -1) {
1355
- error(`Malformed --gate value "${gateVal}": expected <key>=<true|false>`, core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1356
+ error(`Malformed --gate value "${gateVal}": expected <key>=<true|false>`, ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1356
1357
  }
1357
1358
  const gateKey = gateVal.slice(0, eqIdx);
1358
1359
  const gateBoolStr = gateVal.slice(eqIdx + 1);
1359
1360
  if (gateBoolStr !== 'true' && gateBoolStr !== 'false') {
1360
- error(`Malformed --gate value "${gateVal}": bool must be true or false`, core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1361
+ error(`Malformed --gate value "${gateVal}": bool must be true or false`, ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1361
1362
  }
1362
1363
  setGates[gateKey] = gateBoolStr === 'true';
1363
1364
  gi++; // skip consumed value
@@ -1369,7 +1370,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1369
1370
  if (runtimeIdx !== -1) {
1370
1371
  const runtimeVal = args[runtimeIdx + 1];
1371
1372
  if (!runtimeVal || runtimeVal.startsWith('--')) {
1372
- error('Missing value for --runtime', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1373
+ error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1373
1374
  }
1374
1375
  setRuntime = runtimeVal;
1375
1376
  }
@@ -1378,7 +1379,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1378
1379
  if (scopeIdx !== -1) {
1379
1380
  const scopeVal = args[scopeIdx + 1];
1380
1381
  if (!scopeVal || scopeVal.startsWith('--')) {
1381
- error('Missing value for --scope', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1382
+ error('Missing value for --scope', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
1382
1383
  }
1383
1384
  setScope = scopeVal;
1384
1385
  }
@@ -1392,7 +1393,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1392
1393
  } else {
1393
1394
  error(
1394
1395
  `Unknown capability subcommand: ${capSubcommand}. Available: state, set`,
1395
- core.ERROR_REASON ? core.ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
1396
+ ERROR_REASON ? ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
1396
1397
  );
1397
1398
  }
1398
1399
  break;
@@ -1484,7 +1485,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1484
1485
  cwd,
1485
1486
  raw,
1486
1487
  error,
1487
- output: core.output,
1488
+ output: output,
1488
1489
  });
1489
1490
  if (!handled) docs.cmdDocsInit(cwd, raw);
1490
1491
  break;
@@ -1810,7 +1811,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1810
1811
  // ─── Research Store ────────────────────────────────────────────────────
1811
1812
  //
1812
1813
  // research-store get <key> [--kind <k>]
1813
- // -> getResearch(cwd, key, { homeDir }); searches both tiers; core.output(result, raw)
1814
+ // -> getResearch(cwd, key, { homeDir }); searches both tiers; output(result, raw)
1814
1815
  // (--kind is accepted for backward compatibility but no longer drives tier selection)
1815
1816
  // research-store put <key> --content <str> --source <s> --provider <p>
1816
1817
  // --confidence <c> --kind <k>
@@ -1834,7 +1835,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1834
1835
  }
1835
1836
  // --kind is accepted but no longer drives tier selection; getResearch searches both tiers
1836
1837
  const result = researchStore.getResearch(cwd, key, { homeDir });
1837
- core.output(result, raw);
1838
+ output(result, raw);
1838
1839
  } else if (subcommand === 'put') {
1839
1840
  const key = args[2];
1840
1841
  if (!key || key.startsWith('--')) {
@@ -1866,7 +1867,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1866
1867
  error('Usage: gsd-tools research-store put <key> --content <str> --source <s> --provider <p> --confidence <c> --kind <k>', ERROR_REASON.USAGE);
1867
1868
  }
1868
1869
  const entry = researchStore.putResearch(cwd, key, { content, source, provider, confidence, kind }, { homeDir });
1869
- core.output(entry, raw);
1870
+ output(entry, raw);
1870
1871
  } else {
1871
1872
  error('Unknown research-store subcommand. Available: get, put', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1872
1873
  }
@@ -1902,14 +1903,14 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1902
1903
  const { ecosystem = '', config: planConfig = {}, questions } = planInput;
1903
1904
  const homeDir = process.env.HOME || require('os').homedir();
1904
1905
  const plan = researchProvider.planResearch({ questions, ecosystem, config: planConfig, cwd, homeDir });
1905
- core.output(plan, raw);
1906
+ output(plan, raw);
1906
1907
  break;
1907
1908
  }
1908
1909
 
1909
1910
  // ─── Classify Confidence ──────────────────────────────────────────────
1910
1911
  //
1911
1912
  // classify-confidence --provider <id> [--package <name> --ecosystem <npm|pypi|crates>] [--verified]
1912
- // -> classifyConfidence({ provider, verifiedAgainstOfficial, legitimacyVerdict }); core.output(result, raw)
1913
+ // -> classifyConfidence({ provider, verifiedAgainstOfficial, legitimacyVerdict }); output(result, raw)
1913
1914
  //
1914
1915
  // legitimacyVerdict is CODE-COMPUTED via checkPackages — never caller-supplied — so an agent cannot self-assert OK→HIGH.
1915
1916
 
@@ -1936,7 +1937,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1936
1937
  legitimacyVerdict = results[0] ? results[0].verdict : null;
1937
1938
  }
1938
1939
  const confidence = researchProvider.classifyConfidence({ provider, verifiedAgainstOfficial: verified, legitimacyVerdict });
1939
- core.output({ provider, package: pkg || null, ecosystem: ecosystem || null, legitimacyVerdict, verified, confidence }, raw);
1940
+ output({ provider, package: pkg || null, ecosystem: ecosystem || null, legitimacyVerdict, verified, confidence }, raw);
1940
1941
  break;
1941
1942
  }
1942
1943
 
@@ -1980,7 +1981,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1980
1981
  } catch (pkgErr) {
1981
1982
  error(`package-legitimacy: ${pkgErr && pkgErr.message ? pkgErr.message : String(pkgErr)}`, ERROR_REASON.UNKNOWN);
1982
1983
  }
1983
- core.output(pkgResults, raw);
1984
+ output(pkgResults, raw);
1984
1985
  break;
1985
1986
  }
1986
1987
 
@@ -2101,7 +2102,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
2101
2102
  }
2102
2103
  }
2103
2104
 
2104
- core.output({ valid: errors.length === 0, errors, slots }, raw);
2105
+ output({ valid: errors.length === 0, errors, slots }, raw);
2105
2106
  break;
2106
2107
  }
2107
2108
 
@@ -2114,7 +2115,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
2114
2115
 
2115
2116
  // Read config.json directly for both plan_review.source_grounding_authority
2116
2117
  // and intel.enabled. Neither key is in the config-loader.cjs whitelist that
2117
- // core.loadConfig() returns; plan_review is only in config.cjs's private
2118
+ // config-loader.cjs's loadConfig() whitelist does not return; plan_review is only in config.cjs's private
2118
2119
  // buildConfig(), and intel is a federated capability config key.
2119
2120
  let configuredAuthority = 'grep';
2120
2121
  let intelEnabled = false;
@@ -2138,7 +2139,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
2138
2139
 
2139
2140
  if (subcommand === 'authority') {
2140
2141
  // Pass rawValue as 3rd arg so --raw returns unquoted string (not JSON)
2141
- core.output(effectiveAuthority, raw, effectiveAuthority);
2142
+ output(effectiveAuthority, raw, effectiveAuthority);
2142
2143
  break;
2143
2144
  }
2144
2145
 
@@ -2155,7 +2156,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
2155
2156
  ? authVal
2156
2157
  : effectiveAuthority;
2157
2158
  const result = classifyDriftSeverity({ status: statusVal, authority: authorityForClassify });
2158
- core.output(result, raw);
2159
+ output(result, raw);
2159
2160
  break;
2160
2161
  }
2161
2162
 
@@ -7,8 +7,8 @@
7
7
  * from the prior hand-written .cjs; only types are added.
8
8
  */
9
9
  // eslint-disable-next-line @typescript-eslint/no-require-imports
10
- const core = require("./core.cjs");
11
- const { output, error, ERROR_REASON } = core;
10
+ const io = require("./io.cjs");
11
+ const { output, error, ERROR_REASON } = io;
12
12
  // ─── Constants ────────────────────────────────────────────────────────────────
13
13
  const QUOTA_SENTINELS = [
14
14
  '429',