session-orchestrator 5.9.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.
Files changed (39) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/CHANGELOG.md +33 -2
  6. package/README.md +10 -9
  7. package/docs/USER-GUIDE.md +2 -0
  8. package/docs/ci-setup.md +46 -1
  9. package/docs/events-schema.md +35 -5
  10. package/docs/owner-config-schema.md +18 -1
  11. package/docs/owner-privacy-patterns.md +85 -0
  12. package/docs/scope-collision-guard.md +12 -2
  13. package/hooks/enforce-commands.mjs +9 -19
  14. package/hooks/enforce-scope.mjs +23 -42
  15. package/hooks/hooks-codex.json +1 -1
  16. package/hooks/hooks.json +1 -1
  17. package/hooks/post-bash-write-verify.mjs +38 -21
  18. package/package.json +2 -2
  19. package/scripts/emit-session.mjs +15 -0
  20. package/scripts/lib/gates/gate-full.mjs +6 -0
  21. package/scripts/lib/gates/gate-helpers.mjs +2 -0
  22. package/scripts/lib/named-vault-resolver.mjs +1 -1
  23. package/scripts/lib/owner-yaml.mjs +33 -3
  24. package/scripts/lib/platform.mjs +70 -1
  25. package/scripts/lib/pre-push-remote-gate.mjs +107 -0
  26. package/scripts/lib/quality-gate.mjs +6 -1
  27. package/scripts/lib/redact-spans.mjs +8 -17
  28. package/scripts/lib/scope-gate.mjs +68 -5
  29. package/scripts/lib/validate/check-owner-leakage.mjs +198 -1015
  30. package/scripts/lib/vault-backfill/template.mjs +1 -1
  31. package/scripts/lib/vault-mirror/render-sessions.mjs +15 -3
  32. package/scripts/lib/vault-status/board-writer.mjs +15 -7
  33. package/scripts/migrate-vault-paths.mjs +57 -60
  34. package/scripts/release.mjs +103 -36
  35. package/scripts/vault-consolidate.mjs +19 -4
  36. package/skills/claude-md-drift-check/SKILL.md +1 -1
  37. package/skills/claude-md-drift-check/checker.mjs +14 -3
  38. package/skills/session-end/metrics-collection.md +26 -2
  39. package/skills/wave-executor/references/wave-loop-review.md +26 -2
