session-orchestrator 5.3.0 → 5.4.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 (206) hide show
  1. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
  2. package/.agents/skills/navigator/SKILL.md +22 -0
  3. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  4. package/.agents/skills/test-audit/SKILL.md +21 -0
  5. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  6. package/.claude-plugin/marketplace.json +1 -1
  7. package/.claude-plugin/plugin.json +1 -1
  8. package/.codex-plugin/plugin.json +1 -1
  9. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  10. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  11. package/.codex-plugin/skills/navigator/agents/openai.yaml +5 -0
  12. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  13. package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
  14. package/.cursor/commands/navigator.md +15 -0
  15. package/.cursor/commands/test-audit.md +14 -0
  16. package/.cursor/rules/060-evolve.mdc +27 -12
  17. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  18. package/.cursor/skills/navigator/SKILL.md +13 -0
  19. package/.cursor/skills/test-audit/SKILL.md +12 -0
  20. package/.cursor-plugin/plugin.json +1 -1
  21. package/.orchestrator/policy/blocked-commands.json +1 -1
  22. package/AGENTS.md +9 -31
  23. package/CHANGELOG.md +70 -0
  24. package/README.md +14 -10
  25. package/docs/agent-authoring.md +2 -2
  26. package/docs/baseline.md +10 -1
  27. package/docs/ci-setup.md +57 -4
  28. package/docs/components.md +12 -12
  29. package/docs/events-schema.md +11 -8
  30. package/docs/persona-panel.md +5 -4
  31. package/docs/rule-authoring.md +27 -12
  32. package/docs/scope-collision-guard.md +26 -4
  33. package/docs/session-config-reference.md +55 -6
  34. package/docs/session-config-template.md +11 -2
  35. package/hooks/_lib/hook-import-set.json +27 -23
  36. package/hooks/hooks-codex.json +1 -1
  37. package/hooks/hooks-cursor.json +5 -0
  38. package/hooks/hooks-pi.json +5 -0
  39. package/hooks/hooks.json +6 -1
  40. package/hooks/on-session-start.mjs +53 -0
  41. package/hooks/on-stop.mjs +3 -1
  42. package/hooks/post-tool-batch-wave-signal.mjs +3 -1
  43. package/hooks/pre-bash-memory-propose-audit.mjs +52 -3
  44. package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
  45. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  46. package/hooks/pre-task-scope-disjoint.mjs +559 -72
  47. package/hooks/subagent-telemetry.mjs +96 -19
  48. package/package.json +3 -3
  49. package/pi/prompts/navigator.md +12 -0
  50. package/pi/prompts/test-audit.md +12 -0
  51. package/rules/README.md +6 -1
  52. package/rules/_index.md +3 -3
  53. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  54. package/rules/opt-in-domain/prompt-caching.md +5 -1
  55. package/rules/opt-in-stack/backend-data.md +9 -1
  56. package/rules/opt-in-stack/backend.md +7 -1
  57. package/rules/opt-in-stack/frontend.md +6 -1
  58. package/rules/opt-in-stack/security-web.md +7 -1
  59. package/rules/opt-in-stack/swift.md +5 -1
  60. package/scripts/apply-session-learnings.mjs +492 -0
  61. package/scripts/archive-closed-prds.mjs +2 -1
  62. package/scripts/autopilot.mjs +49 -53
  63. package/scripts/backfill-abandoned-sessions.mjs +1 -1
  64. package/scripts/baseline-archetypes.mjs +1 -1
  65. package/scripts/check-package-manager.mjs +62 -5
  66. package/scripts/cursor-install.mjs +1 -4
  67. package/scripts/emit-session.mjs +296 -2
  68. package/scripts/export-hw-learnings.mjs +22 -3
  69. package/scripts/fleet-checkin.mjs +147 -0
  70. package/scripts/lib/agent-frontmatter.mjs +3 -3
  71. package/scripts/lib/auto-dream.mjs +7 -6
  72. package/scripts/lib/autopilot/flags.mjs +6 -1
  73. package/scripts/lib/autopilot/loop.mjs +10 -6
  74. package/scripts/lib/baseline-archetypes.mjs +41 -22
  75. package/scripts/lib/config/host-paths.mjs +79 -0
  76. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  77. package/scripts/lib/config/reaper.mjs +7 -6
  78. package/scripts/lib/config-schema.mjs +13 -0
  79. package/scripts/lib/config.mjs +71 -10
  80. package/scripts/lib/events.mjs +21 -2
  81. package/scripts/lib/fleet-protocol.mjs +270 -0
  82. package/scripts/lib/gates/gate-baseline.mjs +13 -3
  83. package/scripts/lib/gates/gate-full.mjs +7 -2
  84. package/scripts/lib/gates/gate-helpers.mjs +93 -2
  85. package/scripts/lib/gates/gate-incremental.mjs +23 -22
  86. package/scripts/lib/gates/gate-per-file.mjs +16 -7
  87. package/scripts/lib/instruction-budget-guard.mjs +69 -11
  88. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  89. package/scripts/lib/learnings/io.mjs +154 -26
  90. package/scripts/lib/learnings/schema.mjs +163 -15
  91. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  92. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  93. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  94. package/scripts/lib/named-vault-resolver.mjs +104 -5
  95. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  96. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  97. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  98. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  99. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  100. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  101. package/scripts/lib/orphan-reaper.mjs +43 -1315
  102. package/scripts/lib/owner-yaml.mjs +104 -6
  103. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  104. package/scripts/lib/process-group.mjs +62 -11
  105. package/scripts/lib/project-hygiene.mjs +21 -1
  106. package/scripts/lib/quality-gate.mjs +45 -5
  107. package/scripts/lib/resource-probe.mjs +1 -1
  108. package/scripts/lib/rule-loader.mjs +100 -62
  109. package/scripts/lib/rules-sync.mjs +27 -12
  110. package/scripts/lib/scope-baseline.mjs +3 -4
  111. package/scripts/lib/session-record-repair.mjs +183 -26
  112. package/scripts/lib/session-registry.mjs +27 -2
  113. package/scripts/lib/session-schema/constants.mjs +30 -4
  114. package/scripts/lib/session-schema/validator.mjs +28 -1
  115. package/scripts/lib/session-token-rollup.mjs +245 -52
  116. package/scripts/lib/sessions-canonical.mjs +100 -9
  117. package/scripts/lib/sh-quote.mjs +29 -0
  118. package/scripts/lib/subagents-schema.mjs +30 -3
  119. package/scripts/lib/telemetry/pricing.mjs +118 -71
  120. package/scripts/lib/tests-src-ratio.mjs +140 -23
  121. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  122. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  123. package/scripts/lib/ux-grill/reconcile.mjs +1 -1
  124. package/scripts/lib/validate/check-agents.mjs +1 -1
  125. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  126. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  127. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  128. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  129. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  130. package/scripts/lib/validate-vendored-rules.mjs +206 -81
  131. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  132. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  133. package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
  134. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  135. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  136. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  137. package/scripts/lib/wave-resource-gate.mjs +34 -8
  138. package/scripts/memory-propose.mjs +18 -5
  139. package/scripts/release-session-lock.mjs +2 -1
  140. package/scripts/release.mjs +10 -3
  141. package/scripts/repair-invalid-sessions.mjs +37 -4
  142. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  143. package/scripts/run-quality-gate.mjs +323 -302
  144. package/scripts/site-numbers.mjs +163 -5
  145. package/scripts/sweep-expired-learnings.mjs +56 -5
  146. package/scripts/sweep-expired-rules.mjs +2 -1
  147. package/scripts/validate-plugin.mjs +7 -0
  148. package/scripts/vault-integration-watcher.mjs +2 -1
  149. package/scripts/vault-mirror.mjs +72 -46
  150. package/scripts/wave-scope-binding.mjs +2 -1
  151. package/skills/_shared/config-reading.md +3 -3
  152. package/skills/_shared/fleet-protocol.md +131 -0
  153. package/skills/_shared/state-ownership.md +5 -0
  154. package/skills/autopilot/SKILL.md +25 -11
  155. package/skills/bootstrap/SKILL.md +1 -1
  156. package/skills/bootstrap/_shared-template.md +1 -1
  157. package/skills/bootstrap/fast-template.md +1 -1
  158. package/skills/bootstrap/private-contract.md +2 -1
  159. package/skills/bootstrap/public-fallback.md +6 -3
  160. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  161. package/skills/claude-md-drift-check/SKILL.md +10 -10
  162. package/skills/claude-md-drift-check/checker.mjs +26 -18
  163. package/skills/evolve/SKILL.md +2 -2
  164. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  165. package/skills/memory-cleanup/SKILL.md +1 -1
  166. package/skills/navigator/SKILL.md +193 -0
  167. package/skills/navigator/references/account-check.sh +87 -0
  168. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  169. package/skills/navigator/references/slot-check.sh +114 -0
  170. package/skills/persona-panel/SKILL.md +2 -2
  171. package/skills/persona-panel/persona-format.md +2 -2
  172. package/skills/persona-panel/presets/designer-lens.md +1 -1
  173. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  174. package/skills/persona-panel/presets/pm-lens.md +1 -1
  175. package/skills/plan/mode-retro.md +19 -4
  176. package/skills/quality-gates/SKILL.md +6 -4
  177. package/skills/remote-offload/SKILL.md +4 -2
  178. package/skills/session-end/learning-patterns.md +22 -12
  179. package/skills/session-end/metrics-collection.md +5 -6
  180. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  181. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  182. package/skills/session-end/references/session-summary-template.md +1 -1
  183. package/skills/session-end/session-metrics-write.md +22 -60
  184. package/skills/session-plan/SKILL.md +0 -3
  185. package/skills/session-start/SKILL.md +6 -0
  186. package/skills/session-start/references/operations-contract.md +4 -0
  187. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  188. package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -2
  189. package/skills/test-audit/SKILL.md +152 -0
  190. package/skills/test-audit/references/campaign.md +111 -0
  191. package/skills/test-audit/references/mutate.sh +50 -0
  192. package/skills/wave-executor/SKILL.md +1 -1
  193. package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
  194. package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
  195. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  196. package/templates/personas/README.md +5 -5
  197. package/templates/personas/accounting-compliance.v1.md +1 -1
  198. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  199. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  200. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  201. package/templates/personas/buyer-p3-build.v1.md +1 -1
  202. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  203. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  204. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  205. package/templates/personas/klima-ai-expert.v1.md +1 -1
  206. package/templates/personas/klima-physicist.v1.md +1 -1
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * fleet-checkin.mjs — write this session's fleet check-in (#1462, PRD !53 § 3.2).
4
+ *
5
+ * Thin CLI over `scripts/lib/fleet-protocol.mjs`, called by session-start
6
+ * Phase 7.6 and the operations-contract peer preflight BEFORE the first writing
7
+ * action of a session.
8
+ *
9
+ * Usage:
10
+ * echo '<check-in JSON>' | node scripts/fleet-checkin.mjs
11
+ *
12
+ * Reads one JSON object from stdin, stamps `zeit` (always overwritten), validates
13
+ * it with `validateCheckin()`, and writes it atomically (tmp file + rename in
14
+ * the target directory, mode 0600) to `<navigatorDir>/checkin/<session>.json`.
15
+ * Then reads the navigator lease and emits `orchestrator.fleet.checkin` with the
16
+ * payload `{ session, repo, modus, kandidaten, navigator_state }` — no paths, no
17
+ * quota, no `auftrag_ref`.
18
+ *
19
+ * stdout: one JSON line `{ ok, path, navigator_state, event }`.
20
+ *
21
+ * Exit codes:
22
+ * 0 — check-in written (an event failure only WARNs; `event: false` on stdout)
23
+ * 1 — write error
24
+ * 2 — invalid input (not JSON, not an object, or validation errors; no file)
25
+ */
26
+
27
+ import { randomBytes } from 'node:crypto';
28
+ import { promises as fs } from 'node:fs';
29
+ import os from 'node:os';
30
+ import path from 'node:path';
31
+ import { emitEvent } from './lib/events.mjs';
32
+ import {
33
+ checkinPath,
34
+ navigatorDir,
35
+ readNavigatorLease,
36
+ utcSecondsTimestamp,
37
+ validateCheckin,
38
+ } from './lib/fleet-protocol.mjs';
39
+ import { isMainModule } from './lib/is-main-module.mjs';
40
+
41
+ const PREFIX = 'fleet-checkin:';
42
+
43
+ /** @returns {Promise<string>} */
44
+ async function readStdin() {
45
+ let data = '';
46
+ process.stdin.setEncoding('utf8');
47
+ for await (const chunk of process.stdin) data += chunk;
48
+ return data;
49
+ }
50
+
51
+ /**
52
+ * Create `<navigatorDir>/checkin/` (0700) and write `record` atomically to
53
+ * `target` (0600). Refuses a `checkin/` that is not a real directory, so a
54
+ * planted symlink cannot redirect the chmod or the write.
55
+ * @param {string} target
56
+ * @param {object} record
57
+ */
58
+ async function writeCheckinFile(target, record) {
59
+ const checkinDir = path.dirname(target);
60
+ await fs.mkdir(navigatorDir(), { recursive: true, mode: 0o700 });
61
+ await fs.mkdir(checkinDir, { recursive: true, mode: 0o700 });
62
+ const st = await fs.lstat(checkinDir);
63
+ if (!st.isDirectory()) throw new Error(`${checkinDir} is not a directory`);
64
+ // mkdir's mode does not apply to a directory that already existed.
65
+ await fs.chmod(checkinDir, 0o700);
66
+
67
+ const tmp = path.join(checkinDir, `.${path.basename(target)}.${randomBytes(6).toString('hex')}.tmp`);
68
+ try {
69
+ // `wx`: never follow or reuse a pre-existing path at the tmp name.
70
+ await fs.writeFile(tmp, JSON.stringify(record, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
71
+ // The creation mode is filtered through the umask; pin it explicitly.
72
+ await fs.chmod(tmp, 0o600);
73
+ await fs.rename(tmp, target);
74
+ } catch (err) {
75
+ await fs.rm(tmp, { force: true }).catch(() => {});
76
+ throw err;
77
+ }
78
+ }
79
+
80
+ /** @returns {Promise<number>} exit code */
81
+ async function main() {
82
+ let input;
83
+ try {
84
+ input = JSON.parse(await readStdin());
85
+ } catch {
86
+ process.stderr.write(`${PREFIX} stdin is not valid JSON\n`);
87
+ return 2;
88
+ }
89
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
90
+ process.stderr.write(`${PREFIX} stdin must be a JSON object\n`);
91
+ return 2;
92
+ }
93
+
94
+ const record = { ...input, zeit: utcSecondsTimestamp() };
95
+ const errors = validateCheckin(record);
96
+ if (errors.length > 0) {
97
+ for (const e of errors) process.stderr.write(`${PREFIX} ${e}\n`);
98
+ return 2;
99
+ }
100
+
101
+ const target = checkinPath(record.session);
102
+ try {
103
+ await writeCheckinFile(target, record);
104
+ } catch (err) {
105
+ process.stderr.write(`${PREFIX} write failed: ${err instanceof Error ? err.message : String(err)}\n`);
106
+ return 1;
107
+ }
108
+
109
+ const { state: navigatorState } = await readNavigatorLease();
110
+
111
+ let event = true;
112
+ try {
113
+ const repoRoot = (process.env.CLAUDE_PROJECT_DIR || '').trim() || process.cwd();
114
+ await emitEvent(
115
+ 'orchestrator.fleet.checkin',
116
+ {
117
+ session: record.session,
118
+ repo: record.repo,
119
+ modus: record.modus,
120
+ kandidaten: record.kandidaten,
121
+ navigator_state: navigatorState,
122
+ },
123
+ { repoRoot },
124
+ );
125
+ } catch (err) {
126
+ event = false;
127
+ process.stderr.write(
128
+ `${PREFIX} WARN: check-in written but event emission failed: ${err instanceof Error ? err.message : String(err)}\n`,
129
+ );
130
+ }
131
+
132
+ // Report the path with `~` for the home prefix: stdout lands in transcripts.
133
+ const home = os.homedir();
134
+ const shownPath = target.startsWith(`${home}${path.sep}`) ? `~${target.slice(home.length)}` : target;
135
+ process.stdout.write(JSON.stringify({ ok: true, path: shownPath, navigator_state: navigatorState, event }) + '\n');
136
+ return 0;
137
+ }
138
+
139
+ if (isMainModule(import.meta.url)) {
140
+ main().then(
141
+ (code) => { process.exitCode = code; },
142
+ (err) => {
143
+ process.stderr.write(`${PREFIX} ${err instanceof Error ? err.message : String(err)}\n`);
144
+ process.exitCode = 1;
145
+ },
146
+ );
147
+ }
@@ -21,11 +21,11 @@ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
21
21
 
22
22
  // Aliases per https://code.claude.com/docs/en/sub-agents § model resolution.
23
23
  // Full model IDs (claude-{opus|sonnet|haiku|fable}-N[-N][-YYYYMMDD], e.g.
24
- // "claude-opus-4-7", "claude-sonnet-5", "claude-fable-5-20260101") also
24
+ // "claude-opus-5-5", "claude-sonnet-5", "claude-fable-5-20260101") also
25
25
  // accepted via MODEL_ID_RE. `fable` is a valid upstream model family
26
26
  // alongside opus/sonnet/haiku (#768) — the trailing numeric group is
27
27
  // optional so both the two-part ("claude-sonnet-5") and three-part
28
- // ("claude-opus-4-7") ID shapes validate.
28
+ // ("claude-opus-5-5") ID shapes validate.
29
29
  // Exported so persona-panel modules (catalog-loader.mjs, persona-gate-wave.mjs) can
30
30
  // validate against the SSOT instead of re-declaring (W4 architect MED-1, 2026-05-19).
31
31
  export const ALLOWED_MODEL_ALIASES = new Set(['inherit', 'sonnet', 'opus', 'haiku', 'fable']);
@@ -187,7 +187,7 @@ export function validateAgentFrontmatter(frontmatter) {
187
187
  errors.push({
188
188
  path: 'model',
189
189
  rule: 'enum',
190
- message: `model must be one of ${[...ALLOWED_MODEL_ALIASES].map((m) => JSON.stringify(m)).join('|')} or a full model ID like "claude-opus-4-7" or "claude-sonnet-5" (got ${JSON.stringify(model)})`,
190
+ message: `model must be one of ${[...ALLOWED_MODEL_ALIASES].map((m) => JSON.stringify(m)).join('|')} or a full model ID like "claude-opus-5-5" or "claude-sonnet-5" (got ${JSON.stringify(model)})`,
191
191
  });
192
192
  }
193
193
 
@@ -1,10 +1,11 @@
1
1
  /**
2
- * auto-dream.mjs — Auto-Dream Post-Session Hook helper (issue #502, PRD F2.2).
2
+ * auto-dream.mjs — Memory-maintenance signals and proposal I/O (issue #502).
3
3
  *
4
- * Provides the decision + I/O surface used by session-end Phase 3.6.5 and by
5
- * /memory-cleanup --dry-run / --apply-pending. Decides whether the post-session
6
- * dream should fire, writes the proposed diff to `.orchestrator/pending-dream.md`
7
- * atomically, and applies it in a follow-up session.
4
+ * Provides the due signal used by session-start's maintenance-due probe and
5
+ * the I/O used by /memory-cleanup --dry-run / --apply-pending. The former
6
+ * session-end Phase 3.6.5 nudge was retired in #1288. The manual skill writes
7
+ * `.orchestrator/pending-dream.md` atomically and applies a reviewed proposal;
8
+ * the maintenance probe does not generate proposals or dispatch a cleanup.
8
9
  *
9
10
  * Decision inputs (PRD F2.2 acceptance criteria):
10
11
  * - memory-cleanup-threshold (default 5) — sessions since last cleanup
@@ -118,7 +119,7 @@ export async function readDreamSignals({ repoRoot, memoryDir }) {
118
119
  // ---------------------------------------------------------------------------
119
120
 
120
121
  /**
121
- * Decide whether session-end Phase 3.6.5 should dispatch /memory-cleanup --dry-run.
122
+ * Decide whether session-start should report that memory cleanup is due.
122
123
  *
123
124
  * Rules (PRD F2.2):
124
125
  * - threshold === 0 → never trigger (kill-switch).
@@ -20,7 +20,12 @@ export const FLAG_BOUNDS = Object.freeze({
20
20
  maxSessions: { min: 1, max: 50, default: 5 },
21
21
  maxHours: { min: 0.5, max: 24.0, default: 4.0 },
22
22
  confidenceThreshold: { min: 0.0, max: 1.0, default: 0.85 },
23
- maxTokens: { min: 0, max: 10_000_000, default: 500_000 },
23
+ // Default 0 = token-budget switch OFF until calibrated (HR-101): the budget
24
+ // counts subagent OUTPUT tokens only, and one measured deep session's
25
+ // subagents alone produced 450 499 — a 500 000 default stopped autopilot after
26
+ // about one deep session. Revisit once sessions.jsonl carries enough
27
+ // total_token_output values to pick a threshold that fires rarely.
28
+ maxTokens: { min: 0, max: 10_000_000, default: 0 },
24
29
  });
25
30
 
26
31
  /** Default peer Claude-process count above which `resource-overload` fires when verdict is critical. */
@@ -131,11 +131,14 @@ export async function runLoop(opts = {}) {
131
131
  confidence_threshold: confidenceThreshold,
132
132
  // Persisted for the same reason as the three above (HR-105): a switch whose
133
133
  // input is never recorded cannot be falsified afterwards. `0` = disabled.
134
- // CEILING: reaching a NON-zero value here does not make TOKEN_BUDGET_EXCEEDED
135
- // live under the headless driver — `readTailSession()` in scripts/autopilot.mjs
136
- // returns no `usage`, and sessions.jsonl carries no token field to build one
137
- // from, so `total_tokens_used` stays 0. Revisit when a session record gains
138
- // token counts.
134
+ // Live path (#1436): scripts/emit-session.mjs merges the subagents.jsonl
135
+ // rollup (`total_token_output`, `total_tokens`) into each session record,
136
+ // scripts/autopilot.mjs selects the unique canonical record whose
137
+ // raw_session_id equals the child's --session-id UUID and projects its
138
+ // totals as `usage`; the accumulator sums them into `total_tokens_used`.
139
+ // Zero or multiple matches throw → FAILED_WAVE, never foreign token usage.
140
+ // CEILING: an own record without token totals adds 0 — the budget
141
+ // undercounts, it never over-fires.
139
142
  max_tokens: opts.maxTokens ?? 0,
140
143
  iterations_completed: 0,
141
144
  kill_switch: null,
@@ -271,7 +274,8 @@ export async function runLoop(opts = {}) {
271
274
  }
272
275
  state.iterations_completed += 1;
273
276
 
274
- // Accumulate output tokens; stays 0 when sessionRunner omits `usage` (forward-compat).
277
+ // Accumulate own-record output tokens (total_tokens fallback); missing totals
278
+ // add 0. An unassignable child record already threw → FAILED_WAVE above.
275
279
  if (sessionResult && typeof sessionResult.usage === 'object' && sessionResult.usage !== null) {
276
280
  const outTokens = Number(
277
281
  sessionResult.usage.output_tokens ?? sessionResult.usage.total_tokens
@@ -19,10 +19,11 @@ const BASENAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*\.md$/;
19
19
  const unique = (items) => [...new Set(items)].sort();
20
20
  const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
21
21
 
22
+ /** `message` must never interpolate a baseline root or any other host path. */
22
23
  export class BaselineContractError extends Error {
23
- constructor(reason) { super(`Baseline archetype lookup failed (${reason}).`); this.name = 'BaselineContractError'; this.reason = reason; }
24
+ constructor(reason, message = `Baseline archetype lookup failed (${reason}).`) { super(message); this.name = 'BaselineContractError'; this.reason = reason; }
24
25
  }
25
- function fail(reason = 'invalid-contract') { throw new BaselineContractError(reason); }
26
+ function fail(reason = 'invalid-contract', message) { throw new BaselineContractError(reason, message); }
26
27
  function check(condition) { if (!condition) fail(); }
27
28
  function shape(value, required, optional = []) {
28
29
  check(object(value) && required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => [...required, ...optional].includes(key)));
@@ -122,24 +123,38 @@ process.stdout.write(JSON.stringify(resolveConfiguredBaselinePath(JSON.parse(rea
122
123
  return realpathSync(root);
123
124
  }
124
125
 
125
- /** Resolve a canonical relative source and reject symlinks at every component. */
126
- export function baselineSourcePath(root, source, directory = false) {
126
+ /** Resolve a canonical relative source and reject symlinks at every component.
127
+ * Only an ENOENT thrown by the per-component `lstatSync` itself maps to
128
+ * `missingReason`; every other failure is `unsafe-source`. Existence is never
129
+ * probed with a link-following call (existsSync/statSync/accessSync/realpathSync):
130
+ * a dangling symlink would then read as "missing" instead of `unsafe-source`.
131
+ */
132
+ export function baselineSourcePath(root, source, directory = false, { missingReason = 'unsafe-source' } = {}) {
127
133
  relative(source);
134
+ // Resolved first: a vanished root is unsafe, never a missing source.
135
+ let realRoot;
136
+ try { realRoot = realpathSync(root); } catch { fail('unsafe-source'); }
128
137
  let current = root;
129
- try {
130
- for (const component of source.split('/')) {
131
- current = path.join(current, component);
132
- if (lstatSync(current).isSymbolicLink()) fail('unsafe-source');
133
- }
134
- const stat = lstatSync(current);
135
- if (directory ? !stat.isDirectory() : !stat.isFile()) fail('unsafe-source');
136
- if (!realpathSync(current).startsWith(`${realpathSync(root)}${path.sep}`)) fail('unsafe-source');
137
- return current;
138
- } catch { fail('unsafe-source'); }
138
+ let stat;
139
+ for (const component of source.split('/')) {
140
+ current = path.join(current, component);
141
+ try { stat = lstatSync(current); } catch (error) { fail(error?.code === 'ENOENT' ? missingReason : 'unsafe-source'); }
142
+ if (stat.isSymbolicLink()) fail('unsafe-source');
143
+ }
144
+ if (directory ? !stat.isDirectory() : !stat.isFile()) fail('unsafe-source');
145
+ let real;
146
+ try { real = realpathSync(current); } catch { fail('unsafe-source'); }
147
+ if (!real.startsWith(`${realRoot}${path.sep}`)) fail('unsafe-source');
148
+ return current;
139
149
  }
140
150
 
151
+ const PRODUCER = 'scripts/archetype-manifest.mjs';
141
152
  function projection(root, args, { timeoutMs = 5000, maxBuffer = MAX_BYTES } = {}) {
142
- const script = baselineSourcePath(root, 'scripts/archetype-manifest.mjs');
153
+ let script;
154
+ try { script = baselineSourcePath(root, PRODUCER, false, { missingReason: 'producer-missing' }); } catch (error) {
155
+ if (error?.reason !== 'producer-missing') throw error;
156
+ fail('producer-missing', `Baseline archetype lookup failed (producer-missing): the configured baseline has no ${PRODUCER} exporter — see docs/baseline.md § How bootstrap finds it.`);
157
+ }
143
158
  const result = spawnSync(process.execPath, [script, ...args], {
144
159
  cwd: root, encoding: 'utf8', timeout: Math.min(timeoutMs, 5000), maxBuffer: Math.min(maxBuffer, MAX_BYTES),
145
160
  shell: false, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'],
@@ -195,7 +210,8 @@ export async function loadBaselineArchetypes(options = {}) {
195
210
  let manifest;
196
211
  try { manifest = validateBaselineContract(JSON.parse(projection(root, ['export'], options))); }
197
212
  catch (error) { if (error instanceof BaselineContractError) throw error; fail(); }
198
- for (const item of manifest.archetypes) baselineSourcePath(root, item.templatePath, true);
213
+ // An absent declared source is an incomplete baseline, not a security refusal (#1447 3a).
214
+ for (const item of manifest.archetypes) baselineSourcePath(root, item.templatePath, true, { missingReason: 'invalid-contract' });
199
215
  const facts = collectFacts(repoRoot);
200
216
  const ordered = [...manifest.archetypes].sort((a, b) => a.order - b.order);
201
217
  const archetypes = ordered.map(({ id, order, runtimes, packageManagers, ui, api, deploy }) => ({ id, order, runtimes, packageManagers, ui, api, deploy }));
@@ -220,7 +236,7 @@ export async function loadBaselineArchetypes(options = {}) {
220
236
  if (!ruleTargets.includes(name) || seen.has(name)) fail('invalid-rule-projection');
221
237
  seen.add(name);
222
238
  if (owned.includes(name)) continue;
223
- baselineSourcePath(root, source);
239
+ baselineSourcePath(root, source, false, { missingReason: 'invalid-rule-projection' });
224
240
  baselineRules.push({ source, target: `.claude/rules/${name}` });
225
241
  }
226
242
  if (ruleTargets.some((name) => !seen.has(name))) fail('invalid-rule-projection');
@@ -230,7 +246,9 @@ export async function loadBaselineArchetypes(options = {}) {
230
246
  pluginRuleTargets: ruleTargets.filter((name) => owned.includes(name)), baselineRules,
231
247
  } };
232
248
  } catch (error) {
233
- return { status: 'error', reason: error instanceof BaselineContractError ? error.reason : 'lookup-failed', archetypes: [], selected: null };
249
+ // Only a BaselineContractError message is path-free by construction; others stay opaque.
250
+ const contract = error instanceof BaselineContractError;
251
+ return { status: 'error', reason: contract ? error.reason : 'lookup-failed', ...(contract && { message: error.message }), archetypes: [], selected: null };
234
252
  }
235
253
  }
236
254
 
@@ -290,11 +308,12 @@ function applyCopies(plan) {
290
308
  return { status: 'applied', created, preserved };
291
309
  }
292
310
  function applyFailure(error) {
293
- return { status: 'error', reason: error instanceof BaselineContractError ? error.reason : 'apply-failed', created: [], preserved: [] };
311
+ const contract = error instanceof BaselineContractError;
312
+ return { status: 'error', reason: contract ? error.reason : 'apply-failed', ...(contract && { message: error.message }), created: [], preserved: [] };
294
313
  }
295
314
  async function selectedContext(options) {
296
315
  const result = await loadBaselineArchetypes(options);
297
- if (result.status !== 'private' || !result.selected) fail(result.reason);
316
+ if (result.status !== 'private' || !result.selected) fail(result.reason, result.message);
298
317
  const root = await resolveBaselineLocation(options);
299
318
  if (!root) fail('baseline-unavailable');
300
319
  return { root, selected: result.selected };
@@ -340,7 +359,7 @@ export async function scaffoldBaselineArchetype(options = {}) {
340
359
  const { repoRoot = process.cwd(), projectName } = options;
341
360
  if (typeof projectName !== 'string' || !ID.test(projectName)) fail('invalid-project-name');
342
361
  const { root, selected } = await selectedContext(options);
343
- const template = baselineSourcePath(root, selected.templatePath, true);
362
+ const template = baselineSourcePath(root, selected.templatePath, true, { missingReason: 'invalid-contract' });
344
363
  sourceFiles(template);
345
364
  const shared = baselineSourcePath(root, 'templates/shared', true);
346
365
  sourceFiles(shared);
@@ -401,7 +420,7 @@ export async function syncBootstrapRules(options = {}) {
401
420
  // ordinary plugin rules, just as its minimal scaffold does.
402
421
  const contract = options.minimal ? { status: 'public', selected: null }
403
422
  : await loadBaselineArchetypes({ ...options, repoRoot, pluginRoot, archetype });
404
- if (contract.status === 'error') fail(contract.reason);
423
+ if (contract.status === 'error') fail(contract.reason, contract.message);
405
424
  if (contract.status === 'private' && !contract.selected && !hasUnselectedFastLock(repoRoot)) fail(contract.reason);
406
425
  const before = new Set();
407
426
  for (const name of pluginRuleTargets(pluginRoot)) {
@@ -13,6 +13,17 @@
13
13
  * 3. committed Session Config default (the value the committed CLAUDE.md —
14
14
  * AGENTS.md on Codex CLI — produced)
15
15
  *
16
+ * `vault-dir` has one more tier (agents/vault#319): an owner.yaml `vault-dirs:`
17
+ * cwd match between env and paths.vault-dir. That tier lives in
18
+ * `resolveVaultDir` in scripts/lib/config.mjs, NOT here: this module is one of
19
+ * the three direct CP11 helpers check-owner-leakage.mjs imports, and a standalone
20
+ * vendored copy carries only those three files. A new import edge from here
21
+ * (e.g. to named-baseline-resolver.mjs) makes CP11 fail closed in that copy.
22
+ *
23
+ * `resolveVaultIntegrationHost` is the host-local switch for the
24
+ * `vault-integration` gate itself (SO#1448). It may only LOWER the committed
25
+ * level (strict → warn → off), never raise it.
26
+ *
16
27
  * SYNCHRONOUS by design: `parseSessionConfig` in scripts/lib/config.mjs is sync,
17
28
  * so this layer reuses the SYNC owner loader (`loadOwnerConfig`) and exposes only
18
29
  * sync functions. An empty/whitespace value at any tier is treated as "unset" and
@@ -90,3 +101,71 @@ export function resolveHostPath(key, committedDefault, { env = process.env, owne
90
101
 
91
102
  return committedDefault;
92
103
  }
104
+
105
+ /** vault-integration levels, weakest first. The index order IS the ordering. */
106
+ const VAULT_INTEGRATION_LEVELS = /** @type {readonly string[]} */ (['off', 'warn', 'strict']);
107
+
108
+ /**
109
+ * @param {unknown} v
110
+ * @returns {'off'|'warn'|'strict'|undefined} a valid level, or undefined for unset/invalid
111
+ */
112
+ function coerceVaultIntegrationLevel(v) {
113
+ if (typeof v !== 'string') return undefined;
114
+ const lower = v.trim().toLowerCase();
115
+ return VAULT_INTEGRATION_LEVELS.includes(lower)
116
+ ? /** @type {'off'|'warn'|'strict'} */ (lower)
117
+ : undefined;
118
+ }
119
+
120
+ /**
121
+ * Overlay the host-local `vault-integration` switch onto the committed block
122
+ * (SO#1448). Precedence: env `SO_VAULT_INTEGRATION=off|warn|strict` > owner.yaml
123
+ * `vault-integration: { enabled: false | mode: off|warn|strict }` > committed.
124
+ * An invalid value at a tier counts as unset and falls through, mirroring
125
+ * resolveDispatcherAutonomy in dispatcher-autonomy.mjs.
126
+ *
127
+ * LOWER-ONLY: the host's level is applied only when it is weaker than the
128
+ * committed one. A host that could raise it would mirror repos into the vault
129
+ * that never opted in, so `enabled: true` or a stronger mode is ignored.
130
+ * Applying `off` sets both `enabled: false` and `mode: 'off'`, so either
131
+ * consumer check (`enabled`, `mode != off`) sees the integration as off.
132
+ *
133
+ * @param {{ enabled?: boolean, mode?: string } & Record<string, unknown>} vi — parsed committed block
134
+ * @param {{ env?: Record<string, string|undefined>, ownerConfig?: object }} [ctx] — from loadHostPaths()
135
+ * @returns {Record<string, unknown> & { enabled?: boolean, mode?: string, 'host-override': 'env:SO_VAULT_INTEGRATION'|'owner.yaml'|null }}
136
+ * a copy of `vi`; `host-override` names the tier that lowered it, `null` when none did
137
+ */
138
+ export function resolveVaultIntegrationHost(vi, { env = process.env, ownerConfig } = {}) {
139
+ /** @type {Record<string, unknown> & { enabled?: boolean, mode?: string, 'host-override': 'env:SO_VAULT_INTEGRATION'|'owner.yaml'|null }} */
140
+ const result = { ...vi, 'host-override': null };
141
+
142
+ /** @type {'off'|'warn'|'strict'|undefined} */
143
+ let requested = coerceVaultIntegrationLevel(env?.SO_VAULT_INTEGRATION);
144
+ /** @type {'env:SO_VAULT_INTEGRATION'|'owner.yaml'} */
145
+ let source = 'env:SO_VAULT_INTEGRATION';
146
+ if (requested === undefined) {
147
+ const owner = ownerConfig?.['vault-integration'];
148
+ if (owner !== null && typeof owner === 'object' && !Array.isArray(owner)) {
149
+ requested = owner.enabled === false ? 'off' : coerceVaultIntegrationLevel(owner.mode);
150
+ source = 'owner.yaml';
151
+ }
152
+ }
153
+ if (requested === undefined) return result;
154
+
155
+ const committedLevel =
156
+ vi?.enabled === true ? (coerceVaultIntegrationLevel(vi.mode) ?? 'warn') : 'off';
157
+ if (
158
+ VAULT_INTEGRATION_LEVELS.indexOf(requested) >= VAULT_INTEGRATION_LEVELS.indexOf(committedLevel)
159
+ ) {
160
+ return result;
161
+ }
162
+
163
+ if (requested === 'off') {
164
+ result.enabled = false;
165
+ result.mode = 'off';
166
+ } else {
167
+ result.mode = requested;
168
+ }
169
+ result['host-override'] = source;
170
+ return result;
171
+ }
@@ -10,7 +10,7 @@
10
10
  * after — 'quality' | 'impl-polish' (default: 'quality')
11
11
  * threshold — string spec parsed via parseThreshold (default: 'all')
12
12
  * personas — string[] (each matches SAFE_PERSONA_NAME_RE) (default: [])
13
- * dispatch-model — model alias or full ID (default: 'claude-opus-4-7')
13
+ * dispatch-model — model alias or full ID (default: 'opus' — tracks the current Opus)
14
14
  * mode — 'off' | 'warn' | 'strict' (default: 'off')
15
15
  *
16
16
  * Used by skills/wave-executor/wave-loop.md § 3b: Persona-Gate Hook to decide
@@ -32,7 +32,7 @@ const DEFAULTS = Object.freeze({
32
32
  threshold: 'all',
33
33
  threshold_parsed: null, // cached ParsedThreshold result; populated by _normalizePersonaGateWave
34
34
  personas: [],
35
- 'dispatch-model': 'claude-opus-4-7',
35
+ 'dispatch-model': 'opus',
36
36
  mode: 'off',
37
37
  });
38
38
 
@@ -138,7 +138,7 @@ export function _normalizePersonaGateWave(parsed) {
138
138
  }
139
139
  if (!ALLOWED_MODEL_ALIASES.has(m) && !MODEL_ID_RE.test(m)) {
140
140
  throw new Error(
141
- `persona-gate-wave.dispatch-model must be one of ${[...ALLOWED_MODEL_ALIASES].map((a) => JSON.stringify(a)).join('|')} or a full model ID like "claude-opus-4-7" (got ${JSON.stringify(m)})`,
141
+ `persona-gate-wave.dispatch-model must be one of ${[...ALLOWED_MODEL_ALIASES].map((a) => JSON.stringify(a)).join('|')} or a full model ID like "claude-opus-5-5" (got ${JSON.stringify(m)})`,
142
142
  );
143
143
  }
144
144
  out['dispatch-model'] = m;
@@ -27,18 +27,19 @@ import { preprocessBlockLines } from './block-preprocess.mjs';
27
27
  * Startwerte für Stufe 1 — NOT calibrated. Every number carries its provenance
28
28
  * from the PRD §4 parameter table ("keine Zahl ohne ihre Population").
29
29
  *
30
- * RUNTIME SSOT is `REAPER_DEFAULTS` in `scripts/lib/orphan-reaper.mjs`; these
31
- * literals mirror it deliberately rather than importing it, because that module
32
- * pulls `process-group.mjs` + `resource-probe/parsers.mjs` into the config
33
- * import graph, which every `parseSessionConfig()` caller would then pay for.
34
- * The parity is held by a test, not by an import
30
+ * RUNTIME SSOT is `REAPER_DEFAULTS` in `scripts/lib/orphan-reaper/defaults.mjs`; these
31
+ * literals mirror it rather than importing it. The original reason — the
32
+ * single-file `orphan-reaper.mjs` pulled `process-group.mjs` +
33
+ * `resource-probe/parsers.mjs` into the config import graph — no longer holds
34
+ * since the #1437 split (`defaults.mjs` imports only `node:path`); revisit the
35
+ * mirror there. The parity is held by a test, not by an import
35
36
  * (`tests/lib/config/reaper.test.mjs` § defaults mirror REAPER_DEFAULTS).
36
37
  */
37
38
  /** 300 s — DevWatchdog's hard limit for `tsgo`; the 2026-09-20 orphans were 7–17 min old. */
38
39
  const DEFAULT_MIN_AGE_SECONDS = 300;
39
40
  /** 30 s — DevWatchdog's normal scan cadence; keeps a PostToolBatch storm from taxing every tool call. */
40
41
  const DEFAULT_MIN_SCAN_INTERVAL_SECONDS = 30;
41
- /** 10 000 ms — `DEFAULT_KILL_GRACE_MS` from `dispatch-common.mjs:61`; repo convention, not newly invented. */
42
+ /** 10 000 ms — `DEFAULT_KILL_GRACE_MS` from `process-group.mjs`; repo convention, not newly invented. */
42
43
  const DEFAULT_KILL_GRACE_MS = 10_000;
43
44
  /** 500 ms — wait before reading the effect back; without it the 2026-09-20 check falsely reported "still alive". */
44
45
  const DEFAULT_VERIFY_WAIT_MS = 500;
@@ -120,6 +120,19 @@ export function validateSessionConfig(config) {
120
120
  }
121
121
  }
122
122
 
123
+ // #1465 cross-field: offload-first with no declared host can never place a
124
+ // wave anywhere — advisory (warn-level), it never flips `ok`.
125
+ if (config['offload-first'] === true) {
126
+ const hosts = config['remote-hosts'];
127
+ if (!Array.isArray(hosts) || hosts.length === 0) {
128
+ warnings.push({
129
+ path: 'remote-hosts',
130
+ rule: 'offload-first-cross-field',
131
+ message: 'offload-first: true but no remote-hosts declared — every wave stays local',
132
+ });
133
+ }
134
+ }
135
+
123
136
  if (errors.length > 0) {
124
137
  return { ok: false, errors, warnings };
125
138
  }