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.
- package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.agents/skills/navigator/SKILL.md +22 -0
- package/.agents/skills/navigator/agents/openai.yaml +5 -0
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/navigator/SKILL.md +22 -0
- package/.codex-plugin/skills/navigator/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/navigator.md +15 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +27 -12
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/navigator/SKILL.md +13 -0
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +9 -31
- package/CHANGELOG.md +70 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +57 -4
- package/docs/components.md +12 -12
- package/docs/events-schema.md +11 -8
- package/docs/persona-panel.md +5 -4
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +26 -4
- package/docs/session-config-reference.md +55 -6
- package/docs/session-config-template.md +11 -2
- package/hooks/_lib/hook-import-set.json +27 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/on-session-start.mjs +53 -0
- package/hooks/on-stop.mjs +3 -1
- package/hooks/post-tool-batch-wave-signal.mjs +3 -1
- package/hooks/pre-bash-memory-propose-audit.mjs +52 -3
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +559 -72
- package/hooks/subagent-telemetry.mjs +96 -19
- package/package.json +3 -3
- package/pi/prompts/navigator.md +12 -0
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +6 -1
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +5 -1
- package/rules/opt-in-stack/backend-data.md +9 -1
- package/rules/opt-in-stack/backend.md +7 -1
- package/rules/opt-in-stack/frontend.md +6 -1
- package/rules/opt-in-stack/security-web.md +7 -1
- package/rules/opt-in-stack/swift.md +5 -1
- package/scripts/apply-session-learnings.mjs +492 -0
- package/scripts/archive-closed-prds.mjs +2 -1
- package/scripts/autopilot.mjs +49 -53
- package/scripts/backfill-abandoned-sessions.mjs +1 -1
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/cursor-install.mjs +1 -4
- package/scripts/emit-session.mjs +296 -2
- package/scripts/export-hw-learnings.mjs +22 -3
- package/scripts/fleet-checkin.mjs +147 -0
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +6 -1
- package/scripts/lib/autopilot/loop.mjs +10 -6
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +7 -6
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +71 -10
- package/scripts/lib/events.mjs +21 -2
- package/scripts/lib/fleet-protocol.mjs +270 -0
- package/scripts/lib/gates/gate-baseline.mjs +13 -3
- package/scripts/lib/gates/gate-full.mjs +7 -2
- package/scripts/lib/gates/gate-helpers.mjs +93 -2
- package/scripts/lib/gates/gate-incremental.mjs +23 -22
- package/scripts/lib/gates/gate-per-file.mjs +16 -7
- package/scripts/lib/instruction-budget-guard.mjs +69 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
- package/scripts/lib/learnings/io.mjs +154 -26
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +104 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
- package/scripts/lib/orphan-reaper/scan.mjs +506 -0
- package/scripts/lib/orphan-reaper.mjs +43 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/process-group.mjs +62 -11
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +45 -5
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +100 -62
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +3 -4
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +27 -2
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-token-rollup.mjs +245 -52
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/telemetry/pricing.mjs +118 -71
- package/scripts/lib/tests-src-ratio.mjs +140 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/ux-grill/reconcile.mjs +1 -1
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
- package/scripts/lib/vault-status/board-writer.mjs +48 -11
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +10 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
- package/scripts/run-quality-gate.mjs +323 -302
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +56 -5
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +72 -46
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +131 -0
- package/skills/_shared/state-ownership.md +5 -0
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +1 -1
- package/skills/bootstrap/_shared-template.md +1 -1
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/evolve/SKILL.md +2 -2
- package/skills/evolve/references/evolve-analyze-mode.md +10 -6
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/navigator/SKILL.md +193 -0
- package/skills/navigator/references/account-check.sh +87 -0
- package/skills/navigator/references/fleet-decisions.mjs +231 -0
- package/skills/navigator/references/slot-check.sh +114 -0
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/learning-patterns.md +22 -12
- package/skills/session-end/metrics-collection.md +5 -6
- package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +22 -60
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +6 -0
- package/skills/session-start/references/operations-contract.md +4 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -2
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/wave-executor/SKILL.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
- package/skills/wave-executor/references/wave-loop-review.md +3 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- 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-
|
|
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-
|
|
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-
|
|
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 —
|
|
2
|
+
* auto-dream.mjs — Memory-maintenance signals and proposal I/O (issue #502).
|
|
3
3
|
*
|
|
4
|
-
* Provides the
|
|
5
|
-
* /memory-cleanup --dry-run / --apply-pending.
|
|
6
|
-
*
|
|
7
|
-
* atomically
|
|
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-
|
|
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
|
-
|
|
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
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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': '
|
|
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-
|
|
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
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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 `
|
|
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
|
}
|