session-orchestrator 5.2.0 → 5.3.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 (295) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
  9. package/.agents/skills/close/SKILL.md +5 -1
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +5 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/npm-publish/SKILL.md +4 -2
  41. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  42. package/.agents/skills/persona-panel/SKILL.md +5 -1
  43. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  44. package/.agents/skills/plan/SKILL.md +5 -1
  45. package/.agents/skills/plan/agents/openai.yaml +5 -0
  46. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  47. package/.agents/skills/portfolio/SKILL.md +5 -1
  48. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  49. package/.agents/skills/quality-gates/SKILL.md +3 -1
  50. package/.agents/skills/reconcile/SKILL.md +5 -1
  51. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  52. package/.agents/skills/release/SKILL.md +5 -1
  53. package/.agents/skills/release/agents/openai.yaml +5 -0
  54. package/.agents/skills/remote-offload/SKILL.md +3 -1
  55. package/.agents/skills/repo-audit/SKILL.md +5 -1
  56. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  57. package/.agents/skills/session/SKILL.md +21 -0
  58. package/.agents/skills/session/agents/openai.yaml +5 -0
  59. package/.agents/skills/session-end/SKILL.md +3 -1
  60. package/.agents/skills/session-plan/SKILL.md +3 -1
  61. package/.agents/skills/session-start/SKILL.md +3 -1
  62. package/.agents/skills/spinout/SKILL.md +5 -1
  63. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  64. package/.agents/skills/sunset-review/SKILL.md +5 -1
  65. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  66. package/.agents/skills/templates-ack/SKILL.md +21 -0
  67. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  68. package/.agents/skills/test/SKILL.md +5 -1
  69. package/.agents/skills/test/agents/openai.yaml +5 -0
  70. package/.agents/skills/test-runner/SKILL.md +3 -1
  71. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  72. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  73. package/.agents/skills/ux-grill/SKILL.md +5 -1
  74. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  75. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  76. package/.agents/skills/vault-sync/SKILL.md +3 -1
  77. package/.agents/skills/wave-executor/SKILL.md +3 -1
  78. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  79. package/.claude-plugin/marketplace.json +1 -1
  80. package/.claude-plugin/plugin.json +1 -1
  81. package/.codex-plugin/plugin.json +4 -4
  82. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  83. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  84. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  85. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  86. package/.codex-plugin/skills/session/SKILL.md +1 -1
  87. package/.cursor/commands/eval.md +1 -1
  88. package/.cursor/commands/session.md +1 -1
  89. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  90. package/.cursor/rules/050-plan.mdc +1 -1
  91. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  92. package/.cursor/skills/eval/SKILL.md +1 -1
  93. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  94. package/.cursor-plugin/plugin.json +1 -1
  95. package/.orchestrator/policy/blocked-commands.json +12 -3
  96. package/AGENTS.md +3 -2
  97. package/CHANGELOG.md +136 -0
  98. package/README.md +9 -9
  99. package/SECURITY.md +12 -0
  100. package/agents/dialectic-deriver.md +13 -10
  101. package/agents/eval-judge.md +67 -45
  102. package/agents/skill-applied-judge.md +34 -19
  103. package/commands/session.md +7 -3
  104. package/docs/baseline.md +12 -6
  105. package/docs/codex-setup.md +14 -2
  106. package/docs/components.md +7 -5
  107. package/docs/events-schema.md +56 -9
  108. package/docs/rule-authoring.md +58 -6
  109. package/docs/session-config-reference.md +100 -7
  110. package/docs/session-config-template.md +31 -2
  111. package/docs/telemetry.md +2 -0
  112. package/hooks/_lib/hook-import-set.json +85 -8
  113. package/hooks/_lib/subagent-transcript.mjs +582 -31
  114. package/hooks/config-protection.mjs +11 -3
  115. package/hooks/cwd-change-restore.mjs +11 -3
  116. package/hooks/enforce-commands.mjs +70 -23
  117. package/hooks/enforce-scope.mjs +143 -33
  118. package/hooks/hooks-codex.json +1 -1
  119. package/hooks/hooks.json +1 -1
  120. package/hooks/loop-guard.mjs +11 -3
  121. package/hooks/on-session-end.mjs +58 -23
  122. package/hooks/on-session-start.mjs +48 -11
  123. package/hooks/on-stop.mjs +168 -22
  124. package/hooks/operator-steer.mjs +11 -3
  125. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  126. package/hooks/post-bash-write-verify.mjs +3 -2
  127. package/hooks/post-edit-import-probe.mjs +17 -9
  128. package/hooks/post-edit-validate.mjs +13 -5
  129. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  130. package/hooks/post-tool-batch-wave-signal.mjs +200 -38
  131. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  132. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  133. package/hooks/pre-auq-clarity.mjs +15 -2
  134. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  135. package/hooks/pre-bash-issue-budget.mjs +16 -11
  136. package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
  138. package/hooks/pre-bash-staging-fence.mjs +335 -31
  139. package/hooks/pre-bash-templates-first.mjs +19 -14
  140. package/hooks/pre-task-scope-disjoint.mjs +233 -2
  141. package/hooks/subagent-telemetry.mjs +15 -19
  142. package/hooks/wave-scope-commit-guard.mjs +197 -100
  143. package/monitors/monitors.json +1 -1
  144. package/output-styles/wave-summary.md +1 -1
  145. package/package.json +1 -1
  146. package/pi/prompts/eval.md +1 -1
  147. package/pi/prompts/session.md +1 -1
  148. package/rules/README.md +1 -1
  149. package/rules/opt-in-domain/prompt-caching.md +1 -1
  150. package/rules/opt-in-stack/backend-data.md +1 -1
  151. package/rules/opt-in-stack/backend.md +3 -3
  152. package/rules/opt-in-stack/frontend.md +1 -1
  153. package/rules/opt-in-stack/security-web.md +3 -3
  154. package/rules/opt-in-stack/swift.md +1 -1
  155. package/scripts/autopilot.mjs +23 -2
  156. package/scripts/backfill-abandoned-sessions.mjs +117 -15
  157. package/scripts/check-sessions-integrity.mjs +300 -0
  158. package/scripts/dialectic-deriver.mjs +50 -13
  159. package/scripts/emit-session.mjs +75 -29
  160. package/scripts/eval-session.mjs +65 -3
  161. package/scripts/generate-agents-skills.mjs +102 -29
  162. package/scripts/generate-cursor-adapter.mjs +61 -16
  163. package/scripts/lib/agent-status.mjs +2 -31
  164. package/scripts/lib/auq/clarity.mjs +10 -2
  165. package/scripts/lib/auq/parse.mjs +12 -31
  166. package/scripts/lib/auq/schema.mjs +56 -41
  167. package/scripts/lib/auto-dialectic.mjs +304 -15
  168. package/scripts/lib/autopilot/flags.mjs +12 -1
  169. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  170. package/scripts/lib/autopilot/loop.mjs +14 -1
  171. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  172. package/scripts/lib/ci-status-banner.mjs +376 -16
  173. package/scripts/lib/command-blocker.mjs +275 -28
  174. package/scripts/lib/config/dialectic.mjs +12 -3
  175. package/scripts/lib/config/gate.mjs +74 -0
  176. package/scripts/lib/config/reaper.mjs +162 -0
  177. package/scripts/lib/config.mjs +14 -0
  178. package/scripts/lib/convergence-monitor.mjs +74 -11
  179. package/scripts/lib/ecosystem-health.mjs +11 -0
  180. package/scripts/lib/eval/engine.mjs +421 -53
  181. package/scripts/lib/eval/judge.mjs +463 -40
  182. package/scripts/lib/eval/schema.mjs +10 -1
  183. package/scripts/lib/events-rotation.mjs +221 -25
  184. package/scripts/lib/events-schema.mjs +114 -0
  185. package/scripts/lib/events.mjs +524 -5
  186. package/scripts/lib/frontmatter-guard.mjs +21 -10
  187. package/scripts/lib/gates/gate-baseline.mjs +27 -2
  188. package/scripts/lib/gates/gate-full.mjs +28 -3
  189. package/scripts/lib/gates/gate-helpers.mjs +243 -21
  190. package/scripts/lib/gates/gate-incremental.mjs +28 -3
  191. package/scripts/lib/gates/gate-per-file.mjs +27 -2
  192. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  193. package/scripts/lib/instruction-budget-guard.mjs +146 -4
  194. package/scripts/lib/io.mjs +42 -8
  195. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  196. package/scripts/lib/js-mask.mjs +197 -0
  197. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  198. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  199. package/scripts/lib/orphan-reaper.mjs +1588 -0
  200. package/scripts/lib/peer-cards/merger.mjs +48 -10
  201. package/scripts/lib/peer-cards/reader.mjs +78 -2
  202. package/scripts/lib/process-group.mjs +899 -0
  203. package/scripts/lib/quality-gate.mjs +107 -28
  204. package/scripts/lib/reconcile/backlog.mjs +368 -0
  205. package/scripts/lib/reconcile/engine.mjs +55 -188
  206. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  207. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  208. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  209. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  210. package/scripts/lib/rule-loader.mjs +41 -12
  211. package/scripts/lib/scope-echo.mjs +39 -2
  212. package/scripts/lib/scope-gate.mjs +605 -1
  213. package/scripts/lib/session-close-backfill.mjs +33 -6
  214. package/scripts/lib/session-id.mjs +9 -20
  215. package/scripts/lib/session-invocation.mjs +20 -0
  216. package/scripts/lib/session-schema/constants.mjs +30 -2
  217. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  218. package/scripts/lib/session-schema.mjs +8 -3
  219. package/scripts/lib/session-start-probes.mjs +95 -10
  220. package/scripts/lib/sessions-canonical.mjs +23 -0
  221. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  222. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  223. package/scripts/lib/skill-evidence-window.mjs +891 -0
  224. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  225. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  226. package/scripts/lib/skill-judge.mjs +45 -3
  227. package/scripts/lib/tail-window.mjs +56 -0
  228. package/scripts/lib/telemetry/schema.mjs +30 -0
  229. package/scripts/lib/telemetry/sync.mjs +61 -6
  230. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  231. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  232. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  233. package/scripts/lib/user-invocable-skills.mjs +23 -3
  234. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  235. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  236. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  237. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  238. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  239. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  240. package/scripts/lib/validate/check-rules.mjs +7 -5
  241. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  242. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  243. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  244. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  245. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  246. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  247. package/scripts/lib/validate/confidential-names.mjs +10 -0
  248. package/scripts/lib/validate-vendored-rules.mjs +4 -3
  249. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  250. package/scripts/lib/vault-mirror/process.mjs +10 -3
  251. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  252. package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
  253. package/scripts/lib/vault-yaml.mjs +118 -0
  254. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  255. package/scripts/release-session-lock.mjs +305 -0
  256. package/scripts/release.mjs +30 -5
  257. package/scripts/resolve-session-invocation.mjs +59 -0
  258. package/scripts/run-quality-gate.mjs +156 -17
  259. package/scripts/sweep-expired-rules.mjs +14 -3
  260. package/scripts/validate-plugin.mjs +12 -0
  261. package/scripts/validate-wave-scope.mjs +32 -105
  262. package/scripts/vault-mirror.mjs +9 -1
  263. package/skills/_shared/platform-tools.md +23 -11
  264. package/skills/autopilot/SKILL.md +22 -7
  265. package/skills/claude-md-drift-check/SKILL.md +1 -1
  266. package/skills/convergence-monitoring/README.md +8 -1
  267. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  268. package/skills/convergence-monitoring/SKILL.md +15 -6
  269. package/skills/eval/SKILL.md +39 -24
  270. package/skills/eval/rubric-v1.md +1 -0
  271. package/skills/eval/rubric-v2.md +457 -0
  272. package/skills/evolve/SKILL.md +1 -1
  273. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  274. package/skills/gitlab-ops/SKILL.md +3 -2
  275. package/skills/npm-publish/SKILL.md +1 -1
  276. package/skills/reconcile/SKILL.md +11 -0
  277. package/skills/session-end/SKILL.md +13 -16
  278. package/skills/session-end/discovery-scan.md +1 -1
  279. package/skills/session-end/phase-3-6-tail.md +55 -9
  280. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  281. package/skills/session-end/session-metrics-write.md +10 -0
  282. package/skills/session-plan/SKILL.md +17 -5
  283. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  284. package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
  285. package/skills/ux-grill/SKILL.md +1 -1
  286. package/skills/wave-executor/SKILL.md +8 -4
  287. package/skills/wave-executor/circuit-breaker.md +2 -0
  288. package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
  289. package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
  290. package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
  291. package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
  292. package/.cursor/commands/convergence-monitoring.md +0 -13
  293. package/.cursor/commands/npm-publish.md +0 -13
  294. package/pi/prompts/convergence-monitoring.md +0 -11
  295. package/pi/prompts/npm-publish.md +0 -11
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ /** Stdin-only boundary for commands/session.md; never executes task context. */
3
+ import { readFileSync } from 'node:fs';
4
+ import { parseArgs } from 'node:util';
5
+ import { resolveSessionInvocation } from './lib/session-invocation.mjs';
6
+
7
+ const HELP = `Usage: node scripts/resolve-session-invocation.mjs [--json] < arguments.txt
8
+
9
+ Read the complete /session argument text from UTF-8 stdin.
10
+ Resolve the leading housekeeping, feature, deep or ultradeep token; preserve
11
+ the rest as task context. Empty input defaults to deep. An invalid mode emits
12
+ a warning and resolves to deep, as the session command has always specified.
13
+
14
+ --json Emit one JSON object instead of a human-readable summary
15
+ --help Show this help
16
+ --version Show the plugin version
17
+
18
+ Example: node scripts/resolve-session-invocation.mjs --json < arguments.txt
19
+ Write arguments.txt with a file tool; do not interpolate user text into a shell.
20
+ Exit codes: 0 resolved (including fallback), 1 bad CLI arguments, 2 stdin I/O error
21
+ `;
22
+
23
+ let options;
24
+ try {
25
+ options = parseArgs({
26
+ options: { json: { type: 'boolean' }, help: { type: 'boolean' }, version: { type: 'boolean' } },
27
+ allowPositionals: false,
28
+ }).values;
29
+ } catch (error) {
30
+ process.stderr.write(`session-invocation: ${error.message}\n`);
31
+ process.exit(1);
32
+ }
33
+
34
+ if (options.help) {
35
+ process.stdout.write(HELP);
36
+ } else if (options.version) {
37
+ process.stdout.write(
38
+ `${JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version}\n`,
39
+ );
40
+ } else {
41
+ let input;
42
+ try {
43
+ input = readFileSync(0, 'utf8');
44
+ } catch {
45
+ process.stderr.write('session-invocation: could not read UTF-8 stdin\n');
46
+ process.exit(2);
47
+ }
48
+ const resolved = resolveSessionInvocation(input);
49
+ if (resolved.invalidMode !== undefined) {
50
+ process.stderr.write(
51
+ `Invalid session type '${resolved.invalidMode}'. Valid types: housekeeping, feature, deep (alias: ultradeep). Falling back to deep.\n`,
52
+ );
53
+ }
54
+ process.stdout.write(
55
+ options.json
56
+ ? `${JSON.stringify(resolved)}\n`
57
+ : `Session type: ${resolved.sessionType}${resolved.profile ? ` (profile: ${resolved.profile})` : ''}\nTask context: ${resolved.context}\n`,
58
+ );
59
+ }
@@ -23,9 +23,15 @@
23
23
  * 0 — pass (or informational; non-blocking variants always exit 0)