@@ -851,7 +851,12 @@ export function gateKillFields(run) {
851
851
  * Count fallback is limited to pre-#954 sessions, gates outside the event
852
852
  * wrapper, or missing wave attribution; step 7 falls back to this wave's own
853
853
  * gate output (its header is not written yet), session end to the header. Auto-fix with a known wave
854
- * uses the event; missing counts alone do not permit header fallback.
854
+ * uses the event; missing counts alone do not permit count fallback.
855
+ * Diagnose own-session events without the counts filter to distinguish absent
856
+ * events, missing wave attribution, and fail-fast events without counts; never
857
+ * adopt counts from another session/wave or an unattributed row. A truncated
858
+ * append can be skipped by fromjson?, leaving an older matching run: that alone
859
+ * does not establish the current gate result.
855
860
  *
856
861
  * `suite_platform` comes from this wave's gate run (step 7) or the STATE.md
857
862
  * Wave History header `— suite <passed>/<failed> on <platform>` (session end);
@@ -9,28 +9,19 @@
9
9
  *
10
10
  * Behaviour is unchanged from the original: this module is a move, not a rewrite.
11
11
  *
12
- * DELIBERATE SECOND COPY — `check-owner-leakage.mjs` still carries its OWN inline
13
- * `redactSpans()` and must keep it. That scanner is a documented standalone
14
- * single-file vendoring target (`.claude/rules/security.md` § "Owner-Privacy
15
- * Pre-Commit Hook"): consumer repos copy that ONE file, so a static import of this
16
- * module throws ERR_MODULE_NOT_FOUND in every vendored copy and blocks all commits
17
- * (proven by the three cases in `tests/husky/pre-commit-owner-leakage.test.mjs`).
18
- * Unlike CP11's confidential-names helpers, a redaction sink cannot degrade to
19
- * inert — an absent redaction prints confidential names into a PUBLIC CI log.
20
- * So: THIS module serves in-tree consumers, the inline copy serves the vendored
21
- * path, and `tests/lib/redact-spans.test.mjs` pins the two byte-for-byte against
22
- * each other. Edit one → that drift guard goes red until both agree again.
12
+ * The owner-leakage scanner remains a standalone vendoring target. Since #1530
13
+ * it omits offending line content entirely and redacts private filename segments,
14
+ * so it no longer carries a second copy of this primitive. In-tree consumers
15
+ * that retain surrounding text use this module; its overlap and ordering
16
+ * contracts are covered by tests/lib/redact-spans.test.mjs.
23
17
  */
24
18
 
25
19
  /**
26
20
  * Redact every confidential-name span from `line`, ORDER-INDEPENDENTLY (Fix 1 + Fix 2).
27
21
  *
28
- * This is the single redaction sink for the confidential-names privacy invariant.
29
- * In `check-owner-leakage.mjs` it is applied at the print choke-point over EVERY
30
- * violation's lineContent — not only CP11 hits — because that scanner runs in a
31
- * PUBLIC GitHub-Actions mirror: a confidential customer/repo name that co-occurs
32
- * with a CP1–CP10 hit on the same line (e.g. a name beside an RFC1918 IP that
33
- * fails CP8) would otherwise be echoed verbatim to the public CI log (Fix 1).
22
+ * Consumers must pass every relevant confidential pattern before emitting text.
23
+ * This helper matches the supplied text literally; callers handling reversible
24
+ * encodings must normalize first or omit the sensitive text entirely.
34
25
  *
35
26
  * ORDER-INDEPENDENCE (Fix 2): a naïve chain of `.replace()` calls is order-dependent
36
27
  * — when one configured name is a PREFIX of another (`['acme','acme-corp-secret']`),
@@ -5,14 +5,15 @@
5
5
  * hot-paths — all sync. Re-exported by hardening.mjs as a barrel so existing
6
6
  * importers keep working unchanged.
7
7
  *
8
- * Layering: hook-safe — pure functions only; no I/O at import time;
9
- * ESM-pure for fast hook hot-paths. Hooks (under `hooks/`) import from
10
- * this lib; this lib MUST NOT reverse-import from `hooks/`. Cross-cutting
11
- * invariant for all exports below — see #554 A2.
8
+ * Layering: hook-safe synchronous helpers; no I/O at import time. Guard
9
+ * context lookups read bounded filesystem state when called. Hooks (under
10
+ * `hooks/`) import this ESM-only lib; it MUST NOT reverse-import from hooks.
11
+ * Cross-cutting invariant for all exports below — see #554 A2.
12
12
  */
13
13
 
14
- import { existsSync, readFileSync, realpathSync } from 'node:fs';
14
+ import { existsSync, readFileSync, realpathSync, lstatSync, openSync, fstatSync, readSync, closeSync, constants } from 'node:fs';
15
15
  import path from 'node:path';
16
+ import { resolveSessionRoot, isolatedAgentCheckout, registeredWorktreeRoots } from './platform.mjs';
16
17
 
17
18
  import { tokenizeCommand, splitChainSegments, resolveSegmentVerb } from './command-blocker.mjs';
18
19
 
@@ -106,6 +107,68 @@ export function findOwnScopeFile(projectRoot, ownIds, classify) {
106
107
  return { path: null, foreignPath };
107
108
  }
108
109
 
110
+ /**
111
+ * Guard context: control state belongs to the coordinator, relative grants to
112
+ * the actual isolated checkout. Remote adoption requires unique OWN identity;
113
+ * local unknown/corrupt/foreign/missing dispositions remain unchanged.
114
+ * Root-only discovery deliberately excludes arbitrary worktree subdirectories;
115
+ * revisit when the harness supplies a coordinator-root witness (#1504).
116
+ * @param {{cwd: string, ownIds: Set<string>, classify: Function}} options
117
+ * @returns {{manifestRoot: string, writeRoot: string, path: string|null,
118
+ * foreignPath: string|null, isolated: boolean, error: string|null}}
119
+ */
120
+ export function resolveScopeContext({ cwd, ownIds, classify }) {
121
+ let manifestRoot = resolveSessionRoot(cwd);
122
+ try { manifestRoot = realpathSync.native(manifestRoot); } catch { /* preserve non-existing-root fallback */ }
123
+ const local = findOwnScopeFile(manifestRoot, ownIds, classify);
124
+ const agent = isolatedAgentCheckout(cwd);
125
+ const context = { manifestRoot, writeRoot: agent?.writeRoot ?? manifestRoot,
126
+ ...local, isolated: agent !== null, error: null };
127
+ if (!agent || local.path || ownIds.size === 0) return context;
128
+ const candidates = registeredWorktreeRoots(agent.commonDir);
129
+ if (candidates.error) return { ...context, error: candidates.error };
130
+ const matches = new Map();
131
+ for (const root of candidates.roots) {
132
+ if (root === agent.writeRoot || root === manifestRoot || isolatedAgentCheckout(root)) continue;
133
+ // Inspect precedence candidates separately. An unsafe candidate has unknown
134
+ // ownership: it must never turn an isolated session into a no-scope ALLOW.
135
+ for (const dir of SCOPE_FILE_DIRS) {
136
+ const file = path.join(root, dir, 'wave-scope.json');
137
+ let fd;
138
+ let raw;
139
+ try {
140
+ const before = lstatSync(file);
141
+ if (!before.isFile() || before.size > 1024 * 1024) return { ...context, error: 'unsafe-coordinator-scope' };
142
+ fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
143
+ const opened = fstatSync(fd);
144
+ if (!opened.isFile() || opened.ino !== before.ino || opened.dev !== before.dev || opened.size > 1024 * 1024) return { ...context, error: 'unsafe-coordinator-scope' };
145
+ const buffer = Buffer.alloc(1024 * 1024 + 1);
146
+ let length = 0;
147
+ while (length < buffer.length) {
148
+ const n = readSync(fd, buffer, length, buffer.length - length, null);
149
+ if (n === 0) break;
150
+ length += n;
151
+ }
152
+ if (length > 1024 * 1024) return { ...context, error: 'unsafe-coordinator-scope' };
153
+ raw = buffer.toString('utf8', 0, length);
154
+ } catch (err) {
155
+ if (err?.code === 'ENOENT') continue;
156
+ return { ...context, error: 'unsafe-coordinator-scope' };
157
+ } finally { if (fd !== undefined) closeSync(fd); }
158
+ try {
159
+ const scope = JSON.parse(raw);
160
+ if (classify(scope, ownIds).verdict === 'own') {
161
+ matches.set(realpathSync(file), { root, path: file, foreignPath: null });
162
+ break; // highest-precedence positively owned manifest in this root
163
+ }
164
+ } catch { /* unbound/corrupt remote state is never authority */ }
165
+ }
166
+ }
167
+ if (matches.size > 1) return { ...context, error: 'ambiguous-coordinator-scope' };
168
+ const match = matches.values().next().value;
169
+ return match ? { ...context, manifestRoot: match.root, path: match.path, foreignPath: null } : context;
170
+ }
171
+
109
172
  /**
110
173
  * Read the enforcement level from a scope file.
111
174
  * Defaults to "strict" (fail-closed) on parse error or missing field.