session-orchestrator 3.20.0 → 3.21.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/030-wave-execution.mdc +10 -8
- package/CHANGELOG.md +414 -0
- package/README.md +16 -11
- package/agents/analyst.md +1 -1
- package/agents/architect-reviewer.md +1 -1
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +1 -1
- package/agents/dialectic-deriver.md +1 -1
- package/agents/docs-writer.md +1 -1
- package/agents/memory-proposal-collector.md +1 -1
- package/agents/qa-strategist.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +42 -1
- package/agents/skill-applied-judge.md +1 -1
- package/agents/test-writer.md +1 -1
- package/agents/ui-developer.md +1 -1
- package/agents/ux-evaluator.md +1 -1
- package/commands/release.md +60 -0
- package/docs/migration-v3.md +9 -6
- package/docs/persona-panel.md +3 -1
- package/docs/scope-collision-guard.md +167 -0
- package/hooks/enforce-scope.mjs +103 -3
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +11 -1
- package/hooks/on-session-end.mjs +52 -5
- package/hooks/on-stop.mjs +127 -12
- package/hooks/post-bash-write-verify.mjs +8 -32
- package/hooks/pre-task-scope-disjoint.mjs +1042 -0
- package/package.json +1 -1
- package/pi/prompts/release.md +12 -0
- package/scripts/autopilot.mjs +3 -1
- package/scripts/emit-session.mjs +42 -0
- package/scripts/export-hw-learnings.mjs +61 -2
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/backlog-scan.mjs +106 -15
- package/scripts/lib/build-live-signals.mjs +7 -3
- package/scripts/lib/ci-status-banner.mjs +207 -23
- package/scripts/lib/git-config-drift.mjs +471 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
- package/scripts/lib/io.mjs +193 -7
- package/scripts/lib/learnings/select.mjs +157 -3
- package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
- package/scripts/lib/mirror-issues-banner.mjs +266 -0
- package/scripts/lib/named-vault-resolver.mjs +105 -16
- package/scripts/lib/peer-cards/schema.mjs +6 -2
- package/scripts/lib/reconcile/writer.mjs +69 -30
- package/scripts/lib/redact-spans.mjs +89 -0
- package/scripts/lib/scope-baseline.mjs +77 -17
- package/scripts/lib/scope-gate.mjs +658 -0
- package/scripts/lib/secret-masker.mjs +262 -0
- package/scripts/lib/session-lock.mjs +34 -10
- package/scripts/lib/session-registry.mjs +9 -1
- package/scripts/lib/state-md/mission-status.mjs +21 -12
- package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
- package/scripts/lib/validate/check-agents.mjs +77 -5
- package/scripts/lib/validate/check-commands.mjs +2 -20
- package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +18 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
- package/scripts/lib/validate/check-skills.mjs +191 -0
- package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
- package/scripts/lib/validate/check-unwired-features.mjs +219 -11
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
- package/scripts/lib/validate/frontmatter-block.mjs +61 -0
- package/scripts/lib/validate/tier-inference.mjs +46 -8
- package/scripts/lib/vault-mirror/namespace.mjs +146 -1
- package/scripts/lib/vault-mirror/process.mjs +264 -31
- package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
- package/scripts/lib/vault-status/board-writer.mjs +300 -56
- package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
- package/scripts/lib/vcs-repo-spec.mjs +500 -19
- package/scripts/print-learnings-index.mjs +30 -3
- package/scripts/release.mjs +616 -61
- package/scripts/site-numbers.mjs +1049 -0
- package/scripts/validate-plugin.mjs +61 -0
- package/scripts/validate-wave-scope.mjs +281 -12
- package/scripts/vault-mirror.mjs +26 -1
- package/skills/_shared/monitor-patterns.md +24 -4
- package/skills/_shared/state-ownership.md +17 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -1
- package/skills/debug/SKILL.md +4 -1
- package/skills/discovery/issue-templates.md +4 -4
- package/skills/discovery/probes-code.md +2 -2
- package/skills/discovery/probes-feature.md +6 -6
- package/skills/discovery/probes-infra.md +2 -2
- package/skills/discovery/probes-session.md +5 -5
- package/skills/dispatcher/SKILL.md +10 -1
- package/skills/frontmatter-guard/SKILL.md +9 -1
- package/skills/gitlab-ops/SKILL.md +49 -39
- package/skills/gitlab-portfolio/SKILL.md +10 -1
- package/skills/memory-cleanup/SKILL.md +18 -5
- package/skills/npm-publish/SKILL.md +22 -50
- package/skills/persona-panel/SKILL.md +3 -1
- package/skills/plan/mode-new.md +23 -5
- package/skills/repo-audit/SKILL.md +10 -1
- package/skills/session-end/SKILL.md +44 -2
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/session-metrics-write.md +4 -10
- package/skills/session-plan/SKILL.md +2 -2
- package/skills/session-plan/wave-template.md +1 -1
- package/skills/session-start/SKILL.md +13 -1
- package/skills/spinout/SKILL.md +5 -1
- package/skills/sunset-review/SKILL.md +11 -1
- package/skills/tmux-layout/SKILL.md +7 -2
- package/skills/vault-mirror/SKILL.md +10 -1
- package/skills/vault-sync/SKILL.md +10 -1
- package/skills/vault-sync/validator.mjs +55 -6
- package/skills/wave-executor/wave-loop.md +42 -12
- package/scripts/lib/mission-status-schema.mjs +0 -114
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* redact-spans.mjs — order-independent, overlap-safe span redaction primitive.
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from `scripts/lib/validate/check-owner-leakage.mjs` (issue
|
|
5
|
+
* #974) so that every redaction sink in the repo shares ONE implementation of the
|
|
6
|
+
* overlap-safe span merge rather than each re-deriving it (and each re-deriving
|
|
7
|
+
* the prefix/suffix bug described below). Pure and synchronous — no I/O, no
|
|
8
|
+
* module state, no dependencies — because hot-path consumers import it.
|
|
9
|
+
*
|
|
10
|
+
* Behaviour is unchanged from the original: this module is a move, not a rewrite.
|
|
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.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Redact every confidential-name span from `line`, ORDER-INDEPENDENTLY (Fix 1 + Fix 2).
|
|
27
|
+
*
|
|
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).
|
|
34
|
+
*
|
|
35
|
+
* ORDER-INDEPENDENCE (Fix 2): a naïve chain of `.replace()` calls is order-dependent
|
|
36
|
+
* — when one configured name is a PREFIX of another (`['acme','acme-corp-secret']`),
|
|
37
|
+
* redacting the shorter first destroys the longer's match and leaks a suffix residue
|
|
38
|
+
* (`[REDACTED]-corp-secret`). Instead we compute ALL match spans against the ORIGINAL
|
|
39
|
+
* (unmutated) string across every pattern, merge overlapping/adjacent intervals, and
|
|
40
|
+
* splice `[REDACTED]` per merged interval. No pattern ever sees a string another
|
|
41
|
+
* pattern already rewrote, so prefix/suffix overlap cannot leak regardless of list
|
|
42
|
+
* order.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} line — the raw (already-trimmed) violation lineContent.
|
|
45
|
+
* @param {RegExp[]} patterns — confidential-name regexes (word-boundary, case-insensitive).
|
|
46
|
+
* @returns {string} the line with every configured name span replaced by [REDACTED].
|
|
47
|
+
*/
|
|
48
|
+
export function redactSpans(line, patterns) {
|
|
49
|
+
if (!Array.isArray(patterns) || patterns.length === 0) return line;
|
|
50
|
+
|
|
51
|
+
// 1. Collect [start, end) spans of every match of every pattern against the
|
|
52
|
+
// ORIGINAL line (never a partially-mutated one). Global clone so exec() walks
|
|
53
|
+
// all matches; zero-width guard prevents an infinite loop on a degenerate regex.
|
|
54
|
+
const spans = [];
|
|
55
|
+
for (const re of patterns) {
|
|
56
|
+
const g = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g');
|
|
57
|
+
let m;
|
|
58
|
+
while ((m = g.exec(line)) !== null) {
|
|
59
|
+
if (m[0].length === 0) {
|
|
60
|
+
g.lastIndex += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
spans.push([m.index, m.index + m[0].length]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (spans.length === 0) return line;
|
|
67
|
+
|
|
68
|
+
// 2. Merge overlapping / adjacent intervals (sorted by start).
|
|
69
|
+
spans.sort((a, b) => a[0] - b[0]);
|
|
70
|
+
const merged = [];
|
|
71
|
+
for (const [s, e] of spans) {
|
|
72
|
+
const last = merged[merged.length - 1];
|
|
73
|
+
if (last && s <= last[1]) {
|
|
74
|
+
last[1] = Math.max(last[1], e);
|
|
75
|
+
} else {
|
|
76
|
+
merged.push([s, e]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Splice [REDACTED] per merged interval, left-to-right over the ORIGINAL line.
|
|
81
|
+
let out = '';
|
|
82
|
+
let cursor = 0;
|
|
83
|
+
for (const [s, e] of merged) {
|
|
84
|
+
out += line.slice(cursor, s) + '[REDACTED]';
|
|
85
|
+
cursor = e;
|
|
86
|
+
}
|
|
87
|
+
out += line.slice(cursor);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
@@ -124,6 +124,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
124
124
|
|
|
125
125
|
import { parseStateMd, serializeStateMd, resolveStateMdPath, writeStateMd } from './state-md.mjs';
|
|
126
126
|
import { pathMatchesPattern } from './scope-gate.mjs';
|
|
127
|
+
import { resolveBaselineRange, isQueryFailure } from './vcs-repo-spec.mjs';
|
|
127
128
|
|
|
128
129
|
// ---------------------------------------------------------------------------
|
|
129
130
|
// DRIFT_EXCLUDE_PATTERNS — the SINGLE filter source for both the denominator
|
|
@@ -472,23 +473,60 @@ export async function writeBaseline({ repoRoot, intent, ownerBoundary, plannedFi
|
|
|
472
473
|
*
|
|
473
474
|
* Skip precedence (first match wins, so `reason` is deterministic):
|
|
474
475
|
* `no-state-md` → `unreadable-state-md` → `no-baseline` →
|
|
475
|
-
* `stale-baseline` → `unresolvable-ref`
|
|
476
|
+
* `stale-baseline` → (`no-baseline-ref` | `unresolvable-ref`)
|
|
476
477
|
*
|
|
477
|
-
* `session-start-ref` handling —
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
* - Field present
|
|
483
|
-
*
|
|
478
|
+
* `session-start-ref` handling — THREE outcomes, deliberately distinct.
|
|
479
|
+
* `skills/_shared/state-ownership.md:28` declares the field OPTIONAL
|
|
480
|
+
* ("readers MUST tolerate their absence"), so the absent-field branch is a
|
|
481
|
+
* REACHABLE production path, not a defensive corner:
|
|
482
|
+
*
|
|
483
|
+
* - **Field present**, diff against it succeeds → measured; `refUsed`
|
|
484
|
+
* reports the bare ref verbatim (NOT a range — pre-existing contract,
|
|
485
|
+
* unchanged).
|
|
486
|
+
* - **Field absent** → the range is RESOLVED via `resolveBaselineRange()`
|
|
487
|
+
* (scripts/lib/vcs-repo-spec.mjs): preferred remote `R` →
|
|
488
|
+
* `refs/remotes/<R>/HEAD` → `refs/remotes/<R>/{main,master}` → local
|
|
489
|
+
* `{main,master}`. NOT a skip — `refUsed` reports the range actually
|
|
490
|
+
* resolved (`gitlab/main...HEAD`, `origin/master...HEAD`, …).
|
|
491
|
+
* This REPLACES the hard-coded `'origin/main...HEAD'` literal the
|
|
492
|
+
* function carried until #1039, which hard-coded BOTH the remote name
|
|
493
|
+
* AND the default branch in one string. In any repo whose remote is not
|
|
494
|
+
* named `origin`, or whose default branch is not `main`, that diff
|
|
495
|
+
* always failed → the tripwire skipped → and
|
|
496
|
+
* `skills/wave-executor/wave-loop.md` renders every `skipped === true`
|
|
497
|
+
* with NO WARN. A guard that neither bit nor reported that it was not
|
|
498
|
+
* biting.
|
|
499
|
+
* - **Field absent AND nothing resolves** (fresh `git init`, no remotes,
|
|
500
|
+
* no `main`/`master` anywhere) → skip with `reason: 'no-baseline-ref'`.
|
|
501
|
+
* This is an ABSENCE — there is genuinely no session base to measure
|
|
502
|
+
* against — and is deliberately NOT folded onto `unresolvable-ref`,
|
|
503
|
+
* which means the QUERY failed (not a git repo, git not on PATH, a
|
|
504
|
+
* present-but-dead `session-start-ref` after a rebase/force-push).
|
|
505
|
+
* The split follows `isQueryFailure()` from the same module, the single
|
|
506
|
+
* predicate that separates "I could not ask" from "I asked; the answer
|
|
507
|
+
* is no". Folding them was the pre-#1039 behaviour and made an operator
|
|
508
|
+
* unable to tell a broken checkout from a fresh repo.
|
|
509
|
+
*
|
|
510
|
+
* Named ceiling (BV-004) on the local-branch tail of the resolution chain:
|
|
511
|
+
* when the chain lands on a LOCAL `main`/`master` and HEAD is already that
|
|
512
|
+
* branch, `main...HEAD` has merge-base === HEAD and the diff is empty, so
|
|
513
|
+
* the ratio reads `0` instead of skipping. That is the honest floor of the
|
|
514
|
+
* available information (no remote-tracking ref exists to say where the
|
|
515
|
+
* session started) and it is harmless here because both a `0` ratio and a
|
|
516
|
+
* skip are silent under `wave-loop.md`'s WARN-on-breach rule. Revisit if
|
|
517
|
+
* this function ever gains a non-warn consumer that treats `skipped:false`
|
|
518
|
+
* as "measured successfully".
|
|
484
519
|
*
|
|
485
520
|
* @param {object} args
|
|
486
521
|
* @param {string|undefined} args.repoRoot
|
|
487
522
|
* @param {number} [args.threshold] — breach threshold, `>=` counts as
|
|
488
523
|
* breached (default `2.0`).
|
|
489
|
-
* @returns {{ ok: true, skipped: true, reason: 'no-state-md'|'unreadable-state-md'|'no-baseline'|'stale-baseline'|'unresolvable-ref' }
|
|
524
|
+
* @returns {{ ok: true, skipped: true, reason: 'no-state-md'|'unreadable-state-md'|'no-baseline'|'stale-baseline'|'no-baseline-ref'|'unresolvable-ref' }
|
|
490
525
|
* | { ok: true, skipped: false, filesRatio: number, plannedFiles: number,
|
|
491
526
|
* actualFiles: number, breached: boolean, threshold: number, refUsed: string }}
|
|
527
|
+
* Additive since #1039: `no-baseline-ref` is a NEW member of the skip-reason
|
|
528
|
+
* union. No existing member was renamed or removed, and the measured-result
|
|
529
|
+
* shape (all eight keys) is byte-identical to the pre-#1039 contract.
|
|
492
530
|
*/
|
|
493
531
|
export function computeDrift({ repoRoot, threshold = 2.0 } = {}) {
|
|
494
532
|
const parsedFm = readFrontmatterOrReason(repoRoot);
|
|
@@ -514,18 +552,40 @@ export function computeDrift({ repoRoot, threshold = 2.0 } = {}) {
|
|
|
514
552
|
? baseline.sessionStartRef
|
|
515
553
|
: null;
|
|
516
554
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
555
|
+
// Resolve the diff range. The `session-start-ref` branch keeps its exact
|
|
556
|
+
// pre-#1039 shape (two-dot `<ref>..HEAD`, `refUsed` = the bare ref); only
|
|
557
|
+
// the absent-field branch changed, from a hard-coded literal to a real
|
|
558
|
+
// resolution. See the JSDoc above for the three outcomes.
|
|
559
|
+
let diffRange;
|
|
560
|
+
let refUsed;
|
|
561
|
+
if (rawRef !== null) {
|
|
562
|
+
diffRange = `${rawRef}..HEAD`;
|
|
563
|
+
refUsed = rawRef;
|
|
564
|
+
} else {
|
|
565
|
+
const resolved = resolveBaselineRange({ repoRoot: cwd });
|
|
566
|
+
if (!resolved.ok) {
|
|
567
|
+
// `isQueryFailure()` is the frozen predicate that keeps the two causes
|
|
568
|
+
// apart: a broken/absent git ⇒ `unresolvable-ref` (degraded
|
|
569
|
+
// measurement), a repo that simply has no baseline ref ⇒
|
|
570
|
+
// `no-baseline-ref` (a real, benign repo state).
|
|
571
|
+
return {
|
|
572
|
+
ok: true,
|
|
573
|
+
skipped: true,
|
|
574
|
+
reason: isQueryFailure(resolved.reason) ? 'unresolvable-ref' : 'no-baseline-ref',
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
diffRange = resolved.range;
|
|
578
|
+
refUsed = resolved.range;
|
|
579
|
+
}
|
|
521
580
|
|
|
522
581
|
let stdout;
|
|
523
582
|
try {
|
|
524
|
-
stdout = execFileSync('git',
|
|
583
|
+
stdout = execFileSync('git', ['diff', '--name-only', diffRange], { cwd, encoding: 'utf8' });
|
|
525
584
|
} catch {
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
//
|
|
585
|
+
// The range resolved but the diff against it failed — a present-but-dead
|
|
586
|
+
// `session-start-ref` (rebase, force-push, deleted commit), or a
|
|
587
|
+
// resolved base that vanished between resolution and diff. Neither can
|
|
588
|
+
// produce a trustworthy numerator.
|
|
529
589
|
return { ok: true, skipped: true, reason: 'unresolvable-ref' };
|
|
530
590
|
}
|
|
531
591
|
|