session-orchestrator 5.3.0 → 5.5.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/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/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +28 -13
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- 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 +162 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +154 -4
- package/docs/components.md +11 -11
- package/docs/events-schema.md +25 -19
- package/docs/install.md +14 -2
- package/docs/migration-v4.md +24 -17
- package/docs/persona-panel.md +5 -4
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +40 -16
- package/docs/session-config-reference.md +61 -12
- package/docs/session-config-template.md +11 -2
- package/docs/vault-docs-architecture.md +8 -4
- package/hooks/_lib/hook-import-set.json +42 -29
- package/hooks/_lib/lock-bootstrap.mjs +118 -26
- package/hooks/config-protection.mjs +1 -1
- package/hooks/enforce-commands.mjs +28 -13
- package/hooks/enforce-scope.mjs +201 -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 +57 -2
- package/hooks/on-stop.mjs +70 -168
- package/hooks/post-bash-write-verify.mjs +98 -17
- package/hooks/post-edit-validate.mjs +15 -5
- package/hooks/post-tool-batch-wave-signal.mjs +32 -152
- package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-bash-staging-fence.mjs +29 -8
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +1143 -279
- package/hooks/subagent-telemetry.mjs +96 -19
- package/hooks/wave-scope-commit-guard.mjs +168 -72
- package/package.json +2 -6
- 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 +513 -0
- package/scripts/archive-closed-prds.mjs +40 -12
- package/scripts/autopilot.mjs +57 -54
- package/scripts/backfill-abandoned-sessions.mjs +81 -6
- package/scripts/backfill-learnings-from-vault.mjs +49 -22
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/codex-install.mjs +3 -0
- package/scripts/cursor-install.mjs +47 -6
- package/scripts/emit-session.mjs +298 -2
- package/scripts/export-hw-learnings.mjs +40 -10
- package/scripts/fleet-checkin.mjs +170 -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 +32 -13
- package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/blocked-commands-policy.mjs +40 -0
- package/scripts/lib/common.mjs +41 -0
- 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/vault-integration.mjs +151 -16
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +75 -10
- package/scripts/lib/eval/engine.mjs +13 -0
- package/scripts/lib/events-retention-banner.mjs +393 -0
- package/scripts/lib/events-rotation.mjs +21 -6
- package/scripts/lib/events.mjs +72 -5
- package/scripts/lib/fleet-protocol.mjs +288 -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/hardening.mjs +1 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
- package/scripts/lib/instruction-budget-guard.mjs +77 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
- package/scripts/lib/learnings/io.mjs +274 -37
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/lock-reaper.mjs +38 -14
- package/scripts/lib/maintenance-due-banner.mjs +23 -8
- 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 +147 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
- package/scripts/lib/orphan-reaper/scan.mjs +510 -0
- package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
- package/scripts/lib/orphan-reaper.mjs +47 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/platform.mjs +206 -2
- package/scripts/lib/plugin-update-banner.mjs +3 -1
- package/scripts/lib/process-group.mjs +134 -22
- 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 +119 -29
- package/scripts/lib/scope-echo.mjs +7 -0
- package/scripts/lib/scope-gate.mjs +194 -27
- package/scripts/lib/session-close-backfill.mjs +129 -4
- package/scripts/lib/session-end/tail-runner.mjs +10 -5
- package/scripts/lib/session-lock-shape.mjs +6 -3
- package/scripts/lib/session-lock.mjs +320 -67
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +67 -17
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-start-probes.mjs +242 -68
- package/scripts/lib/session-token-rollup.mjs +252 -54
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sessions-staleness-banner.mjs +3 -1
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +27 -3
- package/scripts/lib/telemetry/pricing.mjs +121 -71
- package/scripts/lib/telemetry/sync.mjs +17 -0
- package/scripts/lib/tests-src-ratio.mjs +215 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
- 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 +388 -27
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
- package/scripts/lib/vault-mirror/namespace.mjs +47 -26
- package/scripts/lib/vault-mirror/process.mjs +350 -62
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
- package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
- package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
- package/scripts/lib/vault-status/board-writer.mjs +53 -12
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- package/scripts/lib/vcs-repo-spec.mjs +8 -0
- 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/materialize-wave-scope.mjs +7 -4
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +11 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
- package/scripts/run-quality-gate.mjs +331 -307
- package/scripts/self-update.mjs +229 -0
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +603 -20
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/validate-wave-scope.mjs +21 -2
- package/scripts/vault-backfill.mjs +50 -37
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +238 -75
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +134 -0
- package/skills/_shared/state-ownership.md +41 -12
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +6 -4
- package/skills/bootstrap/_shared-template.md +97 -2
- package/skills/bootstrap/fast-template.md +4 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/bootstrap/standard-template.md +2 -0
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
- package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
- package/skills/discovery/probes/vault-staleness.mjs +9 -3
- package/skills/docs-orchestrator/SKILL.md +12 -5
- package/skills/docs-orchestrator/audience-mapping.md +5 -3
- package/skills/evolve/SKILL.md +13 -4
- package/skills/evolve/references/evolve-analyze-mode.md +42 -18
- package/skills/memory-cleanup/SKILL.md +1 -1
- 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 +23 -13
- package/skills/session-end/metrics-collection.md +6 -7
- package/skills/session-end/phase-3-2-docs-verification.md +7 -6
- package/skills/session-end/plan-verification.md +25 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
- 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 +65 -68
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +8 -2
- package/skills/session-start/references/operations-contract.md +6 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
- 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 +10 -3
- 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/vault-mirror/SKILL.md +21 -11
- package/skills/wave-executor/SKILL.md +2 -2
- package/skills/wave-executor/circuit-breaker.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
- package/skills/wave-executor/references/wave-loop-review.md +4 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -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,170 @@
|
|
|
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
|
|
20
|
+
* `{ ok, path, navigator_state, navigator_adresse, fallback_due_at, event }`.
|
|
21
|
+
* `navigator_adresse` is the active lease's `adresse` exactly as
|
|
22
|
+
* `readNavigatorLease()` validated it (a ListAgents peer name), else `null` — no
|
|
23
|
+
* active lease, no address in it, or one the validator refused. It is the only
|
|
24
|
+
* value the caller may address a hint to; an unvalidated string never reaches it.
|
|
25
|
+
* `fallback_due_at` is `fallbackDueAt(zeit, navigator_state)`: when the
|
|
26
|
+
* Standard-Auflagen apply if no `auflagen/` file has appeared by then. It is
|
|
27
|
+
* stdout only — the check-in FILE schema, which the navigator reads, is unchanged.
|
|
28
|
+
*
|
|
29
|
+
* Exit codes:
|
|
30
|
+
* 0 — check-in written (an event failure only WARNs; `event: false` on stdout)
|
|
31
|
+
* 1 — write error
|
|
32
|
+
* 2 — invalid input (not JSON, not an object, or validation errors; no file)
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { randomBytes } from 'node:crypto';
|
|
36
|
+
import { promises as fs } from 'node:fs';
|
|
37
|
+
import os from 'node:os';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import { emitEvent } from './lib/events.mjs';
|
|
40
|
+
import {
|
|
41
|
+
checkinPath,
|
|
42
|
+
fallbackDueAt,
|
|
43
|
+
navigatorDir,
|
|
44
|
+
readNavigatorLease,
|
|
45
|
+
utcSecondsTimestamp,
|
|
46
|
+
validateCheckin,
|
|
47
|
+
} from './lib/fleet-protocol.mjs';
|
|
48
|
+
import { isMainModule } from './lib/is-main-module.mjs';
|
|
49
|
+
|
|
50
|
+
const PREFIX = 'fleet-checkin:';
|
|
51
|
+
|
|
52
|
+
/** @returns {Promise<string>} */
|
|
53
|
+
async function readStdin() {
|
|
54
|
+
let data = '';
|
|
55
|
+
process.stdin.setEncoding('utf8');
|
|
56
|
+
for await (const chunk of process.stdin) data += chunk;
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create `<navigatorDir>/checkin/` (0700) and write `record` atomically to
|
|
62
|
+
* `target` (0600). Refuses a `checkin/` that is not a real directory, so a
|
|
63
|
+
* planted symlink cannot redirect the chmod or the write.
|
|
64
|
+
* @param {string} target
|
|
65
|
+
* @param {object} record
|
|
66
|
+
*/
|
|
67
|
+
async function writeCheckinFile(target, record) {
|
|
68
|
+
const checkinDir = path.dirname(target);
|
|
69
|
+
await fs.mkdir(navigatorDir(), { recursive: true, mode: 0o700 });
|
|
70
|
+
await fs.mkdir(checkinDir, { recursive: true, mode: 0o700 });
|
|
71
|
+
const st = await fs.lstat(checkinDir);
|
|
72
|
+
if (!st.isDirectory()) throw new Error(`${checkinDir} is not a directory`);
|
|
73
|
+
// mkdir's mode does not apply to a directory that already existed.
|
|
74
|
+
await fs.chmod(checkinDir, 0o700);
|
|
75
|
+
|
|
76
|
+
const tmp = path.join(checkinDir, `.${path.basename(target)}.${randomBytes(6).toString('hex')}.tmp`);
|
|
77
|
+
try {
|
|
78
|
+
// `wx`: never follow or reuse a pre-existing path at the tmp name.
|
|
79
|
+
await fs.writeFile(tmp, JSON.stringify(record, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
|
|
80
|
+
// The creation mode is filtered through the umask; pin it explicitly.
|
|
81
|
+
await fs.chmod(tmp, 0o600);
|
|
82
|
+
await fs.rename(tmp, target);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
await fs.rm(tmp, { force: true }).catch(() => {});
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @returns {Promise<number>} exit code */
|
|
90
|
+
async function main() {
|
|
91
|
+
let input;
|
|
92
|
+
try {
|
|
93
|
+
input = JSON.parse(await readStdin());
|
|
94
|
+
} catch {
|
|
95
|
+
process.stderr.write(`${PREFIX} stdin is not valid JSON\n`);
|
|
96
|
+
return 2;
|
|
97
|
+
}
|
|
98
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
99
|
+
process.stderr.write(`${PREFIX} stdin must be a JSON object\n`);
|
|
100
|
+
return 2;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const record = { ...input, zeit: utcSecondsTimestamp() };
|
|
104
|
+
const errors = validateCheckin(record);
|
|
105
|
+
if (errors.length > 0) {
|
|
106
|
+
for (const e of errors) process.stderr.write(`${PREFIX} ${e}\n`);
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const target = checkinPath(record.session);
|
|
111
|
+
try {
|
|
112
|
+
await writeCheckinFile(target, record);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
process.stderr.write(`${PREFIX} write failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const navigator = await readNavigatorLease();
|
|
119
|
+
const navigatorState = navigator.state;
|
|
120
|
+
// readNavigatorLease() returns a lease only when it passed validation, its
|
|
121
|
+
// `adresse` included (an invalid one makes the whole lease `unreadable`).
|
|
122
|
+
const navigatorAdresse =
|
|
123
|
+
navigator.state === 'active' && typeof navigator.lease.adresse === 'string' ? navigator.lease.adresse : null;
|
|
124
|
+
|
|
125
|
+
let event = true;
|
|
126
|
+
try {
|
|
127
|
+
const repoRoot = (process.env.CLAUDE_PROJECT_DIR || '').trim() || process.cwd();
|
|
128
|
+
await emitEvent(
|
|
129
|
+
'orchestrator.fleet.checkin',
|
|
130
|
+
{
|
|
131
|
+
session: record.session,
|
|
132
|
+
repo: record.repo,
|
|
133
|
+
modus: record.modus,
|
|
134
|
+
kandidaten: record.kandidaten,
|
|
135
|
+
navigator_state: navigatorState,
|
|
136
|
+
},
|
|
137
|
+
{ repoRoot },
|
|
138
|
+
);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
event = false;
|
|
141
|
+
process.stderr.write(
|
|
142
|
+
`${PREFIX} WARN: check-in written but event emission failed: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Report the path with `~` for the home prefix: stdout lands in transcripts.
|
|
147
|
+
const home = os.homedir();
|
|
148
|
+
const shownPath = target.startsWith(`${home}${path.sep}`) ? `~${target.slice(home.length)}` : target;
|
|
149
|
+
process.stdout.write(
|
|
150
|
+
JSON.stringify({
|
|
151
|
+
ok: true,
|
|
152
|
+
path: shownPath,
|
|
153
|
+
navigator_state: navigatorState,
|
|
154
|
+
navigator_adresse: navigatorAdresse,
|
|
155
|
+
fallback_due_at: fallbackDueAt(record.zeit, navigatorState),
|
|
156
|
+
event,
|
|
157
|
+
}) + '\n',
|
|
158
|
+
);
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (isMainModule(import.meta.url)) {
|
|
163
|
+
main().then(
|
|
164
|
+
(code) => { process.exitCode = code; },
|
|
165
|
+
(err) => {
|
|
166
|
+
process.stderr.write(`${PREFIX} ${err instanceof Error ? err.message : String(err)}\n`);
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -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. */
|
|
@@ -38,6 +38,7 @@ function clampNumber(value, { min, max, fallback }) {
|
|
|
38
38
|
* @property {string|null} kill_switch @property {string|null} kill_switch_detail @property {string[]} sessions
|
|
39
39
|
* @property {string|null} host_class @property {string|null} resource_verdict_at_start
|
|
40
40
|
* @property {boolean} fallback_to_manual @property {boolean} dry_run @property {number} total_tokens_used
|
|
41
|
+
* @property {number} tokens_unknown_sessions — #1487 Pkt 4: iterations whose own record carried no token figure; > 0 makes total_tokens_used a lower bound
|
|
41
42
|
* @property {string|null} worktree_path @property {string|null} parent_run_id @property {number} stall_recovery_count
|
|
42
43
|
* @property {number|null} blocked_by_issue — Phase D (#341): issue number this loop was waiting on (OPEN-4 commit-deps); null when not blocked
|
|
43
44
|
* @property {boolean} aborted_by_cohort — Phase D (#341): set by cohort-abort policy (OPEN-5); mutated by W3 P5 orchestration-glue, not by runLoop itself
|
|
@@ -131,11 +132,19 @@ export async function runLoop(opts = {}) {
|
|
|
131
132
|
confidence_threshold: confidenceThreshold,
|
|
132
133
|
// Persisted for the same reason as the three above (HR-105): a switch whose
|
|
133
134
|
// input is never recorded cannot be falsified afterwards. `0` = disabled.
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
135
|
+
// Live path (#1436): scripts/emit-session.mjs merges the subagents.jsonl
|
|
136
|
+
// rollup (`total_token_output`, `total_tokens`) into each session record,
|
|
137
|
+
// scripts/autopilot.mjs selects the unique canonical record whose
|
|
138
|
+
// raw_session_id equals the child's --session-id UUID and projects its
|
|
139
|
+
// totals as `usage`; the accumulator sums them into `total_tokens_used`.
|
|
140
|
+
// Zero or multiple matches throw → FAILED_WAVE, never foreign token usage.
|
|
141
|
+
// CEILING: an own record without token totals adds 0 — the budget
|
|
142
|
+
// undercounts, it never over-fires. Such sessions are counted in
|
|
143
|
+
// `tokens_unknown_sessions` (#1487 Pkt 4), and the switch still fires on the
|
|
144
|
+
// known lower bound: every fire stays correct, none is lost. Failing closed on
|
|
145
|
+
// an unknown session instead would stop an armed run on every
|
|
146
|
+
// coordinator-direct session (no subagents → no rollup totals); revisit if
|
|
147
|
+
// the session record ever carries the coordinator's own usage.
|
|
139
148
|
max_tokens: opts.maxTokens ?? 0,
|
|
140
149
|
iterations_completed: 0,
|
|
141
150
|
kill_switch: null,
|
|
@@ -146,6 +155,9 @@ export async function runLoop(opts = {}) {
|
|
|
146
155
|
fallback_to_manual: false,
|
|
147
156
|
dry_run: dryRun,
|
|
148
157
|
total_tokens_used: 0,
|
|
158
|
+
// #1487 Pkt 4 — completed iterations whose own record carried no token
|
|
159
|
+
// figure; non-zero means `total_tokens_used` is a lower bound.
|
|
160
|
+
tokens_unknown_sessions: 0,
|
|
149
161
|
// ADR-364 additive fields — forward-compat; callers may omit, defaults are null/0.
|
|
150
162
|
worktree_path: opts.worktreePath ?? null,
|
|
151
163
|
parent_run_id: opts.parentRunId ?? null,
|
|
@@ -271,14 +283,21 @@ export async function runLoop(opts = {}) {
|
|
|
271
283
|
}
|
|
272
284
|
state.iterations_completed += 1;
|
|
273
285
|
|
|
274
|
-
// Accumulate output tokens
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
286
|
+
// Accumulate own-record output tokens (total_tokens fallback). An unassignable
|
|
287
|
+
// child record already threw → FAILED_WAVE above. An own record carrying
|
|
288
|
+
// neither figure has UNKNOWN usage, not 0 (#1487 Pkt 4): it adds nothing, so
|
|
289
|
+
// `total_tokens_used` is a lower bound, and it is counted in
|
|
290
|
+
// `tokens_unknown_sessions` so the record says how much the sum cannot see.
|
|
291
|
+
const usage = sessionResult?.usage;
|
|
292
|
+
const rawTokens = usage !== null && typeof usage === 'object'
|
|
293
|
+
? (usage.output_tokens ?? usage.total_tokens)
|
|
294
|
+
: undefined;
|
|
295
|
+
// `Number(null)` is 0 — a null figure is a missing one, never a measured 0.
|
|
296
|
+
const outTokens = rawTokens === null || rawTokens === undefined ? NaN : Number(rawTokens);
|
|
297
|
+
if (Number.isFinite(outTokens) && outTokens >= 0) {
|
|
298
|
+
cumulativeTokens += outTokens;
|
|
299
|
+
} else {
|
|
300
|
+
state.tokens_unknown_sessions += 1;
|
|
282
301
|
}
|
|
283
302
|
state.total_tokens_used = cumulativeTokens;
|
|
284
303
|
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* `hooks/on-stop.mjs` refreshes it at every turn-end and
|
|
9
9
|
* `hooks/post-tool-batch-wave-signal.mjs` at every tool batch.
|
|
10
10
|
* 2. `autopilot.jsonl` mtime — the legacy marker, used only when no lock path
|
|
11
|
-
* is supplied or the lock is absent/corrupt/
|
|
11
|
+
* is supplied or the lock is absent/corrupt/heartbeat-less (`started_at`
|
|
12
|
+
* is never a stand-in — it does not advance during a run).
|
|
12
13
|
*
|
|
13
14
|
* The header of this module used to claim `autopilot.jsonl` carries "one record
|
|
14
15
|
* per session". It does not: `telemetry.mjs` writes ONE record per /autopilot
|
|
@@ -74,11 +75,13 @@ function readHeartbeatMs(sessionLockPath) {
|
|
|
74
75
|
return null;
|
|
75
76
|
}
|
|
76
77
|
if (typeof lock !== 'object' || lock === null) return null;
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
// `last_heartbeat` only — no `started_at` stand-in, matching `isLockLive()`
|
|
79
|
+
// since the #595 sunset (2026-10-02). `started_at` never advances during a
|
|
80
|
+
// run, so a lock without a heartbeat would read as a stall once 600 s passed
|
|
81
|
+
// (or as fresh within 30 s of its genesis) — a measurement the lock never
|
|
82
|
+
// made. No heartbeat → `null` → the mtime marker, exactly as for a lock
|
|
83
|
+
// with no timestamp at all.
|
|
84
|
+
const raw = lock.last_heartbeat;
|
|
82
85
|
if (typeof raw !== 'string' || raw.length === 0) return null;
|
|
83
86
|
const ms = Date.parse(raw);
|
|
84
87
|
return Number.isFinite(ms) ? ms : null;
|
|
@@ -95,9 +98,10 @@ function readHeartbeatMs(sessionLockPath) {
|
|
|
95
98
|
* @param {string} [opts.autopilotJsonlPath] — path to autopilot.jsonl (default
|
|
96
99
|
* '.orchestrator/metrics/autopilot.jsonl').
|
|
97
100
|
* @param {string} [opts.sessionLockPath] — path to `session.lock`. When supplied
|
|
98
|
-
* and readable, its `last_heartbeat`
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
+
* and readable, its `last_heartbeat` REPLACES the mtime marker. A lock
|
|
102
|
+
* without a parsable `last_heartbeat` contributes nothing (no `started_at`
|
|
103
|
+
* fallback, as in `session-lock.mjs` `isLockLive()`) and the mtime marker is
|
|
104
|
+
* used. Omitted → mtime only, which is what every pre-existing caller gets.
|
|
101
105
|
* @param {number} [opts.stallTimeoutSeconds] — kill-switch threshold (default
|
|
102
106
|
* 600). NOT range-clamped here; caller is responsible for bounds.
|
|
103
107
|
* @param {() => number} [opts.nowMs] — wall-clock supplier (default Date.now).
|
|
@@ -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)) {
|
|
@@ -198,6 +198,46 @@ export function mergePolicies(floor, overlay) {
|
|
|
198
198
|
return { rules: merged, warnings };
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Wave-manifest projection
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The patterns a wave manifest's `blockedCommands` may carry (#1509): the
|
|
207
|
+
* `pattern` of every `severity: 'block'` rule that has NO `type`.
|
|
208
|
+
*
|
|
209
|
+
* A TYPED rule's `pattern` is not a command pattern. `redirect-truncate` rules
|
|
210
|
+
* carry `pattern: ">"` and `path-delete` rules a free-text label; both are
|
|
211
|
+
* decided by their TARGET, type-aware, in hooks/pre-bash-destructive-guard.mjs.
|
|
212
|
+
* `hooks/enforce-commands.mjs` reads every `blockedCommands` entry as a command
|
|
213
|
+
* pattern, so the flattened `">"` denied interpreter payloads such as
|
|
214
|
+
* `bash -c "echo hi > /tmp/x"` (measured 2026-10-03 @ 8292050e). An untyped
|
|
215
|
+
* rule is the one class whose pattern means the same thing to both hooks.
|
|
216
|
+
*
|
|
217
|
+
* An UNKNOWN `type` is excluded as well: its pattern's meaning is unknown, and
|
|
218
|
+
* the destructive guard still evaluates the rule on every Bash call, so leaving
|
|
219
|
+
* it out of the manifest drops no protection — admitting it repeats #1509 for
|
|
220
|
+
* the next rule type.
|
|
221
|
+
*
|
|
222
|
+
* Total: a non-array `rules` (e.g. `loadEffectivePolicy()`'s `null`) yields `[]`.
|
|
223
|
+
*
|
|
224
|
+
* @param {unknown} rules - the `rules` of {@link loadEffectivePolicy}
|
|
225
|
+
* @returns {string[]}
|
|
226
|
+
*/
|
|
227
|
+
export function blockedCommandPatterns(rules) {
|
|
228
|
+
if (!Array.isArray(rules)) return [];
|
|
229
|
+
return rules
|
|
230
|
+
.filter(
|
|
231
|
+
(r) =>
|
|
232
|
+
r !== null &&
|
|
233
|
+
typeof r === 'object' &&
|
|
234
|
+
r.severity === 'block' &&
|
|
235
|
+
(r.type ?? null) === null &&
|
|
236
|
+
typeof r.pattern === 'string',
|
|
237
|
+
)
|
|
238
|
+
.map((r) => r.pattern);
|
|
239
|
+
}
|
|
240
|
+
|
|
201
241
|
// ---------------------------------------------------------------------------
|
|
202
242
|
// Load (with per-path mtime cache)
|
|
203
243
|
// ---------------------------------------------------------------------------
|
package/scripts/lib/common.mjs
CHANGED
|
@@ -54,6 +54,47 @@ export function expandTilde(p) {
|
|
|
54
54
|
return p;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Replace the current user's home directory with `~` wherever it occurs in
|
|
59
|
+
* `text` — the inverse of {@link expandTilde}, for free text as well as a bare
|
|
60
|
+
* path: an `err.message` from `node:fs` (`ENOENT: …, open '/Users/bob/v/x.md'`),
|
|
61
|
+
* git stderr, a resolved vault dir. Every occurrence is replaced.
|
|
62
|
+
*
|
|
63
|
+
* Log lines of the vault writers and the session-end shell land in CI logs and
|
|
64
|
+
* session transcripts, where the home dir is an owner-privacy leak (#1490,
|
|
65
|
+
* `.claude/rules/security.md` § Owner-Privacy).
|
|
66
|
+
*
|
|
67
|
+
* Boundary-aware on both sides. On the right the home dir must end at a path
|
|
68
|
+
* separator, the end of the text, whitespace, a quote/bracket/list delimiter
|
|
69
|
+
* (`'"\`)]},;:`) or a sentence-final `.`, so `/Users/bob` never rewrites
|
|
70
|
+
* `/Users/bobby/x` or `/Users/bob.old`. On the left it must not continue a
|
|
71
|
+
* path segment, so `/data/Users/bob` stays. A home dir that is empty, the
|
|
72
|
+
* filesystem root or a bare drive root is a no-op — collapsing it would
|
|
73
|
+
* rewrite every absolute path. Non-strings pass through unchanged.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} text
|
|
76
|
+
* @param {string} [home] — defaults to `os.homedir()`; an unresolvable home
|
|
77
|
+
* leaves `text` unchanged (a log redactor must never turn an error path into
|
|
78
|
+
* a crash)
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
export function redactHomeDir(text, home) {
|
|
82
|
+
if (typeof text !== 'string' || text.length === 0) return text;
|
|
83
|
+
let h = home;
|
|
84
|
+
if (h === undefined) {
|
|
85
|
+
try {
|
|
86
|
+
h = os.homedir();
|
|
87
|
+
} catch {
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
h = String(h ?? '').replace(/[/\\]+$/, '');
|
|
92
|
+
if (h.length === 0 || /^[a-z]:$/i.test(h)) return text;
|
|
93
|
+
const escaped = h.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
const re = new RegExp(`(?<![\\w.~-])${escaped}(?=$|[/\\\\\\s'"\`)\\]},;:]|\\.(?:$|\\s))`, 'g');
|
|
95
|
+
return text.replace(re, '~');
|
|
96
|
+
}
|
|
97
|
+
|
|
57
98
|
// ---------------------------------------------------------------------------
|
|
58
99
|
// Timestamp helpers
|
|
59
100
|
// ---------------------------------------------------------------------------
|