session-orchestrator 5.7.0 → 5.8.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/.agents/skills/architecture/SKILL.md +3 -0
- package/.agents/skills/architecture/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +1 -0
- package/.agents/skills/eval/SKILL.md +1 -1
- package/.agents/skills/frontmatter-guard/SKILL.md +1 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +1 -0
- package/.agents/skills/hook-development/SKILL.md +1 -0
- package/.agents/skills/mcp-builder/SKILL.md +1 -0
- package/.agents/skills/tmux-layout/SKILL.md +3 -0
- package/.agents/skills/tmux-layout/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +1 -0
- package/.agents/skills/vault-sync/SKILL.md +1 -0
- package/.agents/skills/write-executable-plan/SKILL.md +3 -0
- package/.agents/skills/write-executable-plan/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/architecture/SKILL.md +3 -0
- package/.codex-plugin/skills/architecture/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -0
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/frontmatter-guard/SKILL.md +1 -0
- package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +1 -0
- package/.codex-plugin/skills/hook-development/SKILL.md +1 -0
- package/.codex-plugin/skills/mcp-builder/SKILL.md +1 -0
- package/.codex-plugin/skills/tmux-layout/SKILL.md +3 -0
- package/.codex-plugin/skills/tmux-layout/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/vault-mirror/SKILL.md +1 -0
- package/.codex-plugin/skills/vault-sync/SKILL.md +1 -0
- package/.codex-plugin/skills/write-executable-plan/SKILL.md +3 -0
- package/.codex-plugin/skills/write-executable-plan/agents/openai.yaml +5 -0
- package/.cursor/commands/architecture.md +13 -0
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/tmux-layout.md +13 -0
- package/.cursor/commands/write-executable-plan.md +13 -0
- package/.cursor/skills/architecture/SKILL.md +0 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/tmux-layout/SKILL.md +0 -1
- package/.cursor/skills/write-executable-plan/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +49 -0
- package/README.md +10 -9
- package/agents/eval-judge.md +5 -5
- package/docs/baseline.md +4 -3
- package/docs/codex-setup.md +6 -4
- package/docs/components.md +5 -4
- package/docs/events-schema.md +4 -3
- package/docs/github-mirror-protection.md +3 -2
- package/docs/install.md +39 -3
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +2 -2
- package/docs/session-config-reference.md +7 -5
- package/docs/session-config-template.md +3 -2
- package/evals/README.md +60 -0
- package/evals/instruction-ablation/README.md +147 -0
- package/evals/instruction-ablation/cases/psa-003-foreign-work.json +34 -0
- package/evals/instruction-ablation/cases/sec-007-sql-parameterisation.json +32 -0
- package/evals/instruction-ablation/cases/vbc-001-verify-before-claiming.json +41 -0
- package/evals/instruction-ablation/run.mjs +336 -0
- package/evals/psa-003-foreign-work/case.yaml +16 -0
- package/evals/psa-003-foreign-work/fixture.sh +12 -0
- package/evals/psa-003-foreign-work/graders/foreign-file-intact.md +7 -0
- package/evals/psa-003-foreign-work/graders/names-foreign-work.md +6 -0
- package/evals/psa-003-foreign-work/graders/no-destructive-git.md +8 -0
- package/evals/psa-003-foreign-work/graders/ran-git.md +7 -0
- package/evals/psa-003-foreign-work/prompt.md +6 -0
- package/hooks/_lib/hook-import-set.json +10 -2
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/post-tool-batch-wave-signal.mjs +1 -0
- package/hooks/post-tooluse-frontend-slop.mjs +11 -1
- package/hooks/pre-task-scope-disjoint.mjs +8 -6
- package/monitors/monitors.json +1 -1
- package/package.json +3 -1
- package/pi/prompts/architecture.md +11 -0
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/tmux-layout.md +11 -0
- package/pi/prompts/write-executable-plan.md +11 -0
- package/scripts/backfill-abandoned-sessions.mjs +2 -2
- package/scripts/backfill-sessions.mjs +9 -7
- package/scripts/codex-install.mjs +108 -23
- package/scripts/emit-session.mjs +15 -2
- package/scripts/fleet-auflagen.mjs +54 -0
- package/scripts/generate-agents-skills.mjs +9 -16
- package/scripts/generate-codex-skills.mjs +8 -6
- package/scripts/generate-cursor-adapter.mjs +9 -74
- package/scripts/generate-pi-prompts.mjs +8 -74
- package/scripts/github-protection-audit.mjs +8 -0
- package/scripts/lib/auto-dialectic.mjs +10 -9
- package/scripts/lib/auto-dream.mjs +8 -10
- package/scripts/lib/autopilot/telemetry.mjs +12 -6
- package/scripts/lib/bootstrap-lock-refresh.mjs +8 -12
- package/scripts/lib/config/drift-check.mjs +1 -1
- package/scripts/lib/config/frontend-slop-hook.mjs +6 -0
- package/scripts/lib/config.mjs +5 -1
- package/scripts/lib/convergence-monitor.mjs +5 -3
- package/scripts/lib/ecosystem-health.mjs +5 -3
- package/scripts/lib/eval/engine.mjs +61 -27
- package/scripts/lib/eval/judge.mjs +14 -8
- package/scripts/lib/eval/session-resolve.mjs +7 -2
- package/scripts/lib/events-retention-banner.mjs +5 -2
- package/scripts/lib/events.mjs +19 -5
- package/scripts/lib/fleet-protocol.mjs +98 -1
- package/scripts/lib/instruction-budget-guard.mjs +25 -18
- package/scripts/lib/io.mjs +172 -35
- package/scripts/lib/learnings/candidates.mjs +7 -28
- package/scripts/lib/learnings/io.mjs +7 -2
- package/scripts/lib/learnings/judgment.mjs +5 -8
- package/scripts/lib/learnings/select.mjs +3 -3
- package/scripts/lib/maintenance-due-banner.mjs +2 -2
- package/scripts/lib/memory-proposals/sink.mjs +5 -6
- package/scripts/lib/memory-proposals/store.mjs +3 -5
- package/scripts/lib/mirror-issues-banner.mjs +11 -1
- package/scripts/lib/native-command.mjs +48 -0
- package/scripts/lib/orphan-reaper/scan.mjs +29 -3
- package/scripts/lib/peer-cards/writer.mjs +6 -18
- package/scripts/lib/platform.mjs +113 -4
- package/scripts/lib/plugin-package-stage.mjs +169 -0
- package/scripts/lib/plugin-surface.mjs +121 -0
- package/scripts/lib/quality-gate.mjs +30 -25
- package/scripts/lib/reconcile/idempotency.mjs +9 -10
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +48 -11
- package/scripts/lib/reconcile/writer.mjs +11 -17
- package/scripts/lib/rule-loader.mjs +32 -6
- package/scripts/lib/scope-echo.mjs +12 -2
- package/scripts/lib/scope-gate.mjs +2 -1
- package/scripts/lib/session-close-backfill.mjs +3 -4
- package/scripts/lib/session-id.mjs +2 -1
- package/scripts/lib/session-record-repair.mjs +9 -13
- package/scripts/lib/session-schema/constants.mjs +20 -5
- package/scripts/lib/session-start-probes.mjs +2 -2
- package/scripts/lib/sessions-staleness-banner.mjs +2 -3
- package/scripts/lib/skill-evolution/engine.mjs +6 -9
- package/scripts/lib/skill-evolution/idempotency.mjs +8 -10
- package/scripts/lib/slopcheck.mjs +9 -7
- package/scripts/lib/sunset/walker.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +3 -1
- package/scripts/lib/telemetry-flush-health-banner.mjs +2 -2
- package/scripts/lib/test-sibling-coverage.mjs +167 -0
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +5 -7
- package/scripts/lib/tmux-layout/telemetry.mjs +8 -10
- package/scripts/lib/user-invocable-skills.mjs +37 -10
- package/scripts/lib/ux-grill/run-record.mjs +5 -3
- package/scripts/lib/validate/check-banner-parity.mjs +1 -0
- package/scripts/lib/validate/check-skills.mjs +12 -2
- package/scripts/lib/validate/check-test-fixture-shapes.mjs +3 -3
- package/scripts/lib/validate/check-test-value-bans.mjs +3 -2
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -1
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/vault-archive.mjs +2 -4
- package/scripts/lib/vault-mirror/process.mjs +207 -26
- package/scripts/lib/vault-mirror/utils.mjs +16 -1
- package/scripts/lib/vault-sync-baseline.mjs +9 -8
- package/scripts/lib/wave-transcript-tail.mjs +8 -4
- package/scripts/lib/worktree/meta.mjs +7 -3
- package/scripts/mcp-server.mjs +158 -0
- package/scripts/migrate-sessions-jsonl.mjs +7 -5
- package/scripts/migrate-vault-paths.mjs +7 -4
- package/scripts/print-learnings-index.mjs +2 -1
- package/scripts/relocate-vault-corpus.mjs +5 -3
- package/scripts/run-migrate-v2-cross-repo.mjs +9 -7
- package/scripts/self-update.mjs +179 -39
- package/scripts/site-numbers.mjs +4 -3
- package/scripts/sweep-expired-learnings.mjs +6 -4
- package/scripts/sync-vault-schema.mjs +3 -7
- package/scripts/windows-setup.mjs +45 -0
- package/skills/_shared/fleet-protocol.md +8 -0
- package/skills/architecture/SKILL.md +1 -0
- package/skills/bootstrap/_shared-template.md +5 -2
- package/skills/claude-md-drift-check/SKILL.md +4 -1
- package/skills/claude-md-drift-check/checker.mjs +83 -10
- package/skills/discovery/probes/ssot-code-diff.mjs +3 -3
- package/skills/eval/SKILL.md +12 -13
- package/skills/eval/rubric-v3.md +521 -0
- package/skills/frontmatter-guard/SKILL.md +1 -0
- package/skills/gitlab-portfolio/SKILL.md +1 -0
- package/skills/hook-development/SKILL.md +1 -0
- package/skills/mcp-builder/SKILL.md +1 -0
- package/skills/repo-audit/SKILL.md +2 -2
- package/skills/session-end/SKILL.md +32 -1
- package/skills/session-end/vault-operations.md +12 -2
- package/skills/session-start/SKILL.md +2 -2
- package/skills/session-start/phase-7-1-premise-check.md +2 -1
- package/skills/session-start/references/operations-contract.md +4 -1
- package/skills/tmux-layout/SKILL.md +1 -0
- package/skills/vault-mirror/SKILL.md +1 -0
- package/skills/vault-sync/SKILL.md +6 -6
- package/skills/vault-sync/validator.sh +29 -10
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +4 -4
- package/skills/write-executable-plan/SKILL.md +1 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* test-sibling-coverage.mjs — THE measurement behind the Test-Sibling Expansion
|
|
4
|
+
* figures (#970) in skills/wave-executor/references/wave-loop-scope-manifest.md.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this file exists
|
|
7
|
+
*
|
|
8
|
+
* The expansion grants a GLOB (`tests/**\/{basename}*.test.mjs`) instead of a
|
|
9
|
+
* computed path, and the manifest justified that with three hand-measured counts
|
|
10
|
+
* (439 / 375 / 272 at `730ee9d`). Those numbers lived in prose, went stale, and a
|
|
11
|
+
* second contradicting copy appeared in a code comment (#1026.5, #1030 P3). Same
|
|
12
|
+
* cure as `tests-src-ratio.mjs`: the recipe is code, the `--json` envelope
|
|
13
|
+
* carries its own definition, and the docs point here instead of restating it.
|
|
14
|
+
*
|
|
15
|
+
* ## The recipe
|
|
16
|
+
*
|
|
17
|
+
* universe `git ls-files` (the index — tracked paths only)
|
|
18
|
+
* tests tracked files matching DEFAULT_TEST_PATH_PATTERNS (scope-gate)
|
|
19
|
+
* production tracked files matching a DEFAULT_TEST_SIBLING_RULES `source`
|
|
20
|
+
* that are NOT tests — defined by negation, so a new top-level
|
|
21
|
+
* directory is counted the moment it is committed
|
|
22
|
+
* glob production files where a sibling glob from `testSiblingsFor()`
|
|
23
|
+
* matches a test via `pathMatchesPattern()` — the exact matcher
|
|
24
|
+
* the scope gate grants with, reused rather than re-derived
|
|
25
|
+
* exact production files with a test under `tests/` whose basename is
|
|
26
|
+
* exactly `<stem>.test<ext>`
|
|
27
|
+
* mirror production files whose 1:1 mirror exists: `tests/` + the path
|
|
28
|
+
* with a leading `scripts/` dropped, `<ext>` → `.test<ext>`
|
|
29
|
+
*
|
|
30
|
+
* By construction mirror ⊆ exact ⊆ glob ⊆ production, so the counts must obey
|
|
31
|
+
* mirror ≤ exact ≤ glob ≤ total; a violation means a broken definition.
|
|
32
|
+
*
|
|
33
|
+
* Usage: test-sibling-coverage.mjs [<repo-root>] [--json]
|
|
34
|
+
* Exit: 0 measured · 2 tool error (not a git repo, bad argument)
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { execFileSync } from 'node:child_process';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
|
|
40
|
+
import {
|
|
41
|
+
DEFAULT_TEST_PATH_PATTERNS,
|
|
42
|
+
DEFAULT_TEST_SIBLING_RULES,
|
|
43
|
+
pathMatchesPattern,
|
|
44
|
+
testSiblingsFor,
|
|
45
|
+
} from './scope-gate.mjs';
|
|
46
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
47
|
+
|
|
48
|
+
/** Machine-readable schema tag for the --json envelope. */
|
|
49
|
+
export const SCHEMA = 'test-sibling-coverage/1';
|
|
50
|
+
|
|
51
|
+
const isTest = (p) => DEFAULT_TEST_PATH_PATTERNS.some((pat) => pathMatchesPattern(p, pat));
|
|
52
|
+
const isProduction = (p) =>
|
|
53
|
+
!isTest(p) && DEFAULT_TEST_SIBLING_RULES.some((r) => pathMatchesPattern(p, r.source));
|
|
54
|
+
|
|
55
|
+
/** `scripts/lib/x.mjs` → { stem: 'x', ext: '.mjs' } */
|
|
56
|
+
function stemOf(p) {
|
|
57
|
+
const ext = path.posix.extname(p);
|
|
58
|
+
return { stem: path.posix.basename(p, ext), ext };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The 1:1 mirror path the manifest's "naive mirror" figure counts. */
|
|
62
|
+
export function mirrorPathOf(p) {
|
|
63
|
+
const { ext } = stemOf(p);
|
|
64
|
+
const rel = p.startsWith('scripts/') ? p.slice('scripts/'.length) : p;
|
|
65
|
+
return `tests/${rel.slice(0, rel.length - ext.length)}.test${ext}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Measure the three coverage counts over a path list. Pure.
|
|
70
|
+
* @param {string[]} files — repo-relative POSIX paths
|
|
71
|
+
* @returns {{total: number, glob: number, exact: number, mirror: number, uncovered: string[]}}
|
|
72
|
+
*/
|
|
73
|
+
export function measure(files) {
|
|
74
|
+
const tests = files.filter(isTest);
|
|
75
|
+
const testSet = new Set(tests);
|
|
76
|
+
const testBasenames = new Set(tests.filter((t) => t.startsWith('tests/')).map((t) => path.posix.basename(t)));
|
|
77
|
+
const production = files.filter(isProduction);
|
|
78
|
+
let glob = 0;
|
|
79
|
+
let exact = 0;
|
|
80
|
+
let mirror = 0;
|
|
81
|
+
const uncovered = [];
|
|
82
|
+
for (const p of production) {
|
|
83
|
+
const globs = testSiblingsFor([p]);
|
|
84
|
+
// Linear scan of every test per glob — 0.3 s wall for the whole CLI at 571
|
|
85
|
+
// production files (2026-10-03); revisit if the tracked test count passes ~5000.
|
|
86
|
+
if (globs.some((g) => tests.some((t) => pathMatchesPattern(t, g)))) glob += 1;
|
|
87
|
+
else uncovered.push(p);
|
|
88
|
+
const { stem, ext } = stemOf(p);
|
|
89
|
+
if (testBasenames.has(`${stem}.test${ext}`)) exact += 1;
|
|
90
|
+
if (testSet.has(mirrorPathOf(p))) mirror += 1;
|
|
91
|
+
}
|
|
92
|
+
return { total: production.length, glob, exact, mirror, uncovered };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The self-describing definition block shipped inside every --json envelope. */
|
|
96
|
+
export function definition() {
|
|
97
|
+
return {
|
|
98
|
+
source: 'git ls-files (tracked paths only)',
|
|
99
|
+
tests: `tracked paths matching scope-gate DEFAULT_TEST_PATH_PATTERNS: ${DEFAULT_TEST_PATH_PATTERNS.join(', ')}`,
|
|
100
|
+
total: `production files: tracked paths matching a sibling-rule source (${DEFAULT_TEST_SIBLING_RULES.map((r) => r.source).join(', ')}) that are not tests`,
|
|
101
|
+
glob: `production files where a scope-gate testSiblingsFor() glob (${DEFAULT_TEST_SIBLING_RULES.map((r) => r.sibling).join(', ')}) matches a test via pathMatchesPattern`,
|
|
102
|
+
exact: 'production files with a test under tests/ whose basename is exactly <stem>.test<ext>',
|
|
103
|
+
mirror: 'production files whose mirror tests/<path minus leading scripts/, <ext> -> .test<ext>> is tracked',
|
|
104
|
+
invariant: 'mirror <= exact <= glob <= total',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function git(root, args) {
|
|
109
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pct = (n, d) => (d === 0 ? null : Math.round((n / d) * 1000) / 10);
|
|
113
|
+
|
|
114
|
+
function main(argv) {
|
|
115
|
+
let json = false;
|
|
116
|
+
let root = process.cwd();
|
|
117
|
+
for (const a of argv) {
|
|
118
|
+
if (a === '--json') json = true;
|
|
119
|
+
else if (a.startsWith('--')) {
|
|
120
|
+
process.stderr.write(`Unknown argument '${a}'. Usage: test-sibling-coverage.mjs [<repo-root>] [--json]\n`);
|
|
121
|
+
return 2;
|
|
122
|
+
} else root = path.resolve(a);
|
|
123
|
+
}
|
|
124
|
+
let files;
|
|
125
|
+
let ref;
|
|
126
|
+
let dirty;
|
|
127
|
+
try {
|
|
128
|
+
files = git(root, ['ls-files']).split('\n').filter(Boolean);
|
|
129
|
+
ref = git(root, ['rev-parse', '--short', 'HEAD']).trim();
|
|
130
|
+
// The index IS the measured population: only a staged add/delete/rename
|
|
131
|
+
// makes (ref, counts) unreproducible. --no-optional-locks: no index refresh
|
|
132
|
+
// under a parallel session (PSA-007).
|
|
133
|
+
dirty = git(root, ['--no-optional-locks', 'diff', '--cached', '--name-only', '--diff-filter=ADR']).trim() !== '';
|
|
134
|
+
} catch (error) {
|
|
135
|
+
process.stderr.write(`ERROR: git failed in ${root}: ${error.message}\n`);
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
const r = measure(files);
|
|
139
|
+
const envelope = {
|
|
140
|
+
schema: SCHEMA,
|
|
141
|
+
ref,
|
|
142
|
+
dirty,
|
|
143
|
+
measuredAt: new Date().toISOString().slice(0, 10),
|
|
144
|
+
total: r.total,
|
|
145
|
+
glob: r.glob,
|
|
146
|
+
exact: r.exact,
|
|
147
|
+
mirror: r.mirror,
|
|
148
|
+
percent: { glob: pct(r.glob, r.total), exact: pct(r.exact, r.total), mirror: pct(r.mirror, r.total) },
|
|
149
|
+
uncovered: r.uncovered,
|
|
150
|
+
definition: definition(),
|
|
151
|
+
};
|
|
152
|
+
if (json) {
|
|
153
|
+
process.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
|
|
154
|
+
} else {
|
|
155
|
+
const line = (k) => ` ${k.padEnd(7)} ${String(r[k]).padStart(5)} / ${r.total} (${envelope.percent[k]} %)`;
|
|
156
|
+
process.stdout.write(
|
|
157
|
+
`test-sibling coverage at ${ref}${dirty ? ' (index differs from HEAD)' : ''}, ${envelope.measuredAt}\n`
|
|
158
|
+
+ `${line('glob')}\n${line('exact')}\n${line('mirror')}\n`
|
|
159
|
+
+ ` uncovered by the glob: ${r.uncovered.length} (list: --json)\n`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (isMainModule(import.meta.url)) {
|
|
166
|
+
process.exitCode = main(process.argv.slice(2));
|
|
167
|
+
}
|
|
@@ -15,11 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import path from 'node:path';
|
|
18
|
-
import { readEventsWithRotations } from '../events.mjs';
|
|
18
|
+
import { eventsFilePath, readEventsWithRotations } from '../events.mjs';
|
|
19
19
|
import { isMainModule } from '../is-main-module.mjs';
|
|
20
20
|
|
|
21
|
-
const EVENTS_PATH = '.orchestrator/metrics/events.jsonl';
|
|
22
|
-
|
|
23
21
|
/** Every event this module reports on carries this prefix. */
|
|
24
22
|
const TMUX_EVENT_PREFIX = 'tmux-layout.';
|
|
25
23
|
|
|
@@ -58,11 +56,11 @@ export const REQUIRED_EVENTS_WINDOW_DAYS = null;
|
|
|
58
56
|
* the session path. Revisit if this module gains a hot-path caller or if
|
|
59
57
|
* `max-size-mb` is raised past ~100.
|
|
60
58
|
*
|
|
61
|
-
* @param {string} [eventsPath
|
|
59
|
+
* @param {string} [eventsPath=eventsFilePath()] the repo's one ledger (#1514)
|
|
62
60
|
* @returns {{events: Array<object>, complete: boolean|null, gaps: Array<object>,
|
|
63
61
|
* notices: Array<object>}}
|
|
64
62
|
*/
|
|
65
|
-
export function readTmuxEventsEnvelope(eventsPath =
|
|
63
|
+
export function readTmuxEventsEnvelope(eventsPath = eventsFilePath()) {
|
|
66
64
|
const { events, gaps, complete, notices } = readEventsWithRotations(undefined, {
|
|
67
65
|
filePath: eventsPath,
|
|
68
66
|
});
|
|
@@ -83,10 +81,10 @@ export function readTmuxEventsEnvelope(eventsPath = EVENTS_PATH) {
|
|
|
83
81
|
* existing caller expects. Use the envelope when the completeness of the read
|
|
84
82
|
* matters to the answer.
|
|
85
83
|
*
|
|
86
|
-
* @param {string} [eventsPath
|
|
84
|
+
* @param {string} [eventsPath=eventsFilePath()] the repo's one ledger (#1514)
|
|
87
85
|
* @returns {Array<object>} parsed event records (filtered to tmux-layout.* events)
|
|
88
86
|
*/
|
|
89
|
-
export function readTmuxEvents(eventsPath =
|
|
87
|
+
export function readTmuxEvents(eventsPath = eventsFilePath()) {
|
|
90
88
|
return readTmuxEventsEnvelope(eventsPath).events;
|
|
91
89
|
}
|
|
92
90
|
|
|
@@ -19,17 +19,15 @@ import { appendFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
|
|
21
21
|
import { findProjectRoot } from '../common.mjs';
|
|
22
|
+
import { eventsFilePath } from '../events.mjs';
|
|
22
23
|
import { stampEventSchemaVersion, validateEventRecord } from '../events-schema.mjs';
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* Same shape as scripts/lib/session-close-backfill.mjs § EVENTS_REL.
|
|
31
|
-
*/
|
|
32
|
-
const EVENTS_REL = ['.orchestrator', 'metrics', 'events.jsonl'];
|
|
25
|
+
// The ledger path is resolved against a repo root at write time — NEVER a
|
|
26
|
+
// relative path constant. A relative constant resolves against process.cwd(),
|
|
27
|
+
// which is how ~8k test-emitted tmux events landed in the real ledger: the
|
|
28
|
+
// suite spawns scripts/tmux-layout.mjs with cwd = repo root, so every
|
|
29
|
+
// telemetry write went straight into production telemetry. The path is
|
|
30
|
+
// `eventsFilePath(<root>)` (#1514: the repo's one ledger).
|
|
33
31
|
|
|
34
32
|
/**
|
|
35
33
|
* True when this process is a vitest run, or a child spawned by one
|
|
@@ -56,7 +54,7 @@ function isTestRunner() {
|
|
|
56
54
|
export function emit(eventType, payload = {}, { repoRoot } = {}) {
|
|
57
55
|
try {
|
|
58
56
|
if (!repoRoot && isTestRunner()) return;
|
|
59
|
-
const eventsPath =
|
|
57
|
+
const eventsPath = eventsFilePath(repoRoot || findProjectRoot()); // #1514: the repo's one ledger
|
|
60
58
|
const dir = path.dirname(eventsPath);
|
|
61
59
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
62
60
|
// `stampEventSchemaVersion()` rather than an inline `schema_version:` —
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* Since the #1370 command→skill fold (2026-09-16) `commands/` holds only the two
|
|
7
7
|
* names that cannot be a skill (`session`, `templates-ack` — see
|
|
8
8
|
* `tests/commands/headless-bare-command-availability.test.mjs` for the measured
|
|
9
|
-
* mechanism). Every other slash command IS a skill
|
|
10
|
-
* `user-invocable:
|
|
9
|
+
* mechanism). Every other slash command IS a skill. Claude Code lists a skill in
|
|
10
|
+
* the `/` picker unless its frontmatter says `user-invocable: false` — a MISSING
|
|
11
|
+
* key counts as `true` (#1515, measured 2026-10-03: 37 picker entries against a
|
|
12
|
+
* documented 27, because 10 skills carried no key). Consumers that used to
|
|
11
13
|
* count or link `commands/*.md` therefore need the union:
|
|
12
14
|
*
|
|
13
|
-
* commands/*.md ∪ skills
|
|
15
|
+
* commands/*.md ∪ skills whose `user-invocable` is absent or `true`
|
|
14
16
|
*
|
|
15
17
|
* This module exists so that union has one implementation rather than one per
|
|
16
18
|
* consumer (site-numbers tile, sunset walker linkage, the guard tests). It is
|
|
@@ -29,6 +31,7 @@
|
|
|
29
31
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
30
32
|
import path from 'node:path';
|
|
31
33
|
import { parseAgentFrontmatter } from './agent-frontmatter.mjs';
|
|
34
|
+
import { isHiddenSourceName } from './plugin-surface.mjs';
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* YAML 1.1 boolean lookalikes that YAML 1.2 / js-yaml's CORE_SCHEMA (what
|
|
@@ -85,9 +88,10 @@ function normaliseScalar(text) {
|
|
|
85
88
|
* route through this function; four private re-implementations disagreeing on a
|
|
86
89
|
* quoted value is the defect this replaces.
|
|
87
90
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
+
* Judges a PRESENT value only: `undefined` returns `false`. Whether an ABSENT
|
|
92
|
+
* `user-invocable` key makes a skill a slash command is a question about the
|
|
93
|
+
* whole frontmatter, answered by {@link isUserInvocableSkill} (absent = true,
|
|
94
|
+
* as Claude Code reads it — #1515).
|
|
91
95
|
*
|
|
92
96
|
* Accepted: the boolean `true`, and a string that normalises to `true`
|
|
93
97
|
* case-insensitively — so `"true"`, `'true'`, `True`, `true # note` and
|
|
@@ -155,7 +159,30 @@ export function parseSkillFrontmatter(content) {
|
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
/**
|
|
158
|
-
*
|
|
162
|
+
* Claude Code's picker semantics for one parsed SKILL.md frontmatter: the key
|
|
163
|
+
* ABSENT means invocable; a present key goes through
|
|
164
|
+
* {@link isUserInvocableValue}. Kept apart from that predicate on purpose —
|
|
165
|
+
* the sibling marker `disable-model-invocation` shares the value normaliser
|
|
166
|
+
* but defaults to `false` when absent.
|
|
167
|
+
*
|
|
168
|
+
* Every generator (Cursor, Pi, Codex, agents surface) and every counter routes
|
|
169
|
+
* the skill-level question through here, so an absent key cannot be a slash
|
|
170
|
+
* command for the census and a non-command for the adapters (#1515 MED-2).
|
|
171
|
+
* `check-skills` additionally requires the key to be written down, so the
|
|
172
|
+
* absent branch is the harness-parity fallback, not the expected path.
|
|
173
|
+
*
|
|
174
|
+
* @param {Record<string, unknown>|null|undefined} frontmatter
|
|
175
|
+
* @param {string} [file] path named in a lookalike WARN
|
|
176
|
+
* @returns {boolean} `false` for a missing/non-object frontmatter (fail-closed)
|
|
177
|
+
*/
|
|
178
|
+
export function isUserInvocableSkill(frontmatter, file) {
|
|
179
|
+
if (!frontmatter || typeof frontmatter !== 'object') return false;
|
|
180
|
+
if (!Object.hasOwn(frontmatter, 'user-invocable')) return true;
|
|
181
|
+
return isUserInvocableValue(frontmatter['user-invocable'], file);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Names of skills that appear in the `/` picker (key absent or `true`), sorted.
|
|
159
186
|
*
|
|
160
187
|
* @param {string} repoRoot plugin root (the directory holding `skills/`)
|
|
161
188
|
* @returns {string[]}
|
|
@@ -165,7 +192,7 @@ export function userInvocableSkills(repoRoot) {
|
|
|
165
192
|
if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
|
|
166
193
|
const names = [];
|
|
167
194
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
168
|
-
if (!entry.isDirectory() || entry.name
|
|
195
|
+
if (!entry.isDirectory() || isHiddenSourceName(entry.name)) continue;
|
|
169
196
|
const file = path.join(dir, entry.name, 'SKILL.md');
|
|
170
197
|
if (!existsSync(file) || !statSync(file).isFile()) continue;
|
|
171
198
|
let fm;
|
|
@@ -174,7 +201,7 @@ export function userInvocableSkills(repoRoot) {
|
|
|
174
201
|
} catch {
|
|
175
202
|
continue;
|
|
176
203
|
}
|
|
177
|
-
if (fm &&
|
|
204
|
+
if (fm && isUserInvocableSkill(fm, file)) names.push(entry.name);
|
|
178
205
|
}
|
|
179
206
|
return names.sort();
|
|
180
207
|
}
|
|
@@ -184,7 +211,7 @@ export function commandFileNames(repoRoot) {
|
|
|
184
211
|
const dir = path.join(repoRoot, 'commands');
|
|
185
212
|
if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
|
|
186
213
|
return readdirSync(dir)
|
|
187
|
-
.filter((f) => f.endsWith('.md') && !f
|
|
214
|
+
.filter((f) => f.endsWith('.md') && !isHiddenSourceName(f))
|
|
188
215
|
.map((f) => f.replace(/\.md$/, ''))
|
|
189
216
|
.sort();
|
|
190
217
|
}
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import fs from 'node:fs';
|
|
33
33
|
import path from 'node:path';
|
|
34
34
|
|
|
35
|
+
import { atomicWriteWithBackup, envelopeToError } from '../io.mjs';
|
|
35
36
|
import { findingsPath, runRecordPath } from './paths.mjs';
|
|
36
37
|
import { makeRunRecord } from './schema.mjs';
|
|
37
38
|
|
|
@@ -290,9 +291,10 @@ export function updateRunRecordCompare(repoRoot, runId, compare) {
|
|
|
290
291
|
out.push(raw);
|
|
291
292
|
}
|
|
292
293
|
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
const res = atomicWriteWithBackup(ledger, out.length > 0 ? `${out.join('\n')}\n` : '', {
|
|
295
|
+
tmpPrefix: `.${path.basename(ledger)}.tmp`,
|
|
296
|
+
});
|
|
297
|
+
if (!res.ok) throw envelopeToError(res);
|
|
296
298
|
return { updated, skippedLines };
|
|
297
299
|
}
|
|
298
300
|
|
|
@@ -187,6 +187,7 @@ export function collectMarkdownFiles(pluginRoot) {
|
|
|
187
187
|
const output = execFileSync('git', ['ls-files', '-z', '--', ...SCAN_DIRS], {
|
|
188
188
|
cwd: pluginRoot,
|
|
189
189
|
encoding: 'utf8',
|
|
190
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
190
191
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
191
192
|
env,
|
|
192
193
|
});
|
|
@@ -93,7 +93,7 @@ function collectSkillFiles(skillsDir) {
|
|
|
93
93
|
*
|
|
94
94
|
* Rules: R1 frontmatter block present · R8 block parses as a YAML mapping ·
|
|
95
95
|
* R2 `name` present · R3 `name` is kebab-case · R5 `name` === directory name ·
|
|
96
|
-
* R6 `description` present and non-empty.
|
|
96
|
+
* R6 `description` present and non-empty · R9 `user-invocable` stated explicitly.
|
|
97
97
|
*
|
|
98
98
|
* @param {{ dirName: string, relPath: string, absPath: string }} skill
|
|
99
99
|
* @returns {boolean} true when every rule holds
|
|
@@ -148,6 +148,16 @@ function validateSkillFrontmatter(skill) {
|
|
|
148
148
|
fail(`${skill.relPath}: missing or empty required frontmatter field: description`);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// R9 — `user-invocable` written down. Claude Code lists a skill WITHOUT the
|
|
152
|
+
// key in the `/` picker (#1515: 37 entries against a documented 27, from 10
|
|
153
|
+
// keyless skills). The census and the adapter generators now follow that
|
|
154
|
+
// reading (`isUserInvocableSkill`), but a forgotten key is still an
|
|
155
|
+
// unreviewed picker entry — so the decision must be explicit: `true` for an
|
|
156
|
+
// operator command, `false` for a helper the model dispatches.
|
|
157
|
+
if (!Object.hasOwn(frontmatter, 'user-invocable')) {
|
|
158
|
+
fail(`${skill.relPath}: missing required frontmatter field: user-invocable (Claude Code treats an absent key as true and lists the skill in the / picker — write \`user-invocable: true\` for an operator command or \`user-invocable: false\` for a helper)`);
|
|
159
|
+
}
|
|
160
|
+
|
|
151
161
|
return failed === before;
|
|
152
162
|
}
|
|
153
163
|
|
|
@@ -180,7 +190,7 @@ if (!existsSync(skillsDir)) {
|
|
|
180
190
|
}
|
|
181
191
|
|
|
182
192
|
if (valid === skills.length) {
|
|
183
|
-
pass(`all ${skills.length} ${SKILL_FILE} frontmatter blocks parse as YAML and carry a kebab-case name matching their directory
|
|
193
|
+
pass(`all ${skills.length} ${SKILL_FILE} frontmatter blocks parse as YAML and carry a kebab-case name matching their directory, a non-empty description and an explicit user-invocable`);
|
|
184
194
|
}
|
|
185
195
|
}
|
|
186
196
|
}
|
|
@@ -154,10 +154,10 @@ const MAGIC_COMMENT_SCAN_LINES = 5;
|
|
|
154
154
|
function getTrackedTestFiles() {
|
|
155
155
|
let output;
|
|
156
156
|
try {
|
|
157
|
-
output = execFileSync('git', ['ls-files'], { cwd: pluginRoot, encoding: 'utf8' });
|
|
157
|
+
output = execFileSync('git', ['ls-files'], { cwd: pluginRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 64 * 1024 * 1024 });
|
|
158
158
|
} catch {
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
console.error('Error: unable to enumerate tracked test files with git ls-files');
|
|
160
|
+
process.exit(2);
|
|
161
161
|
}
|
|
162
162
|
return output
|
|
163
163
|
.split('\n')
|
|
@@ -461,9 +461,10 @@ const LOCAL_IMPORT = new RegExp(
|
|
|
461
461
|
function trackedTestFiles() {
|
|
462
462
|
let out;
|
|
463
463
|
try {
|
|
464
|
-
out = execFileSync('git', ['ls-files'], { cwd: repoRoot, encoding: 'utf8' });
|
|
464
|
+
out = execFileSync('git', ['ls-files'], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 64 * 1024 * 1024 });
|
|
465
465
|
} catch {
|
|
466
|
-
|
|
466
|
+
console.error('Error: unable to enumerate tracked test files with git ls-files');
|
|
467
|
+
process.exit(2);
|
|
467
468
|
}
|
|
468
469
|
return out
|
|
469
470
|
.split('\n')
|
|
@@ -114,7 +114,7 @@ function walkDir(root, dir, acc = []) {
|
|
|
114
114
|
export function getTrackedFiles(root) {
|
|
115
115
|
let files;
|
|
116
116
|
try {
|
|
117
|
-
const output = execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8' });
|
|
117
|
+
const output = execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
118
118
|
files = output.split('\n').filter(Boolean).map((f) => join(root, f));
|
|
119
119
|
} catch {
|
|
120
120
|
files = walkDir(root, root);
|
|
@@ -451,7 +451,7 @@ const ALLOWLIST = Object.freeze({
|
|
|
451
451
|
'auto-skill-dispatch':
|
|
452
452
|
'prose-only consumer — skills/using-orchestrator/SKILL.md + skills/_shared/bootstrap-gate.md read this key as an LLM instruction; there is no .mjs gate by design',
|
|
453
453
|
'auto-commit-per-wave':
|
|
454
|
-
'
|
|
454
|
+
'planned no-op (#1219) — only skills/wave-executor/references/wave-loop-review.md names this key, as contract prose; no per-wave commit step runs on it yet (scripts/lib/auto-commit.mjs does not exist)',
|
|
455
455
|
'instruction-budget':
|
|
456
456
|
'dedicated reader outside the parser layer — scripts/lib/instruction-budget-guard.mjs parses this block itself (S2 exemption only; S1 evidence is real)',
|
|
457
457
|
});
|
|
@@ -16,10 +16,8 @@
|
|
|
16
16
|
*
|
|
17
17
|
* ── Frontmatter schema (mirror, not import) ─────────────────────────────────
|
|
18
18
|
* The canonical vault frontmatter schema is the GENERATED Zod block in
|
|
19
|
-
* skills/vault-sync/validator.mjs (Z.104-134). That schema is NOT exported,
|
|
20
|
-
*
|
|
21
|
-
* skills/vault-sync/node_modules/). So the field constraints below are a
|
|
22
|
-
* hand-rolled MIRROR of that schema. If the canonical schema changes, update the
|
|
19
|
+
* skills/vault-sync/validator.mjs (Z.104-134). That schema is NOT exported, so
|
|
20
|
+
* the field constraints below are a hand-rolled MIRROR of that schema. If the canonical schema changes, update the
|
|
23
21
|
* constants here to match. The mirror is proven equivalent by an integration
|
|
24
22
|
* test that runs the REAL validator subprocess over generated output
|
|
25
23
|
* (tests/lib/vault-archive.test.mjs § "REAL validator").
|