session-orchestrator 4.0.0 → 4.0.1

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 (117) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +3 -2
  4. package/.codex-plugin/skills/architecture/SKILL.md +20 -0
  5. package/.codex-plugin/skills/autopilot/SKILL.md +21 -0
  6. package/.codex-plugin/skills/autopilot/agents/openai.yaml +5 -0
  7. package/.codex-plugin/skills/bootstrap/SKILL.md +22 -0
  8. package/.codex-plugin/skills/bootstrap/agents/openai.yaml +5 -0
  9. package/.codex-plugin/skills/brainstorm/SKILL.md +22 -0
  10. package/.codex-plugin/skills/brainstorm/agents/openai.yaml +5 -0
  11. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +17 -0
  12. package/.codex-plugin/skills/close/SKILL.md +21 -0
  13. package/.codex-plugin/skills/close/agents/openai.yaml +5 -0
  14. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +24 -0
  15. package/.codex-plugin/skills/debug/SKILL.md +21 -0
  16. package/.codex-plugin/skills/debug/agents/openai.yaml +5 -0
  17. package/.codex-plugin/skills/discovery/SKILL.md +21 -0
  18. package/.codex-plugin/skills/discovery/agents/openai.yaml +5 -0
  19. package/.codex-plugin/skills/dispatcher/SKILL.md +21 -0
  20. package/.codex-plugin/skills/dispatcher/agents/openai.yaml +5 -0
  21. package/.codex-plugin/skills/docs-orchestrator/SKILL.md +20 -0
  22. package/.codex-plugin/skills/ecosystem-health/SKILL.md +22 -0
  23. package/.codex-plugin/skills/eli5/SKILL.md +21 -0
  24. package/.codex-plugin/skills/eli5/agents/openai.yaml +5 -0
  25. package/.codex-plugin/skills/eval/SKILL.md +21 -0
  26. package/.codex-plugin/skills/eval/agents/openai.yaml +5 -0
  27. package/.codex-plugin/skills/evolve/SKILL.md +21 -0
  28. package/.codex-plugin/skills/evolve/agents/openai.yaml +5 -0
  29. package/.codex-plugin/skills/frontmatter-guard/SKILL.md +17 -0
  30. package/.codex-plugin/skills/gitlab-ops/SKILL.md +22 -0
  31. package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +17 -0
  32. package/.codex-plugin/skills/go/SKILL.md +22 -0
  33. package/.codex-plugin/skills/go/agents/openai.yaml +5 -0
  34. package/.codex-plugin/skills/grill/SKILL.md +21 -0
  35. package/.codex-plugin/skills/grill/agents/openai.yaml +5 -0
  36. package/.codex-plugin/skills/harness-audit/SKILL.md +19 -0
  37. package/.codex-plugin/skills/harness-audit/agents/openai.yaml +5 -0
  38. package/.codex-plugin/skills/hook-development/SKILL.md +17 -0
  39. package/.codex-plugin/skills/mcp-builder/SKILL.md +17 -0
  40. package/.codex-plugin/skills/memory-cleanup/SKILL.md +21 -0
  41. package/.codex-plugin/skills/memory-cleanup/agents/openai.yaml +5 -0
  42. package/.codex-plugin/skills/mode-selector/SKILL.md +19 -0
  43. package/.codex-plugin/skills/npm-publish/SKILL.md +18 -0
  44. package/.codex-plugin/skills/peekaboo-driver/SKILL.md +20 -0
  45. package/.codex-plugin/skills/persona-panel/SKILL.md +22 -0
  46. package/.codex-plugin/skills/persona-panel/agents/openai.yaml +5 -0
  47. package/.codex-plugin/skills/plan/SKILL.md +22 -0
  48. package/.codex-plugin/skills/plan/agents/openai.yaml +5 -0
  49. package/.codex-plugin/skills/playwright-driver/SKILL.md +22 -0
  50. package/.codex-plugin/skills/portfolio/SKILL.md +21 -0
  51. package/.codex-plugin/skills/portfolio/agents/openai.yaml +5 -0
  52. package/.codex-plugin/skills/quality-gates/SKILL.md +22 -0
  53. package/.codex-plugin/skills/reconcile/SKILL.md +21 -0
  54. package/.codex-plugin/skills/reconcile/agents/openai.yaml +5 -0
  55. package/.codex-plugin/skills/release/SKILL.md +22 -0
  56. package/.codex-plugin/skills/release/agents/openai.yaml +5 -0
  57. package/.codex-plugin/skills/remote-offload/SKILL.md +22 -0
  58. package/.codex-plugin/skills/repo-audit/SKILL.md +19 -0
  59. package/.codex-plugin/skills/repo-audit/agents/openai.yaml +5 -0
  60. package/.codex-plugin/skills/session/SKILL.md +21 -0
  61. package/.codex-plugin/skills/session/agents/openai.yaml +5 -0
  62. package/.codex-plugin/skills/session-end/SKILL.md +22 -0
  63. package/.codex-plugin/skills/session-plan/SKILL.md +22 -0
  64. package/.codex-plugin/skills/session-start/SKILL.md +22 -0
  65. package/.codex-plugin/skills/spinout/SKILL.md +21 -0
  66. package/.codex-plugin/skills/spinout/agents/openai.yaml +5 -0
  67. package/.codex-plugin/skills/sunset-review/SKILL.md +21 -0
  68. package/.codex-plugin/skills/sunset-review/agents/openai.yaml +5 -0
  69. package/.codex-plugin/skills/templates-ack/SKILL.md +21 -0
  70. package/.codex-plugin/skills/templates-ack/agents/openai.yaml +5 -0
  71. package/.codex-plugin/skills/test/SKILL.md +21 -0
  72. package/.codex-plugin/skills/test/agents/openai.yaml +5 -0
  73. package/.codex-plugin/skills/test-runner/SKILL.md +22 -0
  74. package/.codex-plugin/skills/tmux-layout/SKILL.md +23 -0
  75. package/.codex-plugin/skills/using-orchestrator/SKILL.md +19 -0
  76. package/.codex-plugin/skills/vault-mirror/SKILL.md +17 -0
  77. package/.codex-plugin/skills/vault-sync/SKILL.md +17 -0
  78. package/.codex-plugin/skills/wave-executor/SKILL.md +22 -0
  79. package/.codex-plugin/skills/write-executable-plan/SKILL.md +24 -0
  80. package/{plugin.json → .cursor-plugin/plugin.json} +5 -2
  81. package/CHANGELOG.md +190 -1
  82. package/README.md +26 -18
  83. package/docs/codex-setup.md +43 -9
  84. package/docs/components.md +3 -2
  85. package/docs/instruction-delivery.md +12 -5
  86. package/docs/migration-v4.md +33 -9
  87. package/hooks/_lib/hook-import-set.json +4 -3
  88. package/hooks/hooks-codex.json +1 -1
  89. package/hooks/hooks.json +1 -1
  90. package/hooks/on-stop.mjs +25 -4
  91. package/package.json +2 -2
  92. package/scripts/generate-codex-skills.mjs +246 -0
  93. package/scripts/generate-hook-import-set.mjs +51 -8
  94. package/scripts/lib/codex/plugin-contract.mjs +6 -0
  95. package/scripts/lib/config/host-paths.mjs +20 -4
  96. package/scripts/lib/gates/gate-full.mjs +7 -3
  97. package/scripts/lib/owner-config-banner.mjs +7 -9
  98. package/scripts/lib/owner-yaml.mjs +8 -1
  99. package/scripts/lib/plugin-update-banner.mjs +10 -2
  100. package/scripts/lib/reconcile/engine.mjs +38 -7
  101. package/scripts/lib/session-schema/constants.mjs +38 -11
  102. package/scripts/lib/session-start-probes.mjs +12 -0
  103. package/scripts/lib/telemetry/schema.mjs +39 -18
  104. package/scripts/lib/telemetry-flush-health-banner.mjs +211 -0
  105. package/scripts/lib/validate/check-codex-skills.mjs +191 -0
  106. package/scripts/lib/validate/check-owner-leakage.mjs +91 -55
  107. package/scripts/lib/validate/check-skill-links.mjs +37 -7
  108. package/scripts/lib/validate/check-test-git-config-target.mjs +192 -12
  109. package/scripts/lib/validate/check-unwired-features.mjs +163 -13
  110. package/scripts/lib/validate/confidential-names.mjs +95 -30
  111. package/scripts/lib/validate/repo-files.mjs +48 -14
  112. package/scripts/release.mjs +109 -18
  113. package/scripts/site-numbers.mjs +344 -8
  114. package/scripts/validate-plugin.mjs +3 -0
  115. package/skills/session-start/SKILL.md +2 -2
  116. package/skills/session-start/references/phase-4-ssot-environment-check.md +5 -0
  117. package/skills/vault-sync/SKILL.md +10 -0
