session-orchestrator 5.8.0 → 5.10.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-plugin/plugin.json +1 -1
- package/CHANGELOG.md +52 -2
- package/README.md +14 -10
- package/docs/USER-GUIDE.md +2 -0
- package/docs/ci-setup.md +46 -1
- package/docs/events-schema.md +71 -1
- package/docs/owner-config-schema.md +18 -1
- package/docs/owner-privacy-patterns.md +85 -0
- package/docs/scope-collision-guard.md +12 -2
- package/hooks/enforce-commands.mjs +9 -19
- package/hooks/enforce-scope.mjs +23 -42
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/post-bash-write-verify.mjs +38 -21
- package/package.json +2 -2
- package/scripts/emit-session.mjs +15 -0
- package/scripts/lib/ci-status-banner.mjs +38 -5
- package/scripts/lib/events-schema.mjs +8 -0
- package/scripts/lib/gates/gate-full.mjs +6 -0
- package/scripts/lib/gates/gate-helpers.mjs +2 -0
- package/scripts/lib/named-vault-resolver.mjs +1 -1
- package/scripts/lib/owner-yaml.mjs +33 -3
- package/scripts/lib/platform.mjs +70 -1
- package/scripts/lib/pre-push-remote-gate.mjs +107 -0
- package/scripts/lib/quality-gate-wave.mjs +40 -0
- package/scripts/lib/quality-gate.mjs +32 -26
- package/scripts/lib/redact-spans.mjs +8 -17
- package/scripts/lib/scope-gate.mjs +68 -5
- package/scripts/lib/session-record-repair.mjs +19 -8
- package/scripts/lib/session-schema/validator.mjs +12 -0
- package/scripts/lib/session-start-probes.mjs +20 -5
- package/scripts/lib/validate/check-owner-leakage.mjs +198 -1015
- package/scripts/lib/vault-backfill/template.mjs +1 -1
- package/scripts/lib/vault-mirror/render-sessions.mjs +15 -3
- package/scripts/lib/vault-status/board-writer.mjs +15 -7
- package/scripts/lib/wave-executor/remote-dispatch.mjs +27 -9
- package/scripts/lib/wave-resource-gate.mjs +2 -1
- package/scripts/migrate-vault-paths.mjs +57 -60
- package/scripts/release.mjs +107 -37
- package/scripts/run-quality-gate.mjs +9 -39
- package/scripts/site-numbers.mjs +4 -0
- package/scripts/vault-consolidate.mjs +19 -4
- package/skills/_shared/fleet-protocol.md +2 -0
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +14 -3
- package/skills/remote-offload/SKILL.md +1 -1
- package/skills/session-end/metrics-collection.md +39 -9
- package/skills/wave-executor/references/wave-loop-dispatch.md +1 -1
- package/skills/wave-executor/references/wave-loop-review.md +37 -7
package/scripts/release.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// + Unreleased folded, drift sweep, tag collision
|
|
24
24
|
// (local, origin, github), github/main mirror parity,
|
|
25
25
|
// npm registry collision, npm token liveness, CI green
|
|
26
|
-
// on HEAD, leakage gate over
|
|
26
|
+
// on HEAD, leakage gate over retained archive filenames and contents.
|
|
27
27
|
// --publish Runs --check first, then token publish via temp
|
|
28
28
|
// userconfig (NPM_TOKEN from .env.local). A
|
|
29
29
|
// target-confirmed npm receipt is the irreversible
|
|
@@ -80,10 +80,14 @@ import {
|
|
|
80
80
|
chmodSync,
|
|
81
81
|
rmSync,
|
|
82
82
|
realpathSync,
|
|
83
|
+
mkdirSync,
|
|
84
|
+
lstatSync,
|
|
83
85
|
} from 'node:fs';
|
|
84
|
-
import { join, relative as relativePath, sep } from 'node:path';
|
|
86
|
+
import { join, relative as relativePath, sep, basename, isAbsolute } from 'node:path';
|
|
85
87
|
import { tmpdir } from 'node:os';
|
|
86
88
|
import { spawnSync } from 'node:child_process';
|
|
89
|
+
import { createHash } from 'node:crypto';
|
|
90
|
+
import { packedFilename } from './lib/plugin-package-stage.mjs';
|
|
87
91
|
import { parseArgs } from 'node:util';
|
|
88
92
|
import { fileURLToPath } from 'node:url';
|
|
89
93
|
|
|
@@ -387,11 +391,14 @@ const INTENTIONAL_TEST_ASSET_PATHS = new Set([
|
|
|
387
391
|
// `scripts/lib/locks/index.mjs` is the one-cycle deprecation shim (5.2.0 CHANGELOG, Changed): its
|
|
388
392
|
// runtime warning names the version it was deprecated IN, so it legitimately carries the previous
|
|
389
393
|
// release literal until its 6.0.0 removal — version history by construction, not drift.
|
|
394
|
+
// `skills/eval/rubric-v3.md` (5.9.0 sweep, measured 2026-10-06): its change log quotes the dated
|
|
395
|
+
// owner decision "rubric-v3 in 5.8.0" (2026-10-05, #1487) — the release that introduced the rubric.
|
|
396
|
+
// Prose history, not a version surface; bumping it would falsify the quote.
|
|
390
397
|
// `site/_census.json` is deliberately NOT allowlisted: it carries a `"version"` field that
|
|
391
398
|
// `--set-version` re-stamps (scripts/site-numbers.mjs --write, see the post-bump hint in main()),
|
|
392
399
|
// so leaving it under the drift sweep makes the sweep double as its staleness gate — a census
|
|
393
400
|
// not re-stamped after a bump fails `--check` instead of shipping the previous version.
|
|
394
|
-
export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|scripts\/lib\/locks\/index\.mjs|CLAUDE\.md|AGENTS\.md|CONTRIBUTING\.md|NOTICE|\.husky\/pre-push|docs\/|tests\/|skills\/npm-publish\/|skills\/architecture\/references\/domain-model\.md|skills\/autopilot\/SKILL\.md|scripts\/release\.mjs|\.orchestrator\/|site\/leaderboard\.json|site\/guide\/index\.html|site\/llms-full\.txt|skills\/release\/SKILL\.md|marketing\/remotion\/(?:README\.md|campaign\.json|render-receipt\.json|src\/ReleaseFilm\.tsx)$)/;
|
|
401
|
+
export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|scripts\/lib\/locks\/index\.mjs|CLAUDE\.md|AGENTS\.md|CONTRIBUTING\.md|NOTICE|\.husky\/pre-push|docs\/|tests\/|skills\/npm-publish\/|skills\/architecture\/references\/domain-model\.md|skills\/autopilot\/SKILL\.md|skills\/eval\/rubric-v3\.md|scripts\/release\.mjs|\.orchestrator\/|site\/leaderboard\.json|site\/guide\/index\.html|site\/llms-full\.txt|skills\/release\/SKILL\.md|marketing\/remotion\/(?:README\.md|campaign\.json|render-receipt\.json|src\/ReleaseFilm\.tsx)$)/;
|
|
395
402
|
|
|
396
403
|
/** Pure check over packed-entry lines. Returns violations: {name, line}[]. */
|
|
397
404
|
export function checkLeakage(lines) {
|
|
@@ -825,6 +832,80 @@ export function evaluateLeakageGate(pack, { minEntries = MIN_PACKED_ENTRIES } =
|
|
|
825
832
|
: { ok: true, detail: `${entries} packed entries, 0 leaks` };
|
|
826
833
|
}
|
|
827
834
|
|
|
835
|
+
/** Strict npm 11/12 inventory, preserving the existing filename policy/floor. */
|
|
836
|
+
export function evaluatePackedInventory(pack, { minEntries = MIN_PACKED_ENTRIES } = {}) {
|
|
837
|
+
if (pack.status !== 0) return { ok: false, detail: 'npm pack failed' };
|
|
838
|
+
try {
|
|
839
|
+
const json = JSON.parse(pack.stdout);
|
|
840
|
+
const records = Array.isArray(json) ? json : json && typeof json === 'object' ? Object.values(json) : [];
|
|
841
|
+
if (records.length !== 1) throw new Error('inventory-shape');
|
|
842
|
+
const record = records[0];
|
|
843
|
+
const filename = packedFilename([record]);
|
|
844
|
+
if (!filename || basename(filename) !== filename || !/^[A-Za-z0-9][A-Za-z0-9._-]*\.tgz$/.test(filename)) throw new Error('archive-name');
|
|
845
|
+
if (!Array.isArray(record.files) || record.files.length > 20000 || record.files.length < minEntries) throw new Error('inventory-floor');
|
|
846
|
+
const paths = record.files.map((entry) => entry?.path);
|
|
847
|
+
if (paths.some((name) => typeof name !== 'string' || !name || name.length > 4096 || isAbsolute(name) || name.split('/').some((part) => !part || part === '.' || part === '..') || (name.includes('\\') || [...name].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127))) || new Set(paths).size !== paths.length) throw new Error('inventory-path');
|
|
848
|
+
const violations = checkLeakage(paths.map((name) => `npm notice 1B ${name}`));
|
|
849
|
+
if (violations.length) return { ok: false, detail: 'forbidden packed filename class: ' + [...new Set(violations.map((v) => v.name))].join(', ') };
|
|
850
|
+
return { ok: true, detail: `${paths.length} packed entries, 0 filename leaks`, record, filename, paths };
|
|
851
|
+
} catch { return { ok: false, detail: 'invalid packed inventory or insufficient entry count' }; }
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function archiveDigest(tarballPath) {
|
|
855
|
+
if (!lstatSync(tarballPath).isFile()) throw new Error('checked archive is not a regular file');
|
|
856
|
+
return createHash('sha256').update(readFileSync(tarballPath)).digest('hex');
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Scan extracted archive bytes, retain that archive for the callback, and always
|
|
861
|
+
* clean up. Command/scan failures never call the publication callback. The
|
|
862
|
+
* 20,000-entry inventory ceiling matches the scanner; revisit for larger packs.
|
|
863
|
+
*/
|
|
864
|
+
export async function withCheckedPackage(repoRoot, callback, {
|
|
865
|
+
runImpl = run, env = process.env, minEntries = MIN_PACKED_ENTRIES, cleanupImpl = rmSync,
|
|
866
|
+
} = {}) {
|
|
867
|
+
const work = mkdtempSync(join(tmpdir(), 'so-release-pack-'));
|
|
868
|
+
try {
|
|
869
|
+
let artifact;
|
|
870
|
+
let detail;
|
|
871
|
+
try {
|
|
872
|
+
const opts = { cwd: repoRoot, env: { ...env, npm_config_loglevel: 'notice' }, timeout: 60000 };
|
|
873
|
+
const pack = runImpl('npm', ['pack', '--json', '--ignore-scripts', '--pack-destination', work], opts);
|
|
874
|
+
const inventory = evaluatePackedInventory(pack, { minEntries });
|
|
875
|
+
if (!inventory.ok) return inventory;
|
|
876
|
+
const tarballPath = join(work, inventory.filename);
|
|
877
|
+
const digest = archiveDigest(tarballPath);
|
|
878
|
+
// Refuse links/special members BEFORE extraction and check the file census.
|
|
879
|
+
const members = runImpl('tar', ['-tzf', tarballPath], opts);
|
|
880
|
+
const types = runImpl('tar', ['-tvzf', tarballPath], opts);
|
|
881
|
+
if (members.status !== 0 || types.status !== 0 || types.stdout.trim().split('\n').some((line) => !/^[-d]/.test(line))) throw new Error('archive-list');
|
|
882
|
+
const names = members.stdout.trim().split('\n');
|
|
883
|
+
const files = names.filter((name) => !name.endsWith('/'));
|
|
884
|
+
const expected = new Set(inventory.paths.map((name) => `package/${name}`));
|
|
885
|
+
if (names.some((name) => !name.startsWith('package/') || name.split('/').some((part) => part === '..' || part === '.')) || files.length !== expected.size || new Set(files).size !== files.length || files.some((name) => !expected.has(name))) throw new Error('archive-census');
|
|
886
|
+
const unpacked = join(work, 'unpacked');
|
|
887
|
+
mkdirSync(unpacked);
|
|
888
|
+
const extracted = runImpl('tar', ['-xzf', tarballPath, '-C', unpacked], opts);
|
|
889
|
+
if (extracted.status !== 0) throw new Error('archive-extraction');
|
|
890
|
+
const inventoryPath = join(work, 'inventory.json');
|
|
891
|
+
writeFileSync(inventoryPath, JSON.stringify([inventory.record]), { mode: 0o600 });
|
|
892
|
+
const scanner = fileURLToPath(new URL('./lib/validate/check-owner-leakage.mjs', import.meta.url));
|
|
893
|
+
const scan = runImpl(process.execPath, [scanner, join(unpacked, 'package'), '--require-owner-patterns', '--packed-files', inventoryPath], opts);
|
|
894
|
+
const summaries = [...(scan.stdout || '').matchAll(/^Results: (\d+) passed, (\d+) failed \((\d+) scanned files\)$/gm)];
|
|
895
|
+
if (scan.status !== 0 || summaries.length !== 1 || Number(summaries[0][1]) < 1 || Number(summaries[0][2]) !== 0 || Number(summaries[0][3]) !== inventory.paths.length) return { ok: false, detail: 'required owner policy / packed content scan failed or incomplete' };
|
|
896
|
+
if (archiveDigest(tarballPath) !== digest) throw new Error('archive-changed');
|
|
897
|
+
chmodSync(tarballPath, 0o400);
|
|
898
|
+
artifact = { tarballPath, digest };
|
|
899
|
+
detail = `${inventory.paths.length} packed files, filenames and contents verified`;
|
|
900
|
+
} catch { return { ok: false, detail: 'packed archive preparation or content verification failed' }; }
|
|
901
|
+
return { ok: true, detail, value: await callback(artifact) };
|
|
902
|
+
} finally {
|
|
903
|
+
// Cleanup must never replace an irreversible receipt or the original error.
|
|
904
|
+
try { cleanupImpl(work, { recursive: true, force: true }); }
|
|
905
|
+
catch { console.error('release: temporary package cleanup failed; release outcome unchanged'); }
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
828
909
|
/**
|
|
829
910
|
* Remote-branch parity verdict over `git ls-remote <remote> refs/heads/<branch>`.
|
|
830
911
|
*
|
|
@@ -1206,22 +1287,6 @@ async function preflight(repoRoot, target, { skipCi = false } = {}) {
|
|
|
1206
1287
|
add('ci-green-on-head', ciRows.gitlab.ok, ciRows.gitlab.detail);
|
|
1207
1288
|
add('ci-green-on-head-github', ciRows.github.ok, ciRows.github.detail);
|
|
1208
1289
|
|
|
1209
|
-
// 7. Leakage gate over the actual pack file list.
|
|
1210
|
-
// `npm_config_loglevel` is INHERITED, and `npm pack --dry-run` writes its whole
|
|
1211
|
-
// file listing as `npm notice` lines. Any ancestor that ran under `npm run
|
|
1212
|
-
// --silent` (the pre-push gate does exactly that) therefore hands this child a
|
|
1213
|
-
// silent loglevel and the listing is EMPTY with exit 0 — measured: 818 notice
|
|
1214
|
-
// lines normally, 0 under `npm_config_loglevel=silent`. The blind-scan floor
|
|
1215
|
-
// turns that into a correct fail-closed verdict, but a red leakage row that
|
|
1216
|
-
// means "we could not look" is not the row anyone reads it as. Pin the level
|
|
1217
|
-
// rather than inherit it, so the gate's input never depends on its caller.
|
|
1218
|
-
const pack = run('npm', ['pack', '--dry-run'], {
|
|
1219
|
-
cwd: repoRoot,
|
|
1220
|
-
env: { ...process.env, npm_config_loglevel: 'notice' },
|
|
1221
|
-
});
|
|
1222
|
-
const leakage = evaluateLeakageGate(pack);
|
|
1223
|
-
add('leakage-gate', leakage.ok, leakage.detail);
|
|
1224
|
-
|
|
1225
1290
|
return checks;
|
|
1226
1291
|
}
|
|
1227
1292
|
|
|
@@ -1404,7 +1469,7 @@ export function waitForRegistryPropagation(repoRoot, target, deps = {}) {
|
|
|
1404
1469
|
* reads as unconfirmed, `publish()` throws, and `main()` exits 2 = "pre-receipt,
|
|
1405
1470
|
* safe to rerun" while the registry already holds the version. That is the worst
|
|
1406
1471
|
* failure this file can produce, and it is the same inherited-silent trap the
|
|
1407
|
-
* leakage gate's `npm pack --
|
|
1472
|
+
* leakage gate's `npm pack --json` was already pinned against (see preflight
|
|
1408
1473
|
* step 7). The pin makes the receipt independent of the caller's environment.
|
|
1409
1474
|
*
|
|
1410
1475
|
* Exported because `publish()` itself deliberately is not (it is the
|
|
@@ -1415,10 +1480,11 @@ export function waitForRegistryPropagation(repoRoot, target, deps = {}) {
|
|
|
1415
1480
|
* @param {string} userconfigPath — the 0600 temp npmrc carrying the token
|
|
1416
1481
|
* @returns {{cmd: string, args: string[], opts: {cwd: string, env: object}}}
|
|
1417
1482
|
*/
|
|
1418
|
-
export function publishInvocation(repoRoot, userconfigPath) {
|
|
1483
|
+
export function publishInvocation(repoRoot, userconfigPath, artifact) {
|
|
1484
|
+
if (!artifact?.tarballPath || !artifact.digest || archiveDigest(artifact.tarballPath) !== artifact.digest) throw new Error('refusing publish without unchanged checked archive');
|
|
1419
1485
|
return {
|
|
1420
1486
|
cmd: 'npm',
|
|
1421
|
-
args: ['publish', '--access', 'public', '--userconfig', userconfigPath],
|
|
1487
|
+
args: ['publish', artifact.tarballPath, '--ignore-scripts', '--access', 'public', '--userconfig', userconfigPath],
|
|
1422
1488
|
opts: { cwd: repoRoot, env: { ...process.env, npm_config_loglevel: 'notice' } },
|
|
1423
1489
|
};
|
|
1424
1490
|
}
|
|
@@ -1431,7 +1497,7 @@ function publish(repoRoot, target, deps = {}) {
|
|
|
1431
1497
|
const token = loadNpmToken(repoRoot);
|
|
1432
1498
|
const runImpl = deps.runImpl ?? run;
|
|
1433
1499
|
const res = withTempUserconfig(token, (tmpRc) => {
|
|
1434
|
-
const call = publishInvocation(repoRoot, tmpRc);
|
|
1500
|
+
const call = publishInvocation(repoRoot, tmpRc, deps.artifact);
|
|
1435
1501
|
return runImpl(call.cmd, call.args, call.opts);
|
|
1436
1502
|
});
|
|
1437
1503
|
const receipt = evaluatePublishReceipt(res, target);
|
|
@@ -1916,20 +1982,24 @@ async function main() {
|
|
|
1916
1982
|
if (values.check || values.publish) {
|
|
1917
1983
|
const target = readPackageVersion(repoRoot);
|
|
1918
1984
|
const checks = await preflight(repoRoot, target, { skipCi: values['skip-ci'] });
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1985
|
+
const checked = await withCheckedPackage(repoRoot, async (artifact) => {
|
|
1986
|
+
checks.push({ name: 'leakage-gate', ok: true, detail: 'packed filenames and required owner-policy content scan verified' });
|
|
1987
|
+
const ok = printChecks(checks, values.json && !values.publish, target);
|
|
1988
|
+
if (!ok) return 1;
|
|
1989
|
+
if (!values.publish) return 0;
|
|
1990
|
+
console.log(`\nPublishing ${PACKAGE_NAME}@${target} ...`);
|
|
1991
|
+
console.log(' Publish and post-receipt reconciliation may take several minutes; do not kill it mid-run.');
|
|
1992
|
+
const outcome = await runPublishRelease(repoRoot, target, {
|
|
1993
|
+
publishImpl: (root, version) => publish(root, version, { artifact }),
|
|
1994
|
+
});
|
|
1995
|
+
return printPublishOutcome(outcome, target);
|
|
1996
|
+
});
|
|
1997
|
+
if (!checked.ok) {
|
|
1998
|
+
checks.push({ name: 'leakage-gate', ok: false, detail: checked.detail });
|
|
1999
|
+
printChecks(checks, values.json && !values.publish, target);
|
|
2000
|
+
return 1;
|
|
2001
|
+
}
|
|
2002
|
+
return checked.value;
|
|
1933
2003
|
}
|
|
1934
2004
|
|
|
1935
2005
|
console.error('Nothing to do — pass --check, --publish, or --set-version X.Y.Z (see --help).');
|
|
@@ -55,8 +55,8 @@ import {
|
|
|
55
55
|
resolveGateTimeoutMs,
|
|
56
56
|
} from './lib/gates/gate-helpers.mjs';
|
|
57
57
|
import { buildCommandSignature, spawnInGroup } from './lib/process-group.mjs';
|
|
58
|
-
import {
|
|
59
|
-
import {
|
|
58
|
+
import { resolveWaveNumber } from './lib/quality-gate-wave.mjs';
|
|
59
|
+
import { readProcessLocalSessionIds } from './lib/session-identity/own-session.mjs';
|
|
60
60
|
import { gateKillFields } from './lib/quality-gate.mjs';
|
|
61
61
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
62
62
|
import { shellQuote } from './lib/sh-quote.mjs';
|
|
@@ -266,42 +266,6 @@ function resolveLedgerRoot(value) {
|
|
|
266
266
|
return null;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
/**
|
|
270
|
-
* Resolve the active wave number from the wave-scope sidecar (#966 step 1).
|
|
271
|
-
*
|
|
272
|
-
* Mirrors `resolveWave()` in `hooks/pre-bash-memory-propose-audit.mjs` — the
|
|
273
|
-
* same `.{pi,cursor,codex,claude}/wave-scope.json` precedence via
|
|
274
|
-
* {@link findOwnScopeFile}, skipping a PEER session's manifest in the same
|
|
275
|
-
* working copy (#1504: a peer-only result is no wave of ours, never the peer's
|
|
276
|
-
* number) — with ONE deliberate difference: the hook returns `0` for "no
|
|
277
|
-
* wave-scope file", this returns `null`.
|
|
278
|
-
*
|
|
279
|
-
* Absent is not zero. A human running `npm run quality-gate` from a `git push`
|
|
280
|
-
* has no wave at all, and that is the common case; publishing `wave_number: 0`
|
|
281
|
-
* would invent a wave 0 that every consumer then has to special-case. The
|
|
282
|
-
* caller spreads the result so the KEY is omitted, exactly as `counts` is.
|
|
283
|
-
*
|
|
284
|
-
* A non-positive or non-numeric `wave` field is treated the same way — waves
|
|
285
|
-
* are 1-indexed, so `0` on disk carries no more information than an absent file.
|
|
286
|
-
*
|
|
287
|
-
* Never throws.
|
|
288
|
-
*
|
|
289
|
-
* @param {string} projectDir — directory whose wave-scope sidecar to read.
|
|
290
|
-
* @returns {number|null} positive wave number, or `null` when there is no wave.
|
|
291
|
-
*/
|
|
292
|
-
function resolveWaveNumber(projectDir) {
|
|
293
|
-
try {
|
|
294
|
-
const ownIds = new Set(readProcessLocalSessionIds({ env: process.env, hookInput: null }));
|
|
295
|
-
const { path: waveFile } = findOwnScopeFile(projectDir, ownIds, classifyManifestSession);
|
|
296
|
-
if (!waveFile || !existsSync(waveFile)) return null;
|
|
297
|
-
const wave = JSON.parse(readFileSync(waveFile, 'utf8'))?.wave;
|
|
298
|
-
if (typeof wave !== 'number' || !Number.isFinite(wave) || wave <= 0) return null;
|
|
299
|
-
return Math.trunc(wave);
|
|
300
|
-
} catch {
|
|
301
|
-
return null;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
269
|
// ---------------------------------------------------------------------------
|
|
306
270
|
// Main — the CLI body. Guarded below so a bare `import()` runs none of it.
|
|
307
271
|
// ---------------------------------------------------------------------------
|
|
@@ -471,7 +435,13 @@ async function main() {
|
|
|
471
435
|
//
|
|
472
436
|
// stdout is PIPED (not inherited) so the suite counts the gate already computed
|
|
473
437
|
// can be lifted straight off its JSON envelope into telemetry (#954) instead of
|
|
474
|
-
// travelling as prose through the STATE.md header.
|
|
438
|
+
// travelling as prose through the STATE.md header. Both skill readers select
|
|
439
|
+
// the last matching flat event by semantic_session_id + wave_number; there is
|
|
440
|
+
// no platform field on the payload. Count fallback (wave-loop step 7: this
|
|
441
|
+
// wave's own gate output; session end: the STATE.md header) is limited to
|
|
442
|
+
// pre-#954 sessions, gates outside the event wrapper, or missing wave
|
|
443
|
+
// attribution (see docs/events-schema.md).
|
|
444
|
+
// The envelope is re-emitted
|
|
475
445
|
// verbatim below, so the stdout contract is unchanged — a gate sub-script writes
|
|
476
446
|
// exactly one JSON line at the very end (its own child commands are captured by
|
|
477
447
|
// `runCheck`), so nothing streamed before and nothing streams now.
|
package/scripts/site-numbers.mjs
CHANGED
|
@@ -996,11 +996,15 @@ export const SITEMAP_FILE = 'sitemap.xml';
|
|
|
996
996
|
* Unknown either way is an ERROR, never a skip (the `METRIC_IDS` rule): a
|
|
997
997
|
* sitemap `<loc>` missing here would never be dated, and a page carrying cells
|
|
998
998
|
* that is missing here would drift unseen.
|
|
999
|
+
*
|
|
1000
|
+
* A page without counted cells (for example /lead) never has its <lastmod>
|
|
1001
|
+
* raised automatically: bump it by hand whenever its content changes.
|
|
999
1002
|
*/
|
|
1000
1003
|
export const SITEMAP_PAGES = Object.freeze([
|
|
1001
1004
|
{ page: 'index.html', loc: 'https://session-orchestrator.com/' },
|
|
1002
1005
|
{ page: 'de/index.html', loc: 'https://session-orchestrator.com/de' },
|
|
1003
1006
|
{ page: 'guide/index.html', loc: 'https://session-orchestrator.com/guide' },
|
|
1007
|
+
{ page: 'lead/index.html', loc: 'https://session-orchestrator.com/lead' },
|
|
1004
1008
|
{ page: 'impressum/index.html', loc: 'https://session-orchestrator.com/impressum' },
|
|
1005
1009
|
{ page: 'datenschutz/index.html', loc: 'https://session-orchestrator.com/datenschutz' },
|
|
1006
1010
|
]);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* vault-consolidate.mjs — One-shot vault consolidation migration (PRD F1.1, Issue #499).
|
|
4
4
|
*
|
|
5
5
|
* Folds the redundant vault at `~/Projects/vault/` into the canonical
|
|
6
|
-
*
|
|
6
|
+
* host-configured destination, with per-file conflict resolution, idempotent
|
|
7
7
|
* re-runs, and a compressed backup of the source side BEFORE any write.
|
|
8
8
|
*
|
|
9
9
|
* Design — two-phase coordinator-driven flow for merge resolution
|
|
@@ -95,10 +95,10 @@ import {
|
|
|
95
95
|
stageBackup,
|
|
96
96
|
compressAndCleanupBackup,
|
|
97
97
|
} from './lib/vault-consolidate-fs.mjs';
|
|
98
|
+
import { loadHostPaths, resolveHostPath } from './lib/config/host-paths.mjs';
|
|
98
99
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
99
100
|
|
|
100
101
|
const DEFAULT_SOURCE = '~/Projects/vault';
|
|
101
|
-
const DEFAULT_CANONICAL = '~/Projects/Bernhard/vault';
|
|
102
102
|
|
|
103
103
|
// ---------------------------------------------------------------------------
|
|
104
104
|
// CLI parsing
|
|
@@ -122,7 +122,7 @@ Options:
|
|
|
122
122
|
--source <path> Path to the redundant (source) vault.
|
|
123
123
|
Default: ${DEFAULT_SOURCE}
|
|
124
124
|
--canonical <path> Path to the canonical vault.
|
|
125
|
-
Default:
|
|
125
|
+
Default: SO_VAULT_DIR or owner.yaml paths.vault-dir (required when source exists)
|
|
126
126
|
--json Newline-delimited JSON output (one record per
|
|
127
127
|
file, plus a final summary record).
|
|
128
128
|
--resolve <rel>=<choice> Record a merge decision for the file at the
|
|
@@ -215,7 +215,9 @@ async function main() {
|
|
|
215
215
|
const isJson = parsed.values.json === true;
|
|
216
216
|
|
|
217
217
|
const sourceRoot = path.resolve(expandTilde(parsed.values.source ?? DEFAULT_SOURCE));
|
|
218
|
-
const
|
|
218
|
+
const canonicalTarget = parsed.values.canonical ?? resolveHostPath('vault-dir', null, loadHostPaths());
|
|
219
|
+
const canonicalRoot = typeof canonicalTarget === 'string' && canonicalTarget.trim()
|
|
220
|
+
? path.resolve(expandTilde(canonicalTarget.trim())) : null;
|
|
219
221
|
|
|
220
222
|
// Parse --resolve flags into a Map<relPath, "src"|"dst"|"skip">
|
|
221
223
|
const resolutions = new Map();
|
|
@@ -255,6 +257,11 @@ async function main() {
|
|
|
255
257
|
process.exit(0);
|
|
256
258
|
}
|
|
257
259
|
|
|
260
|
+
if (!canonicalRoot) {
|
|
261
|
+
process.stderr.write('ERROR: canonical vault is unconfigured — pass --canonical or set SO_VAULT_DIR / owner.yaml paths.vault-dir\n');
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
258
265
|
if (!(await isDir(canonicalRoot))) {
|
|
259
266
|
process.stderr.write(
|
|
260
267
|
`ERROR: canonical vault not found at ${canonicalRoot} — refusing to consolidate\n`
|
|
@@ -262,6 +269,14 @@ async function main() {
|
|
|
262
269
|
process.exit(2);
|
|
263
270
|
}
|
|
264
271
|
|
|
272
|
+
if (await fs.realpath(sourceRoot) === await fs.realpath(canonicalRoot)) {
|
|
273
|
+
const summary = { kind:'summary', mode:isDryRun ? 'dry-run' : 'apply', source:sourceRoot,
|
|
274
|
+
canonical:canonicalRoot, counts:{copy:0,'skip-already-present':0,merge:0,'conflict-needs-review':0},
|
|
275
|
+
notice:'source and canonical resolve to the same vault — nothing to do' };
|
|
276
|
+
process.stdout.write(isJson ? JSON.stringify(summary) + '\n' : summary.notice + '\n');
|
|
277
|
+
process.exit(0);
|
|
278
|
+
}
|
|
279
|
+
|
|
265
280
|
// -------------------------------------------------------------------------
|
|
266
281
|
// Output helpers (close over isJson + the per-run records buffer)
|
|
267
282
|
// -------------------------------------------------------------------------
|
|
@@ -8,6 +8,8 @@ knows paths, fields, deadlines and lease validity; `scripts/fleet-checkin.mjs` w
|
|
|
8
8
|
This file describes them and never redefines them. The coordinator itself is not part of this
|
|
9
9
|
package: it is operated outside the plugin, which owns its operational detail (how it measures,
|
|
10
10
|
which conditions it issues, how it acquires and hands over the lease).
|
|
11
|
+
A public coordinator that implements this contract is lead-session-orchestrator
|
|
12
|
+
(https://session-orchestrator.com/lead); any other tool that writes the same files works too.
|
|
11
13
|
|
|
12
14
|
**Grundsatz (CSM-003).** Zustand liegt in Dateien; Nachrichten sind nur Hinweise auf eine Datei.
|
|
13
15
|
Der Navigator erteilt keine Freigaben — maßgeblich ist der Owner-Auftrag der Session. Eine Datei,
|
|
@@ -34,7 +34,7 @@ PHASE 1 IMPLEMENTED (2026-04-19). Session-end opt-in quality gate. Upstream of `
|
|
|
34
34
|
|
|
35
35
|
| # | Check | What it scans | How |
|
|
36
36
|
|---|-------|---------------|-----|
|
|
37
|
-
| 1 | `path-resolver` | Every absolute path `/Users/…` in scope files | `existsSync(path)` |
|
|
37
|
+
| 1 | `path-resolver` | Every absolute path `/Users/…` in scope files | `existsSync(path)`; missing paths under validated owner.yaml `drift-check.foreign-host-prefixes` are warnings (#937) |
|
|
38
38
|
| 2 | `project-count-sync` | Hardcoded "N registered" / "N projects" claims next to `01-projects/` | compare to `ls -d 01-projects/*/` |
|
|
39
39
|
| 3 | `issue-reference-freshness` | `#NN` in forward-looking sections (What's Next, Backlog, Open Issues, Offene Themen, Todo, Next Steps) | `glab issue view NN --repo <origin>` |
|
|
40
40
|
| 4 | `session-file-existence` | `50-sessions/YYYY-MM-DD-*.md` references anywhere in scope | `existsSync(vault/50-sessions/<file>)` |
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import { readFileSync, readdirSync, existsSync, statSync, lstatSync, realpathSync } from 'node:fs';
|
|
34
|
-
import { join, relative, resolve } from 'node:path';
|
|
34
|
+
import { join, relative, resolve, posix } from 'node:path';
|
|
35
35
|
import { execFileSync } from 'node:child_process';
|
|
36
36
|
import { createRequire } from 'node:module';
|
|
37
37
|
import { resolveInstructionFile } from '../../scripts/lib/common.mjs';
|
|
@@ -41,6 +41,7 @@ import { isSessionConfigHeading } from '../../scripts/lib/config/section-extract
|
|
|
41
41
|
import { FRONTMATTER_NOT_AT_TOP, isRuleExpired, parseGlobsFrontmatter } from '../../scripts/lib/rule-loader.mjs';
|
|
42
42
|
import { resolveRepoSpec } from '../../scripts/lib/vcs-repo-spec.mjs';
|
|
43
43
|
import { userInvocableSkills } from '../../scripts/lib/user-invocable-skills.mjs';
|
|
44
|
+
import { loadOwnerConfig } from '../../scripts/lib/owner-yaml.mjs';
|
|
44
45
|
import { learningKeyOf } from '../../scripts/lib/learnings/kebab.mjs';
|
|
45
46
|
|
|
46
47
|
const FORWARD_HEADING_RE =
|
|
@@ -1016,6 +1017,9 @@ function main() {
|
|
|
1016
1017
|
if (!args.skipSessionFiles) checksRun.push('session-file-existence');
|
|
1017
1018
|
for (const s of activeSurfaces) checksRun.push(s.id);
|
|
1018
1019
|
|
|
1020
|
+
const foreignHostPrefixes = args.skipPathResolver ? [] :
|
|
1021
|
+
(loadOwnerConfig().config['drift-check']?.['foreign-host-prefixes'] ?? [])
|
|
1022
|
+
.map((prefix) => prefix.replace(/\/+$/, ''));
|
|
1019
1023
|
const errors = [];
|
|
1020
1024
|
const warnings = [];
|
|
1021
1025
|
// Informational findings — never block, never count as a warning. Currently
|
|
@@ -1731,9 +1735,16 @@ function main() {
|
|
|
1731
1735
|
while ((m = pathRegex.exec(line)) !== null) {
|
|
1732
1736
|
const p = m[0].replace(/[.,;:)\]`"']+$/, '');
|
|
1733
1737
|
if (!existsSync(p)) {
|
|
1734
|
-
|
|
1738
|
+
// Lexical containment must use the effective POSIX path: '..' can
|
|
1739
|
+
// escape an allowed foreign directory. Keep p for diagnostics.
|
|
1740
|
+
const normalizedPath = posix.normalize(p);
|
|
1741
|
+
const foreign = foreignHostPrefixes.some((prefix) =>
|
|
1742
|
+
normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`));
|
|
1743
|
+
(foreign ? warnings : errors).push({
|
|
1735
1744
|
check: 'path-resolver', file: rel, line: lineNum,
|
|
1736
|
-
message:
|
|
1745
|
+
message: foreign
|
|
1746
|
+
? `Known foreign-host path does not exist on this host: ${p}`
|
|
1747
|
+
: `Absolute path does not exist: ${p}`,
|
|
1737
1748
|
extracted: p,
|
|
1738
1749
|
});
|
|
1739
1750
|
}
|
|
@@ -30,7 +30,7 @@ Exit codes (`offload --help`, measured 2026-09-02): `0` ok · `1` usage/config
|
|
|
30
30
|
|
|
31
31
|
The gate decides, not the coordinator. `applyOffloadDecision()` in `scripts/lib/wave-resource-gate.mjs` fires when the resource verdict is `reduce` or `coordinator-direct` (and, with `offload-first: true`, on `proceed` as well — see below), and only AFTER the HR-004 heavy-repo cap — a capped wave that offloads still respects the cap. It never probes the network; the coordinator supplies a readiness WITNESS:
|
|
32
32
|
|
|
33
|
-
- `opts.remoteReady` — `{ [alias]: boolean }`, built from the SessionStart banner line `Offload <alias>: ready=yes
|
|
33
|
+
- `opts.remoteReady` — `{ [alias]: boolean }`, built from the SessionStart banner line `Offload <alias>: headless-ready=yes …` (legacy `ready=yes` only without new readiness markers; `gate-ready` does not authorize Claude dispatch), or
|
|
34
34
|
- `opts.probeFn` — an async `(alias) => boolean` fallback, consulted only for aliases `remoteReady` doesn't answer for (backed by `remoteDoctor()`, i.e. `offload doctor -H <alias> --brief` parsed by `parseDoctorLine()`).
|
|
35
35
|
|
|
36
36
|
With neither supplied, no host counts as ready and the wave stays local — the gate fails toward local, never toward an unverified host. A role in `NEVER_FOREIGN_ROLES` (`scripts/lib/wave-executor/dispatch-common.mjs`: `impl-core`, `security-review`, `migration`, `release`, `secrets`, `incident`, `refactor-crosscut`) is never offloaded regardless of readiness.
|
|
@@ -172,19 +172,49 @@ Finalize session metrics by reading the wave data accumulated during execution:
|
|
|
172
172
|
> - `effectiveness`: ALWAYS populated from Phase 1 plan verification results, and CONSTRUCTED EXPLICITLY in the METRICS_ENTRY snippet (#773) — never deferred to a "remember to add" optional step (that omission is how `carryover: 0` slipped past 41 records). `completion_rate` = `completed / planned_issues` (0.0-1.0, where 0.0 means nothing was completed). **`carryover` counting rule (#773):** `carryover` is the **length of the Phase 1.65 gate carry-list** — `autoCarry` ∪ the middle-band `ask` items the operator LEFT SELECTED ∪ the answered-question `impliesWork: true` candidates — NOT the raw Phase 1.2+1.3 candidate count. On the fail-open skip (gate disabled / headless / AUQ unavailable), EVERY candidate carries, so `carryover` = the full candidate-list length. Count the gate's OUTPUT (what reaches Phase 5 Step 3 filing), not its INPUT.
|
|
173
173
|
> - `effectiveness.override_ratio` (#730/H5): OPTIONAL nested field = `overridden_findings / max(total_findings_surfaced, 1)` (float 0.0-1.0). Populate ONLY when Phase 2.6 (Broken-Window Budget) ran this session (`broken-window-budget.enabled: true`). OMIT (do NOT write null/0) otherwise — **absent = "not measured"**, `0.0` = "measured, nothing overridden". `overridden_findings` = the summed `count` of the `orchestrator.finding.overridden` events emitted this session; `total_findings_surfaced` = every MED/LOW+ finding surfaced across Phase 1.8 + wave reviewers.
|
|
174
174
|
> - `waves[].agent_count_planned` / `waves[].agent_count_started` / `waves[].agent_count_completed` (#724/#1115): OPTIONAL per-wave fields, sourced from `wave-loop.md` § Capture wave metrics step 7 — mirror its definitions exactly, do not re-derive them here. `agent_count_planned` = agents named in the session plan for this wave. `agent_count_started` = distinct agents whose `agent-<id>.meta.json` sidecar is present, after any silent-drop re-dispatch — NOT "produced a tool-result" (under background dispatch the launch ack is a result and would count an agent that never ran). `agent_count_completed` = distinct agents whose task-notification (`<status>completed</status>`) arrived. Omit each field when the wave did not measure it — **absent = "not measured"**, never zero-fill; `0` would read as "measured, no agent started", which is the opposite of an unmeasured wave. The two gaps carry the diagnosis: `agent_count_planned > agent_count_started` after re-dispatch is a persistent silent drop, `agent_count_started > agent_count_completed` at wave end is an agent that started and never returned. Both are also logged to STATE.md `## Deviations` by wave-loop.md, so a record and a deviation entry should agree.
|
|
175
|
-
> - `waves[].suite_passed` / `waves[].suite_failed` / `waves[].suite_platform` (#944): OPTIONAL per-wave fields.
|
|
176
|
-
> **`suite_passed` / `suite_failed`: read the event FIRST, the STATE.md header only as fallback (#966 step 3).** Since #954/#967 the between-waves gate wrapper `scripts/run-quality-gate.mjs` emits `orchestrator.quality_gate.{passed,failed}` with a machine-measured `counts: {passed, failed, total}` AND the `wave_number` it resolved from the `wave-scope.json` sidecar, so per-wave attribution needs no wall-clock window join. Payload fields are flat at the record's top level; for each wave
|
|
175
|
+
> - `waves[].suite_passed` / `waves[].suite_failed` / `waves[].suite_platform` (#944): OPTIONAL per-wave fields. Treat counts and header platform independently; absent = "not measured", `suite_failed: 0` = "measured, zero failures".
|
|
176
|
+
> **`suite_passed` / `suite_failed`: read the event FIRST, the STATE.md header only as fallback (#966 step 3).** Since #954/#967 the between-waves gate wrapper `scripts/run-quality-gate.mjs` emits `orchestrator.quality_gate.{passed,failed}` with a machine-measured `counts: {passed, failed, total}` AND the `wave_number` it resolved from the `wave-scope.json` sidecar, so per-wave attribution needs no wall-clock window join. Payload fields are flat at the record's top level; for each wave of this session:
|
|
177
177
|
>
|
|
178
178
|
> ```bash
|
|
179
|
-
> jq -
|
|
180
|
-
>
|
|
181
|
-
> | select(.
|
|
182
|
-
> | .
|
|
179
|
+
> jq -cR --argjson w "$WAVE" --arg s "$SEMANTIC_SESSION_ID" '
|
|
180
|
+
> fromjson? | objects
|
|
181
|
+
> | select((.event | type) == "string" and (.event | startswith("orchestrator.quality_gate.")))
|
|
182
|
+
> | select(.semantic_session_id == $s and .wave_number == $w and .["counts"] != null)
|
|
183
|
+
> | .["counts"]
|
|
184
|
+
> ' .orchestrator/metrics/events.jsonl | tail -1
|
|
183
185
|
> ```
|
|
184
186
|
>
|
|
185
|
-
>
|
|
186
|
-
>
|
|
187
|
-
>
|
|
187
|
+
> `WAVE` and `SEMANTIC_SESSION_ID` come from the existing own session/wave identity; never construct either for this lookup. Filtering by both is mandatory: the ledger accumulates across sessions, and a different session or wave must not supply these counts. `counts.passed` → `suite_passed`, `counts.failed` → `suite_failed`. `tail -1` selects the last matching gate run of the wave, including auto-fix. `-R` with `fromjson? | objects` skips an unreadable ledger line (a torn tail from a killed writer, #1401) instead of letting one parse error end `jq`. The old pipeline could emit an earlier matching value before the parse error; `tail -1` retained that stale value and could hide the jq failure. Known limit: an interrupted `appendFile` can leave a truncated record that `fromjson?` discards completely; `tail -1` can still select an older matching run, which alone is not evidence of the current gate result. The library emits one record per `runQualityGateWithRetry` call, not per retry; `attempts` is not a record count.
|
|
188
|
+
>
|
|
189
|
+
> An empty counts selector cannot distinguish a missing event, missing wave attribution,
|
|
190
|
+
> or an attributed event without counts after fail-fast. Inspect the own session's
|
|
191
|
+
> records without the counts filter before deciding whether a documented count
|
|
192
|
+
> fallback case applies:
|
|
193
|
+
>
|
|
194
|
+
> ```bash
|
|
195
|
+
> jq -cR --argjson w "$WAVE" --arg s "$SEMANTIC_SESSION_ID" '
|
|
196
|
+
> fromjson? | objects
|
|
197
|
+
> | select((.event | type) == "string" and (.event | startswith("orchestrator.quality_gate.")))
|
|
198
|
+
> | select(.semantic_session_id == $s)
|
|
199
|
+
> | select(.wave_number == $w or .wave_number == null)
|
|
200
|
+
> | {event, semantic_session_id, wave_number, counts}
|
|
201
|
+
> ' .orchestrator/metrics/events.jsonl
|
|
202
|
+
> ```
|
|
203
|
+
>
|
|
204
|
+
> This is a diagnostic, not a replacement counts source. No rows means no readable
|
|
205
|
+
> matching event; a row without `wave_number` is not proof of this wave's run;
|
|
206
|
+
> a row for this wave without `counts` supplies no measurement. Keep the session
|
|
207
|
+
> filter and never copy counts from another session or wave, or from a row without
|
|
208
|
+
> wave attribution. An event without `semantic_session_id` is not attributable by
|
|
209
|
+
> this query either; diagnose missing session attribution from independent evidence
|
|
210
|
+
> of the own gate run, not by adopting an unrelated unattributed event. Missing
|
|
211
|
+
> attribution or an empty result alone does not prove a fallback source exists.
|
|
212
|
+
>
|
|
213
|
+
> **Count fallback is limited to three cases:** pre-#954 sessions, a gate run outside the event wrapper, or missing wave attribution (including an auto-fix call without a readable own wave scope). An event without `semantic_session_id` cannot match the selector — emitters set it only from the session lock via `sessionAttribution()` — and is treated like a gate run outside the wrapper. In those cases only, read the STATE.md Wave History header `— suite <passed>/<failed> on <platform>`. An auto-fix call with a known wave uses the event selector above. An empty selector alone is not a fallback case: an event without `counts`, for example after fail-fast before the test suite, supplies no replacement zeros and does not by itself permit count fallback.
|
|
214
|
+
>
|
|
215
|
+
> If no counts are available and none of those fallback cases applies, omit `suite_passed` and `suite_failed`. Absent means "not measured"; a present `suite_failed: 0` means "measured, zero failures". Never zero-fill missing measurements.
|
|
216
|
+
>
|
|
217
|
+
> **`suite_platform` comes separately and exclusively from the STATE.md Wave History header. There is no platform field in the event payload.** Omit it when no header platform measurement is available; do not infer it from `variant`, runner OS, or session platform. Event-derived counts may still be mirrored into the existing header as compatibility output, not a second measurement source; keep the header and sessions.jsonl contracts unchanged.
|
|
188
218
|
> - `open_questions_asked` / `open_questions_answered` / `open_questions_deferred` (#773): the three open-question counts from the Phase 1.65 gate's AUQ Call 2 (identical to the `questions_*` payload fields on the `orchestrator.handover.gated` event). Top-level, additive, non-negative integers. Populate ONLY when the gate ran an interactive triage ("Closen + Triage" path). OMIT all three (do NOT write `0`) when the gate was skipped (fail-open / headless / disabled) or took the fast-path — absent = "not measured", `0` = "measured, zero questions". Validator accepts absent/null/non-negative-integer.
|
|
189
219
|
> - `stagnation_events`: populated ONLY when ≥1 stagnation event was logged to `events.jsonl` during this session. When `total == 0`, the field is omitted from the JSONL entry.
|
|
190
220
|
> - `grounding_injections`: populated ONLY when ≥1 `orchestrator.grounding.injected` event was logged to `events.jsonl` during this session. When `count == 0`, the field is omitted from the JSONL entry.
|
|
@@ -84,7 +84,7 @@ const gate = await evaluateWaveResourceGate({
|
|
|
84
84
|
|
|
85
85
|
**The offload witness is supplied, never probed (#1160).** The gate's own JSDoc states it: `@param {Record<string, boolean>} [opts.remoteReady] — #1160 readiness witness per declared host alias. The gate never probes the network itself; without a witness no host counts as ready and the decision stays local.` So an `offload` decision is impossible unless the coordinator passes one of two things alongside `config`/`plannedAgents`/`waveRole`:
|
|
86
86
|
|
|
87
|
-
- `remoteReady: { '<alias>': true }` — built from the SessionStart banner line `Offload <alias>: ready=yes
|
|
87
|
+
- `remoteReady: { '<alias>': true }` — built from the SessionStart banner line `Offload <alias>: headless-ready=yes` (legacy `ready=yes` only without new readiness markers; `gate-ready` does not authorize Claude dispatch), which was measured at session start and needs no new network call here; or
|
|
88
88
|
- `probeFn: remoteReadyProbe` from `scripts/lib/wave-executor/remote-dispatch.mjs` — `@param {(alias: string) => Promise<boolean>} [opts.probeFn] — optional async witness, consulted only for aliases absent from `remoteReady`. Default null.` Use it when the banner is stale or absent; it costs a live probe per alias.
|
|
89
89
|
|
|
90
90
|
Passing neither is a valid choice, not a bug: the gate then behaves exactly as it did before #1160 and reduces locally.
|
|
@@ -378,15 +378,45 @@ If the commit itself fails (e.g., nothing to commit, pre-commit hook rejects), d
|
|
|
378
378
|
**Copy the two counts off the gate's own event — do not re-read them from the terminal (#966 step 3).** `scripts/run-quality-gate.mjs`, the wrapper that fires between waves, emits `orchestrator.quality_gate.{passed,failed}` carrying a machine-measured `counts: {passed, failed, total}` (admitted by `admitSuiteCounts()`) plus the `wave_number` it resolved from `wave-scope.json`. Payload fields are flat at the record's top level:
|
|
379
379
|
|
|
380
380
|
```bash
|
|
381
|
-
jq -
|
|
382
|
-
|
|
383
|
-
| select(.
|
|
384
|
-
| .
|
|
381
|
+
jq -cR --argjson w "$WAVE" --arg s "$SEMANTIC_SESSION_ID" '
|
|
382
|
+
fromjson? | objects
|
|
383
|
+
| select((.event | type) == "string" and (.event | startswith("orchestrator.quality_gate.")))
|
|
384
|
+
| select(.semantic_session_id == $s and .wave_number == $w and .["counts"] != null)
|
|
385
|
+
| .["counts"]
|
|
386
|
+
' .orchestrator/metrics/events.jsonl | tail -1
|
|
385
387
|
```
|
|
386
388
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
389
|
+
`WAVE` and `SEMANTIC_SESSION_ID` come from the existing own session/wave identity; never construct either for this lookup. Filtering by both is mandatory: the ledger accumulates across sessions, and a different session or wave must not supply these counts. `counts.passed` → `suite_passed`, `counts.failed` → `suite_failed`. `tail -1` selects the last matching gate run of the wave, including auto-fix. `-R` with `fromjson? | objects` skips an unreadable ledger line (a torn tail from a killed writer, #1401) instead of letting one parse error end `jq`. The old pipeline could emit an earlier matching value before the parse error; `tail -1` retained that stale value and could hide the jq failure. Known limit: an interrupted `appendFile` can leave a truncated record that `fromjson?` discards completely; `tail -1` can still select an older matching run, which alone is not evidence of the current gate result. The library emits one record per `runQualityGateWithRetry` call, not per retry; `attempts` is not a record count.
|
|
390
|
+
|
|
391
|
+
An empty counts selector cannot distinguish a missing event, missing wave attribution,
|
|
392
|
+
or an attributed event without counts after fail-fast. Inspect the own session's
|
|
393
|
+
records without the counts filter before deciding whether a documented count
|
|
394
|
+
fallback case applies:
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
jq -cR --argjson w "$WAVE" --arg s "$SEMANTIC_SESSION_ID" '
|
|
398
|
+
fromjson? | objects
|
|
399
|
+
| select((.event | type) == "string" and (.event | startswith("orchestrator.quality_gate.")))
|
|
400
|
+
| select(.semantic_session_id == $s)
|
|
401
|
+
| select(.wave_number == $w or .wave_number == null)
|
|
402
|
+
| {event, semantic_session_id, wave_number, counts}
|
|
403
|
+
' .orchestrator/metrics/events.jsonl
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
This is a diagnostic, not a replacement counts source. No rows means no readable
|
|
407
|
+
matching event; a row without `wave_number` is not proof of this wave's run;
|
|
408
|
+
a row for this wave without `counts` supplies no measurement. Keep the session
|
|
409
|
+
filter and never copy counts from another session or wave, or from a row without
|
|
410
|
+
wave attribution. An event without `semantic_session_id` is not attributable by
|
|
411
|
+
this query either; diagnose missing session attribution from independent evidence
|
|
412
|
+
of the own gate run, not by adopting an unrelated unattributed event. Missing
|
|
413
|
+
attribution or an empty result alone does not prove a fallback source exists.
|
|
414
|
+
|
|
415
|
+
**Count fallback is limited to three cases:** pre-#954 sessions, a gate run outside the event wrapper, or missing wave attribution (including an auto-fix call without a readable own wave scope). An event without `semantic_session_id` cannot match the selector — emitters set it only from the session lock via `sessionAttribution()` — and is treated like a gate run outside the wrapper. In those cases only, fall back to the gate output you read for this wave — the same run whose counts go into the § 3a header — and say so in the progress update. Do not read the STATE.md Wave History header here: this step runs before § 3a writes this wave's header, so the newest header line there belongs to the previous wave. An auto-fix call with a known wave uses the event selector above. An empty selector alone is not a fallback case: an event without `counts`, for example after fail-fast before the test suite, supplies no replacement zeros and does not by itself permit count fallback.
|
|
416
|
+
|
|
417
|
+
If no counts are available and none of those fallback cases applies, omit `suite_passed` and `suite_failed`. Absent means "not measured"; a present `suite_failed: 0` means "measured, zero failures". Never zero-fill missing measurements.
|
|
418
|
+
|
|
419
|
+
**`suite_platform` comes separately from that same gate run of this wave — the platform it ran on, which § 3a then writes as `on <platform>` into the header. There is no platform field in the event payload.** Omit it when the platform of that run is not known; never infer it from `variant`. Event-derived counts may still be mirrored into the existing header as compatibility output, not a second measurement source; keep the header and sessions.jsonl contracts unchanged.
|
|
390
420
|
Append this wave record to the session metrics `waves` array.
|
|
391
421
|
|
|
392
422
|
7a. **Scope drift tripwire (S2 — #896, warn-only)**: session-cumulative and filtered through `DRIFT_EXCLUDE_PATTERNS`, so its `filesRatio` is NOT expected to agree with this wave's unfiltered `files_changed` from step 7. Only the committed part of its numerator starts at `session-start-ref`; the uncommitted and untracked parts read the checkout as it is now, so files already dirty at session start and a same-checkout peer's files count too (an over-report, named as a ceiling in `computeDrift()`'s JSDoc). Call `computeDrift()` from the same `scripts/lib/scope-baseline.mjs` module as `wave-loop-dispatch.md` § 0a Scope Baseline Freeze. Never blocks — exit code stays 0 and the next wave is dispatched regardless of the result.
|