session-orchestrator 3.22.0 → 3.23.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.
Files changed (268) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/commands/autopilot-multi.md +14 -0
  5. package/.cursor/commands/autopilot.md +14 -0
  6. package/.cursor/commands/bootstrap.md +14 -0
  7. package/.cursor/commands/brainstorm.md +14 -0
  8. package/.cursor/commands/close.md +13 -0
  9. package/.cursor/commands/contract-version-bump.md +14 -0
  10. package/.cursor/commands/debug.md +14 -0
  11. package/.cursor/commands/discovery.md +14 -0
  12. package/.cursor/commands/dispatcher.md +14 -0
  13. package/.cursor/commands/eli5.md +14 -0
  14. package/.cursor/commands/eval.md +14 -0
  15. package/.cursor/commands/evolve.md +14 -0
  16. package/.cursor/commands/go.md +14 -0
  17. package/.cursor/commands/grill.md +14 -0
  18. package/.cursor/commands/harness-audit.md +13 -0
  19. package/.cursor/commands/journey-audit.md +14 -0
  20. package/.cursor/commands/memory-cleanup.md +14 -0
  21. package/.cursor/commands/persona-panel.md +14 -0
  22. package/.cursor/commands/plan.md +14 -0
  23. package/.cursor/commands/portfolio.md +14 -0
  24. package/.cursor/commands/reconcile.md +14 -0
  25. package/.cursor/commands/release.md +14 -0
  26. package/.cursor/commands/repo-audit.md +13 -0
  27. package/.cursor/commands/session.md +14 -0
  28. package/.cursor/commands/spinout.md +14 -0
  29. package/.cursor/commands/sunset-review.md +14 -0
  30. package/.cursor/commands/templates-ack.md +14 -0
  31. package/.cursor/commands/test.md +14 -0
  32. package/.cursor/hooks.json +60 -0
  33. package/.cursor/rules/000-session-orchestrator.mdc +8 -0
  34. package/.cursor/rules/010-session-workflow.mdc +9 -1
  35. package/.cursor/rules/020-quality-gates.mdc +1 -1
  36. package/.cursor/rules/030-wave-execution.mdc +1 -1
  37. package/.cursor/rules/050-plan.mdc +2 -2
  38. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  39. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  40. package/.cursor/skills/architecture/SKILL.md +13 -0
  41. package/.cursor/skills/autopilot/SKILL.md +12 -0
  42. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  43. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  44. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  45. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  46. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  47. package/.cursor/skills/daily/SKILL.md +12 -0
  48. package/.cursor/skills/debug/SKILL.md +13 -0
  49. package/.cursor/skills/discovery/SKILL.md +13 -0
  50. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  51. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  52. package/.cursor/skills/domain-model/SKILL.md +13 -0
  53. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  54. package/.cursor/skills/eli5/SKILL.md +13 -0
  55. package/.cursor/skills/eval/SKILL.md +12 -0
  56. package/.cursor/skills/evolve/SKILL.md +13 -0
  57. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  58. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  60. package/.cursor/skills/grill/SKILL.md +13 -0
  61. package/.cursor/skills/hook-development/SKILL.md +13 -0
  62. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  63. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  64. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  65. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  66. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  67. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  68. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  69. package/.cursor/skills/plan/SKILL.md +13 -0
  70. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  71. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  72. package/.cursor/skills/reconcile/SKILL.md +12 -0
  73. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  74. package/.cursor/skills/session-end/SKILL.md +13 -0
  75. package/.cursor/skills/session-plan/SKILL.md +13 -0
  76. package/.cursor/skills/session-start/SKILL.md +13 -0
  77. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  78. package/.cursor/skills/spinout/SKILL.md +12 -0
  79. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  80. package/.cursor/skills/test-runner/SKILL.md +13 -0
  81. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  82. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  83. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  84. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  85. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  86. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  87. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  88. package/.mcp.json +4 -1
  89. package/CHANGELOG.md +168 -0
  90. package/README.md +18 -15
  91. package/agents/AGENTS.md +23 -4
  92. package/agents/code-implementer.md +2 -1
  93. package/agents/db-specialist.md +2 -1
  94. package/agents/docs-writer.md +3 -1
  95. package/agents/eval-judge.md +1 -1
  96. package/agents/session-reviewer.md +7 -1
  97. package/agents/test-writer.md +2 -1
  98. package/agents/ui-developer.md +2 -1
  99. package/commands/bootstrap.md +2 -2
  100. package/commands/close.md +3 -1
  101. package/commands/go.md +1 -1
  102. package/commands/journey-audit.md +43 -0
  103. package/docs/USER-GUIDE.md +2 -2
  104. package/docs/ci-setup.md +14 -0
  105. package/docs/codex-setup.md +64 -0
  106. package/docs/components.md +6 -6
  107. package/docs/cursor-setup.md +26 -47
  108. package/docs/events-schema.md +76 -4
  109. package/docs/github-mirror-protection.md +197 -0
  110. package/docs/pi-setup.md +2 -0
  111. package/docs/rule-authoring.md +3 -1
  112. package/docs/scope-collision-guard.md +49 -2
  113. package/docs/session-config-reference.md +26 -4
  114. package/docs/session-config-template.md +4 -3
  115. package/docs/telemetry.md +22 -0
  116. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  117. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  118. package/hooks/enforce-scope.mjs +64 -0
  119. package/hooks/hooks-codex.json +1 -1
  120. package/hooks/hooks-cursor.json +201 -20
  121. package/hooks/hooks-pi.json +1 -1
  122. package/hooks/hooks.json +2 -2
  123. package/hooks/on-session-end.mjs +211 -10
  124. package/hooks/on-session-start.mjs +214 -11
  125. package/hooks/on-stop.mjs +48 -9
  126. package/hooks/post-subagent-discovery-validator.mjs +34 -3
  127. package/hooks/post-tool-batch-wave-signal.mjs +11 -2
  128. package/hooks/pre-bash-issue-budget.mjs +117 -4
  129. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  130. package/hooks/pre-bash-staging-fence.mjs +4 -0
  131. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  132. package/hooks/skill-invocation-telemetry.mjs +21 -10
  133. package/monitors/monitors.json +6 -0
  134. package/package.json +1 -1
  135. package/pi/prompts/journey-audit.md +12 -0
  136. package/rules/_index.md +9 -1
  137. package/rules/always-on/ask-via-tool.md +62 -0
  138. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  139. package/rules/always-on/build-value.md +47 -0
  140. package/rules/always-on/cross-session-messaging.md +59 -0
  141. package/rules/always-on/loop-and-monitor.md +221 -0
  142. package/rules/always-on/parallel-sessions.md +142 -12
  143. package/rules/always-on/receiving-review.md +108 -0
  144. package/rules/always-on/test-value.md +40 -0
  145. package/rules/always-on/verification-before-completion.md +77 -0
  146. package/scripts/archive-closed-prds.mjs +258 -18
  147. package/scripts/autopilot.mjs +5 -0
  148. package/scripts/backfill-evidence-digest.mjs +376 -0
  149. package/scripts/cursor-install.mjs +89 -48
  150. package/scripts/export-hw-learnings.mjs +143 -2
  151. package/scripts/express-path.mjs +299 -0
  152. package/scripts/generate-cursor-adapter.mjs +253 -0
  153. package/scripts/github-protection-audit.mjs +358 -0
  154. package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
  155. package/scripts/lib/build-live-signals.mjs +24 -5
  156. package/scripts/lib/ci-status-banner.mjs +158 -11
  157. package/scripts/lib/command-blocker.mjs +70 -0
  158. package/scripts/lib/config/reconcile.mjs +79 -4
  159. package/scripts/lib/config/section-extractor.mjs +235 -36
  160. package/scripts/lib/config-schema.mjs +9 -1
  161. package/scripts/lib/config.mjs +57 -6
  162. package/scripts/lib/convergence-monitor.mjs +13 -2
  163. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  164. package/scripts/lib/dispatcher/cli.mjs +2 -2
  165. package/scripts/lib/express-path.mjs +327 -0
  166. package/scripts/lib/file-lock.mjs +22 -4
  167. package/scripts/lib/gates/gate-full.mjs +81 -8
  168. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  169. package/scripts/lib/git-config-drift.mjs +134 -5
  170. package/scripts/lib/host-identity.mjs +247 -2
  171. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  172. package/scripts/lib/issue-budget.mjs +229 -30
  173. package/scripts/lib/learnings/io.mjs +55 -10
  174. package/scripts/lib/learnings/schema.mjs +95 -28
  175. package/scripts/lib/lock-reaper.mjs +7 -1
  176. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  177. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  178. package/scripts/lib/memory-banner.mjs +5 -2
  179. package/scripts/lib/memory-paths.mjs +15 -6
  180. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  181. package/scripts/lib/platform.mjs +72 -9
  182. package/scripts/lib/plugin-root.mjs +143 -19
  183. package/scripts/lib/project-hygiene.mjs +43 -3
  184. package/scripts/lib/quality-gate.mjs +271 -13
  185. package/scripts/lib/reconcile/emitter.mjs +87 -19
  186. package/scripts/lib/reconcile/engine.mjs +281 -13
  187. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  188. package/scripts/lib/reconcile/renderer.mjs +148 -3
  189. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  190. package/scripts/lib/reconcile/writer.mjs +415 -84
  191. package/scripts/lib/rule-loader.mjs +37 -2
  192. package/scripts/lib/rules-sync.mjs +51 -8
  193. package/scripts/lib/scope-gate.mjs +90 -0
  194. package/scripts/lib/session-close-backfill.mjs +369 -28
  195. package/scripts/lib/session-discovery.mjs +13 -3
  196. package/scripts/lib/session-end/phase-skip.mjs +37 -4
  197. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  198. package/scripts/lib/session-id.mjs +30 -14
  199. package/scripts/lib/session-identity/own-session.mjs +159 -0
  200. package/scripts/lib/session-lock.mjs +85 -30
  201. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  202. package/scripts/lib/session-schema/validator.mjs +40 -0
  203. package/scripts/lib/session-start-probes.mjs +608 -0
  204. package/scripts/lib/session-transition.mjs +277 -0
  205. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  206. package/scripts/lib/spiral-carryover.mjs +90 -9
  207. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  208. package/scripts/lib/state-md/mission-status.mjs +350 -52
  209. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  210. package/scripts/lib/state-md.mjs +12 -2
  211. package/scripts/lib/telemetry/sync.mjs +46 -8
  212. package/scripts/lib/validate/check-agents.mjs +66 -0
  213. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  214. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  215. package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
  216. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  217. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  218. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  219. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  220. package/scripts/lib/validate/check-unwired-features.mjs +340 -32
  221. package/scripts/lib/validate/repo-files.mjs +275 -0
  222. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  223. package/scripts/lib/vault-mirror/process.mjs +99 -43
  224. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  225. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  226. package/scripts/lib/vault-status/board-writer.mjs +211 -10
  227. package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
  228. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  229. package/scripts/lib/wave-transcript-tail.mjs +869 -0
  230. package/scripts/materialize-wave-scope.mjs +209 -12
  231. package/scripts/mcp-server.sh +11 -2
  232. package/scripts/parse-config.mjs +65 -0
  233. package/scripts/token-audit.sh +9 -2
  234. package/scripts/validate-plugin.mjs +3 -0
  235. package/scripts/validate-wave-scope.mjs +67 -0
  236. package/scripts/vault-mirror.mjs +203 -34
  237. package/skills/_shared/monitor-patterns.md +31 -5
  238. package/skills/_shared/parallel-aware-auq.md +1 -1
  239. package/skills/_shared/parallel-aware-preamble.md +4 -2
  240. package/skills/_shared/platform-tools.md +11 -5
  241. package/skills/_shared/state-ownership.md +29 -2
  242. package/skills/autopilot/SKILL.md +5 -1
  243. package/skills/bootstrap/SKILL.md +3 -3
  244. package/skills/bootstrap/_shared-template.md +18 -10
  245. package/skills/bootstrap/deep-template.md +10 -6
  246. package/skills/bootstrap/fast-template.md +15 -8
  247. package/skills/bootstrap/standard-template.md +10 -6
  248. package/skills/claude-md-drift-check/checker.mjs +39 -11
  249. package/skills/dispatcher/SKILL.md +1 -1
  250. package/skills/journey-audit/SKILL.md +269 -0
  251. package/skills/peekaboo-driver/SKILL.md +15 -3
  252. package/skills/persona-panel/SKILL.md +1 -1
  253. package/skills/reconcile/SKILL.md +41 -1
  254. package/skills/session-end/SKILL.md +17 -4
  255. package/skills/session-end/metrics-collection.md +7 -4
  256. package/skills/session-end/phase-3-6-tail.md +11 -3
  257. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  258. package/skills/session-plan/SKILL.md +6 -1
  259. package/skills/session-plan/wave-template.md +1 -0
  260. package/skills/session-start/SKILL.md +30 -16
  261. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  262. package/skills/session-start/phase-8-5-express-path.md +77 -12
  263. package/skills/vault-sync/validator.mjs +31 -0
  264. package/skills/wave-executor/SKILL.md +4 -2
  265. package/skills/wave-executor/circuit-breaker.md +34 -9
  266. package/skills/wave-executor/wave-loop.md +102 -19
  267. package/templates/_shared/journey-manifest.md +110 -0
  268. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -43,6 +43,7 @@ import {
43
43
  formatBlockReason,
44
44
  resolveIssueBudgetSessionId,
45
45
  } from './issue-budget.mjs';