24
24
  * 1 — script error (bad arguments, missing dependencies)
25
25
  * 2 — gate failed (full-gate only: typecheck/test/lint errors)
26
+ * 124 — the gate sub-script exceeded the wall-clock ceiling and its whole
27
+ * process GROUP was killed (SIGTERM→SIGKILL). Same value coreutils
28
+ * `timeout(1)` uses. Applies to EVERY variant, including the otherwise
29
+ * non-blocking ones: a killed gate measured nothing, so reporting it as
30
+ * 0 would be a false green (Epic #1425 A3).
26
31
  *
27
32
  * The gate sub-scripts in scripts/lib/gates/ are NOT reimplemented here; they are
28
- * invoked via child_process.spawn('bash', [path, ...]) with the required env vars.
33
+ * invoked via spawnInGroup() — a detached, process-group-leading shell — with the
34
+ * required env vars.
29
35
  *
30
36
  * References:
31
37
  * scripts/run-quality-gate.sh — original shell orchestrator
@@ -37,12 +43,18 @@
37
43
  import { existsSync, readFileSync, statSync } from 'node:fs';
38
44
  import { join, dirname, resolve } from 'node:path';
39
45
  import { fileURLToPath } from 'node:url';
40
- import { spawnSync } from 'node:child_process';
46
+ import { spawn } from 'node:child_process';
41
47
 
42
48
  import { die, warn } from './lib/common.mjs';
43
49
  import { loadQualityGatesPolicy, resolveCommand } from './lib/quality-gates-policy.mjs';
44
50
  import { emitEvent, sessionAttribution } from './lib/events.mjs';
45
- import { admitSuiteCounts } from './lib/gates/gate-helpers.mjs';
51
+ import {
52
+ admitSuiteCounts,
53
+ publishGateOutcome,
54
+ resolveGateTimeoutMs,
55
+ } from './lib/gates/gate-helpers.mjs';
56
+ import { buildCommandSignature, spawnInGroup } from './lib/process-group.mjs';
57
+ import { readProcessLocalSessionIds } from './lib/session-identity/own-session.mjs';
46
58
  import { findScopeFile } from './lib/scope-gate.mjs';
47
59
 
48
60
  // ---------------------------------------------------------------------------
@@ -62,6 +74,24 @@ const VALID_VARIANTS = ['baseline', 'incremental', 'full-gate', 'per-file'];
62
74
  */
63
75
  const GATE_STDOUT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
64
76
 
77
+ /**
78
+ * Head-room between the ceiling the gate's OWN commands run under
79
+ * (`resolveGateTimeoutMs()`, applied per command inside `runCheck`) and the one
80
+ * this wrapper applies to the gate sub-script as a whole.
81
+ *
82
+ * The inner cap must fire FIRST: it kills exactly the wedged command and still
83
+ * lets the gate print its JSON envelope, name the failing check on stderr and
84
+ * exit 2. This outer cap is the backstop for the case the inner one cannot
85
+ * reach — a gate wedged outside `runCheck`, or a kill ladder that left a
86
+ * survivor holding the pipe open.
87
+ *
88
+ * Named ceiling (BV-004): 60 s. It only has to cover one full SIGTERM→grace→
89
+ * SIGKILL→verify ladder (`DEFAULT_KILL_GRACE_MS` 10 s + 0.5 s verify + 1 s
90
+ * deadline slack ≈ 11.5 s) plus the gate's own JSON write. Revisit if the grace
91
+ * period is ever raised past ~45 s.
92
+ */
93
+ const GATE_OUTER_TIMEOUT_RESERVE_MS = 60_000;
94
+
65
95
  const DEFAULT_TEST_CMD = 'npm test';
66
96
  const DEFAULT_TYPECHECK_CMD = 'npm run typecheck';
67
97
  const DEFAULT_LINT_CMD = 'npm run lint';
@@ -88,7 +118,8 @@ if (argv.includes('-h') || argv.includes('--help')) {
88
118
  'Exit codes:\n' +
89
119
  ' 0 — pass (non-blocking variants always exit 0)\n' +
90
120
  ' 1 — script error (bad arguments, missing dependencies)\n' +
91
- ' 2 — gate failed (full-gate only)\n',
121
+ ' 2 — gate failed (full-gate only)\n' +
122
+ ' 124 — gate timed out; its process group was killed (SIGTERM→SIGKILL)\n',
92
123
  );
93
124
  process.exit(0);
94
125
  }
