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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +3 -2
- package/.codex-plugin/skills/architecture/SKILL.md +20 -0
- package/.codex-plugin/skills/autopilot/SKILL.md +21 -0
- package/.codex-plugin/skills/autopilot/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/bootstrap/SKILL.md +22 -0
- package/.codex-plugin/skills/bootstrap/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/brainstorm/SKILL.md +22 -0
- package/.codex-plugin/skills/brainstorm/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +17 -0
- package/.codex-plugin/skills/close/SKILL.md +21 -0
- package/.codex-plugin/skills/close/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +24 -0
- package/.codex-plugin/skills/debug/SKILL.md +21 -0
- package/.codex-plugin/skills/debug/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/discovery/SKILL.md +21 -0
- package/.codex-plugin/skills/discovery/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/dispatcher/SKILL.md +21 -0
- package/.codex-plugin/skills/dispatcher/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/docs-orchestrator/SKILL.md +20 -0
- package/.codex-plugin/skills/ecosystem-health/SKILL.md +22 -0
- package/.codex-plugin/skills/eli5/SKILL.md +21 -0
- package/.codex-plugin/skills/eli5/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/eval/SKILL.md +21 -0
- package/.codex-plugin/skills/eval/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/evolve/SKILL.md +21 -0
- package/.codex-plugin/skills/evolve/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/frontmatter-guard/SKILL.md +17 -0
- package/.codex-plugin/skills/gitlab-ops/SKILL.md +22 -0
- package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +17 -0
- package/.codex-plugin/skills/go/SKILL.md +22 -0
- package/.codex-plugin/skills/go/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/grill/SKILL.md +21 -0
- package/.codex-plugin/skills/grill/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/harness-audit/SKILL.md +19 -0
- package/.codex-plugin/skills/harness-audit/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/hook-development/SKILL.md +17 -0
- package/.codex-plugin/skills/mcp-builder/SKILL.md +17 -0
- package/.codex-plugin/skills/memory-cleanup/SKILL.md +21 -0
- package/.codex-plugin/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/mode-selector/SKILL.md +19 -0
- package/.codex-plugin/skills/npm-publish/SKILL.md +18 -0
- package/.codex-plugin/skills/peekaboo-driver/SKILL.md +20 -0
- package/.codex-plugin/skills/persona-panel/SKILL.md +22 -0
- package/.codex-plugin/skills/persona-panel/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/plan/SKILL.md +22 -0
- package/.codex-plugin/skills/plan/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/playwright-driver/SKILL.md +22 -0
- package/.codex-plugin/skills/portfolio/SKILL.md +21 -0
- package/.codex-plugin/skills/portfolio/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/quality-gates/SKILL.md +22 -0
- package/.codex-plugin/skills/reconcile/SKILL.md +21 -0
- package/.codex-plugin/skills/reconcile/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/release/SKILL.md +22 -0
- package/.codex-plugin/skills/release/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/remote-offload/SKILL.md +22 -0
- package/.codex-plugin/skills/repo-audit/SKILL.md +19 -0
- package/.codex-plugin/skills/repo-audit/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/session/SKILL.md +21 -0
- package/.codex-plugin/skills/session/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/session-end/SKILL.md +22 -0
- package/.codex-plugin/skills/session-plan/SKILL.md +22 -0
- package/.codex-plugin/skills/session-start/SKILL.md +22 -0
- package/.codex-plugin/skills/spinout/SKILL.md +21 -0
- package/.codex-plugin/skills/spinout/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/sunset-review/SKILL.md +21 -0
- package/.codex-plugin/skills/sunset-review/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/templates-ack/SKILL.md +21 -0
- package/.codex-plugin/skills/templates-ack/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/test/SKILL.md +21 -0
- package/.codex-plugin/skills/test/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/test-runner/SKILL.md +22 -0
- package/.codex-plugin/skills/tmux-layout/SKILL.md +23 -0
- package/.codex-plugin/skills/using-orchestrator/SKILL.md +19 -0
- package/.codex-plugin/skills/vault-mirror/SKILL.md +17 -0
- package/.codex-plugin/skills/vault-sync/SKILL.md +17 -0
- package/.codex-plugin/skills/wave-executor/SKILL.md +22 -0
- package/.codex-plugin/skills/write-executable-plan/SKILL.md +24 -0
- package/{plugin.json → .cursor-plugin/plugin.json} +5 -2
- package/CHANGELOG.md +190 -1
- package/README.md +26 -18
- package/docs/codex-setup.md +43 -9
- package/docs/components.md +3 -2
- package/docs/instruction-delivery.md +12 -5
- package/docs/migration-v4.md +33 -9
- package/hooks/_lib/hook-import-set.json +4 -3
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-stop.mjs +25 -4
- package/package.json +2 -2
- package/scripts/generate-codex-skills.mjs +246 -0
- package/scripts/generate-hook-import-set.mjs +51 -8
- package/scripts/lib/codex/plugin-contract.mjs +6 -0
- package/scripts/lib/config/host-paths.mjs +20 -4
- package/scripts/lib/gates/gate-full.mjs +7 -3
- package/scripts/lib/owner-config-banner.mjs +7 -9
- package/scripts/lib/owner-yaml.mjs +8 -1
- package/scripts/lib/plugin-update-banner.mjs +10 -2
- package/scripts/lib/reconcile/engine.mjs +38 -7
- package/scripts/lib/session-schema/constants.mjs +38 -11
- package/scripts/lib/session-start-probes.mjs +12 -0
- package/scripts/lib/telemetry/schema.mjs +39 -18
- package/scripts/lib/telemetry-flush-health-banner.mjs +211 -0
- package/scripts/lib/validate/check-codex-skills.mjs +191 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +91 -55
- package/scripts/lib/validate/check-skill-links.mjs +37 -7
- package/scripts/lib/validate/check-test-git-config-target.mjs +192 -12
- package/scripts/lib/validate/check-unwired-features.mjs +163 -13
- package/scripts/lib/validate/confidential-names.mjs +95 -30
- package/scripts/lib/validate/repo-files.mjs +48 -14
- package/scripts/release.mjs +109 -18
- package/scripts/site-numbers.mjs +344 -8
- package/scripts/validate-plugin.mjs +3 -0
- package/skills/session-start/SKILL.md +2 -2
- package/skills/session-start/references/phase-4-ssot-environment-check.md +5 -0
- package/skills/vault-sync/SKILL.md +10 -0
package/scripts/site-numbers.mjs
CHANGED
|
@@ -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').
|
|
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
|
|
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
|
-
|
|
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
|
-
`${
|
|
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)
|
|
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
|
|
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
|
```
|