session-orchestrator 3.16.0 → 3.17.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/CHANGELOG.md +25 -0
- package/README.md +13 -11
- package/docs/README.md +2 -1
- package/docs/components.md +2 -2
- package/docs/pi-setup.md +1 -1
- package/docs/session-config-reference.md +65 -0
- package/docs/session-config-template.md +27 -0
- package/docs/telemetry/telemetry-claims.md +204 -0
- package/docs/telemetry.md +158 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/skill-invocation-telemetry.mjs +109 -10
- package/package.json +12 -2
- package/scripts/compute-grounding-injection.sh +18 -3
- package/scripts/dialectic-deriver.mjs +7 -2
- package/scripts/lib/auto-dialectic.mjs +11 -2
- package/scripts/lib/auto-dream.mjs +16 -5
- package/scripts/lib/build-live-signals.mjs +7 -4
- package/scripts/lib/config/context-coverage.mjs +82 -0
- package/scripts/lib/config/moc-staleness.mjs +98 -0
- package/scripts/lib/config/worktree-orphans.mjs +138 -0
- package/scripts/lib/config.mjs +15 -0
- package/scripts/lib/context-coverage-banner.mjs +223 -0
- package/scripts/lib/dispatcher/enumerate.mjs +151 -31
- package/scripts/lib/dispatcher/rank.mjs +22 -8
- package/scripts/lib/evolve/autonomy-verdict.mjs +5 -0
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +54 -7
- package/scripts/lib/harness-audit/categories/category4.mjs +13 -2
- package/scripts/lib/moc-staleness-banner.mjs +267 -0
- package/scripts/lib/session-end/worktree-orphan-sweep.mjs +252 -0
- package/scripts/lib/session-schema/filters.mjs +88 -0
- package/scripts/lib/session-schema.mjs +1 -0
- package/scripts/lib/skill-health/join.mjs +35 -9
- package/scripts/lib/telemetry/anon-id.mjs +141 -0
- package/scripts/lib/telemetry/consent.mjs +299 -0
- package/scripts/lib/telemetry/paths.mjs +27 -0
- package/scripts/lib/telemetry/queue.mjs +287 -0
- package/scripts/lib/telemetry/schema.mjs +384 -0
- package/scripts/lib/telemetry/sync.mjs +312 -0
- package/scripts/lib/vault-status/board-writer.mjs +63 -5
- package/scripts/lib/vault-status/narrative-mirror.mjs +13 -7
- package/scripts/mcp-server.sh +15 -3
- package/scripts/telemetry.mjs +250 -0
- package/skills/npm-publish/SKILL.md +81 -0
- package/skills/session-end/SKILL.md +74 -1
- package/skills/session-start/SKILL.md +77 -1
- package/skills/vault-sync/SKILL.md +1 -1
- package/skills/vault-sync/package-lock.json +3 -3
- package/skills/vault-sync/validator.mjs +121 -34
|
@@ -159,14 +159,27 @@ const _schemaHash = (() => {
|
|
|
159
159
|
const args = process.argv.slice(2);
|
|
160
160
|
const checkExpires = args.includes('--check-expires');
|
|
161
161
|
|
|
162
|
-
// Parse --mode <hard|warn|off|baseline|diff|full> (default: hard)
|
|
162
|
+
// Parse --mode <hard|strict|warn|off|baseline|diff|full> (default: hard)
|
|
163
163
|
// hard — legacy alias; identical to full enforcement
|
|
164
|
+
// strict — alias of hard (#835). The Session Config schema
|
|
165
|
+
// (scripts/lib/config-schema.mjs VAULT_MODE_VALUES) uses
|
|
166
|
+
// strict|warn|off, so a caller forwarding the configured value
|
|
167
|
+
// verbatim would otherwise hit exit 2. Normalized to 'hard' at
|
|
168
|
+
// parse time so exactly one internal value flows downstream.
|
|
164
169
|
// full — enforce: exit 1 on errors
|
|
165
170
|
// warn — report but exit 0
|
|
166
171
|
// off — skip entirely
|
|
167
172
|
// baseline — write snapshot then exit 0
|
|
168
173
|
// diff — compare against snapshot, emit JSON diff
|
|
169
174
|
// Parse --exclude <glob> (repeatable)
|
|
175
|
+
const MODE_VALUES = new Set(['hard', 'strict', 'warn', 'off', 'baseline', 'diff', 'full']);
|
|
176
|
+
const MODE_EXPECTED = 'hard|strict|warn|off|baseline|diff|full';
|
|
177
|
+
|
|
178
|
+
/** Normalize CLI mode aliases to the single internal value used downstream. */
|
|
179
|
+
function normalizeMode(v) {
|
|
180
|
+
return v === 'strict' ? 'hard' : v;
|
|
181
|
+
}
|
|
182
|
+
|
|
170
183
|
let mode = 'hard';
|
|
171
184
|
const excludePatterns = [];
|
|
172
185
|
|
|
@@ -204,22 +217,22 @@ for (let i = 0; i < args.length; i++) {
|
|
|
204
217
|
const a = args[i];
|
|
205
218
|
if (a === '--mode') {
|
|
206
219
|
const v = args[i + 1];
|
|
207
|
-
if (v
|
|
208
|
-
mode = v;
|
|
220
|
+
if (MODE_VALUES.has(v)) {
|
|
221
|
+
mode = normalizeMode(v);
|
|
209
222
|
} else {
|
|
210
223
|
process.stderr.write(
|
|
211
|
-
`validator.mjs: invalid --mode value "${v}" (expected
|
|
224
|
+
`validator.mjs: invalid --mode value "${v}" (expected ${MODE_EXPECTED})\n`,
|
|
212
225
|
);
|
|
213
226
|
process.exit(2);
|
|
214
227
|
}
|
|
215
228
|
i++;
|
|
216
229
|
} else if (a.startsWith('--mode=')) {
|
|
217
230
|
const v = a.slice('--mode='.length);
|
|
218
|
-
if (v
|
|
219
|
-
mode = v;
|
|
231
|
+
if (MODE_VALUES.has(v)) {
|
|
232
|
+
mode = normalizeMode(v);
|
|
220
233
|
} else {
|
|
221
234
|
process.stderr.write(
|
|
222
|
-
`validator.mjs: invalid --mode value "${v}" (expected
|
|
235
|
+
`validator.mjs: invalid --mode value "${v}" (expected ${MODE_EXPECTED})\n`,
|
|
223
236
|
);
|
|
224
237
|
process.exit(2);
|
|
225
238
|
}
|
|
@@ -335,13 +348,29 @@ if (process.env.VAULT_DIR) {
|
|
|
335
348
|
}
|
|
336
349
|
}
|
|
337
350
|
|
|
351
|
+
// Directories the crawler never descends into. These are structurally
|
|
352
|
+
// invisible: their notes are neither checked NOR available as link targets.
|
|
338
353
|
const EXCLUDED_DIRS = new Set([
|
|
339
354
|
'node_modules',
|
|
340
355
|
'.git',
|
|
341
356
|
'.obsidian',
|
|
342
|
-
'90-archive',
|
|
343
357
|
]);
|
|
344
358
|
|
|
359
|
+
// Top-level directories whose notes are skipped by the CHECK set but remain in
|
|
360
|
+
// the link-target register (#833). Before this split, '90-archive' lived in
|
|
361
|
+
// EXCLUDED_DIRS, so archiving a note silently turned every inbound
|
|
362
|
+
// [[wiki-link]] into a dangling-link warning. The register and the check-set
|
|
363
|
+
// are now decoupled: walk() still visits these notes (so they resolve links),
|
|
364
|
+
// the validation loop skips them (so their frontmatter never blocks a close).
|
|
365
|
+
const CHECK_EXCLUDED_TOP_DIRS = new Set(['90-archive']);
|
|
366
|
+
|
|
367
|
+
/** True when relPath's FIRST path segment is a check-excluded top-level dir. */
|
|
368
|
+
function isArchived(relPath) {
|
|
369
|
+
const p = String(relPath).replace(/\\/g, '/');
|
|
370
|
+
const top = p.split('/', 1)[0];
|
|
371
|
+
return CHECK_EXCLUDED_TOP_DIRS.has(top);
|
|
372
|
+
}
|
|
373
|
+
|
|
345
374
|
function emit(obj) {
|
|
346
375
|
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
347
376
|
}
|
|
@@ -354,6 +383,7 @@ if (mode === 'off') {
|
|
|
354
383
|
vault_dir: vaultDir,
|
|
355
384
|
files_checked: 0,
|
|
356
385
|
excluded_count: 0,
|
|
386
|
+
archived_skipped_count: 0,
|
|
357
387
|
files_skipped_no_frontmatter: 0,
|
|
358
388
|
errors: [],
|
|
359
389
|
warnings: [],
|
|
@@ -415,14 +445,6 @@ function parseFrontmatter(raw) {
|
|
|
415
445
|
}
|
|
416
446
|
}
|
|
417
447
|
|
|
418
|
-
// ── Build link index (filename -> path) ─────────────────────────────────────
|
|
419
|
-
const fileIndex = new Map(); // basename-without-ext -> [absolute paths]
|
|
420
|
-
for (const f of mdFiles) {
|
|
421
|
-
const key = basename(f, '.md');
|
|
422
|
-
if (!fileIndex.has(key)) fileIndex.set(key, []);
|
|
423
|
-
fileIndex.get(key).push(f);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
448
|
// ── Wiki-link regex — captures link body for target parsing ─────────────────
|
|
427
449
|
const WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
|
|
428
450
|
|
|
@@ -450,9 +472,70 @@ function extractWikiLinks(content) {
|
|
|
450
472
|
return [...targets];
|
|
451
473
|
}
|
|
452
474
|
|
|
475
|
+
// ── Pass 1: read every file ONCE ────────────────────────────────────────────
|
|
476
|
+
// Builds the record set the link-target register (below) and the validation
|
|
477
|
+
// loop (further down) both consume. Net-neutral on I/O versus the previous
|
|
478
|
+
// shape, which read the file once but ran the frontmatter match twice.
|
|
479
|
+
// A malformed note must never abort this pass — every failure is recorded on
|
|
480
|
+
// the record and surfaced later by the validation loop.
|
|
481
|
+
const records = [];
|
|
482
|
+
for (const file of mdFiles) {
|
|
483
|
+
const rel = relative(vaultDir, file);
|
|
484
|
+
let raw;
|
|
485
|
+
try {
|
|
486
|
+
raw = readFileSync(file, 'utf8');
|
|
487
|
+
} catch (err) {
|
|
488
|
+
records.push({ file, rel, readError: err.message || String(err) });
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
let fm;
|
|
492
|
+
let links;
|
|
493
|
+
try {
|
|
494
|
+
fm = parseFrontmatter(raw);
|
|
495
|
+
const body = raw.slice(raw.match(FRONTMATTER_RE)?.[0].length || 0);
|
|
496
|
+
links = extractWikiLinks(body);
|
|
497
|
+
} catch (err) {
|
|
498
|
+
fm = { hasFrontmatter: true, parseError: err.message || String(err) };
|
|
499
|
+
links = [];
|
|
500
|
+
}
|
|
501
|
+
records.push({ file, rel, fm, links });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ── Link-target register ────────────────────────────────────────────────────
|
|
505
|
+
// Keys: basename-without-ext, frontmatter `id`, and every frontmatter `aliases`
|
|
506
|
+
// entry (#833). Keys are normalized NFC + lowercase at BOTH insert and lookup:
|
|
507
|
+
// NFC is required, not cosmetic — APFS returns decomposed (NFD) filenames and
|
|
508
|
+
// this is a German-language corpus, so "Übung" from a filename and "Übung" from
|
|
509
|
+
// a YAML string would otherwise be different strings. Values are already
|
|
510
|
+
// arrays, so a case-collision (Topic.md + topic.md) merely appends.
|
|
511
|
+
const fileIndex = new Map(); // normalized key -> [absolute paths]
|
|
512
|
+
|
|
513
|
+
function indexKey(k) {
|
|
514
|
+
return String(k).normalize('NFC').toLowerCase();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function addIndexKey(key, file) {
|
|
518
|
+
if (typeof key !== 'string' || key.length === 0) return;
|
|
519
|
+
const k = indexKey(key);
|
|
520
|
+
if (!fileIndex.has(k)) fileIndex.set(k, []);
|
|
521
|
+
fileIndex.get(k).push(file);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
for (const rec of records) {
|
|
525
|
+
addIndexKey(basename(rec.file, '.md'), rec.file);
|
|
526
|
+
const data = rec.fm && rec.fm.hasFrontmatter && !rec.fm.parseError ? rec.fm.data : null;
|
|
527
|
+
if (!data || typeof data !== 'object') continue;
|
|
528
|
+
// Type-guard: `id` must be a string, `aliases` an array of strings. A note
|
|
529
|
+
// with `aliases: some-scalar` contributes no alias keys instead of throwing.
|
|
530
|
+
if (typeof data.id === 'string') addIndexKey(data.id, rec.file);
|
|
531
|
+
if (Array.isArray(data.aliases)) {
|
|
532
|
+
for (const alias of data.aliases) addIndexKey(alias, rec.file);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
453
536
|
function resolveWikiLink(target, sourceFile) {
|
|
454
537
|
// Target may be a bare name ("my-note") or a path ("01-projects/foo/_overview").
|
|
455
|
-
// Try exact path first (relative to vault), then
|
|
538
|
+
// Try exact path first (relative to vault), then register lookup.
|
|
456
539
|
const candidate1 = resolve(vaultDir, target.endsWith('.md') ? target : target + '.md');
|
|
457
540
|
if (existsSync(candidate1)) return true;
|
|
458
541
|
|
|
@@ -463,9 +546,8 @@ function resolveWikiLink(target, sourceFile) {
|
|
|
463
546
|
);
|
|
464
547
|
if (existsSync(candidate2)) return true;
|
|
465
548
|
|
|
466
|
-
// Try
|
|
467
|
-
|
|
468
|
-
if (fileIndex.has(key)) return true;
|
|
549
|
+
// Try register lookup anywhere in the vault (basename / id / alias)
|
|
550
|
+
if (fileIndex.has(indexKey(basename(target, '.md')))) return true;
|
|
469
551
|
|
|
470
552
|
return false;
|
|
471
553
|
}
|
|
@@ -476,29 +558,31 @@ const warnings = [];
|
|
|
476
558
|
let filesChecked = 0;
|
|
477
559
|
let filesSkippedNoFrontmatter = 0;
|
|
478
560
|
let excludedCount = 0;
|
|
561
|
+
let archivedSkippedCount = 0;
|
|
479
562
|
|
|
480
563
|
const todayIso = new Date().toISOString().slice(0, 10);
|
|
481
564
|
|
|
482
|
-
|
|
483
|
-
|
|
565
|
+
// ── Pass 2: validate ────────────────────────────────────────────────────────
|
|
566
|
+
for (const rec of records) {
|
|
567
|
+
const { file, rel, fm } = rec;
|
|
484
568
|
if (isExcluded(rel)) {
|
|
485
569
|
excludedCount++;
|
|
486
570
|
continue;
|
|
487
571
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
572
|
+
// Archived notes stay in the link-target register but are never CHECKED.
|
|
573
|
+
if (isArchived(rel)) {
|
|
574
|
+
archivedSkippedCount++;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (rec.readError) {
|
|
492
578
|
errors.push({
|
|
493
579
|
file: rel,
|
|
494
580
|
path: '',
|
|
495
|
-
message: `Cannot read file: ${
|
|
581
|
+
message: `Cannot read file: ${rec.readError}`,
|
|
496
582
|
});
|
|
497
583
|
continue;
|
|
498
584
|
}
|
|
499
585
|
|
|
500
|
-
const fm = parseFrontmatter(raw);
|
|
501
|
-
|
|
502
586
|
if (!fm.hasFrontmatter) {
|
|
503
587
|
filesSkippedNoFrontmatter++;
|
|
504
588
|
continue;
|
|
@@ -527,10 +611,8 @@ for (const file of mdFiles) {
|
|
|
527
611
|
// Even if frontmatter is invalid, still check wiki-links to surface all problems.
|
|
528
612
|
}
|
|
529
613
|
|
|
530
|
-
// Wiki-link check
|
|
531
|
-
const
|
|
532
|
-
const links = extractWikiLinks(body);
|
|
533
|
-
for (const target of links) {
|
|
614
|
+
// Wiki-link check (links were extracted in pass 1)
|
|
615
|
+
for (const target of rec.links) {
|
|
534
616
|
if (!resolveWikiLink(target, file)) {
|
|
535
617
|
warnings.push({
|
|
536
618
|
file: rel,
|
|
@@ -589,6 +671,7 @@ if (mode === 'diff') {
|
|
|
589
671
|
vault_dir: vaultDir,
|
|
590
672
|
files_checked: filesChecked,
|
|
591
673
|
excluded_count: excludedCount,
|
|
674
|
+
archived_skipped_count: archivedSkippedCount,
|
|
592
675
|
files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
|
|
593
676
|
errors,
|
|
594
677
|
warnings,
|
|
@@ -608,6 +691,7 @@ if (mode === 'diff') {
|
|
|
608
691
|
vault_dir: vaultDir,
|
|
609
692
|
files_checked: filesChecked,
|
|
610
693
|
excluded_count: excludedCount,
|
|
694
|
+
archived_skipped_count: archivedSkippedCount,
|
|
611
695
|
files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
|
|
612
696
|
errors,
|
|
613
697
|
warnings,
|
|
@@ -629,6 +713,7 @@ if (mode === 'diff') {
|
|
|
629
713
|
vault_dir: vaultDir,
|
|
630
714
|
files_checked: filesChecked,
|
|
631
715
|
excluded_count: excludedCount,
|
|
716
|
+
archived_skipped_count: archivedSkippedCount,
|
|
632
717
|
files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
|
|
633
718
|
});
|
|
634
719
|
|
|
@@ -640,7 +725,8 @@ if (mode === 'diff') {
|
|
|
640
725
|
}
|
|
641
726
|
|
|
642
727
|
// ── mode=full | hard | warn ───────────────────────────────────────────────
|
|
643
|
-
// 'full' and 'hard' are identical (hard is the legacy alias
|
|
728
|
+
// 'full' and 'hard' are identical (hard is the legacy alias; 'strict' was
|
|
729
|
+
// normalized to 'hard' at parse time — see normalizeMode, #835).
|
|
644
730
|
// In warn mode, errors are reported but the status is "ok" for exit-code purposes.
|
|
645
731
|
// The errors array is still populated so the caller can surface them as warnings.
|
|
646
732
|
const status = hasErrors ? (mode === 'warn' ? 'ok' : 'invalid') : 'ok';
|
|
@@ -650,6 +736,7 @@ emit({
|
|
|
650
736
|
vault_dir: vaultDir,
|
|
651
737
|
files_checked: filesChecked,
|
|
652
738
|
excluded_count: excludedCount,
|
|
739
|
+
archived_skipped_count: archivedSkippedCount,
|
|
653
740
|
files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
|
|
654
741
|
errors,
|
|
655
742
|
warnings,
|