session-orchestrator 5.4.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/060-evolve.mdc +5 -5
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +95 -3
- package/README.md +11 -11
- package/docs/ci-setup.md +120 -23
- package/docs/components.md +7 -7
- package/docs/events-schema.md +17 -14
- package/docs/install.md +14 -2
- package/docs/migration-v4.md +24 -17
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/scope-collision-guard.md +15 -13
- package/docs/session-config-reference.md +7 -7
- package/docs/vault-docs-architecture.md +8 -4
- package/hooks/_lib/hook-import-set.json +17 -8
- 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.json +1 -1
- package/hooks/on-session-start.mjs +4 -2
- package/hooks/on-stop.mjs +70 -170
- 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 -154
- package/hooks/pre-bash-memory-propose-audit.mjs +19 -7
- package/hooks/pre-bash-staging-fence.mjs +29 -8
- package/hooks/pre-task-scope-disjoint.mjs +584 -207
- package/hooks/wave-scope-commit-guard.mjs +168 -72
- package/package.json +2 -6
- package/scripts/apply-session-learnings.mjs +35 -14
- package/scripts/archive-closed-prds.mjs +38 -11
- package/scripts/autopilot.mjs +8 -1
- package/scripts/backfill-abandoned-sessions.mjs +80 -5
- package/scripts/backfill-learnings-from-vault.mjs +49 -22
- package/scripts/codex-install.mjs +3 -0
- package/scripts/cursor-install.mjs +46 -2
- package/scripts/emit-session.mjs +5 -3
- package/scripts/export-hw-learnings.mjs +18 -7
- package/scripts/fleet-checkin.mjs +26 -3
- package/scripts/lib/autopilot/loop.mjs +25 -10
- package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
- package/scripts/lib/blocked-commands-policy.mjs +40 -0
- package/scripts/lib/common.mjs +41 -0
- package/scripts/lib/config/vault-integration.mjs +151 -16
- package/scripts/lib/config.mjs +5 -1
- 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 +51 -3
- package/scripts/lib/fleet-protocol.mjs +20 -2
- package/scripts/lib/hardening.mjs +1 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
- package/scripts/lib/instruction-budget-guard.mjs +8 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +387 -20
- package/scripts/lib/learnings/io.mjs +125 -16
- package/scripts/lib/lock-reaper.mjs +38 -14
- package/scripts/lib/maintenance-due-banner.mjs +23 -8
- package/scripts/lib/named-vault-resolver.mjs +48 -5
- package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +101 -69
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +79 -27
- package/scripts/lib/orphan-reaper/scan.mjs +4 -0
- package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
- package/scripts/lib/orphan-reaper.mjs +5 -1
- package/scripts/lib/platform.mjs +206 -2
- package/scripts/lib/plugin-update-banner.mjs +3 -1
- package/scripts/lib/process-group.mjs +83 -22
- package/scripts/lib/scope-baseline.mjs +116 -25
- 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-registry.mjs +41 -16
- package/scripts/lib/session-start-probes.mjs +242 -68
- package/scripts/lib/session-token-rollup.mjs +39 -34
- package/scripts/lib/sessions-staleness-banner.mjs +3 -1
- package/scripts/lib/tail-window.mjs +27 -3
- package/scripts/lib/telemetry/pricing.mjs +5 -2
- package/scripts/lib/telemetry/sync.mjs +17 -0
- package/scripts/lib/tests-src-ratio.mjs +77 -2
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +320 -32
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
- 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 +18 -11
- package/scripts/lib/vault-status/board-writer.mjs +5 -1
- package/scripts/lib/vcs-repo-spec.mjs +8 -0
- package/scripts/materialize-wave-scope.mjs +7 -4
- package/scripts/release.mjs +1 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +14 -2
- package/scripts/run-quality-gate.mjs +8 -5
- package/scripts/self-update.mjs +229 -0
- package/scripts/sweep-expired-learnings.mjs +560 -28
- package/scripts/validate-wave-scope.mjs +21 -2
- package/scripts/vault-backfill.mjs +50 -37
- package/scripts/vault-mirror.mjs +170 -33
- package/skills/_shared/fleet-protocol.md +74 -71
- package/skills/_shared/state-ownership.md +38 -14
- package/skills/autopilot/SKILL.md +1 -1
- package/skills/bootstrap/SKILL.md +5 -3
- package/skills/bootstrap/_shared-template.md +96 -1
- package/skills/bootstrap/fast-template.md +3 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
- package/skills/bootstrap/standard-template.md +2 -0
- 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 +11 -2
- package/skills/evolve/references/evolve-analyze-mode.md +37 -17
- package/skills/session-end/learning-patterns.md +3 -3
- package/skills/session-end/metrics-collection.md +1 -1
- 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 +11 -1
- package/skills/session-end/session-metrics-write.md +45 -10
- package/skills/session-start/SKILL.md +3 -3
- package/skills/session-start/references/operations-contract.md +5 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
- package/skills/session-start/references/phase-4-ssot-environment-check.md +8 -1
- package/skills/vault-mirror/SKILL.md +21 -11
- package/skills/wave-executor/SKILL.md +1 -1
- package/skills/wave-executor/circuit-breaker.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +1 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
- package/skills/wave-executor/references/wave-loop-review.md +2 -2
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
- package/.agents/skills/navigator/SKILL.md +0 -22
- package/.agents/skills/navigator/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/navigator/SKILL.md +0 -22
- package/.codex-plugin/skills/navigator/agents/openai.yaml +0 -5
- package/.cursor/commands/navigator.md +0 -15
- package/.cursor/skills/navigator/SKILL.md +0 -13
- package/pi/prompts/navigator.md +0 -12
- package/skills/navigator/SKILL.md +0 -193
- package/skills/navigator/references/account-check.sh +0 -87
- package/skills/navigator/references/fleet-decisions.mjs +0 -231
- package/skills/navigator/references/slot-check.sh +0 -114
package/hooks/enforce-scope.mjs
CHANGED
|
@@ -107,7 +107,12 @@ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
|
107
107
|
let isPathInside;
|
|
108
108
|
let relativeFromRoot;
|
|
109
109
|
let resolveProjectDir;
|
|
110
|
-
let
|
|
110
|
+
let resolveSessionRoot;
|
|
111
|
+
// The launch dir the harness states — platform.mjs's one definition of the env
|
|
112
|
+
// chain, so the relocation trace below cannot drift from the clamp (#1504).
|
|
113
|
+
let launchDirFromEnv;
|
|
114
|
+
// #1504 point 6 — the manifest that governs THIS session; bound from scope-gate below.
|
|
115
|
+
let findOwnScopeFile;
|
|
111
116
|
let pathMatchesPattern;
|
|
112
117
|
let suggestForScopeViolation;
|
|
113
118
|
let readJson;
|
|
@@ -197,8 +202,8 @@ async function bootstrap() {
|
|
|
197
202
|
|
|
198
203
|
({ readStdin, emitAllow, emitDeny, emitWarn } = modules.io);
|
|
199
204
|
({ isPathInside, relativeFromRoot } = modules.pathUtils);
|
|
200
|
-
({ resolveProjectDir } = modules.platform);
|
|
201
|
-
({
|
|
205
|
+
({ resolveProjectDir, resolveSessionRoot, launchDirFromEnv } = modules.platform);
|
|
206
|
+
({ pathMatchesPattern, suggestForScopeViolation } = modules.hardening);
|
|
202
207
|
({ readJson } = modules.common);
|
|
203
208
|
({
|
|
204
209
|
classifyEmptyScope,
|
|
@@ -206,6 +211,7 @@ async function bootstrap() {
|
|
|
206
211
|
sessionStartedAtMs,
|
|
207
212
|
gradeScopeEntry,
|
|
208
213
|
canonicalizeGrantPrefix,
|
|
214
|
+
findOwnScopeFile,
|
|
209
215
|
} = modules.scopeGate);
|
|
210
216
|
({ readProcessLocalSessionIds, classifyManifestSession } = modules.sessionIdentity);
|
|
211
217
|
}
|
|
@@ -224,7 +230,13 @@ async function main() {
|
|
|
224
230
|
// Gate 2: file_path must be a non-empty string
|
|
225
231
|
if (!filePath || typeof filePath !== 'string') return emitAllow();
|
|
226
232
|
|
|
227
|
-
|
|
233
|
+
// #1492 — the SESSION's working copy, not the launch dir: after
|
|
234
|
+
// `EnterWorktree`, `$CLAUDE_PROJECT_DIR` still names the dir the session was
|
|
235
|
+
// launched in, while the manifest lives in the worktree. Never above the launch
|
|
236
|
+
// dir; a harness subagent worktree (`isolation: "worktree"`) is lifted to the
|
|
237
|
+
// launch checkout it sits under — still unenforced in an entered-worktree
|
|
238
|
+
// session (named ceiling in `resolveSessionRoot`, scripts/lib/platform.mjs).
|
|
239
|
+
const projectRootRaw = resolveSessionRoot(input.cwd);
|
|
228
240
|
|
|
229
241
|
// Resolve symlinks in the project root itself so that realpath(file) comparisons
|
|
230
242
|
// are consistent. On macOS /tmp → /private/tmp; mismatches would cause false denials.
|
|
@@ -235,9 +247,26 @@ async function main() {
|
|
|
235
247
|
projectRoot = projectRootRaw;
|
|
236
248
|
}
|
|
237
249
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
250
|
+
// `new Set(...)` is load-bearing: `readProcessLocalSessionIds` returns a
|
|
251
|
+
// string[], and `classifyManifestSession` does `ownIds instanceof Set ?
|
|
252
|
+
// ownIds : new Set()` — a bare array would silently become EMPTY, making
|
|
253
|
+
// every manifest read `unknown`. Process-local only (#1194, see Gate 3b).
|
|
254
|
+
const ownIds = new Set(readProcessLocalSessionIds({ hookInput: input }));
|
|
255
|
+
|
|
256
|
+
// Gate 3: no wave-scope.json → nothing to enforce.
|
|
257
|
+
//
|
|
258
|
+
// #1504 point 6 — the first manifest that is not provably a peer's, in the
|
|
259
|
+
// `.pi` > `.cursor` > `.codex` > `.claude` order. `findScopeFile` stopped at
|
|
260
|
+
// the first file that exists, so a peer's `.pi`/`.cursor` manifest made Gate
|
|
261
|
+
// 3b stand down and this session's own `.claude/wave-scope.json` was never
|
|
262
|
+
// read. Only when EVERY candidate is foreign does the first of them come back
|
|
263
|
+
// (`foreignPath`), so Gate 3b's stand-down below stays unchanged.
|
|
264
|
+
const located = findOwnScopeFile(projectRoot, ownIds, classifyManifestSession);
|
|
265
|
+
const scopePath = located.path ?? located.foreignPath;
|
|
266
|
+
if (!scopePath) {
|
|
267
|
+
await recordHiddenManifestOncePerWave({ projectRootRaw, projectRoot, ownIds, filePath });
|
|
268
|
+
return emitAllow();
|
|
269
|
+
}
|
|
241
270
|
|
|
242
271
|
// SECURITY-REQ-08: read scope file once; pass parsed object to all subsequent checks
|
|
243
272
|
//
|
|
@@ -305,11 +334,7 @@ async function main() {
|
|
|
305
334
|
// `enforcement: "off"` already grants in the same file — this gate adds no new
|
|
306
335
|
// authority, and the manifest is the coordinator's own artefact either way.
|
|
307
336
|
{
|
|
308
|
-
// `
|
|
309
|
-
// string[], and `classifyManifestSession` does `ownIds instanceof Set ?
|
|
310
|
-
// ownIds : new Set()` — a bare array would silently become EMPTY, making
|
|
311
|
-
// every manifest read `unknown`.
|
|
312
|
-
const ownIds = new Set(readProcessLocalSessionIds({ hookInput: input }));
|
|
337
|
+
// `ownIds` is the process-local set built above Gate 3.
|
|
313
338
|
const { verdict, manifestIds } = classifyManifestSession(scope, ownIds);
|
|
314
339
|
if (verdict === 'foreign') {
|
|
315
340
|
// Observability only, and deliberately NOT emitWarn: this branch is hit on
|
|
@@ -489,6 +514,7 @@ async function main() {
|
|
|
489
514
|
if (matchedGrant !== null) {
|
|
490
515
|
const grade = gradeScopeEntry(matchedGrant, { resolve: canonicalizeGrantPrefix });
|
|
491
516
|
if (grade?.verdict === 'error') {
|
|
517
|
+
await recordRefusedGrantOncePerWave({ scope, scopePath, projectRoot, input, grant: matchedGrant, grade, filePath: resolvedPath });
|
|
492
518
|
return emitWarn(
|
|
493
519
|
`Gate 5b honoured an out-of-repo grant that scripts/validate-wave-scope.mjs would REFUSE ` +
|
|
494
520
|
`before dispatch — allowedPaths ${grade.message}. The write to '${resolvedPath}' is ` +
|
|
@@ -538,7 +564,9 @@ async function main() {
|
|
|
538
564
|
// dispatched wave agent's. See {@link classifyCaller} for the measurement.
|
|
539
565
|
// A subagent falls through to the gates below — under a manifest that does
|
|
540
566
|
// not grant the path (Discovery's `allowedPaths: []`) that is a DENY.
|
|
541
|
-
|
|
567
|
+
// #1492: the LAUNCH dir too — the harness keys auto-memory on the path it
|
|
568
|
+
// was launched with, which after `EnterWorktree` is no longer the root.
|
|
569
|
+
const memoryDirs = await ownMemoryDirs([resolveProjectDir(), projectRootRaw, projectRoot]);
|
|
542
570
|
const caller = classifyCaller(input);
|
|
543
571
|
if (caller !== 'subagent' && memoryDirs.some((dir) => isInsideDir(resolvedPath, dir))) {
|
|
544
572
|
// One event per decision point, awaited before emitAllow() —
|
|
@@ -652,6 +680,155 @@ async function main() {
|
|
|
652
680
|
return emitAllow();
|
|
653
681
|
}
|
|
654
682
|
|
|
683
|
+
/**
|
|
684
|
+
* #1485 point 1 — the catch event for Gate 5b's WARN branch, ONCE per wave.
|
|
685
|
+
*
|
|
686
|
+
* `scripts/validate-wave-scope.mjs` has no mechanical caller; the Gate 5b WARN
|
|
687
|
+
* is the only place a refused grant is seen at all, and a `systemMessage` is not
|
|
688
|
+
* countable. Owner decision 2026-10-02, option (b): the FIRST WARN of a wave
|
|
689
|
+
* writes `orchestrator.scope.refused_grant_allowed`, so "how many waves ran on
|
|
690
|
+
* a manifest the validator would refuse" becomes a ledger query (HR-105). The
|
|
691
|
+
* WARN itself is unchanged and still fires on every write.
|
|
692
|
+
*
|
|
693
|
+
* DEDUPE: a marker `.orchestrator/tmp/gate5b-refused-grant/<session>-w<wave>`
|
|
694
|
+
* created with `flag: 'wx'` (O_CREAT|O_EXCL). Exclusive create is atomic, so of
|
|
695
|
+
* two hook processes racing on the first WARN exactly one wins and emits — no
|
|
696
|
+
* lock, no read-modify-write. The session joins the key because wave numbers
|
|
697
|
+
* restart in every session sharing this working copy. `.orchestrator/tmp/` is
|
|
698
|
+
* gitignored; one marker per (session, wave) that ever hit a refused grant.
|
|
699
|
+
*
|
|
700
|
+
* Any marker failure OTHER than EEXIST (unwritable dir, ENOTDIR) still emits:
|
|
701
|
+
* over-reporting a rare event beats re-creating the silent gap this closes.
|
|
702
|
+
*
|
|
703
|
+
* COST: runs only on the Gate 5b `error` branch — an ordinary in-repo write
|
|
704
|
+
* never reaches it, so the hot path pays nothing. NAMED CEILING (BV-004): a
|
|
705
|
+
* legacy manifest without a session id and a payload without one share the key
|
|
706
|
+
* `no-session-w<N>` across sessions; revisit if such manifests are still seen.
|
|
707
|
+
*
|
|
708
|
+
* Never throws — observability must not change the WARN.
|
|
709
|
+
*
|
|
710
|
+
* @param {{scope: Record<string, unknown>, scopePath: string, projectRoot: string,
|
|
711
|
+
* input: Record<string, unknown>, grant: string, grade: {code?: string},
|
|
712
|
+
* filePath: string}} facts
|
|
713
|
+
* @returns {Promise<void>}
|
|
714
|
+
*/
|
|
715
|
+
async function recordRefusedGrantOncePerWave({ scope, scopePath, projectRoot, input, grant, grade, filePath }) {
|
|
716
|
+
try {
|
|
717
|
+
const session = String(
|
|
718
|
+
scope.session_id ?? scope.session ?? input?.session_id ?? process.env.CLAUDE_CODE_SESSION_ID ?? 'no-session',
|
|
719
|
+
);
|
|
720
|
+
const key = `${session}-w${scope.wave ?? 'unknown'}`.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 160);
|
|
721
|
+
const markerDir = path.join(projectRoot, '.orchestrator', 'tmp', 'gate5b-refused-grant');
|
|
722
|
+
try { await fs.mkdir(markerDir, { recursive: true }); } catch { /* the create below decides */ }
|
|
723
|
+
try {
|
|
724
|
+
await fs.writeFile(path.join(markerDir, key), `${new Date().toISOString()}\n`, { flag: 'wx' });
|
|
725
|
+
} catch (e) {
|
|
726
|
+
if (e?.code === 'EEXIST') return; // this wave already reported
|
|
727
|
+
}
|
|
728
|
+
const { emitEvent } = await import('../scripts/lib/events.mjs');
|
|
729
|
+
await emitEvent(
|
|
730
|
+
'orchestrator.scope.refused_grant_allowed',
|
|
731
|
+
{
|
|
732
|
+
hook: HOOK_NAME,
|
|
733
|
+
manifest: scopePath,
|
|
734
|
+
wave: scope.wave,
|
|
735
|
+
grant,
|
|
736
|
+
code: grade?.code,
|
|
737
|
+
file_path: filePath,
|
|
738
|
+
},
|
|
739
|
+
{ repoRoot: projectRoot },
|
|
740
|
+
);
|
|
741
|
+
} catch { /* observability is best-effort — never changes the WARN */ }
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* #1504 point 3 — the catch event for a session root that HID an active wave,
|
|
746
|
+
* ONCE per (session, wave, session root).
|
|
747
|
+
*
|
|
748
|
+
* `resolveSessionRoot()` answers with the repo the payload `cwd` sits in. A wave
|
|
749
|
+
* agent that `cd`s into a nested or sibling repo (or a session that entered a
|
|
750
|
+
* worktree) therefore resolves a root without a manifest, and Gate 3 allows
|
|
751
|
+
* every write there while the wave's manifest sits at the launch dir, unread.
|
|
752
|
+
* The revisit trigger `resolveSessionRoot` names for exactly that relocation
|
|
753
|
+
* ("an `orchestrator.scope.*` record") could never fire: this allow left no
|
|
754
|
+
* trace. Behaviour stays ALLOW — this is the trace, not a new deny.
|
|
755
|
+
*
|
|
756
|
+
* Fires only when BOTH hold, so the ordinary no-manifest allow — every write in
|
|
757
|
+
* every repo without a wave — stays silent (HR-101):
|
|
758
|
+
* 1. the harness states a launch dir and it is not the resolved session root;
|
|
759
|
+
* 2. the launch root carries a manifest that is not provably a peer's
|
|
760
|
+
* ({@link findOwnScopeFile}: own, or unbound/legacy — one that WOULD have
|
|
761
|
+
* been enforced). A peer's manifest at the launch root hides nothing of ours.
|
|
762
|
+
*
|
|
763
|
+
* COST on the hot path: rung 1 is an env read plus a string compare — no
|
|
764
|
+
* filesystem call while the session root IS the launch dir, i.e. for every
|
|
765
|
+
* session that never left its checkout. One `realpath` when only the spelling
|
|
766
|
+
* differs (macOS `/var` → `/private/var`); the ≤4 `existsSync` of rung 2 only
|
|
767
|
+
* for a genuinely relocated root.
|
|
768
|
+
*
|
|
769
|
+
* DEDUPE: the {@link recordRefusedGrantOncePerWave} pattern — an exclusive
|
|
770
|
+
* create (`flag: 'wx'`) of `<launch>/.orchestrator/tmp/scope-root-relocated/<key>`.
|
|
771
|
+
* The event goes to the LAUNCH repo's ledger, where the wave's other records
|
|
772
|
+
* live. The session root's basename joins the key so a second relocated repo in
|
|
773
|
+
* one wave is reported too. NAMED CEILING (BV-004): two relocated roots sharing a
|
|
774
|
+
* basename in one wave report once; revisit if a record ever names the wrong repo.
|
|
775
|
+
*
|
|
776
|
+
* Never throws — observability must not change the allow.
|
|
777
|
+
*
|
|
778
|
+
* @param {{projectRootRaw: string, projectRoot: string, ownIds: Set<string>,
|
|
779
|
+
* filePath: string}} facts
|
|
780
|
+
* @returns {Promise<void>}
|
|
781
|
+
*/
|
|
782
|
+
async function recordHiddenManifestOncePerWave({ projectRootRaw, projectRoot, ownIds, filePath }) {
|
|
783
|
+
try {
|
|
784
|
+
const launchRaw = launchDirFromEnv();
|
|
785
|
+
if (launchRaw === '') return;
|
|
786
|
+
const launchResolved = path.resolve(launchRaw);
|
|
787
|
+
if (launchResolved === projectRootRaw || launchResolved === projectRoot) return;
|
|
788
|
+
let launch;
|
|
789
|
+
try {
|
|
790
|
+
launch = await fs.realpath(launchResolved);
|
|
791
|
+
} catch {
|
|
792
|
+
return; // a launch dir that does not exist holds no manifest
|
|
793
|
+
}
|
|
794
|
+
if (launch === projectRoot) return;
|
|
795
|
+
|
|
796
|
+
const { path: manifest } = findOwnScopeFile(launch, ownIds, classifyManifestSession);
|
|
797
|
+
if (!manifest) return;
|
|
798
|
+
|
|
799
|
+
let scope = {};
|
|
800
|
+
try {
|
|
801
|
+
const parsed = JSON.parse(await fs.readFile(manifest, 'utf8'));
|
|
802
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) scope = parsed;
|
|
803
|
+
} catch { /* unreadable → keyed and reported as wave `unknown` */ }
|
|
804
|
+
|
|
805
|
+
const session = String(scope.session_id ?? scope.session ?? [...ownIds][0] ?? 'no-session');
|
|
806
|
+
const key = `${session}-w${scope.wave ?? 'unknown'}-${path.basename(projectRoot)}`
|
|
807
|
+
.replace(/[^A-Za-z0-9._-]/g, '_')
|
|
808
|
+
.slice(0, 160);
|
|
809
|
+
const markerDir = path.join(launch, '.orchestrator', 'tmp', 'scope-root-relocated');
|
|
810
|
+
try { await fs.mkdir(markerDir, { recursive: true }); } catch { /* the create below decides */ }
|
|
811
|
+
try {
|
|
812
|
+
await fs.writeFile(path.join(markerDir, key), `${new Date().toISOString()}\n`, { flag: 'wx' });
|
|
813
|
+
} catch (e) {
|
|
814
|
+
if (e?.code === 'EEXIST') return; // this root already reported for this wave
|
|
815
|
+
}
|
|
816
|
+
const { emitEvent } = await import('../scripts/lib/events.mjs');
|
|
817
|
+
await emitEvent(
|
|
818
|
+
'orchestrator.scope.manifest_hidden_by_session_root',
|
|
819
|
+
{
|
|
820
|
+
hook: HOOK_NAME,
|
|
821
|
+
manifest,
|
|
822
|
+
launch_root: launch,
|
|
823
|
+
session_root: projectRoot,
|
|
824
|
+
wave: scope.wave,
|
|
825
|
+
file_path: path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath),
|
|
826
|
+
},
|
|
827
|
+
{ repoRoot: launch },
|
|
828
|
+
);
|
|
829
|
+
} catch { /* observability is best-effort — never changes the allow */ }
|
|
830
|
+
}
|
|
831
|
+
|
|
655
832
|
/**
|
|
656
833
|
* `mtimeMs` of a file, or `null` when it cannot be stat'ed.
|
|
657
834
|
*
|
|
@@ -679,21 +856,22 @@ async function mtimeMsOf(file) {
|
|
|
679
856
|
/**
|
|
680
857
|
* #1295 — the Claude Code auto-memory directories that belong to THIS repo.
|
|
681
858
|
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
* encodes the path it was LAUNCHED with, while this hook
|
|
686
|
-
* canonical root (on macOS `/tmp` →
|
|
687
|
-
*
|
|
859
|
+
* One path per distinct root (per home spelling): the encoding of the launch
|
|
860
|
+
* dir the harness saw (`CLAUDE_PROJECT_DIR` / cwd), of the session root
|
|
861
|
+
* (#1492 — a worktree the session entered) and of its realpath. All are needed
|
|
862
|
+
* because Claude Code encodes the path it was LAUNCHED with, while this hook
|
|
863
|
+
* compares against the canonical session root (on macOS `/tmp` →
|
|
864
|
+
* `/private/tmp`). Keeping the launch dir keeps the pre-#1492 grant exactly
|
|
865
|
+
* (the launch dir WAS the root then); the session-root entries name the
|
|
866
|
+
* worktree this session works in. A sibling repo's root encodes to none of them.
|
|
688
867
|
*
|
|
689
868
|
* Returns `[]` when the encoder cannot be loaded, so the caller falls through to
|
|
690
869
|
* its deny (fail-closed).
|
|
691
870
|
*
|
|
692
|
-
* @param {string}
|
|
693
|
-
* @param {string} projectRoot — its realpath
|
|
871
|
+
* @param {string[]} candidateRoots — launch dir, session root, its realpath
|
|
694
872
|
* @returns {Promise<string[]>}
|
|
695
873
|
*/
|
|
696
|
-
async function ownMemoryDirs(
|
|
874
|
+
async function ownMemoryDirs(candidateRoots) {
|
|
697
875
|
try {
|
|
698
876
|
const [{ encodeProjectDir }, { homedir }] = await Promise.all([
|
|
699
877
|
import('../scripts/lib/wave-transcript-tail.mjs'),
|
|
@@ -710,7 +888,7 @@ async function ownMemoryDirs(projectRootRaw, projectRoot) {
|
|
|
710
888
|
} catch { /* non-existent home — the raw form is all there is */ }
|
|
711
889
|
const homes = new Set([home, homeReal]);
|
|
712
890
|
const roots = new Set(
|
|
713
|
-
|
|
891
|
+
candidateRoots.filter((p) => typeof p === 'string' && p.length > 0),
|
|
714
892
|
);
|
|
715
893
|
const dirs = [];
|
|
716
894
|
for (const h of homes) {
|
package/hooks/hooks-codex.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "echo '🎯 Session Orchestrator v5.
|
|
10
|
+
"command": "echo '🎯 Session Orchestrator v5.5.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
|
|
11
11
|
"async": false
|
|
12
12
|
},
|
|
13
13
|
{
|
package/hooks/hooks.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "echo '🎯 Session Orchestrator v5.
|
|
9
|
+
"command": "echo '🎯 Session Orchestrator v5.5.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
|
|
10
10
|
"async": false
|
|
11
11
|
},
|
|
12
12
|
{
|
|
@@ -1098,8 +1098,10 @@ async function main() {
|
|
|
1098
1098
|
|
|
1099
1099
|
// Phase 4 measurement probes — the mechanical caller (#1128).
|
|
1100
1100
|
//
|
|
1101
|
-
// `skills/session-start/SKILL.md` § Phase 4 names
|
|
1102
|
-
// and entry functions
|
|
1101
|
+
// `skills/session-start/SKILL.md` § Phase 4 names 20 probes with module paths
|
|
1102
|
+
// and entry functions (2026-10-02; the `PROBES` registry in
|
|
1103
|
+
// `scripts/lib/session-start-probes.mjs` is the count's source of truth).
|
|
1104
|
+
// Measured 2026-08-23 at `4f6404e` (a smaller family then), not one of them had
|
|
1103
1105
|
// a caller anywhere in hooks/, npm scripts, .gitlab-ci.yml or .husky/ — the
|
|
1104
1106
|
// only caller was the prose itself, and across 336 recorded session starts
|
|
1105
1107
|
// there was no event proving any of them had ever run. Built, documented,
|