46
+ import { resolveProjectDir } from './platform.mjs';
46
47
  import { resolveRepoSpec } from './vcs-repo-spec.mjs';
47
48
 
48
49
  /**
@@ -83,6 +84,27 @@ function isIssueCreateArgv(cmd, args) {
83
84
  return args[0] === 'issue' && (args[1] === 'create' || args[1] === 'new');
84
85
  }
85
86
 
87
+ /**
88
+ * Under vitest, a `repoRoot` that resolves to the ambient working copy is almost
89
+ * always the default-parameter leak (#1105): the test meant to pass a synthetic
90
+ * `mkdtemp` root but did not, and the issue-budget ledger would be charged in
91
+ * the operator's real tree. Production keeps the cwd default unchanged; this
92
+ * gate only fires when `process.env.VITEST` is set.
93
+ *
94
+ * @param {string} budgetRoot — ledger root about to be passed to `chargeIssueBudget`
95
+ * @returns {string|null} refusal message, or null when charging may proceed
96
+ */
97
+ function refuseAmbientLedgerUnderVitest(budgetRoot) {
98
+ if (!process.env.VITEST) return null;
99
+ if (path.resolve(budgetRoot) === path.resolve(process.cwd())) {
100
+ return (
101
+ 'spiral-carryover: refusing to charge the issue-budget ledger at the ambient ' +
102
+ 'repo root under VITEST — pass an explicit synthetic repoRoot'
103
+ );
104
+ }
105
+ return null;
106
+ }
107
+
86
108
  /**
87
109
  * Run a CLI command and return { ok, stdout, stderr }. Never throws.
88
110
  *
@@ -97,14 +119,46 @@ function isIssueCreateArgv(cmd, args) {
97
119
  * exactly the session-end promise at SKILL.md:319 / :1113 being preserved. It
98
120
  * bites for any FUTURE non-exempt producer added to this module.
99
121
  *
122
+ * REPO BINDING (#1058 follow-on). The ledger root is the `repoRoot` the CALLER
123
+ * named — the same value that already decides the `-R` host-pinning spec a few
124
+ * lines down. Before this parameter existed, `runCli` re-derived it from
125
+ * `process.env.CLAUDE_PROJECT_DIR || process.cwd()`, so one call could file an
126
+ * issue into repo A (via `-R`) while charging repo B's budget ledger. Two
127
+ * answers to "which repo" inside one call path is the defect; the caller's
128
+ * answer is the authoritative one.
129
+ *
130
+ * That split brain was also a live test leak, measured 2026-08-23: a sandboxed
131
+ * `CLAUDE_PROJECT_DIR=$(mktemp -d) npx vitest run tests/lib/spiral-carryover.test.mjs`
132
+ * left `{"sessionId":"1c2e5507-…","count":0,"exempt":15}` in the sandbox — 15
133
+ * bookings per run, carrying the REAL session id, which under a plain
134
+ * `npm test` land in this repo's live issue-budget ledger (since #1141 the
135
+ * per-session file `.orchestrator/runtime/issue-budget/<hash>.json`; the path
136
+ * is owned by `budgetStateRel` in `scripts/lib/issue-budget.mjs` — no caller,
137
+ * including this one, spells it out).
138
+ *
139
+ * The `repoRoot`-less fallback is `resolveProjectDir()` from `platform.mjs` —
140
+ * the SAME resolver `hooks/pre-bash-issue-budget.mjs` uses, so both producers
141
+ * of this ledger agree on which file they are charging. It strictly supersedes
142
+ * the hand-rolled expression it replaces: env fast-path first (including the
143
+ * Codex/Cursor/pi variants), then a walk-up for the instruction file — `CLAUDE.md`
144
+ * on Claude Code, `AGENTS.md` on Codex CLI (transparent aliases) — or `.git`, then cwd.
145
+ *
100
146
  * @param {string} cmd
101
147
  * @param {string[]} args
148
+ * @param {string} [repoRoot] — repo whose issue-budget ledger this call is
149
+ * charged against. Falls back to `resolveProjectDir()` when absent.
102
150
  * @returns {{ ok: boolean, stdout: string, stderr: string, budgetBlocked?: boolean }}
103
151
  */
