session-orchestrator 5.3.0 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +28 -13
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +9 -31
- package/CHANGELOG.md +162 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +154 -4
- package/docs/components.md +11 -11
- package/docs/events-schema.md +25 -19
- package/docs/install.md +14 -2
- package/docs/migration-v4.md +24 -17
- package/docs/persona-panel.md +5 -4
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +40 -16
- package/docs/session-config-reference.md +61 -12
- package/docs/session-config-template.md +11 -2
- package/docs/vault-docs-architecture.md +8 -4
- package/hooks/_lib/hook-import-set.json +42 -29
- package/hooks/_lib/lock-bootstrap.mjs +118 -26
- package/hooks/config-protection.mjs +1 -1
- package/hooks/enforce-commands.mjs +28 -13
- package/hooks/enforce-scope.mjs +201 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/on-session-start.mjs +57 -2
- package/hooks/on-stop.mjs +70 -168
- package/hooks/post-bash-write-verify.mjs +98 -17
- package/hooks/post-edit-validate.mjs +15 -5
- package/hooks/post-tool-batch-wave-signal.mjs +32 -152
- package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-bash-staging-fence.mjs +29 -8
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +1143 -279
- package/hooks/subagent-telemetry.mjs +96 -19
- package/hooks/wave-scope-commit-guard.mjs +168 -72
- package/package.json +2 -6
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +6 -1
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +5 -1
- package/rules/opt-in-stack/backend-data.md +9 -1
- package/rules/opt-in-stack/backend.md +7 -1
- package/rules/opt-in-stack/frontend.md +6 -1
- package/rules/opt-in-stack/security-web.md +7 -1
- package/rules/opt-in-stack/swift.md +5 -1
- package/scripts/apply-session-learnings.mjs +513 -0
- package/scripts/archive-closed-prds.mjs +40 -12
- package/scripts/autopilot.mjs +57 -54
- package/scripts/backfill-abandoned-sessions.mjs +81 -6
- package/scripts/backfill-learnings-from-vault.mjs +49 -22
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/codex-install.mjs +3 -0
- package/scripts/cursor-install.mjs +47 -6
- package/scripts/emit-session.mjs +298 -2
- package/scripts/export-hw-learnings.mjs +40 -10
- package/scripts/fleet-checkin.mjs +170 -0
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +6 -1
- package/scripts/lib/autopilot/loop.mjs +32 -13
- package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/blocked-commands-policy.mjs +40 -0
- package/scripts/lib/common.mjs +41 -0
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +7 -6
- package/scripts/lib/config/vault-integration.mjs +151 -16
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +75 -10
- package/scripts/lib/eval/engine.mjs +13 -0
- package/scripts/lib/events-retention-banner.mjs +393 -0
- package/scripts/lib/events-rotation.mjs +21 -6
- package/scripts/lib/events.mjs +72 -5
- package/scripts/lib/fleet-protocol.mjs +288 -0
- package/scripts/lib/gates/gate-baseline.mjs +13 -3
- package/scripts/lib/gates/gate-full.mjs +7 -2
- package/scripts/lib/gates/gate-helpers.mjs +93 -2
- package/scripts/lib/gates/gate-incremental.mjs +23 -22
- package/scripts/lib/gates/gate-per-file.mjs +16 -7
- package/scripts/lib/hardening.mjs +1 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
- package/scripts/lib/instruction-budget-guard.mjs +77 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
- package/scripts/lib/learnings/io.mjs +274 -37
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/lock-reaper.mjs +38 -14
- package/scripts/lib/maintenance-due-banner.mjs +23 -8
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +147 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
- package/scripts/lib/orphan-reaper/scan.mjs +510 -0
- package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
- package/scripts/lib/orphan-reaper.mjs +47 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/platform.mjs +206 -2
- package/scripts/lib/plugin-update-banner.mjs +3 -1
- package/scripts/lib/process-group.mjs +134 -22
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +45 -5
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +100 -62
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +119 -29
- package/scripts/lib/scope-echo.mjs +7 -0
- package/scripts/lib/scope-gate.mjs +194 -27
- package/scripts/lib/session-close-backfill.mjs +129 -4
- package/scripts/lib/session-end/tail-runner.mjs +10 -5
- package/scripts/lib/session-lock-shape.mjs +6 -3
- package/scripts/lib/session-lock.mjs +320 -67
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +67 -17
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-start-probes.mjs +242 -68
- package/scripts/lib/session-token-rollup.mjs +252 -54
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sessions-staleness-banner.mjs +3 -1
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +27 -3
- package/scripts/lib/telemetry/pricing.mjs +121 -71
- package/scripts/lib/telemetry/sync.mjs +17 -0
- package/scripts/lib/tests-src-ratio.mjs +215 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/ux-grill/reconcile.mjs +1 -1
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
- package/scripts/lib/vault-mirror/namespace.mjs +47 -26
- package/scripts/lib/vault-mirror/process.mjs +350 -62
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
- package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
- package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
- package/scripts/lib/vault-status/board-writer.mjs +53 -12
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- package/scripts/lib/vcs-repo-spec.mjs +8 -0
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/materialize-wave-scope.mjs +7 -4
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +11 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
- package/scripts/run-quality-gate.mjs +331 -307
- package/scripts/self-update.mjs +229 -0
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +603 -20
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/validate-wave-scope.mjs +21 -2
- package/scripts/vault-backfill.mjs +50 -37
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +238 -75
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +134 -0
- package/skills/_shared/state-ownership.md +41 -12
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +6 -4
- package/skills/bootstrap/_shared-template.md +97 -2
- package/skills/bootstrap/fast-template.md +4 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/bootstrap/standard-template.md +2 -0
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
- package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
- package/skills/discovery/probes/vault-staleness.mjs +9 -3
- package/skills/docs-orchestrator/SKILL.md +12 -5
- package/skills/docs-orchestrator/audience-mapping.md +5 -3
- package/skills/evolve/SKILL.md +13 -4
- package/skills/evolve/references/evolve-analyze-mode.md +42 -18
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/learning-patterns.md +23 -13
- package/skills/session-end/metrics-collection.md +6 -7
- package/skills/session-end/phase-3-2-docs-verification.md +7 -6
- package/skills/session-end/plan-verification.md +25 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +65 -68
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +8 -2
- package/skills/session-start/references/operations-contract.md +6 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/vault-mirror/SKILL.md +21 -11
- package/skills/wave-executor/SKILL.md +2 -2
- package/skills/wave-executor/circuit-breaker.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
- package/skills/wave-executor/references/wave-loop-review.md +4 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Bring every LOCAL install of Session Orchestrator to the newest version in
|
|
4
|
+
* one command: this clone, the Claude Code plugin, and the Codex plugin.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS: an update was three independent manual acts, and the
|
|
7
|
+
* second one has a silent hole. Measured 2026-10-02 (5.3.0 → 5.4.0, Claude
|
|
8
|
+
* Code 2.1.287): `claude plugin update` succeeded but left the cache WITHOUT
|
|
9
|
+
* node_modules — `claude plugin list --json` carried
|
|
10
|
+
* `noteDetails: [{type: "dependencies-refused"}]` because package.json sets
|
|
11
|
+
* `overrides`. Hooks survive that (no static third-party imports), skill
|
|
12
|
+
* scripts and the MCP server do not. This script reads that state back and
|
|
13
|
+
* installs the runtime dependencies itself.
|
|
14
|
+
*
|
|
15
|
+
* Steps (each skipped when its harness is not installed on this host):
|
|
16
|
+
* 1. repo — `git fetch origin` + `git merge --ff-only origin/main`, then
|
|
17
|
+
* `npm ci` when HEAD moved or node_modules is missing.
|
|
18
|
+
* 2. claude — marketplace update + plugin update, then verify version and
|
|
19
|
+
* runtime dependencies in the reported installPath.
|
|
20
|
+
* 3. codex — only if the plugin is already installed in Codex: re-run
|
|
21
|
+
* scripts/codex-install.mjs (refreshes the installed bundle).
|
|
22
|
+
*
|
|
23
|
+
* Never deletes an old plugin cache directory: the running session still
|
|
24
|
+
* loads it until the harness restarts.
|
|
25
|
+
*
|
|
26
|
+
* Exit codes: 0 all steps ok/skipped · 1 usage error · 2 a step failed.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { spawnSync } from 'node:child_process';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
|
|
34
|
+
import { isMainModule } from './lib/is-main-module.mjs';
|
|
35
|
+
|
|
36
|
+
const SO_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
37
|
+
const PLUGIN_ID = 'session-orchestrator@kanevry';
|
|
38
|
+
const MARKETPLACE = 'kanevry';
|
|
39
|
+
const COMMAND_TIMEOUT_MS = 300_000;
|
|
40
|
+
// `codex plugin list --available --json` is 2.56 MB on a host with several
|
|
41
|
+
// marketplaces (measured 2026-10-02); the 1 MiB default fails with ENOBUFS.
|
|
42
|
+
const MAX_BUFFER = 64 * 1024 * 1024;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Pick the session-orchestrator entry from `claude plugin list --json`.
|
|
46
|
+
* Accepts both the bare-array and the `{plugins: [...]}` shape.
|
|
47
|
+
* @param {unknown} listJson
|
|
48
|
+
* @returns {{version?: string, installPath?: string, enabled?: boolean, noteDetails?: Array<{type?: string}>} | null}
|
|
49
|
+
*/
|
|
50
|
+
export function findClaudeEntry(listJson) {
|
|
51
|
+
const entries = Array.isArray(listJson) ? listJson : listJson?.plugins;
|
|
52
|
+
if (!Array.isArray(entries)) return null;
|
|
53
|
+
return entries.find((e) => e?.id === PLUGIN_ID && e?.scope === 'user')
|
|
54
|
+
?? entries.find((e) => e?.id === PLUGIN_ID)
|
|
55
|
+
?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Runtime dependencies of the package at `dir` that have no directory under
|
|
60
|
+
* `dir/node_modules`. A presence check, not a version check: its job is to
|
|
61
|
+
* catch the harness having skipped the install entirely.
|
|
62
|
+
* @param {string} dir
|
|
63
|
+
* @returns {string[]}
|
|
64
|
+
*/
|
|
65
|
+
export function missingRuntimeDeps(dir) {
|
|
66
|
+
const pkg = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
67
|
+
return Object.keys(pkg.dependencies ?? {})
|
|
68
|
+
.filter((name) => !existsSync(path.join(dir, 'node_modules', name)));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseArguments(argv) {
|
|
72
|
+
const opts = { dryRun: false, json: false, skipPull: false, help: false };
|
|
73
|
+
for (const arg of argv) {
|
|
74
|
+
if (arg === '--dry-run') opts.dryRun = true;
|
|
75
|
+
else if (arg === '--json') opts.json = true;
|
|
76
|
+
else if (arg === '--skip-pull') opts.skipPull = true;
|
|
77
|
+
else if (arg === '--help' || arg === '-h') opts.help = true;
|
|
78
|
+
else throw Object.assign(new Error(`Unknown argument '${arg}'. Run with --help.`), { usage: true });
|
|
79
|
+
}
|
|
80
|
+
return opts;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const HELP = `Usage: node scripts/self-update.mjs [--dry-run] [--skip-pull] [--json]
|
|
84
|
+
|
|
85
|
+
Update this clone, the Claude Code plugin and (if installed) the Codex plugin
|
|
86
|
+
to the newest Session Orchestrator, and verify each one afterwards.
|
|
87
|
+
|
|
88
|
+
--dry-run Print what would run; change nothing
|
|
89
|
+
--skip-pull Leave the clone as it is (e.g. right after a release)
|
|
90
|
+
--json Print the per-step result as JSON
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
function makeRunner({ dryRun, json }) {
|
|
94
|
+
const log = (line) => { if (!json) process.stdout.write(`${line}\n`); };
|
|
95
|
+
/** Run a command; read-only queries also run under --dry-run. */
|
|
96
|
+
function run(cmd, args, { cwd = SO_ROOT, mutates = true } = {}) {
|
|
97
|
+
const shown = `${cmd} ${args.join(' ')}`;
|
|
98
|
+
if (dryRun && mutates) {
|
|
99
|
+
log(` [dry-run] ${shown}${cwd === SO_ROOT ? '' : ` (in ${cwd})`}`);
|
|
100
|
+
return { ok: true, stdout: '' };
|
|
101
|
+
}
|
|
102
|
+
const r = spawnSync(cmd, args, { cwd, encoding: 'utf8', timeout: COMMAND_TIMEOUT_MS, maxBuffer: MAX_BUFFER });
|
|
103
|
+
if (r.error) return { ok: false, detail: `${shown}: ${r.error.message}` };
|
|
104
|
+
if (r.status !== 0) {
|
|
105
|
+
return { ok: false, detail: `${shown} exited ${r.status}: ${(r.stderr || r.stdout || '').trim().slice(-400)}` };
|
|
106
|
+
}
|
|
107
|
+
return { ok: true, stdout: r.stdout };
|
|
108
|
+
}
|
|
109
|
+
return { run, log };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function onPath(bin) {
|
|
113
|
+
return spawnSync('which', [bin], { encoding: 'utf8' }).status === 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function repoVersion() {
|
|
117
|
+
return JSON.parse(readFileSync(path.join(SO_ROOT, 'package.json'), 'utf8')).version;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function updateRepo({ run, log }, opts) {
|
|
121
|
+
if (opts.skipPull) return { step: 'repo', status: 'skipped', detail: '--skip-pull' };
|
|
122
|
+
const before = run('git', ['rev-parse', 'HEAD'], { mutates: false });
|
|
123
|
+
if (!before.ok) return { step: 'repo', status: 'failed', detail: before.detail };
|
|
124
|
+
const branch = run('git', ['branch', '--show-current'], { mutates: false }).stdout?.trim();
|
|
125
|
+
if (branch !== 'main') {
|
|
126
|
+
return { step: 'repo', status: 'failed', detail: `clone is on '${branch}', not main — switch or use --skip-pull` };
|
|
127
|
+
}
|
|
128
|
+
log('repo: fetching origin and fast-forwarding main');
|
|
129
|
+
for (const [cmd, args] of [['git', ['fetch', 'origin']], ['git', ['merge', '--ff-only', 'origin/main']]]) {
|
|
130
|
+
const r = run(cmd, args);
|
|
131
|
+
if (!r.ok) return { step: 'repo', status: 'failed', detail: r.detail };
|
|
132
|
+
}
|
|
133
|
+
const after = run('git', ['rev-parse', 'HEAD'], { mutates: false });
|
|
134
|
+
const moved = before.stdout !== after.stdout;
|
|
135
|
+
if (moved || !existsSync(path.join(SO_ROOT, 'node_modules'))) {
|
|
136
|
+
const r = run('npm', ['ci']);
|
|
137
|
+
if (!r.ok) return { step: 'repo', status: 'failed', detail: r.detail };
|
|
138
|
+
}
|
|
139
|
+
return { step: 'repo', status: 'ok', detail: `${repoVersion()}${moved ? ' (updated)' : ' (already current)'}` };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function updateClaude({ run, log }, opts) {
|
|
143
|
+
if (!onPath('claude')) return { step: 'claude', status: 'skipped', detail: 'claude not on PATH' };
|
|
144
|
+
log('claude: refreshing marketplace and plugin');
|
|
145
|
+
for (const args of [['plugin', 'marketplace', 'update', MARKETPLACE], ['plugin', 'update', PLUGIN_ID]]) {
|
|
146
|
+
const r = run('claude', args);
|
|
147
|
+
if (!r.ok) return { step: 'claude', status: 'failed', detail: r.detail };
|
|
148
|
+
}
|
|
149
|
+
const list = run('claude', ['plugin', 'list', '--json'], { mutates: false });
|
|
150
|
+
if (!list.ok) return { step: 'claude', status: 'failed', detail: list.detail };
|
|
151
|
+
let entry;
|
|
152
|
+
try { entry = findClaudeEntry(JSON.parse(list.stdout)); } catch (e) {
|
|
153
|
+
return { step: 'claude', status: 'failed', detail: `plugin list --json unparseable: ${e.message}` };
|
|
154
|
+
}
|
|
155
|
+
if (!entry) return { step: 'claude', status: 'skipped', detail: `${PLUGIN_ID} not installed` };
|
|
156
|
+
if (!entry.installPath || !existsSync(entry.installPath)) {
|
|
157
|
+
return { step: 'claude', status: 'failed', detail: `installPath missing: ${entry.installPath}` };
|
|
158
|
+
}
|
|
159
|
+
const missing = missingRuntimeDeps(entry.installPath);
|
|
160
|
+
if (missing.length > 0) {
|
|
161
|
+
const refused = (entry.noteDetails ?? []).some((n) => n?.type === 'dependencies-refused');
|
|
162
|
+
log(`claude: ${missing.length} runtime deps missing${refused ? ' (harness refused the install)' : ''} — installing`);
|
|
163
|
+
const r = run('npm', ['ci', '--omit=dev'], { cwd: entry.installPath });
|
|
164
|
+
if (!r.ok) return { step: 'claude', status: 'failed', detail: r.detail };
|
|
165
|
+
const still = opts.dryRun ? [] : missingRuntimeDeps(entry.installPath);
|
|
166
|
+
if (still.length > 0) return { step: 'claude', status: 'failed', detail: `still missing after npm ci: ${still.join(', ')}` };
|
|
167
|
+
}
|
|
168
|
+
const want = repoVersion();
|
|
169
|
+
// The marketplace tracks GitHub main, so it can trail this clone until the
|
|
170
|
+
// release is pushed — report it, do not fail on it.
|
|
171
|
+
const lag = entry.version !== want ? `; clone is ${want} — marketplace not yet at it` : '';
|
|
172
|
+
return {
|
|
173
|
+
step: 'claude',
|
|
174
|
+
status: 'ok',
|
|
175
|
+
detail: `${entry.version}${missing.length ? `, installed ${missing.length} missing deps` : ''}${lag}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function updateCodex({ run, log }, opts) {
|
|
180
|
+
if (!onPath('codex')) return { step: 'codex', status: 'skipped', detail: 'codex not on PATH' };
|
|
181
|
+
const list = run('codex', ['plugin', 'list'], { mutates: false });
|
|
182
|
+
if (!list.ok) return { step: 'codex', status: 'failed', detail: list.detail };
|
|
183
|
+
if (!new RegExp(`^${PLUGIN_ID}\\s+installed`, 'm').test(list.stdout)) {
|
|
184
|
+
return { step: 'codex', status: 'skipped', detail: 'plugin not installed in Codex (install: node scripts/codex-install.mjs)' };
|
|
185
|
+
}
|
|
186
|
+
log('codex: re-running codex-install');
|
|
187
|
+
const r = run(process.execPath, [path.join(SO_ROOT, 'scripts', 'codex-install.mjs'), '--json']);
|
|
188
|
+
if (!r.ok) return { step: 'codex', status: 'failed', detail: r.detail };
|
|
189
|
+
if (opts.dryRun) return { step: 'codex', status: 'ok', detail: 'dry-run' };
|
|
190
|
+
try {
|
|
191
|
+
return { step: 'codex', status: 'ok', detail: JSON.parse(r.stdout).pluginVersion };
|
|
192
|
+
} catch {
|
|
193
|
+
return { step: 'codex', status: 'ok', detail: 'installed (version not reported)' };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function main(opts) {
|
|
198
|
+
const io = makeRunner(opts);
|
|
199
|
+
const results = [];
|
|
200
|
+
// Order matters: the Codex installer installs FROM this clone, so the clone
|
|
201
|
+
// must be current first. A failed repo step stops the chain for that reason.
|
|
202
|
+
results.push(updateRepo(io, opts));
|
|
203
|
+
if (results[0].status !== 'failed') {
|
|
204
|
+
results.push(updateClaude(io, opts));
|
|
205
|
+
results.push(updateCodex(io, opts));
|
|
206
|
+
}
|
|
207
|
+
const ok = results.every((r) => r.status !== 'failed');
|
|
208
|
+
if (opts.json) {
|
|
209
|
+
process.stdout.write(`${JSON.stringify({ ok, dryRun: opts.dryRun, results }, null, 2)}\n`);
|
|
210
|
+
} else {
|
|
211
|
+
io.log('');
|
|
212
|
+
for (const r of results) io.log(` ${r.status.padEnd(7)} ${r.step.padEnd(7)} ${r.detail}`);
|
|
213
|
+
io.log(ok
|
|
214
|
+
? '\nRestart Claude Code / Codex to load the new version. Old cache folders stay until then — delete them only after the restart.'
|
|
215
|
+
: '\nA step failed (a failed repo step skips the plugin steps). Fix it and re-run — every step is idempotent.');
|
|
216
|
+
}
|
|
217
|
+
return ok ? 0 : 2;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (isMainModule(import.meta.url)) {
|
|
221
|
+
try {
|
|
222
|
+
const opts = parseArguments(process.argv.slice(2));
|
|
223
|
+
if (opts.help) process.stdout.write(HELP);
|
|
224
|
+
else process.exitCode = main(opts);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
process.stderr.write(`ERROR: ${error.message}\n`);
|
|
227
|
+
process.exitCode = error.usage ? 1 : 2;
|
|
228
|
+
}
|
|
229
|
+
}
|
package/scripts/site-numbers.mjs
CHANGED
|
@@ -91,6 +91,13 @@
|
|
|
91
91
|
* silent skip — a generator that quietly stops filling a surface is the exact
|
|
92
92
|
* failure this file exists to end.
|
|
93
93
|
*
|
|
94
|
+
* ## The sitemap `<lastmod>` (`site/sitemap.xml`, #1484)
|
|
95
|
+
*
|
|
96
|
+
* `--write` changes what `/` and `/de` SHOW, so it also dates them in the
|
|
97
|
+
* sitemap: `lastmod` becomes max(lastmod, the page's own `counted-at` cell).
|
|
98
|
+
* `--check` reports a `lastmod` older than that cell as drift. Rule, mapping and
|
|
99
|
+
* reasons: `SITEMAP_PAGES`.
|
|
100
|
+
*
|
|
94
101
|
* Exit codes (`.claude/rules/cli-design.md`):
|
|
95
102
|
* 0 — no drift (--check) / files updated or already current (--write)
|
|
96
103
|
* 1 — drift found (--check), or a contract violation in either mode
|
|
@@ -99,7 +106,7 @@
|
|
|
99
106
|
*/
|
|
100
107
|
|
|
101
108
|
import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
102
|
-
import { join, resolve, relative } from 'node:path';
|
|
109
|
+
import { join, resolve, relative, sep } from 'node:path';
|
|
103
110
|
import { execFileSync } from 'node:child_process';
|
|
104
111
|
|
|
105
112
|
import { writeStdoutLineSync, writeJsonAtomicSync } from './lib/io.mjs';
|
|
@@ -963,6 +970,118 @@ export function syncCensusBlocks(siteDir, values, { write = false } = {}) {
|
|
|
963
970
|
return CENSUS_BLOCK_FILES.map((file) => syncCensusBlock(siteDir, values, { write, file }));
|
|
964
971
|
}
|
|
965
972
|
|
|
973
|
+
// ---------------------------------------------------------------------------
|
|
974
|
+
// The sitemap <lastmod> in site/sitemap.xml (#1484)
|
|
975
|
+
// ---------------------------------------------------------------------------
|
|
976
|
+
|
|
977
|
+
/** The sitemap, relative to the SITE directory. */
|
|
978
|
+
export const SITEMAP_FILE = 'sitemap.xml';
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Every page ↔ sitemap `<loc>`, explicitly.
|
|
982
|
+
*
|
|
983
|
+
* Why the sitemap is this generator's business: `--write` changes what `/` and
|
|
984
|
+
* `/de` show (version card, number cards, "Counted on … at commit …"), and the
|
|
985
|
+
* release that ran it left their `lastmod` behind. Measured 2026-10-01: `/`
|
|
986
|
+
* claimed 2026-09-08 while its content was counted 2026-09-22.
|
|
987
|
+
*
|
|
988
|
+
* The rule takes no second clock. A page's `counted-at` cell is the date this
|
|
989
|
+
* script last changed that page — `rewrite()` stamps every cell in one pass —
|
|
990
|
+
* so `lastmod` becomes max(lastmod, that cell), and never moves backwards. A
|
|
991
|
+
* page without a `counted-at` cell (the guide and the legal pages, whose only
|
|
992
|
+
* cell is the header version) is never dated: a version bump in a page header
|
|
993
|
+
* is not a content change.
|
|
994
|
+
*
|
|
995
|
+
* Unknown either way is an ERROR, never a skip (the `METRIC_IDS` rule): a
|
|
996
|
+
* sitemap `<loc>` missing here would never be dated, and a page carrying cells
|
|
997
|
+
* that is missing here would drift unseen.
|
|
998
|
+
*/
|
|
999
|
+
export const SITEMAP_PAGES = Object.freeze([
|
|
1000
|
+
{ page: 'index.html', loc: 'https://session-orchestrator.com/' },
|
|
1001
|
+
{ page: 'de/index.html', loc: 'https://session-orchestrator.com/de' },
|
|
1002
|
+
{ page: 'guide/index.html', loc: 'https://session-orchestrator.com/guide' },
|
|
1003
|
+
{ page: 'impressum/index.html', loc: 'https://session-orchestrator.com/impressum' },
|
|
1004
|
+
{ page: 'datenschutz/index.html', loc: 'https://session-orchestrator.com/datenschutz' },
|
|
1005
|
+
]);
|
|
1006
|
+
|
|
1007
|
+
const URL_BLOCK_RE = /<url>([\s\S]*?)<\/url>/g;
|
|
1008
|
+
const ISO_DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Judge (and optionally rewrite) the `<lastmod>` of every sitemap entry. Only the
|
|
1012
|
+
* date text of an entry that moves is rewritten; the rest of the file is handed
|
|
1013
|
+
* back byte-for-byte.
|
|
1014
|
+
*
|
|
1015
|
+
* @param {string} siteDir
|
|
1016
|
+
* @param {Array<{page:string, metrics:string[], countedAt:string|null}>} pages
|
|
1017
|
+
* every HTML file under `siteDir` carrying metric cells: its site-relative path
|
|
1018
|
+
* (`/`-separated), its metric ids, and its `counted-at` cell as it stands AFTER
|
|
1019
|
+
* this run (`null` when it has none)
|
|
1020
|
+
* @param {{write?: boolean}} [opts]
|
|
1021
|
+
* @returns {{present:boolean, file:string, errors?:string[],
|
|
1022
|
+
* drift?:Array<{loc:string, line:number, actual:string, expected:string}>,
|
|
1023
|
+
* written?:boolean}}
|
|
1024
|
+
*/
|
|
1025
|
+
export function syncSitemapLastmod(siteDir, pages, { write = false } = {}) {
|
|
1026
|
+
const abs = join(siteDir, SITEMAP_FILE);
|
|
1027
|
+
// Absent file: like the census blocks, not this generator's to create.
|
|
1028
|
+
if (!existsSync(abs) || !statSync(abs).isFile()) return { present: false, file: abs };
|
|
1029
|
+
|
|
1030
|
+
const errors = [];
|
|
1031
|
+
const dated = new Map(); // loc → the counting date its page carries
|
|
1032
|
+
for (const p of pages) {
|
|
1033
|
+
const entry = SITEMAP_PAGES.find((e) => e.page === p.page);
|
|
1034
|
+
if (!entry) {
|
|
1035
|
+
errors.push(
|
|
1036
|
+
`${p.page} carries metric cells but has no sitemap mapping — known pages: ` +
|
|
1037
|
+
SITEMAP_PAGES.map((e) => e.page).join(', '),
|
|
1038
|
+
);
|
|
1039
|
+
} else if (p.countedAt !== null) {
|
|
1040
|
+
if (ISO_DAY_RE.test(p.countedAt)) dated.set(entry.loc, p.countedAt);
|
|
1041
|
+
else errors.push(`${p.page}: counted-at cell "${p.countedAt}" is not a YYYY-MM-DD date`);
|
|
1042
|
+
} else if (p.metrics.some((m) => m !== 'version')) {
|
|
1043
|
+
// Counted cells with no stamp to date them by: their changes would never
|
|
1044
|
+
// reach the sitemap.
|
|
1045
|
+
errors.push(`${p.page} carries counted cells but no counted-at cell — its lastmod cannot be dated`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const text = readFileSync(abs, 'utf8');
|
|
1050
|
+
const drift = [];
|
|
1051
|
+
const seen = new Set();
|
|
1052
|
+
const next = text.replace(URL_BLOCK_RE, (block, inner, offset) => {
|
|
1053
|
+
const loc = /<loc>([^<]*)<\/loc>/.exec(inner)?.[1].trim();
|
|
1054
|
+
if (loc === undefined || !SITEMAP_PAGES.some((e) => e.loc === loc)) {
|
|
1055
|
+
errors.push(
|
|
1056
|
+
`unknown sitemap <loc> "${loc ?? '(none)'}" — known: ${SITEMAP_PAGES.map((e) => e.loc).join(', ')}`,
|
|
1057
|
+
);
|
|
1058
|
+
return block;
|
|
1059
|
+
}
|
|
1060
|
+
seen.add(loc);
|
|
1061
|
+
const lm = /<lastmod>([^<]*)<\/lastmod>/.exec(block);
|
|
1062
|
+
if (!lm || !ISO_DAY_RE.test(lm[1])) {
|
|
1063
|
+
errors.push(`<lastmod> of ${loc} is missing or not a YYYY-MM-DD date`);
|
|
1064
|
+
return block;
|
|
1065
|
+
}
|
|
1066
|
+
const date = dated.get(loc);
|
|
1067
|
+
// ISO days compare as strings. Never backwards: a lastmod a hand edit dated
|
|
1068
|
+
// after the count stays.
|
|
1069
|
+
if (date === undefined || date <= lm[1]) return block;
|
|
1070
|
+
drift.push({ loc, line: lineOf(text, offset + lm.index), actual: lm[1], expected: date });
|
|
1071
|
+
return `${block.slice(0, lm.index)}<lastmod>${date}</lastmod>${block.slice(lm.index + lm[0].length)}`;
|
|
1072
|
+
});
|
|
1073
|
+
for (const loc of dated.keys()) {
|
|
1074
|
+
if (!seen.has(loc)) errors.push(`${loc} carries a counted-at cell but has no sitemap <url> entry`);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const out = { present: true, file: abs, errors, drift, written: false };
|
|
1078
|
+
if (write && errors.length === 0 && next !== text) {
|
|
1079
|
+
writeFileSync(abs, next, 'utf8');
|
|
1080
|
+
out.written = true;
|
|
1081
|
+
}
|
|
1082
|
+
return out;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
966
1085
|
// ---------------------------------------------------------------------------
|
|
967
1086
|
// Markup
|
|
968
1087
|
// ---------------------------------------------------------------------------
|
|
@@ -1134,6 +1253,7 @@ function printHelp() {
|
|
|
1134
1253
|
writeStdoutLineSync(' --check (default) report drift, change nothing; exit 1 on drift');
|
|
1135
1254
|
writeStdoutLineSync(` --write rewrite the cells in place; also refreshes ${CENSUS_FILE.join('/')}`);
|
|
1136
1255
|
writeStdoutLineSync(' (skipped when --site points outside the repo\'s own site/)');
|
|
1256
|
+
writeStdoutLineSync(` and dates each counted page's <lastmod> in <site>/${SITEMAP_FILE}`);
|
|
1137
1257
|
writeStdoutLineSync(' --json machine-readable envelope on stdout');
|
|
1138
1258
|
writeStdoutLineSync(' --site <dir> site directory (default <repo-root>/site)');
|
|
1139
1259
|
writeStdoutLineSync(' --version print the package version');
|
|
@@ -1208,12 +1328,18 @@ export function main(argv = process.argv.slice(2), env = {}) {
|
|
|
1208
1328
|
// the source producing it is corrupt, and writing the rest would be a
|
|
1209
1329
|
// partial write reported as success.
|
|
1210
1330
|
let rejectedTotal = 0;
|
|
1331
|
+
// Every page carrying cells, for the sitemap's <lastmod> (syncSitemapLastmod).
|
|
1332
|
+
const pages = [];
|
|
1211
1333
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1334
|
+
// Repo-relative when the file is inside the repo; absolute otherwise (a
|
|
1335
|
+
// `--site` fixture in $TMPDIR would otherwise render as ../../../../var/...).
|
|
1336
|
+
const display = (abs) => {
|
|
1215
1337
|
const r = relative(root, abs);
|
|
1216
|
-
|
|
1338
|
+
return r && !r.startsWith('..') ? r : abs;
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
for (const abs of files) {
|
|
1342
|
+
const rel = display(abs);
|
|
1217
1343
|
const html = readFileSync(abs, 'utf8');
|
|
1218
1344
|
const spans = inspectHtml(html, values);
|
|
1219
1345
|
spanTotal += spans.length;
|
|
@@ -1250,6 +1376,16 @@ export function main(argv = process.argv.slice(2), env = {}) {
|
|
|
1250
1376
|
}
|
|
1251
1377
|
}
|
|
1252
1378
|
|
|
1379
|
+
if (spans.length > 0) {
|
|
1380
|
+
const stamp = spans.find((s) => s.metric === 'counted-at');
|
|
1381
|
+
pages.push({
|
|
1382
|
+
page: relative(siteDir, abs).split(sep).join('/'),
|
|
1383
|
+
metrics: spans.map((s) => s.metric),
|
|
1384
|
+
// As the page stands AFTER this run: a written file carries every value.
|
|
1385
|
+
countedAt: stamp ? (written > 0 ? values['counted-at'] : stamp.actual.trim()) : null,
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1253
1389
|
report.push({
|
|
1254
1390
|
file: rel,
|
|
1255
1391
|
spans: spans.length,
|
|
@@ -1294,6 +1430,17 @@ export function main(argv = process.argv.slice(2), env = {}) {
|
|
|
1294
1430
|
// read `llmsFull`; `censusBlocks` below carries all of them.
|
|
1295
1431
|
const llms = censusBlocks[CENSUS_BLOCK_FILES.indexOf(LLMS_FULL_FILE)];
|
|
1296
1432
|
|
|
1433
|
+
// The sitemap <lastmod>: a date older than the counting stamp on its page is
|
|
1434
|
+
// drift, like a stale number. Written from the pages as they stand after the
|
|
1435
|
+
// loop above, so page and sitemap come from the same run.
|
|
1436
|
+
const sitemap = syncSitemapLastmod(siteDir, pages, { write: args.write });
|
|
1437
|
+
const sitemapRel = display(sitemap.file);
|
|
1438
|
+
for (const e of sitemap.errors ?? []) {
|
|
1439
|
+
stderr(`Error: ${sitemapRel}: ${e}`);
|
|
1440
|
+
contractTotal += 1;
|
|
1441
|
+
}
|
|
1442
|
+
driftTotal += sitemap.drift?.length ?? 0;
|
|
1443
|
+
|
|
1297
1444
|
// The named silent-failure class: a generator that matches nothing, changes
|
|
1298
1445
|
// nothing, and reports success. Zero spans means the markup contract is not in
|
|
1299
1446
|
// the page — that is a defect, in BOTH modes, never a no-op.
|
|
@@ -1390,6 +1537,13 @@ export function main(argv = process.argv.slice(2), env = {}) {
|
|
|
1390
1537
|
stale: b.stale === true,
|
|
1391
1538
|
written: b.written === true,
|
|
1392
1539
|
})),
|
|
1540
|
+
sitemap: {
|
|
1541
|
+
file: sitemap.present ? sitemapRel : null,
|
|
1542
|
+
present: sitemap.present,
|
|
1543
|
+
errors: sitemap.errors ?? [],
|
|
1544
|
+
drift: sitemap.drift ?? [],
|
|
1545
|
+
written: sitemap.written === true,
|
|
1546
|
+
},
|
|
1393
1547
|
censusWritten,
|
|
1394
1548
|
ok,
|
|
1395
1549
|
},
|
|
@@ -1404,6 +1558,7 @@ export function main(argv = process.argv.slice(2), env = {}) {
|
|
|
1404
1558
|
censusBlocks
|
|
1405
1559
|
.map((b, i) => (b.written ? ` + ${CENSUS_BLOCK_FILES[i]}` : ''))
|
|
1406
1560
|
.join('') +
|
|
1561
|
+
(sitemap.written ? ` + ${SITEMAP_FILE}` : '') +
|
|
1407
1562
|
(censusWritten ? ` + ${CENSUS_FILE.join('/')}` : ''),
|
|
1408
1563
|
);
|
|
1409
1564
|
} else {
|
|
@@ -1416,6 +1571,9 @@ export function main(argv = process.argv.slice(2), env = {}) {
|
|
|
1416
1571
|
}
|
|
1417
1572
|
}
|
|
1418
1573
|
for (const l of censusDriftLines) stdout(l);
|
|
1574
|
+
for (const d of sitemap.drift ?? []) {
|
|
1575
|
+
stdout(`DRIFT ${sitemapRel}:${d.line} lastmod ${d.loc}: sitemap says "${d.actual}", page counted "${d.expected}"`);
|
|
1576
|
+
}
|
|
1419
1577
|
stdout(
|
|
1420
1578
|
driftTotal === 0 && !noSpans && contractTotal === 0
|
|
1421
1579
|
? `site-numbers: ${spanTotal} metric cell(s) current across ${files.length} file(s)`
|