@@ -66,6 +66,31 @@
66
66
  * --write rewrite the span contents in place, and refresh `site/_census.json`
67
67
  * from the same measurement (only when --site is the repo's `site/`).
68
68
  *
69
+ * ## The two usage metrics (`npm-downloads-30d`, `github-stars`)
70
+ *
71
+ * These two are the only ones whose source is NOT this repository: they are
72
+ * fetched over the network, and only under `--write` (5 s timeout). `--check`
73
+ * never opens a socket — it compares the page against `site/_census.json`, which
74
+ * IS their truth between writes. That is what keeps the CI/build guard offline
75
+ * and deterministic; a `--check` that fetched would go red whenever npm's API
76
+ * was slow, which is a signal about npm and not about the page.
77
+ *
78
+ * On a fetch failure `--write` keeps the previous snapshot value and warns. It
79
+ * never writes a placeholder over a real number: the number on the page stays
80
+ * the last one that was actually measured, and the WARN names why it did not
81
+ * move. With no snapshot to keep, the metric simply has no value and the run
82
+ * fails loudly (the existing partial-census guard) rather than shipping "n/a".
83
+ *
84
+ * ## The census blocks in `site/llms-full.txt` and `site/llms.txt`
85
+ *
86
+ * The same measurement also fills a marker-bounded line in every file listed in
87
+ * `CENSUS_BLOCK_FILES` (`<!-- census:start -->` … `<!-- census:end -->`) — the
88
+ * two plain-text surfaces LLM crawlers read. Only the text BETWEEN the
89
+ * markers is rewritten; everything else in that file is hand-authored prose and
90
+ * is handed back byte-for-byte. A missing end marker is a hard error, never a
91
+ * silent skip — a generator that quietly stops filling a surface is the exact
92
+ * failure this file exists to end.
93
+ *
69
94
  * Exit codes (`.claude/rules/cli-design.md`):
70
95
  * 0 — no drift (--check) / files updated or already current (--write)
71
96
  * 1 — drift found (--check), or a contract violation in either mode
@@ -327,7 +352,7 @@ export function isDirty(root) {
327
352
  ['--no-optional-locks', 'status', '--porcelain', '--untracked-files=no'],
328
353
  { cwd: root, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] },
329
354
  );