@@ -355,6 +386,11 @@ if (!existsSync(gatePath)) {
355
386
  die(`Gate script not found: ${gatePath}`);
356
387
  }
357
388
 
389
+ // Resolved BEFORE the spawn (it used to sit beside the telemetry block below):
390
+ // the gate-process ledger the spawn writes is pinned to the same root as the
391
+ // event, for the same reason — see the spawn's `repoRoot` comment.
392
+ const ledgerRoot = resolveLedgerRoot(ledgerRootArg);
393
+
358
394
  // `npm_config_loglevel` is INHERITED by every descendant, and the pre-push hook
359
395
  // invokes this script as `npm run --silent quality-gate` — which sets it to
360
396
  // `silent`. That level then reached the gate's own children: `npm pack
@@ -368,37 +404,141 @@ if (!existsSync(gatePath)) {
368
404
  // independent of how the gate itself was invoked. `--silent` still does its real
369
405
  // job — keeping THIS process's stdout to the single JSON envelope — because the
370
406
  // children's output is captured by `runCheck`, never streamed.
407
+ /**
408
+ * Per-command wall-clock ceiling for PATH B (#1425 A3 / #1432).
409
+ *
410
+ * Precedence, highest first:
411
+ * 1. `SO_GATE_TIMEOUT_MS` — the OPERATOR override. It is inherited by every
412
+ * descendant, so a gate child that re-derives the ceiling gets the same
413
+ * answer; `resolveGateTimeoutMs()` already reads it.
414
+ * 2. Session Config `gate.timeout-path-b-ms` — the COMMITTED default.
415
+ * 3. `DEFAULT_GATE_TIMEOUT_MS` (900 000), via `resolveGateTimeoutMs()`.
416
+ *
417
+ * Published to the gate sub-script as `GATE_TIMEOUT_MS` — a DIFFERENT name from
418
+ * the operator override on purpose: writing the resolved value back into
419
+ * `SO_GATE_TIMEOUT_MS` would make a committed default indistinguishable from an
420
+ * operator decision for every process further down the tree.
421
+ */
422
+ const configuredGateTimeoutMs = (() => {
423
+ const block = configJson !== null && typeof configJson === 'object' ? configJson.gate : null;
424
+ const raw = block && typeof block === 'object' ? block['timeout-path-b-ms'] : undefined;
425
+ return Number.isFinite(raw) && raw > 0 ? raw : null;
426
+ })();
427
+ const operatorTimeoutOverride = (process.env.SO_GATE_TIMEOUT_MS || '').trim() !== '';
428
+ const commandTimeoutMs = operatorTimeoutOverride || configuredGateTimeoutMs === null
429
+ ? resolveGateTimeoutMs()
430
+ : configuredGateTimeoutMs;
431
+
432
+ // Gate-process REGISTER root — the same precedence the telemetry destination
433
+ // uses below (`--ledger-root` > project-dir env > cwd), so a sandboxed test that
434
+ // only sets CLAUDE_PROJECT_DIR never writes register lines into the checkout.
435
+ const gateLedgerRoot =
436
+ ledgerRoot ?? process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_PROJECT_DIR ?? repoRoot;
437
+
371
438
  const env = {
372
439
  ...process.env,
373
440
  npm_config_loglevel: 'notice',
374
441
  TYPECHECK_CMD,
375
442
  TEST_CMD,
376
443
  LINT_CMD,
444
+ GATE_TIMEOUT_MS: String(commandTimeoutMs),
445
+ // Same resolution the wrapper uses for its OWN spawn (`--ledger-root` > repo
446
+ // root): the gate sub-scripts pass it to runCheck() so every register line of
447
+ // one gate run lands in ONE ledger (#1425 A4, W5 fix-pass).
448
+ GATE_LEDGER_ROOT: gateLedgerRoot,
377
449
  FILES: files,
378
450
  SESSION_START_REF: sessionStartRef,
379
451
  };
380
452
 
453
+ /**
454
+ * POSIX single-quote one argument for the shell `spawnInGroup` runs the command
455
+ * through. The gate path is derived from `import.meta.url`, so it carries
456
+ * whatever the checkout path carries — a space in it must not split the command.
457
+ *
458
+ * @param {string} value
459
+ * @returns {string}
460
+ */
461
+ function shellQuote(value) {
462
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
463
+ }
464
+
465
+ // The gate sub-script runs as the LEADER OF ITS OWN PROCESS GROUP under a
466
+ // wall-clock ceiling (Epic #1425 A3). Before this, `spawnSync('node', [gatePath])`
467
+ // had NO timeout at all and no group semantics: a wedged `tsgo` or vitest worker
468
+ // two levels down was reparented to PPID 1 and kept its memory (2026-09-20: four
469
+ // such orphans, up to 8.0 GB RSS each, host at 13 % free).
470
+ //
381
471
  // stdout is PIPED (not inherited) so the suite counts the gate already computed
382
472
  // can be lifted straight off its JSON envelope into telemetry (#954) instead of
383
473
  // travelling as prose through the STATE.md header. The envelope is re-emitted
384
474
  // verbatim below, so the stdout contract is unchanged — a gate sub-script writes
385
475
  // exactly one JSON line at the very end (its own child commands are captured by
386
- // `runCheck`), so nothing streamed before and nothing streams now. stderr stays
387
- // inherited, keeping warnings live.
388
- const result = spawnSync('node', [gatePath], {
476
+ // `runCheck`), so nothing streamed before and nothing streams now.
477
+ //
478
+ // stderr stays INHERITED, and that is why `spawnFn` is overridden here rather
479
+ // than left at its default: `spawnInGroup` merges stdout and stderr into one
480
+ // capture, which would interleave the gate's failure disclosure (hundreds of
481
+ // lines, #1149) into the single JSON document every consumer parses off stdout.
482
+ // Handing it a child with `stderr: 'inherit'` leaves `child.stderr` null, the
483
+ // module's own `child.stderr?.on(…)` a no-op, and the gate's warnings live on
484
+ // the operator's terminal exactly as before.
485
+ const gateCommand = `node ${shellQuote(gatePath)}`;
486
+ // Derived from the SAME resolved per-command ceiling published as
487
+ // `GATE_TIMEOUT_MS` above — not from `resolveGateTimeoutMs()` a second time.
488
+ // The outer cap must stay strictly ABOVE the inner one so the inner kill fires
489
+ // first and the gate can still print its envelope; re-deriving here would leave
490
+ // the outer cap at the 900 000 default while a committed
491
+ // `gate.timeout-path-b-ms` above it made the inner cap the later of the two.
492
+ const gateTimeoutMs = commandTimeoutMs + GATE_OUTER_TIMEOUT_RESERVE_MS;
493
+ const result = await spawnInGroup(gateCommand, {
494
+ cwd: repoRoot,
389
495
  env,
390
- stdio: ['inherit', 'pipe', 'inherit'],
391
- encoding: 'utf8',
392
- maxBuffer: GATE_STDOUT_MAX_BUFFER_BYTES,
496
+ timeoutMs: gateTimeoutMs,
497
+ maxOutputBytes: GATE_STDOUT_MAX_BUFFER_BYTES,
498
+ // The ledger is what the orphan reaper (#1425 B) reads to tell its OWN gate
499
+ // processes from every other `node` on the host, so it is pinned to the same
500
+ // root the telemetry is — under the pre-push hook the tree the gate runs in
501
+ // is deleted seconds later, and the record with it.
502
+ repoRoot: gateLedgerRoot,
503
+ commandSignature: buildCommandSignature(gateCommand),
504
+ // The OWNER the ledger records, from the PROCESS-LOCAL witness only — the same
505
+ // rule `scripts/lib/quality-gate.mjs` follows for the gate commands it spawns
506
+ // (`.claude/rules/identity-and-locks.md`: rank witnesses, never union them).
507
+ // `.orchestrator/session.lock` is deliberately NOT consulted: it is a
508
+ // repo-GLOBAL artefact any session in this working copy may hold, so reading
509
+ // it would stamp a PEER's id onto our own gate process and point the orphan
510
+ // reaper (#1425 B) at the wrong owner. No witness → `null`, which the ledger
511
+ // records as "owner unknown" — the state every record carried before this.
512
+ sessionId: [...readProcessLocalSessionIds({ env: process.env, hookInput: null })][0] ?? null,
513
+ // stdin is IGNORED, not inherited. A gate sub-script reads no stdin, but a
514
+ // DETACHED child is in its own process group and is therefore not the
515
+ // terminal's foreground group: the first read from an inherited TTY earns it
516
+ // SIGTTIN, which stops the whole group until the outer ceiling kills it. An
517
+ // ignored stdin turns that hang into an immediate EOF.
518
+ spawnFn: (command, options) => spawn(command, { ...options, stdio: ['ignore', 'pipe', 'inherit'] }),
393
519
  });
394
520
 
395
- const gateStdout = typeof result.stdout === 'string' ? result.stdout : '';
396
- if (gateStdout) process.stdout.write(gateStdout);
397
-
398
- if (result.error && typeof result.status !== 'number') {
399
- die(`Failed to run gate script: ${result.error.message}`);
521
+ // `pid: -1` is `spawnInGroup`'s spawn-failure channel (it never rejects).
522
+ if (result.pid === -1) {
523
+ die(`Failed to run gate script: ${result.fullOutput.trim()}`);
400
524
  }
401
525
 
526
+ // On timeout the child was KILLED before it could write its envelope, so its
527
+ // capture is at best a partial JSON document. Publishing that would hand every
528
+ // stdout consumer a parse error where a named failure belongs; publishing the
529
+ // partial text AND an envelope would break the one-document contract. So the
530
+ // capture goes to stderr, where the operator can still read it, and stdout
531
+ // carries a complete `gate-timeout` envelope instead.
532
+ //
533
+ // The DECISION lives in `publishGateOutcome` (gate-helpers.mjs) and only the
534
+ // WRITES live here: this branch needs a real 16-minute gate to reach, so while
535
+ // the decision was inline it was pinned by nothing.
536
+ const outcome = publishGateOutcome({ result, variant, timeoutMs: gateTimeoutMs });
537
+ const gateStdout = result.timedOut ? '' : result.fullOutput;
538
+ if (outcome.stderr) process.stderr.write(outcome.stderr);
539
+ if (outcome.stdout) process.stdout.write(outcome.stdout);
540
+ for (const line of outcome.warnings) warn(line);
541
+
402
542
  // Quality-gate telemetry — one canonical event per gate run via emitEvent
403
543
  // (single emission path). `sessionAttribution` is the shared helper in
404
544
  // events.mjs (#941); this CLI wrapper runs against the CWD `repoRoot`, so the
@@ -433,8 +573,7 @@ if (result.error && typeof result.status !== 'number') {
433
573
  //
434
574
  // Best-effort: a telemetry failure must NEVER alter the gate's authoritative
435
575
  // exit code — which is why the counts parse also lives inside this try.
436
- const exitCode = result.status ?? 1;
437
- const ledgerRoot = resolveLedgerRoot(ledgerRootArg);
576
+ const exitCode = outcome.exitCode;
438
577
  try {
439
578
  const counts = suiteCountsFromGateStdout(gateStdout);
440
579
  // The names behind `counts.failed`. Absent, never `[]` — see
@@ -115,15 +115,25 @@ export function parseArgs(argv) {
115
115
  return args;
116
116
  }
117
117
 
118
- /** Roll the plan up into the summary both output modes render. */
119
- function summarize(plan, args, applied) {
118
+ /**
119
+ * Roll the plan up into the summary both output modes render.
120
+ *
121
+ * `header_raises` is additive (#1388 P7): a header-raise is an
122
+ * `action: 'rewrite'` plan carrying `reason: 'header-raise'`, so it was counted
123
+ * inside `rewrites` and invisible on the human stdout line although `--json`
124
+ * always carried `plans[].reason`. It is a SUBSET of `rewrites`, never a
125
+ * sibling category — the two are deliberately not disjoint.
126
+ */
127
+ export function summarize(plan, args, applied) {
120
128
  const counts = { rewrite: 0, delete: 0, keep: 0 };
121
129
  for (const p of plan.plans) counts[p.action] = (counts[p.action] ?? 0) + 1;
130
+ const headerRaises = plan.plans.filter((p) => p.reason === 'header-raise').length;
122
131
  return {
123
132
  dry_run: args.dryRun,
124
133
  grace_days: args.graceDays,
125
134
  files_scanned: plan.plans.length,
126
135
  rewrites: counts.rewrite,
136
+ header_raises: headerRaises,
127
137
  deletes: counts.delete,
128
138
  keeps: counts.keep,
129
139
  expired_entries: plan.plans.reduce((n, p) => n + p.expiredPairIds.length, 0),
@@ -163,7 +173,8 @@ async function main(argv) {
163
173
  else {
164
174
  process.stdout.write(
165
175
  `sweep-expired-rules: ${args.dryRun ? 'dry-run' : 'applied'} — ` +
166
- `${summary.files_scanned} generated rule file(s), ${summary.rewrites} rewrite(s), ` +
176
+ `${summary.files_scanned} generated rule file(s), ${summary.rewrites} rewrite(s) ` +
177
+ `(${summary.header_raises} header-raise(s)), ` +
167
178
  `${summary.deletes} delete(s), ${summary.expired_entries} expired entr(ies), ` +
168
179
  `${summary.unresolved_pairs} unresolved pair(s), ${summary.skipped.length} skipped, ` +
169
180
  `${summary.malformed_lines} malformed learnings line(s)\n`,
@@ -433,6 +433,18 @@ if (runCheck('check-dead-bridge.mjs') !== 0) checkFailed = 1;
433
433
  process.stdout.write('\n');
434
434
  if (runCheck('check-entry-guard.mjs') !== 0) checkFailed = 1;
435
435
 
436
+ // BLOCKING (#1422), the complement of the check above: `check-entry-guard`
437
+ // judges the FORM of a guard that already exists, so a registered hook with NO
438
+ // guard at all passes it trivially — the #1393 regression class exactly. This
439
+ // one asks the other half of the question over the union of the four platform
440
+ // manifests: does every registered hook run its entry only under
441
+ // `isMainModule(import.meta.url)` / `invokedAsScript()`, and does no profile
442
+ // gate sit at module top level where its `process.exit(0)` kills a mere
443
+ // importer. Blocking honestly: 27/27 registered hooks were guarded at the
444
+ // moment it landed, because #1393 drained the backlog first.
445
+ process.stdout.write('\n');
446
+ if (runCheck('check-hook-entry-guards.mjs') !== 0) checkFailed = 1;
447
+
436
448
  // FAIL-capable, unlike the WARN-only censuses below: R2+R4 were measured at
437
449
  // 1 hit / 1 TP / 0 FP each against the full test corpus, so this check cannot be
438
450
  // red on arrival for anything but a real instance of the class. The alternatives
@@ -69,7 +69,6 @@
69
69
  * 2 — I/O error (file not found, unreadable stdin, unreadable sidecar file)
70
70
  */
71
71
 
72
- import path from 'node:path';
73
72
  import { readFileSync, existsSync, statSync } from 'node:fs';
74
73
  import { execFileSync } from 'node:child_process';
75
74
  import { warn } from './lib/common.mjs';
@@ -86,6 +85,17 @@ import {
86
85
  // which roles are allowed to grant zero paths.
87
86
  isReadOnlyWaveRole,
88
87
  READ_ONLY_WAVE_ROLES,
88
+ // #1398 cond. 4 / #1405 / #1406 — THE grading predicate for an absolute Gate 5b
89
+ // grant. Lives in scope-gate.mjs so this CLI and `hooks/enforce-scope.mjs`
90
+ // grade one and the same way (BV-003): before the move, only this CLI could
91
+ // grade at all, so the hook honoured grants the CLI would have refused and a
92
+ // manifest that skipped the CLI was never graded.
93
+ gradeScopeEntry,
94
+ // #1398 cond. 4 — the `resolve` injection the predicate above takes. Moved
95
+ // into scope-gate.mjs (from a private copy here) once the hot-path cost was
96
+ // measured rather than assumed, so this CLI and `hooks/enforce-scope.mjs` pass
97
+ // the SAME resolver and can no longer reach different verdicts for one grant.
98
+ canonicalizeGrantPrefix,
89
99
  // Aliased: `expandTestSiblings` is ALSO the name of the pre-existing
90
100
  // boolean parameter threaded through validate()/assertSubsetOrDie for the
91
101
  // #970 flag. Aliasing the import avoids shadowing that parameter rather than
@@ -338,79 +348,6 @@ function validateSessionBinding(obj, errors, warnings) {
338
348
  }
339
349
  }
340
350
 
341
- /**
342
- * Literal filesystem-root forms — POSIX "/" and the Windows equivalents "\"
343
- * and a bare drive root ("C:\", "C:\\", ...). Checked independently of
344
- * `path.isAbsolute()` because that primitive is platform-native: on a POSIX
345
- * host (this repo's dev/CI hosts) it never reports `C:\` as absolute, so a
346
- * Windows-literal-root entry would otherwise slip past every check below.
347
- * @type {ReadonlySet<string>}
348
- */
349
- const FILESYSTEM_ROOT_LITERALS = new Set(['/', '\\']);
350
- const WINDOWS_DRIVE_ROOT_RE = /^[A-Za-z]:\\+$/;
351
-
352
- /**
353
- * @param {string} entry
354
- * @returns {boolean}
355
- */
356
- function isFilesystemRootLiteral(entry) {
357
- return FILESYSTEM_ROOT_LITERALS.has(entry) || WINDOWS_DRIVE_ROOT_RE.test(entry);
358
- }
359
-
360
- /**
361
- * Well-known top-level system/home directories. A FIXED DENYLIST (not a
362
- * segment-count threshold): the #792 legitimate grant
363
- * (`/private/tmp/<session>/scratchpad/**`) is itself a single-segment-deep
364
- * grant under an unusual root ("private"), so any segment-count heuristic
365
- * tight enough to catch `/etc/**` risks catching that too, or must be tuned
366
- * loosely enough to leave a gap. A fixed list of the directories a
367
- * hallucinated/mis-copied wave-scope entry could plausibly land on is
368
- * predictable, auditable in a one-line diff, and does not touch legitimate
369
- * deep grants under any other root.
370
- * @type {ReadonlySet<string>}
371
- */
372
- const DENIED_ABSOLUTE_TOP_SEGMENTS = new Set([
373
- 'etc',
374
- 'Users',
375
- 'home',
376
- 'root',
377
- 'bin',
378
- 'sbin',
379
- 'usr',
380
- 'System',
381
- 'var',
382
- 'boot',
383
- 'dev',
384
- 'proc',
385
- 'sys',
386
- 'Library',
387
- 'Applications',
388
- 'Windows',
389
- ]);
390
-
391
- /**
392
- * The top-level path segment of an absolute POSIX-style entry (the segment
393
- * immediately after the leading "/"), if it is on the denylist above.
394
- * @param {string} entry
395
- * @returns {string|null}
396
- */
397
- function deniedTopSegment(entry) {
398
- const first = entry.split('/').filter(Boolean)[0];
399
- return first && DENIED_ABSOLUTE_TOP_SEGMENTS.has(first) ? first : null;
400
- }
401
-
402
- /**
403
- * Does this entry contain a glob wildcard? Mirrors this codebase's own glob
404
- * convention (`isGlobScopeEntry` in scripts/lib/scope-gate.mjs / #796): `*`
405
- * is the sole wildcard metachar used in allowedPaths/fileScope entries
406
- * throughout this repo (no `?`/`[]`/`{}` glob syntax is supported or tested
407
- * anywhere else in scope-gate.mjs or enforce-scope.mjs).
408
- * @param {string} entry
409
- * @returns {boolean}
410
- */
411
- function hasWildcard(entry) {
412
- return entry.includes('*');
413
- }
414
351
 
415
352
  /**
416
353
  * Validate allowedPaths array: must exist, be an array of non-empty strings,
@@ -463,37 +400,27 @@ function validateAllowedPaths(obj, errors, warnings) {
463
400
  errors.push('allowedPaths contains empty string');
464
401
  continue;
465
402
  }
466
- // #870: an explicit absolute entry is a SANCTIONED out-of-repo grant — mirrors
467
- // hooks/enforce-scope.mjs Gate 5b (matchesAbsoluteAllowlist), which honours ANY
468
- // syntactically-absolute allowedPaths entry (path.isAbsolute) against the
469
- // realpath-resolved write candidate. Using `path.isAbsolute` (not a hand-rolled
470
- // `startsWith('/')`) keeps the same platform-native semantics Gate 5b uses.
471
- // WARN, not reject the validator must not contradict the hook it validates
472
- // for (#792 / #870) EXCEPT for the narrow catastrophic subclass below
473
- // (#870-followup), which hard-rejects regardless of what Gate 5b would do
474
- // with it: pre-flight validation exists precisely to catch a grant this bad
475
- // before the hook is ever consulted.
476
- if (path.isAbsolute(entry) || isFilesystemRootLiteral(entry)) {
477
- if (isFilesystemRootLiteral(entry)) {
478
- errors.push(
479
- `allowedPaths grants the entire filesystem root: ${entry} — refused unconditionally, this can never be a valid wave scope`,
480
- );
481
- } else {
482
- const denied = deniedTopSegment(entry);
483
- if (denied) {
484
- errors.push(
485
- `allowedPaths contains a well-known system/home directory grant: ${entry} (top-level segment "${denied}" is denylisted) — refused, scope a narrower path instead`,
486
- );
487
- } else if (!hasWildcard(entry)) {
488
- errors.push(
489
- `allowedPaths contains a bare absolute file grant with no wildcard: ${entry} — a single concrete out-of-repo file has no established legitimate use in this codebase; scope a glob instead`,
490
- );
491
- } else {
492
- warnings.push(
493
- `allowedPaths contains an absolute (out-of-repo) path: ${entry} — honoured by hooks/enforce-scope.mjs Gate 5b; verify this grant is intentional`,
494
- );
495
- }
496
- }
403
+ // #870 / #1398 / #1405 / #1406 — grade the entry as a Gate 5b grant through
404
+ // the SHARED predicate (`scripts/lib/scope-gate.mjs` gradeScopeEntry), the
405
+ // same one `hooks/enforce-scope.mjs` Gate 5b calls. Most absolute entries are
406
+ // a SANCTIONED out-of-repo grant and only WARN the validator must not
407
+ // contradict the hook it validates for (#792/#870) — while the catastrophic
408
+ // subclasses (filesystem root, denylisted system directory in either its
409
+ // literal or its canonical spelling, a home grant at or above the user level
410
+ // or into a credential directory, a tilde that nothing expands, and a
411
+ // non-canonical prefix Gate 5b can never match) hard-reject here, BEFORE the
412
+ // hook is ever consulted. `null` = an ordinary relative entry, nothing to say.
413
+ //
414
+ // SAME PREDICATE, SAME RESOLVER (#1398 cond. 4). The resolver used to live
415
+ // here, which made the two callers' ARGUMENT shapes differ and let one grant
416
+ // reach two verdicts (`/tmp/x/**`: hook `warn`, this CLI `error`). Since the
417
+ // hot-path cost was measured (+0.067 ms per Gate 5b hit — see
418
+ // {@link canonicalizeGrantPrefix}) both callers pass this exact function, so
419
+ // the only remaining asymmetry is the CONSEQUENCE: exit-1 refusal here,
420
+ // allow-with-notice there.
421
+ const grade = gradeScopeEntry(entry, { resolve: canonicalizeGrantPrefix });
422
+ if (grade !== null) {
423
+ (grade.verdict === 'error' ? errors : warnings).push(`allowedPaths ${grade.message}`);
497
424
  }
498
425
  // Reject path traversal: any `../` segment. INDEPENDENT of the absolute checks
499
426
  // above — an absolute entry that ALSO contains `../` must still be rejected
@@ -485,6 +485,10 @@ async function main() {
485
485
  kind,
486
486
  force,
487
487
  vaultName,
488
+ // #1389: the repo whose `.vault.yaml` declares the namespace slug. This CLI
489
+ // has no repo-root flag, so it is the invocation cwd — bound ONCE here and
490
+ // shared by every processor call and the auto-commit below.
491
+ repoRoot: process.cwd(),
488
492
  qualityMinNarrativeChars,
489
493
  qualityMinConfidence,
490
494
  };
@@ -693,7 +697,11 @@ async function main() {
693
697
  // Opt-in: triggers only when --session-id is provided. Callers (session-end, evolve)
694
698
  // pass it explicitly; bare invocations stay quiet to preserve legacy behaviour.
695
699
  if (!dryRun && !noCommit && sessionIdArg) {
696
- autoCommitVaultMirror(resolve(vaultDir), sessionIdArg, resolveRepoNamespace({ vaultName }));
700
+ autoCommitVaultMirror(
701
+ resolve(vaultDir),
702
+ sessionIdArg,
703
+ resolveRepoNamespace({ vaultName, repoRoot: ctx.repoRoot }),
704
+ );
697
705
  }
698
706
  }
699
707
 
@@ -8,15 +8,17 @@ The current platform is determined by the `scripts/lib/platform.mjs` library:
8
8
  - `$SO_PLATFORM` = `claude` | `codex` | `cursor` | `pi`
9
9
  - Environment: `$CLAUDE_PLUGIN_ROOT` (Claude Code), `$CODEX_PLUGIN_ROOT` (Codex CLI), `$CURSOR_RULES_DIR` (Cursor IDE), or `$PI_PLUGIN_ROOT` (Pi)
10
10
 
11
- ## Identical Tools (no mapping needed)
12
-
13
- These tools have the same name and behavior on all platforms. Cursor IDE uses equivalent built-in tools for file operations and terminal commands.
14
- - **Read** read file contents
15
- - **Write** create/overwrite files
16
- - **Edit** string replacement in files
17
- - **Bash** execute shell commands
18
- - **Glob** file pattern matching
19
- - **Grep** content search (ripgrep)
11
+ ## File and Shell Tool Adaptation
12
+
13
+ Skill bodies use Claude Code names as operations, not as a requirement that every
14
+ harness expose those exact tools. Use the current runtime's available tools.
15
+ Measured in Codex Desktop 0.153.4 on 2026-09-19: `exec_command` runs shell
16
+ commands (`cat`, `rg`, `rg --files` for Read/Grep/Glob); `apply_patch` performs
17
+ file edits. These may be exposed through a `functions.exec` tools namespace.
18
+ Never call an unavailable `Read`, `Bash`, or `Skill` tool by name. An instruction
19
+ to invoke `session-orchestrator:<name>` means read and follow the canonical
20
+ `skills/<name>/SKILL.md` relative to the plugin root when no Skill tool exists.
21
+ Keep the user's project as the working directory for project operations.
20
22
 
21
23
  ## Platform-Specific Tool Mapping
22
24
 
@@ -24,7 +26,7 @@ These tools have the same name and behavior on all platforms. Cursor IDE uses eq
24
26
 
25
27
  | Function | Claude Code | Codex CLI | Cursor IDE | Pi |
26
28
  |----------|------------|-----------|------------|----|
27
- | Present choices to user | `AskUserQuestion` tool with structured options | Numbered Markdown list as plain text, wait for user reply | Numbered Markdown list (same as Codex) | Numbered Markdown list v1; native UI adapter planned |
29
+ | Present choices to user | `AskUserQuestion` tool with structured options | Available native user-input tool, respecting its mode restrictions; plain-text fallback when unavailable | Available native choice tool or numbered Markdown list | Numbered Markdown list v1; native UI adapter planned |
28
30
  | Dispatch subagent | `Agent({ description, prompt, subagent_type })` | Native multi-agent collaboration namespace: `spawn_agent`, `list_agents`, `wait_agent`, `send_message`, `followup_task`, `interrupt_agent`, `close_agent`. Measured 2026-08-25 on `codex-cli 0.141.0` — `codex features list` → `multi_agent stable true`. | No native in-session Agent tool. Coordinator-side foreign dispatch via the headless `cursor-agent` CLI exists (`scripts/lib/wave-executor/foreign-dispatch.mjs`, #1150) — measured 2026-08-25 on `cursor-agent 2026.08.11-e8db854`. | Sequential execution v1. Do not assume native subagents until the Pi SDK dispatcher exists. |
29
31
  | Track tasks | `TaskCreate` / `TaskUpdate` / `TaskList` | Plain-text checklist in response context | Plain-text checklist (same as Codex) | Plain-text checklist (same as Codex) |
30
32
  | Enter plan mode | `EnterPlanMode` / `ExitPlanMode` tools | `/plan` slash command (prompt-level, not tool-based) | Instruction-based: "Focus on analysis and planning. Do not modify files until the user approves." | `/plan` prompt template; use instruction-based planning when tool mode is unavailable |
@@ -37,7 +39,13 @@ When a skill instructs "Use the AskUserQuestion tool", apply this pattern:
37
39
 
38
40
  **On Claude Code:** Use the AskUserQuestion tool with structured options as documented.
39
41
 
40
- **On Codex CLI / Cursor IDE / Pi:** Present the same choices as a numbered Markdown list and ask the user to respond:
42
+ **On Codex:** Prefer the available native user-input tool. In the measured Desktop
43
+ 0.153.4 runtime (2026-09-19), `request_user_input_async` allows a question while
44
+ independent work continues; `request_user_input` has mode restrictions. Follow
45
+ the live tool contract. Existing user authorization remains valid; do not ask
46
+ again merely because a workflow names `AskUserQuestion`.
47
+
48
+ **When no suitable native tool is available (including Cursor IDE / Pi):** Present the same choices as a numbered Markdown list and ask the user to respond:
41
49
  ```
42
50
  Choose one:
43
51
  1. Option A — description
@@ -85,6 +93,10 @@ No Session Orchestrator Pi subagent dispatcher exists in v1. Execute wave tasks
85
93
 
86
94
  Skills use `model-preference` (Claude), `model-preference-codex` (Codex), `model-preference-cursor` (Cursor), and active-model fallback on Pi in YAML frontmatter.
87
95
 
96
+ These are preferences, not authority to replace a user's selected model,
97
+ reasoning effort, or service tier. Preserve the current settings unless the user
98
+ requests a change; inherit them for native subagents.
99
+
88
100
  ## State Directory
89
101
 
90
102
  - **Claude Code:** `.claude/` (STATE.md, wave-scope.json)