104
- function runCli(cmd, args) {
152
+ function runCli(cmd, args, repoRoot) {
105
153
  if (isIssueCreateArgv(cmd, args)) {
106
154
  try {
107
- const repoRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
155
+ const budgetRoot = (typeof repoRoot === 'string' && repoRoot.trim())
156
+ ? repoRoot
157
+ : (resolveProjectDir() || process.cwd());
158
+ const vitestLedgerRefusal = refuseAmbientLedgerUnderVitest(budgetRoot);
159
+ if (vitestLedgerRefusal) {
160
+ return { ok: false, stdout: '', stderr: vitestLedgerRefusal, budgetBlocked: true };
161
+ }
108
162
  // The harness exports CLAUDE_CODE_SESSION_ID (measured 2026-08-21: it is
109
163
  // present in the Bash tool environment this module runs in). There is no
110
164
  // CLAUDE_SESSION_ID — reading that name made this whole block dead code
@@ -113,16 +167,43 @@ function runCli(cmd, args) {
113
167
  let currentSession = null;
114
168
  if (typeof nativeRawId === 'string' && nativeRawId.length > 0) {
115
169
  try {
116
- currentSession = JSON.parse(
117
- readFileSync(path.join(repoRoot, '.orchestrator', 'current-session.json'), 'utf8'),
170
+ const parsed = JSON.parse(
171
+ readFileSync(path.join(budgetRoot, '.orchestrator', 'current-session.json'), 'utf8'),
118
172
  );
173
+ // SESSION BINDING (#1058). `current-session.json` is repo-global —
174
+ // every session sharing this working copy writes the same path — so
175
+ // the file in hand may describe a PEER session, not us.
176
+ //
177
+ // The binding itself is not new: `resolveIssueBudgetSessionId`
178
+ // already requires `currentSession.session_id === nativeRawId` before
179
+ // it will adopt the file's semantic label, and falls back to the raw
180
+ // env id otherwise. What was missing is that the mismatch was
181
+ // INVISIBLE — the comparison lives one module over, so a foreign file
182
+ // was discarded with no trace, which is the same error class as using
183
+ // it silently. Deciding it HERE makes the outcome identical and the
184
+ // reason audible; it deliberately does not restate the wider
185
+ // multi-id-space classifier `scripts/lib/quality-gate.mjs` needs
186
+ // (that reader has no raw env id guaranteed in hand — this one does,
187
+ // by the `typeof nativeRawId === 'string'` guard above).
188
+ const foreignId = typeof parsed?.session_id === 'string' && parsed.session_id.length > 0
189
+ ? parsed.session_id
190
+ : null;
191
+ if (foreignId !== null && foreignId !== nativeRawId) {
192
+ process.stderr.write(
193
+ '⚠️ spiral-carryover: .orchestrator/current-session.json belongs to another ' +
194
+ `session (${foreignId}) — using the native session id for issue-budget ` +
195
+ 'accounting. Another session is active in this working copy (PSA-001).\n',
196
+ );
197
+ } else {
198
+ currentSession = parsed;
199
+ }
119
200
  } catch {
120
201
  // Missing or malformed records conservatively retain the native env key.
121
202
  }
122
203
  }
123
204
  const titleIdx = args.indexOf('--title');
124
205
  const verdict = chargeIssueBudget({
125
- repoRoot,
206
+ repoRoot: budgetRoot,
126
207
  sessionId: resolveIssueBudgetSessionId(nativeRawId, currentSession),
127
208
  command: [cmd, ...args].join(' '),
128
209
  title: titleIdx >= 0 ? (args[titleIdx + 1] ?? null) : null,
@@ -211,7 +292,7 @@ async function findExistingLabeledIssue({
211
292
  'number,url,body',
212
293
  ];
213
294
  if (spec) args.push('-R', spec);
214
- const res = runCli('gh', args);
295
+ const res = runCli('gh', args, repoRoot);
215
296
  if (!res.ok) return { exists: false };
216
297
  let arr;
217
298
  try {
@@ -231,7 +312,7 @@ async function findExistingLabeledIssue({
231
312
  // Default: gitlab via glab.
232
313
  const args = ['issue', 'list', '--label', label, '--per-page', '100', '--output', 'json'];
233
314
  if (spec) args.push('-R', spec);
234
- const res = runCli('glab', args);
315
+ const res = runCli('glab', args, repoRoot);
235
316
  if (!res.ok) return { exists: false };
236
317
  let arr;
237
318
  try {
@@ -420,7 +501,7 @@ export async function createSpiralCarryoverIssue({
420
501
  }
421
502
  if (spec) args.push('-R', spec);
422
503
 
423
- const res = runCli(cmd, args);
504
+ const res = runCli(cmd, args, repoRoot);
424
505
  if (!res.ok) {
425
506
  return { created: false, skipped: 'error', error: res.stderr.trim() || 'CLI invocation failed' };
426
507
  }
@@ -609,7 +690,7 @@ export async function createBrokenWindowIssue({
609
690
  }
610
691
  if (spec) args.push('-R', spec);
611
692
 
612
- const res = runCli(cmd, args);
693
+ const res = runCli(cmd, args, repoRoot);
613
694
  if (!res.ok) {
614
695
  return { created: false, skipped: 'error', error: res.stderr.trim() || 'CLI invocation failed' };
615
696
  }
@@ -239,11 +239,20 @@ function extractFrontmatterBlock(text) {
239
239
  *
240
240
  * Scoped to the frontmatter block only (see the "Frontmatter-safe round-trip
241
241
  * guard" comment above `DEFAULT_STATE_MD_SIZE_CEILING_BYTES` for the scoping
242
- * rationale). Content with no parseable frontmatter (`parseStateMd(after) ===
243
- * null`) is treated as SAFE — there is nothing frontmatter-shaped to verify,
244
- * and this keeps non-STATE.md-shaped writes (e.g. arbitrary test fixtures)
245
- * ungated by this check, matching `evaluateSizeCeiling`'s content-agnostic
246
- * posture for that case.
242
+ * rationale).
243
+ *
244
+ * `parseStateMd(after) === null` has TWO causes and they get OPPOSITE verdicts,
245
+ * because collapsing them is what made this guard blind:
246
+ * - NO frontmatter fence in `after` → SAFE. There is nothing frontmatter-shaped
247
+ * to verify, and non-STATE.md-shaped writes (arbitrary test fixtures, plain
248
+ * body text) stay ungated, matching `evaluateSizeCeiling`'s content-agnostic
249
+ * posture for that case.
250
+ * - A fence IS present but its contents do not parse → UNSAFE. A write whose
251
+ * frontmatter this repo's own parser cannot read back is precisely the
252
+ * corruption class this guard exists to refuse; treating it as "nothing to
253
+ * verify" let the single worst input pass unchecked (the 2026-08-24 #1111
254
+ * regression, where one flow item nulled the whole document, sat in exactly
255
+ * this hole).
247
256
  *
248
257
  * Never throws — mirrors the never-throw contract of yaml-parser.mjs.
249
258
  *
@@ -251,12 +260,20 @@ function extractFrontmatterBlock(text) {
251
260
  * @returns {{ unsafe: boolean, reason: string|null }}
252
261
  */
253
262
  export function evaluateFrontmatterSafe(after) {
263
+ const beforeBlock = extractFrontmatterBlock(after);
254
264
  const parsed = parseStateMd(after);
255
265
  if (parsed === null) {
256
- return { unsafe: false, reason: null };
266
+ if (beforeBlock === null) {
267
+ return { unsafe: false, reason: null };
268
+ }
269
+ return {
270
+ unsafe: true,
271
+ reason:
272
+ `frontmatter block is present but parseStateMd could not read it back ` +
273
+ `(${Buffer.byteLength(beforeBlock, 'utf8')}B unparseable)`,
274
+ };
257
275
  }
258
276
  const reserialized = serializeStateMd(parsed);
259
- const beforeBlock = extractFrontmatterBlock(after);
260
277
  const afterBlock = extractFrontmatterBlock(reserialized);
261
278
  if (beforeBlock !== afterBlock) {
262
279
  return {
@@ -320,7 +337,10 @@ function writeFileAtomic(filePath, contents) {
320
337
  * deliberately left unshipped when issue #739 first landed, because the
321
338
  * yaml-parser asymmetry it exists to catch made it false-positive on ordinary
322
339
  * content at the time) — #747 closed that asymmetry in yaml-parser.mjs, so
323
- * the check is safe to enforce. It catches FUTURE serializer/parser drift
340
+ * the check is safe to enforce. A frontmatter fence that is present but does
341
+ * NOT parse back is refused by the same guard (see `evaluateFrontmatterSafe`);
342
+ * only content with no fence at all leaves it inert. It catches FUTURE
343
+ * serializer/parser drift
324
344
  * that reintroduces a non-idempotent frontmatter round-trip — the exact
325
345
  * incident class behind #739 — rather than the symptom (file size) the
326
346
  * size-ceiling guard was limited to. Same non-throw, WARN-then-refuse
@@ -395,6 +415,19 @@ export async function writeStateMd(repoRoot, transformer, opts = {}) {
395
415
  return { written: false, path: statePath, contents: before, reason: 'frontmatter-unsafe' };
396
416
  }
397
417
 
418
+ // A list item the parser cannot represent is dropped on READ (see
419
+ // yaml-parser.mjs `parseBlockValue`), so this write is what makes the drop
420
+ // permanent on disk. Dropping is the right call — the alternatives are a
421
+ // mangled key or a dead no-op — but it must not be SILENT, which is the
422
+ // whole complaint the per-item scoping answers. Name the items, then write
423
+ // anyway: blocking here would re-create the no-op this fix removed.
424
+ const droppedItems = parseStateMd(before)?.warnings;
425
+ if (droppedItems !== undefined) {
426
+ process.stderr.write(
427
+ `⚠ writeStateMd: ${statePath} — dropping ${droppedItems.length} unrepresentable list item(s): ` +
428
+ `${droppedItems.map((w) => `${w.key}[${w.index}] (${w.reason})`).join(', ')}\n`
429
+ );
430
+ }
398
431
  writeFileAtomic(statePath, after);
399
432
  return { written: true, path: statePath, contents: after };
400
433
  },