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
@@ -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
- errors.push({
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: `Absolute path does not exist: ${p}`,
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
  }
@@ -184,9 +184,33 @@ Finalize session metrics by reading the wave data accumulated during execution:
184
184
  > ' .orchestrator/metrics/events.jsonl | tail -1
185
185
  > ```
186
186
  >
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` and silently empty the selector behind `| tail -1`. The library emits one record per `runQualityGateWithRetry` call, not per retry; `attempts` is not a record count.
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
188
  >
189
- > **Header fallback for counts 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 header fallback.
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.
190
214
  >
191
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.
192
216
  >
@@ -386,9 +386,33 @@ If the commit itself fails (e.g., nothing to commit, pre-commit hook rejects), d
386
386
  ' .orchestrator/metrics/events.jsonl | tail -1
387
387
  ```
388
388
 
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` and silently empty the selector behind `| tail -1`. The library emits one record per `runQualityGateWithRetry` call, not per retry; `attempts` is not a record count.
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
390
 
391
- **Fallback for counts 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 header fallback.
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.
392
416
 
393
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.
394
418