330
- return out.split('\n').filter(Boolean).length > 0;
355
+ return out.split('\n').some(Boolean);
331
356
  } catch {
332
357
  return null;
333
358
  }
@@ -458,6 +483,37 @@ export const METRIC_DEFS = Object.freeze([
458
483
  source: 'jq length .orchestrator/policy/blocked-commands.json',
459
484
  compute: (root) => fmtCount(countBlockedCommands(root)),
460
485
  },
486
+ {
487
+ id: 'npm-downloads-30d',
488
+ provenance: false,
489
+ // Network-sourced: absent from EVERY offline run, which is most of them
490
+ // (--check never fetches). The snapshot is therefore not a fresh-clone
491
+ // convenience here but the metric's normal answer between two --write runs.
492
+ snapshotFallback: true,
493
+ network: true,
494
+ url: 'https://api.npmjs.org/downloads/point/last-month/session-orchestrator',
495
+ field: 'downloads',
496
+ source:
497
+ 'curl -s https://api.npmjs.org/downloads/point/last-month/session-orchestrator | jq .downloads (fetched only under --write)',
498
+ compute: (root, ctx) => fmtCount(ctx?.usage?.['npm-downloads-30d'] ?? null),
499
+ },
500
+ {
501
+ id: 'github-stars',
502
+ provenance: false,
503
+ snapshotFallback: true,
504
+ network: true,
505
+ url: 'https://api.github.com/repos/Kanevry/session-orchestrator',
506
+ field: 'stargazers_count',
507
+ // The API answers unauthenticated requests but rejects ones without a
508
+ // User-Agent, and the Accept header pins the response schema version.
509
+ headers: {
510
+ Accept: 'application/vnd.github+json',
511
+ 'User-Agent': 'session-orchestrator-site-numbers',
512
+ },
513
+ source:
514
+ 'curl -s https://api.github.com/repos/Kanevry/session-orchestrator | jq .stargazers_count (fetched only under --write)',
515
+ compute: (root, ctx) => fmtCount(ctx?.usage?.['github-stars'] ?? null),
516
+ },
461
517
  {
462
518
  id: 'counted-at',
463
519
  provenance: true,
@@ -606,7 +662,7 @@ export function writeCensusSnapshot(root, values) {
606
662
  * `fromSnapshot` lists the metrics answered by `site/_census.json` because
607
663
  * their live source was absent.
608
664
  */
609
- export function collect(root) {
665
+ export function collect(root, ctx = {}) {
610
666
  const values = {};
611
667
  const missing = [];
612
668
  const warnings = [];
@@ -619,8 +675,8 @@ export function collect(root) {
619
675
  // LIVE FIRST, always. The snapshot is a fallback, never a cache: reading it
620
676
  // first (or memoising a live value into it) would freeze the page on the
621
677
  // last release's numbers while the repository moved on.
622
- let v = def.compute(root);
623
- if ((v === null || v === undefined || v === '') && def.snapshotFallback === true) {
678
+ let v = def.compute(root, ctx);
679
+ if ((v === null || v === undefined || v === '') && def.snapshotFallback) {
624
680
  if (snapshot === undefined) snapshot = readCensusSnapshot(root);
625
681
  const s = snapshot?.[def.id];
626
682
  if (s !== undefined) {
@@ -632,9 +688,17 @@ export function collect(root) {
632
688
  else values[def.id] = String(v);
633
689
  }
634
690
 
635
- if (fromSnapshot.length > 0) {
691
+ // Only the REPOSITORY-sourced fallbacks are worth a warning. For the two
692
+ // network metrics the snapshot is not a degraded answer but the designed one
693
+ // between two `--write` runs — warning about them would fire on every single
694
+ // `--check`, and a warning that always fires is a broken instrument
695
+ // (`.claude/rules/host-resources.md` HR-101).
696
+ const localFallbacks = fromSnapshot.filter(
697
+ (id) => !METRIC_DEFS.find((d) => d.id === id)?.network,
698
+ );
699
+ if (localFallbacks.length > 0) {
636
700
  warnings.push(
637
- `${fromSnapshot.join(', ')} read from ${CENSUS_FILE.join('/')} — the live source is absent under ${root} ` +
701
+ `${localFallbacks.join(', ')} read from ${CENSUS_FILE.join('/')} — the live source is absent under ${root} ` +
638
702
  '(expected in a fresh clone / tarball build; the snapshot is only as current as the last --write)',
639
703
  );
640
704
  }
@@ -662,6 +726,215 @@ export function collect(root) {
662
726
  return { values, missing, warnings, fromSnapshot };
663
727
  }
664
728
 
729
+ // ---------------------------------------------------------------------------
730
+ // The two network metrics
731
+ // ---------------------------------------------------------------------------
732
+
733
+ /** Metrics whose source is an HTTP endpoint rather than this repository. */
734
+ export const NETWORK_METRIC_DEFS = Object.freeze(METRIC_DEFS.filter((d) => d.network));
735
+
736
+ /** Hard ceiling on a single usage fetch. Revisit if a source starts answering slower. */
737
+ export const FETCH_TIMEOUT_MS = 5000;
738
+
739
+ /**
740
+ * Fetch the usage metrics. CALLED ONLY UNDER `--write` — see the header for why
741
+ * `--check` must stay offline.
742
+ *
743
+ * Every failure mode (timeout, non-2xx, unparseable body, missing/non-numeric
744
+ * field) lands in the same place: a WARN naming the reason and the snapshot
745
+ * value that will be kept, and NO entry in the returned map. `collect()` then
746
+ * falls back to the snapshot exactly as it does for a fresh clone's ledgers —
747
+ * so a failed fetch degrades to "the last measured number", never to a
748
+ * placeholder written over a real one.
749
+ *
750
+ * @param {string} root repo root (its `site/_census.json` supplies the WARN's value)
751
+ * @param {{fetchImpl?: Function, timeoutMs?: number, warn?: (s:string)=>void}} [opts]
752
+ * @returns {Promise<Record<string, number>>} only the metrics that actually resolved
753
+ */
754
+ export async function fetchUsageMetrics(root, opts = {}) {
755
+ // Offline switch (tests, air-gapped CI): SO_SITE_NUMBERS_OFFLINE=1 skips every
756
+ // network call and lets the snapshot answer, exactly like a failed fetch would,
757
+ // minus the per-metric WARN noise. Measured 2026-09-07: two fixture tests that
758
+ // spawned --write went red on a GitHub 403 rate limit; a test must never depend
759
+ // on api.github.com being reachable.
760
+ if (process.env.SO_SITE_NUMBERS_OFFLINE === '1') {
761
+ (opts.warn ?? ((m) => process.stderr.write(`${m}\n`)))(
762
+ 'site-numbers: SO_SITE_NUMBERS_OFFLINE=1 — usage metrics answered from the snapshot',
763
+ );
764
+ return {};
765
+ }
766
+ const doFetch = opts.fetchImpl ?? globalThis.fetch;
767
+ const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
768
+ const warn = opts.warn ?? ((s) => process.stderr.write(`${s}\n`));
769
+ const out = {};
770
+ let snapshot;
771
+
772
+ for (const def of NETWORK_METRIC_DEFS) {
773
+ try {
774
+ const res = await doFetch(def.url, {
775
+ signal: AbortSignal.timeout(timeoutMs),
776
+ headers: def.headers ?? {},
777
+ });
778
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
779
+ const body = await res.json();
780
+ const v = body?.[def.field];
781
+ if (typeof v !== 'number' || !Number.isFinite(v)) {
782
+ throw new Error(`no numeric "${def.field}" field in the response`);
783
+ }
784
+ out[def.id] = Math.trunc(v);
785
+ } catch (err) {
786
+ if (snapshot === undefined) snapshot = readCensusSnapshot(root);
787
+ const kept = snapshot?.[def.id];
788
+ warn(
789
+ `WARN site-numbers: ${def.id} fetch failed (${err?.message ?? String(err)}), keeping snapshot ` +
790
+ `${kept ?? '(none — this metric will have no value and the run will fail)'}`,
791
+ );
792
+ }
793
+ }
794
+ return out;
795
+ }
796
+
797
+ // ---------------------------------------------------------------------------
798
+ // The marker-bounded census line in site/llms-full.txt
799
+ // ---------------------------------------------------------------------------
800
+
801
+ /** Files the census line lives in, relative to the SITE directory. */
802
+ export const LLMS_FULL_FILE = 'llms-full.txt';
803
+ export const LLMS_FILE = 'llms.txt';
804
+
805
+ /**
806
+ * Every file carrying a marker-bounded census block.
807
+ *
808
+ * A CONSTANT LIST rather than a second code path per file: `site/llms.txt` used
809
+ * to hand-maintain its own "## Surface" line, and it drifted exactly as the page
810
+ * had — measured 2026-09-07 it claimed `661 vitest test files` while every other
811
+ * surface (and the repository) said 662, and `--check` exited 0 because the
812
+ * generator did not know that file existed. Adding a file to this array is the
813
+ * whole change needed to bring it under the guard.
814
+ */
815
+ export const CENSUS_BLOCK_FILES = Object.freeze([LLMS_FULL_FILE, LLMS_FILE]);
816
+
817
+ export const CENSUS_START = '<!-- census:start -->';
818
+ export const CENSUS_END = '<!-- census:end -->';
819
+
820
+ /**
821
+ * The metric ids the census line carries, with the label each one gets.
822
+ *
823
+ * A SUBSET of METRIC_IDS on purpose: the line is prose for a reader, not the
824
+ * machine receipt (`site/_census.json` is that, and carries all of them).
825
+ * `version`, `counted-at` and `counted-sha` are not in this list because they
826
+ * are rendered as the line's opening clause, not as `label value` pairs.
827
+ */
828
+ export const CENSUS_LINE_FIELDS = Object.freeze([
829
+ { id: 'skills', label: 'skills' },
830
+ { id: 'commands', label: 'commands' },
831
+ { id: 'agents', label: 'agents' },
832
+ { id: 'hooks', label: 'hooks' },
833
+ { id: 'tests', label: 'test files' },
834
+ { id: 'sessions', label: 'sessions' },
835
+ { id: 'learnings', label: 'learnings' },
836
+ { id: 'npm-downloads-30d', label: 'npm downloads (30d)' },
837
+ { id: 'github-stars', label: 'GitHub stars' },
838
+ ]);
839
+
840
+ /**
841
+ * Render the census line.
842
+ *
843
+ * The `Version X.Y.Z` opening is load-bearing beyond this file: `SURFACES` in
844
+ * `scripts/release.mjs` matches `/Version\s+(\d+\.\d+\.\d+)/g` against
845
+ * `site/llms-full.txt`, so the literal must keep exactly this shape or the
846
+ * release drift check stops seeing the surface it guards.
847
+ *
848
+ * @returns {{line: string, head: string, tail: string}}
849
+ * `head` is the provenance clause (warn-only, like the spans), `tail` the
850
+ * counts (real drift). Split so `--check` can tell the two apart.
851
+ */
852
+ export function renderCensusLine(values) {
853
+ const head = `Version ${values.version} · counted ${values['counted-at']} at ${values['counted-sha']}`;
854
+ const tail = CENSUS_LINE_FIELDS.map((f) => `${f.label} ${values[f.id]}`).join(' · ');
855
+ return { line: `${head} · ${tail}`, head, tail };
856
+ }
857
+
858
+ /**
859
+ * Replace the text between the two markers, and nothing else.
860
+ *
861
+ * A start marker without an end marker is a HARD ERROR rather than an append:
862
+ * guessing where the block ends would let one bad edit swallow the rest of a
863
+ * hand-authored file.
864
+ *
865
+ * @returns {{ok: true, text: string, changed: boolean, current: string}
866
+ * | {ok: false, reason: string}}
867
+ */
868
+ export function rewriteCensusBlock(text, line) {
869
+ const start = text.indexOf(CENSUS_START);
870
+ const end = text.indexOf(CENSUS_END);
871
+ if (start === -1 && end === -1) return { ok: false, reason: 'no census markers' };
872
+ if (start === -1) return { ok: false, reason: `"${CENSUS_END}" without "${CENSUS_START}"` };
873
+ if (end === -1) return { ok: false, reason: `"${CENSUS_START}" without "${CENSUS_END}"` };
874
+ if (end < start) return { ok: false, reason: 'census markers are in the wrong order' };
875
+
876
+ const inner = text.slice(start + CENSUS_START.length, end);
877
+ const current = inner.trim();
878
+ const next = `${text.slice(0, start + CENSUS_START.length)}\n${line}\n${text.slice(end)}`;
879
+ return { ok: true, text: next, changed: next !== text, current };
880
+ }
881
+
882
+ /**
883
+ * Judge (and optionally rewrite) the census block of ONE file under `siteDir`.
884
+ *
885
+ * Mirrors the span policy: a difference only in the provenance clause is
886
+ * `stale` (warn-only — a claim about the past does not become false when HEAD
887
+ * moves), a difference in the counts is `drift`.
888
+ *
889
+ * @param {{write?:boolean, file?:string}} [opts] `file` is relative to `siteDir`
890
+ * and defaults to `llms-full.txt`; `syncCensusBlocks` iterates CENSUS_BLOCK_FILES.
891
+ * @returns {{present:boolean, error?:string, drift?:boolean, stale?:boolean,
892
+ * written?:boolean, file:string}}
893
+ */
894
+ export function syncCensusBlock(siteDir, values, { write = false, file = LLMS_FULL_FILE } = {}) {
895
+ const abs = join(siteDir, file);
896
+ // Absent file: not this generator's business to create one. Every HTML
897
+ // fixture directory in the test suite is such a case.
898
+ if (!existsSync(abs) || !statSync(abs).isFile()) return { present: false, file: abs };
899
+
900
+ const { line, tail } = renderCensusLine(values);
901
+ const text = readFileSync(abs, 'utf8');
902
+ const res = rewriteCensusBlock(text, line);
903
+ if (!res.ok) return { present: true, file: abs, error: res.reason };
904
+
905
+ // Same split as the spans: `counted <date> at <sha>` lagging is a claim about
906
+ // the past, not a wrong number.
907
+ const provenanceOnly =
908
+ res.current !== line &&
909
+ res.current.startsWith(`Version ${values.version} · `) &&
910
+ res.current.endsWith(tail);
911
+ const out = {
912
+ present: true,
913
+ file: abs,
914
+ drift: res.current !== line && !provenanceOnly,
915
+ stale: provenanceOnly,
916
+ written: false,
917
+ };
918
+ if (write && res.changed) {
919
+ writeFileSync(abs, res.text, 'utf8');
920
+ out.written = true;
921
+ }
922
+ return out;
923
+ }
924
+
925
+ /**
926
+ * Judge (and optionally rewrite) EVERY file in `CENSUS_BLOCK_FILES`.
927
+ *
928
+ * One measurement, N surfaces — the same reason `writeCensusSnapshot` takes an
929
+ * already-computed `values` map: two census runs inside one invocation can
930
+ * disagree across midnight or a concurrent ledger append.
931
+ *
932
+ * @returns {Array<ReturnType<typeof syncCensusBlock>>} in CENSUS_BLOCK_FILES order
933
+ */
934
+ export function syncCensusBlocks(siteDir, values, { write = false } = {}) {
935
+ return CENSUS_BLOCK_FILES.map((file) => syncCensusBlock(siteDir, values, { write, file }));
936
+ }
937
+
665
938
  // ---------------------------------------------------------------------------
666
939
  // Markup
667
940
  // ---------------------------------------------------------------------------
@@ -881,7 +1154,7 @@ export function main(argv = process.argv.slice(2), env = {}) {
881
1154
  return 2;
882
1155
  }
883
1156
 
884
- const { values, missing, warnings, fromSnapshot } = collect(root);
1157
+ const { values, missing, warnings, fromSnapshot } = collect(root, { usage: env.usage });
885
1158
  if (missing.length > 0) {
886
1159
  stderr(
887
1160
  `Error: could not measure ${missing.join(', ')} under ${root} — ` +
@@ -961,6 +1234,38 @@ export function main(argv = process.argv.slice(2), env = {}) {
961
1234
  });
962
1235
  }
963
1236
 
1237
+ // The marker-bounded census line in site/llms-full.txt. Judged with the same
1238
+ // split as the spans (counts = drift, provenance = warn-only) and rewritten
1239
+ // BETWEEN the markers only.
1240
+ const censusDriftLines = [];
1241
+ const censusBlocks = syncCensusBlocks(siteDir, values, { write: args.write });
1242
+ for (const b of censusBlocks) {
1243
+ if (b.error) {
1244
+ stderr(
1245
+ `Error: ${b.file}: ${b.error} — the census block contract is broken ` +
1246
+ `(expected "${CENSUS_START}" … "${CENSUS_END}")`,
1247
+ );
1248
+ contractTotal += 1;
1249
+ } else if (b.present) {
1250
+ if (b.drift && !args.write) {
1251
+ // Held back rather than printed here: under --json, stdout carries the
1252
+ // envelope and NOTHING else (`cli-design.md` — data on stdout). Printing
1253
+ // it inline put a bare DRIFT line in front of the JSON and made the
1254
+ // envelope unparseable, which is the same class of silent failure as an
1255
+ // unjudged surface: a consumer sees a parse error, not a drift report.
1256
+ censusDriftLines.push(`DRIFT ${b.file}: the census block does not match this measurement`);
1257
+ driftTotal += 1;
1258
+ } else if (b.drift) {
1259
+ driftTotal += 1;
1260
+ } else if (b.stale) {
1261
+ stderr(`stale ${b.file}: only the counted date/sha of the census block lags`);
1262
+ }
1263
+ }
1264
+ }
1265
+ // Kept as its own name in the --json envelope for the consumers that already
1266
+ // read `llmsFull`; `censusBlocks` below carries all of them.
1267
+ const llms = censusBlocks[CENSUS_BLOCK_FILES.indexOf(LLMS_FULL_FILE)];
1268
+
964
1269
  // The named silent-failure class: a generator that matches nothing, changes
965
1270
  // nothing, and reports success. Zero spans means the markup contract is not in
966
1271
  // the page — that is a defect, in BOTH modes, never a no-op.
@@ -1040,6 +1345,23 @@ export function main(argv = process.argv.slice(2), env = {}) {
1040
1345
  written: writtenTotal,
1041
1346
  rejected: rejectedTotal,
1042
1347
  fromSnapshot,
1348
+ llmsFull: {
1349
+ file: llms.present ? relative(root, llms.file) : null,
1350
+ present: llms.present,
1351
+ error: llms.error ?? null,
1352
+ drift: llms.drift === true,
1353
+ stale: llms.stale === true,
1354
+ written: llms.written === true,
1355
+ },
1356
+ censusBlocks: censusBlocks.map((b, i) => ({
1357
+ name: CENSUS_BLOCK_FILES[i],
1358
+ file: b.present ? relative(root, b.file) : null,
1359
+ present: b.present,
1360
+ error: b.error ?? null,
1361
+ drift: b.drift === true,
1362
+ stale: b.stale === true,
1363
+ written: b.written === true,
1364
+ })),
1043
1365
  censusWritten,
1044
1366
  ok,
1045
1367
  },
@@ -1051,6 +1373,9 @@ export function main(argv = process.argv.slice(2), env = {}) {
1051
1373
  stdout(
1052
1374
  `site-numbers: wrote ${writtenTotal} value(s) across ${files.length} file(s) in ${siteDir}` +
1053
1375
  ` (${spanTotal} metric cells @ ${values['counted-sha']}${dirty ? '+dirty' : ''})` +
1376
+ censusBlocks
1377
+ .map((b, i) => (b.written ? ` + ${CENSUS_BLOCK_FILES[i]}` : ''))
1378
+ .join('') +
1054
1379
  (censusWritten ? ` + ${CENSUS_FILE.join('/')}` : ''),
1055
1380
  );
1056
1381
  } else {
@@ -1062,6 +1387,7 @@ export function main(argv = process.argv.slice(2), env = {}) {
1062
1387
  stderr(`stale ${f.file}:${s.line} ${s.metric}: "${s.actual}" → would become "${s.expected}" on --write`);
1063
1388
  }
1064
1389
  }
1390
+ for (const l of censusDriftLines) stdout(l);
1065
1391
  stdout(
1066
1392
  driftTotal === 0 && !noSpans && contractTotal === 0
1067
1393
  ? `site-numbers: ${spanTotal} metric cell(s) current across ${files.length} file(s)`
@@ -1078,4 +1404,14 @@ const isMain =
1078
1404
  process.argv[1] !== undefined &&
1079
1405
  resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
1080
1406
 
1081
- if (isMain) process.exit(main());
1407
+ if (isMain) {
1408
+ const argv = process.argv.slice(2);
1409
+ const parsed = parseArgs(argv);
1410
+ // The ONLY network call in this file, and only on the write path — see the
1411
+ // header. A `--check` (the CI/build guard) never reaches this branch.
1412
+ const usage =
1413
+ !parsed.error && parsed.write && !parsed.help && !parsed.version
1414
+ ? await fetchUsageMetrics(resolve(parsed.root ?? process.cwd()))
1415
+ : {};
1416
+ process.exit(main(argv, { usage }));
1417
+ }
@@ -376,6 +376,9 @@ if (runCheck('check-path-utils-canary.mjs') !== 0) checkFailed = 1;
376
376
  process.stdout.write('\n');
377
377
  if (runCheck('check-codex-plugin.mjs') !== 0) checkFailed = 1;
378
378
 
379
+ process.stdout.write('\n');
380
+ if (runCheck('check-codex-skills.mjs') !== 0) checkFailed = 1;
381
+
379
382
  process.stdout.write('\n');
380
383
  if (runCheck('check-pi-package.mjs') !== 0) checkFailed = 1;
381
384
 
@@ -205,7 +205,7 @@ Group issues by:
205
205
 
206
206
  ## Phase 4: SSOT & Environment Check
207
207
 
208
- > Always runs; every finding is a NON-BLOCKING banner in the Session Overview (never a gate — the Full Gate is the Quality wave's job). Covers SSOT freshness, the Baseline quality commands (resolved `.orchestrator/policy/quality-gates.json` → Session Config → defaults, each availability-checked with `command -v`), Pencil design status, plugin + `bootstrap.lock` freshness, and the banner-probe family registered in `scripts/lib/session-start-probes.mjs` (vault-staleness, ci-status, qg-command-drift, peer-cards, loop-readiness, instruction-budget, reconcile-nudge, sessions-staleness, sessions-integrity, owner-config, moc-staleness, context-coverage, claude-md-budget-lint, tests:src-ratio, project-hygiene, mirror-issues, git-config-drift). Per-probe module path, return contract and exact banner wording: [`references/phase-4-ssot-environment-check.md`](references/phase-4-ssot-environment-check.md).
208
+ > Always runs; every finding is a NON-BLOCKING banner in the Session Overview (never a gate — the Full Gate is the Quality wave's job). Covers SSOT freshness, the Baseline quality commands (resolved `.orchestrator/policy/quality-gates.json` → Session Config → defaults, each availability-checked with `command -v`), Pencil design status, plugin + `bootstrap.lock` freshness, and the banner-probe family registered in `scripts/lib/session-start-probes.mjs` (vault-staleness, telemetry-flush-health, ci-status, qg-command-drift, peer-cards, loop-readiness, instruction-budget, reconcile-nudge, sessions-staleness, sessions-integrity, owner-config, moc-staleness, context-coverage, claude-md-budget-lint, tests:src-ratio, project-hygiene, mirror-issues, git-config-drift). Per-probe module path, return contract and exact banner wording: [`references/phase-4-ssot-environment-check.md`](references/phase-4-ssot-environment-check.md).
209
209
 
210
210
  ## Phase 4.5: Resource Health (v3.1.0)
211
211
 
@@ -377,7 +377,7 @@ After user alignment:
377
377
  | `phase-2-5-docs-planning.md` | Phase 2.5 full procedural body — docs-orchestrator config, audience detection, AUQ confirmation, result block emission, non-overlap rules |
378
378
  | (inline) Phase 2.6 | Steering docs gate + load — reads `.orchestrator/steering/{product,tech,structure}.md`; silent no-op when directory absent |
379
379
  | `references/phase-2-7-portfolio-snapshot.md` | Phase 2.7 full procedural body — GitLab Portfolio Snapshot: dry-run aggregation banner; gated on `gitlab-portfolio.enabled: true` + `vault-integration.enabled: true`; dispatches `scripts/lib/gitlab-portfolio/cli.mjs --dry-run`; 8s timeout; never blocks session-start |
380
- | `references/phase-4-ssot-environment-check.md` | Phase 4 full procedural body — SSOT freshness, Baseline quality-command resolution + `command -v` availability check, Pencil status, plugin/`bootstrap.lock` freshness, and the 17-probe banner family (module path, return contract and exact wording per probe) |
380
+ | `references/phase-4-ssot-environment-check.md` | Phase 4 full procedural body — SSOT freshness, Baseline quality-command resolution + `command -v` availability check, Pencil status, plugin/`bootstrap.lock` freshness, and the 18-probe banner family (module path, return contract and exact wording per probe) |
381
381
  | `phase-4-5-resource-health.md` | Phase 4.5 full procedural body — resource probe, adaptive thresholds table, AUQ presentation, session-plan cap handoff |
382
382
  | `references/phase-6-5-forced-reads.md` | Phases 6.5.1 + 6.5.2 full procedural bodies — the two forced-read STATE.md continuity slots: `## What Not To Retry` (`readWhatNotToRetry`, #623) and `## Open Questions` (`readOpenQuestions`, #772), both rendered unconditionally and wrapped via `wrapHistorical` from `scripts/lib/historical-guard.mjs` |
383
383
  | `references/phase-6-6-project-intelligence.md` | Phase 6.6 full procedural body — active-learnings surface from `.orchestrator/metrics/learnings.jsonl`, cap+rank via `learnings-surface-top-n`, grouping by type, and the Surface health block |
@@ -29,6 +29,11 @@
29
29
 
30
30
  Skip silently on `null`; do not block the session in any case. This passage does not restate a severity-mapping table for the probe-stale shape — the `vault-staleness` entry in the probe registry (`scripts/lib/session-start-probes.mjs`, `PROBES`) is the single declaration site for how a probe's result maps onto the rendered banner severity, and (post-N3) needs no custom mapping for this probe at all.
31
31
 
32
+ Additionally, invoke the telemetry-flush-health probe (`scripts/lib/telemetry-flush-health-banner.mjs`) via `checkTelemetryFlushHealth({ repoRoot })` (synchronous — no await). It reads the LAST `orchestrator.telemetry.flush` record from `.orchestrator/metrics/events.jsonl` (tail-read, last 64 KB, scanned backwards so newest wins) and returns TWO shapes: `null` (silent no-op) when the ledger is absent, holds no flush record in that window, or the newest flush record's `reason` does not start with `sandbox:`; otherwise `{severity: 'warn', reason, message}`. Render `result.message` alongside the bootstrap-lock-freshness and vault-staleness banners:
33
+ - **warn** (`reason` starts with `sandbox:`): `"⚠ Telemetry: last flush refused by the sandbox guard (<reason>) — the guard could not complete its checks and failed closed; no ping was sent. See docs/telemetry.md § Sandbox guard."`
34
+
35
+ Why the probe exists (#1255): `scripts/lib/telemetry/sync.mjs` fails CLOSED — it returns `{sandbox: true, reason: 'sandbox:probe-failed'}` when its environment probe cannot complete — and stays deliberately SILENT about it, because it runs inside the SessionEnd teardown budget where a banner has no reader. The refusal reached exactly ONE sink (`hooks/on-session-end.mjs` → `emitEvent('orchestrator.telemetry.flush', …)`) and no consumer ever read it back out (W1-D6 census, 2026-09-07), so a repo that refused every flush for weeks was indistinguishable from a healthy one. This probe is the read-back, at the first moment a human is looking. Newest-wins is load-bearing: a later successful flush clears the warning by itself, so the banner reports the CURRENT state of the channel, never its history.
36
+
32
37
  Additionally, if the current repo has a configured `origin` remote and `glab` (GitLab) or `gh` (GitHub) is available, invoke the CI-status probe (`scripts/lib/ci-status-banner.mjs`) via `await checkCiStatus({ repoRoot: process.cwd() })` — it is `export async function`, so a caller following this line without `await` gets a Promise and renders nothing.
33
38
 
34
39
  The return contract has THREE states, not two (#1031, the same shape as the mirror-issues probe below): `null` means a measured ABSENCE — no usable VCS remote, so there is no CI here; `{status, ok, details, …}` is a real reading (`green` | `red` | `unknown`); and `{severity: 'warn', ok: false, message, degraded}` means the state could NOT be read, where `degraded` is one of `cli-missing | timeout | parse-error | query-failed | git-error`. Render `result.message` verbatim in the degraded case and read it as *"CI state is unknown"* — never as green. Until #1031 all five of those collapsed onto `null`, which in the banner contract reads as all-clear; that collapse is how #1022 (`gh repo view -R` → `unknown shorthand flag`) stayed invisible on every GitHub repo.
@@ -37,6 +37,16 @@ Phase 1 ships a self-contained validator that reads every `.md` file under `VAUL
37
37
 
38
38
  The inline Zod schema is vendored from the canonical source at `projects-baseline/packages/zod-schemas/src/vault-frontmatter.ts`. The skill is intentionally self-contained (no monorepo workspace dependency), so the schema is duplicated with a header comment pointing at the SSOT. Drift is to be caught by a future smoke test that imports the canonical schema and diffs the shape — NOT YET IMPLEMENTED. Until that test exists, any change to the canonical schema must be mirrored here in the same commit.
39
39
 
40
+ ### Dependencies
41
+
42
+ `skills/vault-sync/package.json` pins `yaml ^2.5.0` / `zod ^3.24.0` — intentionally NOT the root's `yaml ^2.9.0` / `zod ^3.25.76`. This is not drift to fix:
43
+
44
+ - Both CI hosts install this sub-package on its own, independent of the root install (`.gitlab-ci.yml:121` and `.github/workflows/test.yml:93`, both: `(cd skills/vault-sync && npm install --no-audit --no-fund)`).
45
+ - The root `package.json` declares no `workspaces`, so `npm ci` at the root never touches this folder's deps; `skills/vault-sync/node_modules/` is the only place `zod` resolves for this skill (`scripts/lib/vault-archive.mjs:19-21`).
46
+ - The `zod ^3.24.0` pin deliberately tracks the projects-baseline version, not this repo's own (`scripts/release.mjs:488`).
47
+
48
+ The root's dependency versions are NOT the SSOT for this folder — do not "fix" this pin to match the root.
49
+
40
50
  ### How session-end invokes it
41
51
 
42
52
  ```