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
|
@@ -66,21 +66,22 @@
|
|
|
66
66
|
* hooks.json wiring is managed separately (W3-C4 scope).
|
|
67
67
|
*/
|
|
68
68
|
|
|
69
|
-
import { execFileSync
|
|
70
|
-
import { readFileSync } from 'node:fs';
|
|
69
|
+
import { execFileSync } from 'node:child_process';
|
|
71
70
|
import { readFile } from 'node:fs/promises';
|
|
72
71
|
import path from 'node:path';
|
|
73
|
-
import { performance } from 'node:perf_hooks';
|
|
74
|
-
import { fileURLToPath } from 'node:url';
|
|
75
72
|
|
|
76
73
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
77
74
|
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
78
75
|
|
|
79
|
-
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
76
|
+
import { getProjectDir, resolveSessionRoot } from '../scripts/lib/platform.mjs';
|
|
80
77
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
81
|
-
import {
|
|
78
|
+
import { findOwnScopeFile } from '../scripts/lib/scope-gate.mjs';
|
|
79
|
+
import {
|
|
80
|
+
classifyManifestSession,
|
|
81
|
+
readProcessLocalSessionIds,
|
|
82
|
+
} from '../scripts/lib/session-identity/own-session.mjs';
|
|
82
83
|
import { atomicMutateJson } from './_lib/atomic-json.mjs';
|
|
83
|
-
import {
|
|
84
|
+
import { maybeTriggerOrphanScan } from '../scripts/lib/orphan-reaper/trigger.mjs';
|
|
84
85
|
|
|
85
86
|
// ---------------------------------------------------------------------------
|
|
86
87
|
// Helpers
|
|
@@ -203,18 +204,23 @@ async function ownsSessionFile(input, sessionFile) {
|
|
|
203
204
|
* wave-scope.json absent"). The file is deleted mid-session at Quality phase
|
|
204
205
|
* transitions and final cleanup, so absence is an expected, non-error state.
|
|
205
206
|
*
|
|
206
|
-
* The manifest is located via `
|
|
207
|
+
* The manifest is located via `findOwnScopeFile()` (#1504), the same precedence
|
|
207
208
|
* every other consumer uses — `.pi` > `.cursor` > `.codex` > `.claude`. The
|
|
208
209
|
* previous hard-coded `.claude/` path made this hook's whole wave-lifecycle
|
|
209
210
|
* fallback structurally dead on Codex CLI, Cursor and pi: the manifest exists,
|
|
210
211
|
* it just is not under `.claude/`, so every batch read 0 and no
|
|
211
212
|
* wave-lifecycle event was ever emitted there.
|
|
212
213
|
*
|
|
213
|
-
*
|
|
214
|
+
* Only THIS session's manifest counts (#1504): `findOwnScopeFile` skips a
|
|
215
|
+
* peer's manifest in the same working copy, and a peer-only result is "no own
|
|
216
|
+
* wave" → 0, never the peer's number recorded as ours.
|
|
217
|
+
*
|
|
218
|
+
* @param {string} projectDir the session root (`resolveSessionRoot`)
|
|
219
|
+
* @param {Set<string>} ownIds this process's own session ids
|
|
214
220
|
* @returns {Promise<number>}
|
|
215
221
|
*/
|
|
216
|
-
async function resolveWaveNumber(projectDir) {
|
|
217
|
-
const waveFile =
|
|
222
|
+
async function resolveWaveNumber(projectDir, ownIds) {
|
|
223
|
+
const { path: waveFile } = findOwnScopeFile(projectDir, ownIds, classifyManifestSession);
|
|
218
224
|
if (waveFile === null) return 0;
|
|
219
225
|
try {
|
|
220
226
|
const raw = await readFile(waveFile, 'utf8');
|
|
@@ -308,147 +314,6 @@ function countFilesChangedSince(projectDir, sha) {
|
|
|
308
314
|
}
|
|
309
315
|
}
|
|
310
316
|
|
|
311
|
-
// ---------------------------------------------------------------------------
|
|
312
|
-
// orphan-reaper trigger (#1432 B4)
|
|
313
|
-
// ---------------------------------------------------------------------------
|
|
314
|
-
//
|
|
315
|
-
// DUPLICATED VERBATIM between the two trigger hooks —
|
|
316
|
-
// hooks/post-tool-batch-wave-signal.mjs <-> hooks/on-stop.mjs. Deliberate, with
|
|
317
|
-
// a named revisit trigger (BV-004): they are the only two trigger points the
|
|
318
|
-
// PRD declares, and a shared `hooks/_lib/reaper-trigger.mjs` would be a third
|
|
319
|
-
// file in the hook import graph. SIZE, measured 2026-09-22 by diffing the two
|
|
320
|
-
// blocks: 138 identical lines — this note said "~40 lines of glue" until then,
|
|
321
|
-
// which is what made the duplicate look cheaper than it is. Extract it the
|
|
322
|
-
// moment a THIRD hook needs the trigger, or the moment the copies must differ.
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Filesystem path of the scan CLI, spawned as a PLAIN argv call.
|
|
326
|
-
*
|
|
327
|
-
* It used to be a `file://` URL handed to `node --input-type=module -e
|
|
328
|
-
* <program>`, because `scripts/lib/orphan-reaper.mjs` had no entry guard. It
|
|
329
|
-
* has one now (`parseReaperCliArgs` + the `isMainModule` tail), so the child's
|
|
330
|
-
* contract lives in that module instead of as source text duplicated here —
|
|
331
|
-
* and nothing this hook builds is a program any more: every variable part is
|
|
332
|
-
* an argv value, which cannot become code whatever the checkout path contains.
|
|
333
|
-
*
|
|
334
|
-
* `fileURLToPath`, not `new URL(...).pathname`: the latter leaves a
|
|
335
|
-
* percent-encoded path for any checkout directory containing a space.
|
|
336
|
-
*/
|
|
337
|
-
const ORPHAN_REAPER_SCRIPT = fileURLToPath(
|
|
338
|
-
new URL('../scripts/lib/orphan-reaper.mjs', import.meta.url),
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Read the `reaper:` block from the project's Session Config host file.
|
|
343
|
-
* Sync + inline, mirroring `hooks/loop-guard.mjs` `loadConfig()` — a hot hook
|
|
344
|
-
* path must not import the full config orchestrator. A missing or unreadable
|
|
345
|
-
* file yields the parser defaults, i.e. DISABLED.
|
|
346
|
-
*
|
|
347
|
-
* @param {string} projectDir
|
|
348
|
-
* @returns {ReturnType<typeof _parseReaper>}
|
|
349
|
-
*/
|
|
350
|
-
function loadReaperConfig(projectDir) {
|
|
351
|
-
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
352
|
-
try {
|
|
353
|
-
return _parseReaper(readFileSync(path.join(projectDir, name), 'utf8'));
|
|
354
|
-
} catch {
|
|
355
|
-
// missing or unreadable — try the next candidate
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return _parseReaper('');
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Trigger the orphan scan, throttled and NON-BLOCKING (PRD FA4).
|
|
363
|
-
*
|
|
364
|
-
* The hook itself does exactly two pieces of I/O — one config read and one
|
|
365
|
-
* `stat` of the throttle marker — and then hands the work to a DETACHED,
|
|
366
|
-
* unref'd child. The scan is never run inline: one `ps` round-trip out of Node
|
|
367
|
-
* was measured at ~47 ms over 287 KB of output, which alone would blow the
|
|
368
|
-
* 50 ms `reaper.max-hook-latency-ms` budget this hook has to stay inside.
|
|
369
|
-
*
|
|
370
|
-
* The marker is stamped BEFORE the spawn, so a spawn that fails still consumes
|
|
371
|
-
* the throttle window — otherwise a broken spawn would be retried on every
|
|
372
|
-
* single tool batch.
|
|
373
|
-
*
|
|
374
|
-
* `reaper.max-hook-latency-ms` is the ceiling on the work above — measured here
|
|
375
|
-
* with `performance.now()` and reported as ONE stderr WARN line when exceeded.
|
|
376
|
-
* A WARN and not an event: this fires from a `PostToolBatch`-class hook, and a
|
|
377
|
-
* per-fire telemetry record is exactly the always-on signal
|
|
378
|
-
* `.claude/rules/host-resources.md` HR-101 calls a broken instrument. The
|
|
379
|
-
* measurement covers preparation only — the scan itself runs detached, which is
|
|
380
|
-
* the whole reason the budget can be held.
|
|
381
|
-
*
|
|
382
|
-
* Never throws: any failure degrades silently (PRD FA4 "lautlos degradieren").
|
|
383
|
-
*
|
|
384
|
-
* @param {object} [opts]
|
|
385
|
-
* @param {string} [opts.projectDir] Repo root; defaults to `getProjectDir()`.
|
|
386
|
-
* @param {number} [opts.now] Injected clock (ms).
|
|
387
|
-
* @param {Function} [opts.spawnFn] Injected `spawn` (tests).
|
|
388
|
-
* @param {Function} [opts.statFn] Injected `statSync` (tests).
|
|
389
|
-
* @param {Function} [opts.writeFn] Injected marker writer (tests).
|
|
390
|
-
* @param {() => number} [opts.clockFn] Injected monotonic clock for the latency
|
|
391
|
-
* budget (tests); defaults to `performance.now`.
|
|
392
|
-
* @returns {Promise<{spawned: boolean, reason: string}>}
|
|
393
|
-
*/
|
|
394
|
-
export async function maybeTriggerOrphanScan({
|
|
395
|
-
projectDir,
|
|
396
|
-
now,
|
|
397
|
-
spawnFn = spawn,
|
|
398
|
-
statFn,
|
|
399
|
-
writeFn,
|
|
400
|
-
clockFn = () => performance.now(),
|
|
401
|
-
} = {}) {
|
|
402
|
-
const startedAt = clockFn();
|
|
403
|
-
/** One WARN line when the preparation overran `reaper.max-hook-latency-ms`. */
|
|
404
|
-
const checkLatency = (budgetMs) => {
|
|
405
|
-
const elapsed = clockFn() - startedAt;
|
|
406
|
-
if (Number.isFinite(budgetMs) && budgetMs > 0 && elapsed > budgetMs) {
|
|
407
|
-
process.stderr.write(
|
|
408
|
-
`orphan-reaper trigger: hook latency ${elapsed.toFixed(1)} ms exceeded `
|
|
409
|
-
+ `reaper.max-hook-latency-ms (${budgetMs} ms)\n`,
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
};
|
|
413
|
-
try {
|
|
414
|
-
const root = typeof projectDir === 'string' && projectDir ? projectDir : getProjectDir();
|
|
415
|
-
const cfg = loadReaperConfig(root);
|
|
416
|
-
// Cheapest gate first: disabled means no stat, no spawn, no module load.
|
|
417
|
-
if (cfg.enabled !== true) return { spawned: false, reason: 'disabled' };
|
|
418
|
-
|
|
419
|
-
// The throttle module only — the scan's ledger, `ps` and kill-ladder code
|
|
420
|
-
// load in the detached child, never on this hook path (#1437).
|
|
421
|
-
const reaper = await import('../scripts/lib/orphan-reaper/scan-throttle.mjs');
|
|
422
|
-
const markerPath = reaper.scanMarkerPath(root);
|
|
423
|
-
const nowMs = typeof now === 'number' ? now : Date.now();
|
|
424
|
-
|
|
425
|
-
if (!reaper.shouldScanNow(markerPath, nowMs, cfg['min-scan-interval-seconds'], { statFn })) {
|
|
426
|
-
checkLatency(cfg['max-hook-latency-ms']);
|
|
427
|
-
return { spawned: false, reason: 'throttled' };
|
|
428
|
-
}
|
|
429
|
-
reaper.touchScanMarker(markerPath, { writeFn });
|
|
430
|
-
|
|
431
|
-
const child = spawnFn(
|
|
432
|
-
process.execPath,
|
|
433
|
-
[
|
|
434
|
-
ORPHAN_REAPER_SCRIPT,
|
|
435
|
-
'--repo-root', root,
|
|
436
|
-
'--mode', cfg.mode,
|
|
437
|
-
'--min-age-seconds', String(cfg['min-age-seconds']),
|
|
438
|
-
'--kill-grace-ms', String(cfg['kill-grace-ms']),
|
|
439
|
-
'--verify-wait-ms', String(cfg['verify-wait-ms']),
|
|
440
|
-
'--false-alarm-window', String(cfg['false-alarm-window']),
|
|
441
|
-
],
|
|
442
|
-
{ detached: true, stdio: 'ignore' },
|
|
443
|
-
);
|
|
444
|
-
if (child && typeof child.unref === 'function') child.unref();
|
|
445
|
-
checkLatency(cfg['max-hook-latency-ms']);
|
|
446
|
-
return { spawned: true, reason: 'spawned' };
|
|
447
|
-
} catch {
|
|
448
|
-
return { spawned: false, reason: 'error' };
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
317
|
// ---------------------------------------------------------------------------
|
|
453
318
|
// Main
|
|
454
319
|
// ---------------------------------------------------------------------------
|
|
@@ -596,7 +461,10 @@ async function main() {
|
|
|
596
461
|
// when an N+1 transition already closed wave N, SessionEnd sees
|
|
597
462
|
// last_wave_completed === last_wave and stays silent.
|
|
598
463
|
try {
|
|
599
|
-
const wave = await resolveWaveNumber(
|
|
464
|
+
const wave = await resolveWaveNumber(
|
|
465
|
+
resolveSessionRoot(input?.cwd),
|
|
466
|
+
new Set(readProcessLocalSessionIds({ hookInput: input })),
|
|
467
|
+
);
|
|
600
468
|
if (wave > 0) {
|
|
601
469
|
// Read last_wave from the just-written session file (after the
|
|
602
470
|
// last_batch RMW above, so we observe the latest persisted value).
|
|
@@ -715,7 +583,17 @@ async function main() {
|
|
|
715
583
|
// `process.exit(0)` in a `finally`: an un-awaited spawn would race the exit.
|
|
716
584
|
// The await is bounded by design — one config read, one stat, one detached
|
|
717
585
|
// spawn, all measured under `reaper.max-hook-latency-ms` (50 ms).
|
|
718
|
-
|
|
586
|
+
//
|
|
587
|
+
// The returned reason is deliberately NOT recorded here (#1489): this hook
|
|
588
|
+
// emits no record per batch, and adding one is the per-fire telemetry HR-101
|
|
589
|
+
// rejects. The throttle marker is one file per repo, so a defeated marker
|
|
590
|
+
// (`spawned-unthrottled`) shows on the next Stop/SubagentStop record, whose
|
|
591
|
+
// `reaper_trigger` key hooks/on-stop.mjs fills from the same trigger.
|
|
592
|
+
//
|
|
593
|
+
// Rooted at the SESSION root (#1492 point 2, #1504 point 4): the quality gate
|
|
594
|
+
// registers its children in the ledger of the tree it runs in, which for an
|
|
595
|
+
// entered worktree is the worktree — the launch dir holds none of them.
|
|
596
|
+
await maybeTriggerOrphanScan({ projectDir: resolveSessionRoot(input?.cwd) });
|
|
719
597
|
}
|
|
720
598
|
|
|
721
599
|
// Entry guard (#1393): run only as the node script the harness execs — a bare
|
|
@@ -29,7 +29,12 @@ import { readFile } from 'node:fs/promises';
|
|
|
29
29
|
import os from 'node:os';
|
|
30
30
|
import path from 'node:path';
|
|
31
31
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
32
|
-
import {
|
|
32
|
+
import { resolveSessionRoot } from '../scripts/lib/platform.mjs';
|
|
33
|
+
import { findOwnScopeFile } from '../scripts/lib/scope-gate.mjs';
|
|
34
|
+
import {
|
|
35
|
+
classifyManifestSession,
|
|
36
|
+
readProcessLocalSessionIds,
|
|
37
|
+
} from '../scripts/lib/session-identity/own-session.mjs';
|
|
33
38
|
|
|
34
39
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
35
40
|
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
@@ -201,14 +206,16 @@ async function resolveSessionId(input, projectDir) {
|
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
/**
|
|
204
|
-
* Resolve the current wave number from
|
|
205
|
-
* Returns 0 when the file is absent or unparseable
|
|
209
|
+
* Resolve the current wave number from THIS session's wave-scope.json.
|
|
210
|
+
* Returns 0 when the file is absent or unparseable — and when the only
|
|
211
|
+
* manifest is a peer's (#1504): its number is not this session's wave.
|
|
206
212
|
*
|
|
207
|
-
* @param {string} projectDir
|
|
213
|
+
* @param {string} projectDir the session root (`resolveSessionRoot`)
|
|
214
|
+
* @param {Set<string>} ownIds this process's own session ids
|
|
208
215
|
* @returns {Promise<number>}
|
|
209
216
|
*/
|
|
210
|
-
async function resolveWave(projectDir) {
|
|
211
|
-
const waveFile =
|
|
217
|
+
async function resolveWave(projectDir, ownIds) {
|
|
218
|
+
const { path: waveFile } = findOwnScopeFile(projectDir, ownIds, classifyManifestSession);
|
|
212
219
|
if (!waveFile || !existsSync(waveFile)) return 0;
|
|
213
220
|
try {
|
|
214
221
|
const raw = await readFile(waveFile, 'utf8');
|
|
@@ -247,7 +254,12 @@ async function main() {
|
|
|
247
254
|
const sessionId = await resolveSessionId(input, projectDir);
|
|
248
255
|
|
|
249
256
|
// G5 — resolve wave
|
|
250
|
-
|
|
257
|
+
// Read at the SESSION root the scope guards use (#1504 point 4), not at the
|
|
258
|
+
// launch dir above, which stays put while the session works in a worktree.
|
|
259
|
+
const wave = await resolveWave(
|
|
260
|
+
resolveSessionRoot(input.cwd),
|
|
261
|
+
new Set(readProcessLocalSessionIds({ hookInput: input })),
|
|
262
|
+
);
|
|
251
263
|
|
|
252
264
|
// G6 — derive the non-reversible argv summary (#1415). Nothing downstream
|
|
253
265
|
// ever sees the command text: hash for grouping, known flag NAMES for
|
|
@@ -75,6 +75,7 @@ import crypto from 'node:crypto';
|
|
|
75
75
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
76
76
|
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
77
77
|
import { stableHostname } from '../scripts/lib/host-identity.mjs';
|
|
78
|
+
import { dotGitAncestor, launchDirFromEnv } from '../scripts/lib/platform.mjs';
|
|
78
79
|
|
|
79
80
|
// ---------------------------------------------------------------------------
|
|
80
81
|
// Constants
|
|
@@ -400,16 +401,36 @@ function deriveAgentId() {
|
|
|
400
401
|
}
|
|
401
402
|
|
|
402
403
|
/**
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
404
|
+
* The repo whose fence directory this `git add` is recorded in — the SAME root
|
|
405
|
+
* the reader uses (#1504 point 4). The only reader, `hooks/wave-scope-commit-guard.mjs`,
|
|
406
|
+
* runs as git's pre-commit hook and walks `<git rev-parse --show-toplevel>/.orchestrator/staging-fence/`,
|
|
407
|
+
* i.e. the work-tree root the commit is made in. `dotGitAncestor(cwd)` is that
|
|
408
|
+
* root without a spawn: the nearest `.git` entry at or above the payload `cwd` —
|
|
409
|
+
* the worktree root for a session that entered a worktree (measured 2026-10-03:
|
|
410
|
+
* launch dir `<main>`, cwd `<worktree>/sub` → `<worktree>`, git toplevel
|
|
411
|
+
* `<worktree>`), the repo root for a launch in a repo SUBDIRECTORY. The launch
|
|
412
|
+
* dir (`$CLAUDE_PROJECT_DIR`) was wrong in both: the fence landed where no
|
|
413
|
+
* commit guard looked, and every cross-agent overlap passed.
|
|
406
414
|
*
|
|
415
|
+
* Deliberately NOT `resolveSessionRoot()`: its clamp answers the launch
|
|
416
|
+
* SUBDIRECTORY of a monorepo package, while git's toplevel is the repo root.
|
|
417
|
+
*
|
|
418
|
+
* Fallback when the payload carries no `cwd`, or `cwd` is in no repo (where no
|
|
419
|
+
* commit can read a fence anyway): the launch dir from env, then `process.cwd()`.
|
|
420
|
+
*
|
|
421
|
+
* NAMED CEILING (BV-004): the root follows the payload `cwd`, not the command —
|
|
422
|
+
* `git -C <other-tree> add` or `cd <other-tree> && git add` records the intent
|
|
423
|
+
* in the CURRENT tree's fence dir while the commit in `<other-tree>` reads its
|
|
424
|
+
* own, so that overlap stays unseen (as before). Revisit if a fence miss is
|
|
425
|
+
* ever traced to a redirected `git add`; the fix then is to resolve the root
|
|
426
|
+
* from the tokenized command's `-C` / leading `cd`.
|
|
427
|
+
*
|
|
428
|
+
* @param {object|null} input the hook payload
|
|
407
429
|
* @returns {string}
|
|
408
430
|
*/
|
|
409
|
-
function
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
?? process.cwd();
|
|
431
|
+
function resolveFenceRoot(input) {
|
|
432
|
+
const cwd = typeof input?.cwd === 'string' ? input.cwd.trim() : '';
|
|
433
|
+
return (cwd !== '' && dotGitAncestor(cwd)) || launchDirFromEnv() || process.cwd();
|
|
413
434
|
}
|
|
414
435
|
|
|
415
436
|
/**
|
|
@@ -494,7 +515,7 @@ async function main() {
|
|
|
494
515
|
if (!recognised) return emitAllow();
|
|
495
516
|
|
|
496
517
|
// G5 — derive paths.
|
|
497
|
-
const projectDir =
|
|
518
|
+
const projectDir = resolveFenceRoot(input);
|
|
498
519
|
const agentId = deriveAgentId();
|
|
499
520
|
const fenceFile = path.join(
|
|
500
521
|
projectDir,
|