session-orchestrator 5.2.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/session.md +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +136 -0
- package/README.md +9 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/baseline.md +12 -6
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +7 -5
- package/docs/events-schema.md +56 -9
- package/docs/rule-authoring.md +58 -6
- package/docs/session-config-reference.md +100 -7
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +85 -8
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +168 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +233 -2
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +1 -1
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/session.md +1 -1
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +117 -15
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +75 -29
- package/scripts/eval-session.mjs +65 -3
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +146 -4
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +4 -3
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +30 -5
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/sweep-expired-rules.mjs +14 -3
- package/scripts/validate-plugin.mjs +12 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-mirror.mjs +9 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/autopilot/SKILL.md +22 -7
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +1 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +17 -5
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +8 -4
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/check-sessions-integrity.mjs
|
|
3
|
+
//
|
|
4
|
+
// CLI front-end for `checkSessionsIntegrity` (GitLab #1417).
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS EXISTS:
|
|
7
|
+
// session-end step 4a (`skills/session-end/session-metrics-write.md`) has to
|
|
8
|
+
// prove that the session record it JUST appended to
|
|
9
|
+
// `.orchestrator/metrics/sessions.jsonl` is schema-valid AND renderable by
|
|
10
|
+
// vault-mirror — step 4 before it only proves the line is JSON. The #1408
|
|
11
|
+
// record parsed fine and was still invalid (`ended_at` instead of
|
|
12
|
+
// `completed_at`, four required fields missing), so vault-mirror dropped it as
|
|
13
|
+
// `skipped-invalid` and that session got no vault note; nobody was told until
|
|
14
|
+
// the NEXT session-start banner. Until now the check existed only as a
|
|
15
|
+
// `node --input-type=module -e` one-liner inside the skill body, because the
|
|
16
|
+
// function had no CLI entry point (`scripts/repair-invalid-sessions.mjs` is a
|
|
17
|
+
// REPAIR path whose `--dry-run` exit reports repairability, not validity).
|
|
18
|
+
//
|
|
19
|
+
// WHAT IT CHECKS (exactly what the banner checks — no second opinion here):
|
|
20
|
+
// every parseable record in the ledger is run through `validateSession()` AND
|
|
21
|
+
// through vault-mirror's real render path. `--session-id` narrows the VERDICT
|
|
22
|
+
// to one record; other sessions' findings are still printed (they are not this
|
|
23
|
+
// close's to fix) but never decide the exit code.
|
|
24
|
+
//
|
|
25
|
+
// WHY A MISSING RECORD EXITS 1 (the deliberate call, #1417):
|
|
26
|
+
// `checkSessionsIntegrity` is silent both when a named record is sound and
|
|
27
|
+
// when it does not exist — "no finding" cannot tell those apart. Reporting the
|
|
28
|
+
// absent case as success would make this guard fail-open in precisely the
|
|
29
|
+
// shape it exists to catch: a close that proceeds with NO ledger record, one
|
|
30
|
+
// notch worse than the #1408 invalid one. So with `--session-id` the ledger is
|
|
31
|
+
// additionally read for that id and its absence is a named failure ("record
|
|
32
|
+
// not found"), never a silent pass. Without `--session-id` there is no id to
|
|
33
|
+
// be absent, and an empty/missing ledger is simply nothing to judge (exit 0).
|
|
34
|
+
//
|
|
35
|
+
// USAGE:
|
|
36
|
+
// node scripts/check-sessions-integrity.mjs [--repo-root <path>] [--session-id <id>] [--json]
|
|
37
|
+
//
|
|
38
|
+
// EXIT CODES:
|
|
39
|
+
// 0 every judged record validates and mirrors
|
|
40
|
+
// 1 a judged record is schema-invalid, is dropped by vault-mirror, or (with
|
|
41
|
+
// --session-id) is not in the ledger at all
|
|
42
|
+
// 2 tool error — unknown/incomplete flag, or an unreadable repo root
|
|
43
|
+
//
|
|
44
|
+
// Node stdlib only apart from two repo-local imports; safe to call from a hook,
|
|
45
|
+
// a skill body or CI without any dependency install.
|
|
46
|
+
|
|
47
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
48
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
49
|
+
import { fileURLToPath } from 'node:url';
|
|
50
|
+
|
|
51
|
+
import { checkSessionsIntegrity, SESSIONS_PATH } from './lib/sessions-integrity-banner.mjs';
|
|
52
|
+
import { isMainModule } from './lib/is-main-module.mjs';
|
|
53
|
+
|
|
54
|
+
const SELF_DIR = dirname(fileURLToPath(import.meta.url));
|
|
55
|
+
|
|
56
|
+
const HELP_TEXT = `check-sessions-integrity.mjs — session-ledger schema + vault-mirror gate (#1417)
|
|
57
|
+
|
|
58
|
+
USAGE
|
|
59
|
+
node scripts/check-sessions-integrity.mjs [options]
|
|
60
|
+
|
|
61
|
+
DESCRIPTION
|
|
62
|
+
Runs every parseable record in .orchestrator/metrics/sessions.jsonl through
|
|
63
|
+
validateSession() AND through vault-mirror's real render path — the same two
|
|
64
|
+
populations the session-start banner reports. A record can pass one and fail
|
|
65
|
+
the other, so both are checked and reported separately.
|
|
66
|
+
|
|
67
|
+
This is the command form of session-end step 4a: after appending a session
|
|
68
|
+
record, prove it is sound BEFORE the close continues.
|
|
69
|
+
|
|
70
|
+
OPTIONS
|
|
71
|
+
--repo-root <path> Repo root holding .orchestrator/metrics/ (default: cwd).
|
|
72
|
+
--session-id <id> Judge only the record(s) carrying this session_id. Other
|
|
73
|
+
sessions' findings are still printed but never decide the
|
|
74
|
+
exit code — they are pre-existing, not this close's to fix.
|
|
75
|
+
An id with NO record in the ledger is a FAILURE (exit 1),
|
|
76
|
+
not a silent pass: absent and sound are indistinguishable
|
|
77
|
+
to the checker, and the fail-open reading is the dangerous
|
|
78
|
+
one.
|
|
79
|
+
--json Emit one JSON result object on stdout (diagnostics stay
|
|
80
|
+
on stderr).
|
|
81
|
+
--help, -h Show this help and exit.
|
|
82
|
+
--version Print the package version and exit.
|
|
83
|
+
|
|
84
|
+
EXIT CODES
|
|
85
|
+
0 every judged record validates and mirrors
|
|
86
|
+
1 a judged record is schema-invalid, is dropped by vault-mirror, or (with
|
|
87
|
+
--session-id) is not in the ledger at all
|
|
88
|
+
2 tool error — unknown/incomplete flag, or an unreadable repo root
|
|
89
|
+
|
|
90
|
+
EXAMPLES
|
|
91
|
+
node scripts/check-sessions-integrity.mjs
|
|
92
|
+
node scripts/check-sessions-integrity.mjs --session-id main-2026-09-21-session-4
|
|
93
|
+
node scripts/check-sessions-integrity.mjs --json | jq '.findings'
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse argv. Returns `error` (a message) instead of throwing, so the caller
|
|
98
|
+
* owns the exit code.
|
|
99
|
+
*
|
|
100
|
+
* @param {string[]} argv
|
|
101
|
+
* @returns {{opts: {repoRoot: string|null, sessionId: string|null, json: boolean,
|
|
102
|
+
* help: boolean, version: boolean}, error: string|null}}
|
|
103
|
+
*/
|
|
104
|
+
export function parseArgs(argv) {
|
|
105
|
+
const opts = { repoRoot: null, sessionId: null, json: false, help: false, version: false };
|
|
106
|
+
for (let i = 0; i < argv.length; i++) {
|
|
107
|
+
const arg = argv[i];
|
|
108
|
+
switch (arg) {
|
|
109
|
+
case '--repo-root':
|
|
110
|
+
case '--session-id': {
|
|
111
|
+
const value = argv[i + 1];
|
|
112
|
+
// A flag whose value is missing — or is itself the next flag — must be a
|
|
113
|
+
// usage error, never a silently empty filter that judges nothing.
|
|
114
|
+
if (typeof value !== 'string' || value === '' || value.startsWith('--')) {
|
|
115
|
+
return { opts, error: `${arg} requires a value` };
|
|
116
|
+
}
|
|
117
|
+
if (arg === '--repo-root') opts.repoRoot = value;
|
|
118
|
+
else opts.sessionId = value;
|
|
119
|
+
i += 1;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case '--json':
|
|
123
|
+
opts.json = true;
|
|
124
|
+
break;
|
|
125
|
+
case '--help':
|
|
126
|
+
case '-h':
|
|
127
|
+
opts.help = true;
|
|
128
|
+
break;
|
|
129
|
+
case '--version':
|
|
130
|
+
opts.version = true;
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
return { opts, error: `unknown flag: ${arg}` };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { opts, error: null };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Count the ledger records carrying `sessionId`. Unparseable lines are skipped —
|
|
141
|
+
* the same posture the banner takes: this judges schema integrity, not file
|
|
142
|
+
* corruption.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} repoRoot
|
|
145
|
+
* @param {string} sessionId
|
|
146
|
+
* @returns {number}
|
|
147
|
+
*/
|
|
148
|
+
function countLedgerRecords(repoRoot, sessionId) {
|
|
149
|
+
const filePath = join(repoRoot, SESSIONS_PATH);
|
|
150
|
+
if (!existsSync(filePath)) return 0;
|
|
151
|
+
let matched = 0;
|
|
152
|
+
for (const line of readFileSync(filePath, 'utf8').split('\n')) {
|
|
153
|
+
if (line.trim() === '') continue;
|
|
154
|
+
let record;
|
|
155
|
+
try {
|
|
156
|
+
record = JSON.parse(line);
|
|
157
|
+
} catch {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (record && typeof record === 'object' && record.session_id === sessionId) matched += 1;
|
|
161
|
+
}
|
|
162
|
+
return matched;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Run the check and return a plain result object. Never throws; never exits.
|
|
167
|
+
*
|
|
168
|
+
* @param {{repoRoot?: string, sessionId?: string|null}} [opts]
|
|
169
|
+
* @returns {{ok: boolean, exitCode: 0|1|2, repoRoot: string, sessionId: string|null,
|
|
170
|
+
* total: number|null, matched: number|null, severity: 'warn'|'alert'|null,
|
|
171
|
+
* message: string|null, findings: {kind: string, line: number, sessionId: string, error: string}[],
|
|
172
|
+
* error: string|null}}
|
|
173
|
+
*/
|
|
174
|
+
export function runSessionsIntegrityCheck({ repoRoot, sessionId = null } = {}) {
|
|
175
|
+
const root = resolve(repoRoot ?? process.cwd());
|
|
176
|
+
const base = {
|
|
177
|
+
ok: false,
|
|
178
|
+
exitCode: 2,
|
|
179
|
+
repoRoot: root,
|
|
180
|
+
sessionId: sessionId ?? null,
|
|
181
|
+
total: null,
|
|
182
|
+
matched: null,
|
|
183
|
+
severity: null,
|
|
184
|
+
message: null,
|
|
185
|
+
findings: [],
|
|
186
|
+
error: null,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
191
|
+
return { ...base, error: `repo root is not a directory: ${root}` };
|
|
192
|
+
}
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return { ...base, error: `repo root unreadable: ${err?.message ?? String(err)}` };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const banner = checkSessionsIntegrity({ repoRoot: root });
|
|
198
|
+
|
|
199
|
+
const findings = banner
|
|
200
|
+
? [
|
|
201
|
+
...banner.schemaInvalid.map((f) => ({ kind: 'schema-invalid', ...f })),
|
|
202
|
+
...banner.mirrorSkipped.map((f) => ({ kind: 'mirror-skipped', ...f })),
|
|
203
|
+
]
|
|
204
|
+
: [];
|
|
205
|
+
|
|
206
|
+
const result = {
|
|
207
|
+
...base,
|
|
208
|
+
total: banner ? banner.total : null,
|
|
209
|
+
severity: banner ? banner.severity : null,
|
|
210
|
+
message: banner ? banner.message : null,
|
|
211
|
+
findings,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (sessionId === null || sessionId === undefined) {
|
|
215
|
+
// No filter: every finding decides.
|
|
216
|
+
const ok = findings.length === 0;
|
|
217
|
+
return { ...result, ok, exitCode: ok ? 0 : 1 };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const matched = countLedgerRecords(root, sessionId);
|
|
221
|
+
const mine = findings.filter((f) => f.sessionId === sessionId);
|
|
222
|
+
|
|
223
|
+
if (matched === 0) {
|
|
224
|
+
return {
|
|
225
|
+
...result,
|
|
226
|
+
matched,
|
|
227
|
+
ok: false,
|
|
228
|
+
exitCode: 1,
|
|
229
|
+
error:
|
|
230
|
+
`no record with session_id "${sessionId}" in ${SESSIONS_PATH} — ` +
|
|
231
|
+
'it was never written, or it was written under a different id. ' +
|
|
232
|
+
'Re-emit it via scripts/emit-session.mjs.',
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const ok = mine.length === 0;
|
|
237
|
+
return { ...result, matched, ok, exitCode: ok ? 0 : 1 };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function readVersion() {
|
|
241
|
+
try {
|
|
242
|
+
const pkg = JSON.parse(readFileSync(join(SELF_DIR, '..', 'package.json'), 'utf8'));
|
|
243
|
+
return typeof pkg.version === 'string' ? pkg.version : 'unknown';
|
|
244
|
+
} catch {
|
|
245
|
+
return 'unknown';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// CLI entry — only when run directly, never on import (an import must do
|
|
250
|
+
// nothing: this module is loaded by tests and is reachable from hook code).
|
|
251
|
+
if (isMainModule(import.meta.url)) {
|
|
252
|
+
const { opts, error } = parseArgs(process.argv.slice(2));
|
|
253
|
+
|
|
254
|
+
if (error !== null) {
|
|
255
|
+
process.stderr.write(`[check-sessions-integrity] ${error}\n`);
|
|
256
|
+
process.stderr.write('Run with --help for usage.\n');
|
|
257
|
+
process.exit(2);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (opts.help) {
|
|
261
|
+
process.stdout.write(HELP_TEXT);
|
|
262
|
+
process.exit(0);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (opts.version) {
|
|
266
|
+
process.stdout.write(`${readVersion()}\n`);
|
|
267
|
+
process.exit(0);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const repoRootArg =
|
|
271
|
+
opts.repoRoot === null ? undefined : isAbsolute(opts.repoRoot) ? opts.repoRoot : resolve(process.cwd(), opts.repoRoot);
|
|
272
|
+
|
|
273
|
+
const result = runSessionsIntegrityCheck({ repoRoot: repoRootArg, sessionId: opts.sessionId });
|
|
274
|
+
|
|
275
|
+
if (opts.json) {
|
|
276
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
277
|
+
process.exit(result.exitCode);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// The banner text covers EVERY record, including other sessions' pre-existing
|
|
281
|
+
// findings — printed for visibility, even when the verdict below is clean.
|
|
282
|
+
if (result.message) process.stderr.write(`${result.message}\n`);
|
|
283
|
+
if (result.error) process.stderr.write(`[check-sessions-integrity] ${result.error}\n`);
|
|
284
|
+
|
|
285
|
+
if (result.exitCode === 2) process.exit(2);
|
|
286
|
+
|
|
287
|
+
if (result.ok) {
|
|
288
|
+
const scope = result.sessionId ? `record "${result.sessionId}"` : 'every ledger record';
|
|
289
|
+
process.stdout.write(`[check-sessions-integrity] OK — ${scope} validates and mirrors\n`);
|
|
290
|
+
process.exit(0);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const scope = result.sessionId ? `session "${result.sessionId}"` : 'the sessions ledger';
|
|
294
|
+
process.stderr.write(
|
|
295
|
+
`[check-sessions-integrity] FAIL — ${scope} is not sound. ` +
|
|
296
|
+
'Re-emit the affected record(s) via scripts/emit-session.mjs ' +
|
|
297
|
+
'(bulk repair: node scripts/repair-invalid-sessions.mjs --apply).\n',
|
|
298
|
+
);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
@@ -49,6 +49,7 @@ import { readPeerCards } from './lib/peer-cards/reader.mjs';
|
|
|
49
49
|
import { filterRealSessions } from './lib/session-schema/filters.mjs';
|
|
50
50
|
import { readCanonicalSessions } from './lib/sessions-canonical.mjs';
|
|
51
51
|
import { recordDialecticRun } from './lib/learnings/evolve-telemetry.mjs';
|
|
52
|
+
import { DEFAULT_BUDGET_TOKENS } from './lib/config/dialectic.mjs';
|
|
52
53
|
|
|
53
54
|
// ---------------------------------------------------------------------------
|
|
54
55
|
// Constants
|
|
@@ -57,8 +58,11 @@ import { recordDialecticRun } from './lib/learnings/evolve-telemetry.mjs';
|
|
|
57
58
|
/** Allowed LLM model identifiers. Per #506 EARS unwanted-behaviour: fail-fast on others. */
|
|
58
59
|
export const ALLOWED_MODELS = Object.freeze(['haiku', 'sonnet', 'opus']);
|
|
59
60
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Default per-call budget (input + output tokens). Per #506 AC2; the input
|
|
63
|
+
* ceiling is `DEFAULT_BUDGET_TOKENS` from the config parser (SSOT, #1380).
|
|
64
|
+
*/
|
|
65
|
+
export const DEFAULT_BUDGET = Object.freeze({ input: DEFAULT_BUDGET_TOKENS, output: 4000 });
|
|
62
66
|
|
|
63
67
|
/** Industry-standard heuristic: ~4 chars per token for English prose. */
|
|
64
68
|
const CHARS_PER_TOKEN = 4;
|
|
@@ -309,19 +313,21 @@ export function buildPrompt(payload, model, randomNonce = () => randomBytes(4).t
|
|
|
309
313
|
'',
|
|
310
314
|
'```diff',
|
|
311
315
|
'# target: user',
|
|
312
|
-
'<
|
|
316
|
+
'<the `## ` sections of USER.md you change or add>',
|
|
313
317
|
'```',
|
|
314
318
|
'',
|
|
315
319
|
'```diff',
|
|
316
320
|
'# target: agent',
|
|
317
|
-
'<
|
|
321
|
+
'<the `## ` sections of AGENT.md you change or add>',
|
|
318
322
|
'```',
|
|
319
323
|
'',
|
|
320
324
|
'Rules:',
|
|
321
325
|
'- Emit at most one block per target. Omit a target entirely when no update is warranted.',
|
|
322
|
-
'- The block body is
|
|
326
|
+
'- The block body is a set of whole `## ` sections — not a unified diff hunk. It is merged per section:',
|
|
327
|
+
' omitted sections stay UNCHANGED (nothing is deleted), so emit ONLY the sections you change or newly',
|
|
328
|
+
' ground, and reuse an existing heading verbatim to update that section.',
|
|
323
329
|
'- Preserve existing peer-card frontmatter; do not include `---` frontmatter lines in your block.',
|
|
324
|
-
'- Do not invent
|
|
330
|
+
'- Do not invent sections without evidence from the supplied learnings or sessions — nothing downstream rejects them.',
|
|
325
331
|
`- Operate within the dialectic budget appropriate for model '${model}'.`,
|
|
326
332
|
'',
|
|
327
333
|
].join('\n');
|
|
@@ -505,6 +511,26 @@ async function readSteering(repoRoot) {
|
|
|
505
511
|
* @property {number} [budget]
|
|
506
512
|
*/
|
|
507
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Record a THROWN abort (`unknown-model`, `subagent-crash`) before the caller
|
|
516
|
+
* rethrows the original error (#1221). The record is guarded on its own:
|
|
517
|
+
* `recordDialecticRun()` is best-effort today, but the rethrow contract must
|
|
518
|
+
* not depend on that staying true — a telemetry failure never replaces the
|
|
519
|
+
* error the caller is about to rethrow.
|
|
520
|
+
*
|
|
521
|
+
* @param {string} repoRoot
|
|
522
|
+
* @param {'unknown-model' | 'subagent-crash'} status
|
|
523
|
+
* @param {number} t0 — `Date.now()` at run start
|
|
524
|
+
* @returns {Promise<void>}
|
|
525
|
+
*/
|
|
526
|
+
async function recordThrownAbort(repoRoot, status, t0) {
|
|
527
|
+
try {
|
|
528
|
+
await recordDialecticRun({ repoRoot, status, durationMs: Date.now() - t0 });
|
|
529
|
+
} catch {
|
|
530
|
+
/* swallowed on purpose — see docblock */
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
508
534
|
/**
|
|
509
535
|
* Run the deriver loop: load inputs → build payload → check budget → dispatch
|
|
510
536
|
* agent → parse response → guard empty-card. Returns a structured verdict.
|
|
@@ -546,15 +572,20 @@ export async function runDialecticDeriver({
|
|
|
546
572
|
|
|
547
573
|
// #1206 — telemetry start marker for the mechanical `orchestrator.dialectic.
|
|
548
574
|
// completed` emits below. Mechanical (measured), not the coordinator's own
|
|
549
|
-
// typed DURATION_MS placeholder — covers every
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
575
|
+
// typed DURATION_MS placeholder — covers every outcome THIS function can
|
|
576
|
+
// see: the returns, and since #1221 the two THROWN aborts (`unknown-model`,
|
|
577
|
+
// `subagent-crash`), recorded at the throw point before the original error
|
|
578
|
+
// is rethrown. Only APPLY-mode success stays with the skill-prose caller —
|
|
579
|
+
// the merge happens one layer up, outside this pure pipeline.
|
|
554
580
|
const t0 = Date.now();
|
|
555
581
|
|
|
556
582
|
// Gate 1: model fail-fast. Throws Error with the canonical message.
|
|
557
|
-
|
|
583
|
+
try {
|
|
584
|
+
validateModel(model);
|
|
585
|
+
} catch (err) {
|
|
586
|
+
await recordThrownAbort(repoRoot, 'unknown-model', t0);
|
|
587
|
+
throw err;
|
|
588
|
+
}
|
|
558
589
|
|
|
559
590
|
// Load inputs (best-effort, never throws on missing).
|
|
560
591
|
const [learnings, sessions, peerCardsResult, steering] = await Promise.all([
|
|
@@ -611,7 +642,13 @@ export async function runDialecticDeriver({
|
|
|
611
642
|
|
|
612
643
|
// Dispatch — DI boundary. Caller wires the real Agent({...}) wrapper or a mock.
|
|
613
644
|
const maxTokens = typeof budget?.output === 'number' ? budget.output : DEFAULT_BUDGET.output;
|
|
614
|
-
|
|
645
|
+
let response;
|
|
646
|
+
try {
|
|
647
|
+
response = await dispatchAgent({ model, prompt, maxTokens });
|
|
648
|
+
} catch (err) {
|
|
649
|
+
await recordThrownAbort(repoRoot, 'subagent-crash', t0);
|
|
650
|
+
throw err;
|
|
651
|
+
}
|
|
615
652
|
|
|
616
653
|
const text = typeof response?.text === 'string' ? response.text : '';
|
|
617
654
|
const { diff } = parseResponse(text);
|
package/scripts/emit-session.mjs
CHANGED
|
@@ -40,11 +40,16 @@ import {
|
|
|
40
40
|
CURRENT_SESSION_SCHEMA_VERSION,
|
|
41
41
|
clampTimestampsMonotonic,
|
|
42
42
|
aliasLegacyEndedAt,
|
|
43
|
+
normalizeWaveKeys,
|
|
43
44
|
} from './lib/session-schema.mjs';
|
|
44
45
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
45
46
|
|
|
46
47
|
export { serializeSessionLineChecked };
|
|
47
48
|
|
|
49
|
+
// A FULL object name only (SHA-1 or SHA-256 repo). A short sha or a symbolic
|
|
50
|
+
// ref (`HEAD`, a branch) would resolve to a DIFFERENT commit when read later.
|
|
51
|
+
const FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
52
|
+
|
|
48
53
|
function parseArgs(argv) {
|
|
49
54
|
const args = { file: '.orchestrator/metrics/sessions.jsonl', entry: null };
|
|
50
55
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -118,6 +123,14 @@ async function main() {
|
|
|
118
123
|
);
|
|
119
124
|
}
|
|
120
125
|
|
|
126
|
+
// 3. Alias legacy per-wave agent-count keys (`agents_completed`, …) onto
|
|
127
|
+
// their canonical `agent_count_*` spelling (#1390 P1), non-clobbering.
|
|
128
|
+
// ONLY this pass of normalizeSession runs on the write path: the rest
|
|
129
|
+
// would tag a missing `schema_version` as 0 (validateSession stamps the
|
|
130
|
+
// current version) and apply top-level aliases that validateSession's
|
|
131
|
+
// contract deliberately does not.
|
|
132
|
+
repaired = normalizeWaveKeys(repaired);
|
|
133
|
+
|
|
121
134
|
// `memory_cleanup_at` derivation (#699 follow-up — Disziplin statt Mechanik).
|
|
122
135
|
// The flag used to be a boolean the coordinator-LLM remembered to pass at
|
|
123
136
|
// session-end; it was forgotten on 2026-08-14 and the cadence marker stalled
|
|
@@ -154,71 +167,104 @@ async function main() {
|
|
|
154
167
|
}
|
|
155
168
|
}
|
|
156
169
|
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
170
|
+
// STATE.md-derived enrichment — ONE read, ONE parse, ONE ownership check for
|
|
171
|
+
// both fields below.
|
|
172
|
+
//
|
|
173
|
+
// `session_profile` (#1247 — GitLab issue "key the effective-sizing row on
|
|
174
|
+
// waves x profile"). The mandatory write path is the coordinator composing
|
|
175
|
+
// `$METRICS_ENTRY` in prose (session-metrics-write.md), which never reliably
|
|
176
|
+
// set this field — so it is read here instead, from this repo's own STATE.md
|
|
177
|
+
// frontmatter via `readSessionProfile()` (scripts/lib/state-md.mjs).
|
|
178
|
+
//
|
|
179
|
+
// `session_start_ref` (#1339 P8) — frontmatter `session-start-ref`, the HEAD
|
|
180
|
+
// sha the wave-executor pinned at session start. `/evolve analyze` needs it
|
|
181
|
+
// for an attributable `<start>..<end>` commit range; without it, it falls
|
|
182
|
+
// back to a time window that also attributes a PARALLEL session's commits to
|
|
183
|
+
// this one (skills/evolve/references/evolve-analyze-mode.md). Only a full
|
|
184
|
+
// hex sha (FULL_SHA_RE) is adopted; anything else is omitted with a WARN.
|
|
185
|
+
// Ceiling: the frontmatter parser coerces an all-decimal-digit value to a
|
|
186
|
+
// number, so such a sha (p ~ 7e-9) is omitted, never adopted as a number.
|
|
187
|
+
// NO END REF, deliberately: this runs at session-end Phase 3.7, BEFORE the
|
|
188
|
+
// Phase 4 close commit exists, so HEAD here would cut the session's own
|
|
189
|
+
// commit out of its range. Consumers use HEAD + `--until=<completed_at>`.
|
|
190
|
+
// Revisit when a post-commit writer exists that can stamp `session_end_ref`.
|
|
162
191
|
//
|
|
163
|
-
// Precedence: an EXPLICIT
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
// never coerced to `''`/`'none'`/a literal `"null"` string.
|
|
192
|
+
// Precedence: an EXPLICIT key on the incoming record (even `null`) wins and
|
|
193
|
+
// is never overwritten — same "explicit assertion beats derivation"
|
|
194
|
+
// convention as `memory_cleanup_at` above. Absence in STATE.md leaves the
|
|
195
|
+
// field OMITTED — never coerced to `''`/`'none'`/a literal `"null"` string.
|
|
168
196
|
//
|
|
169
197
|
// OWNERSHIP: STATE.md belongs to the session named in its OWN frontmatter
|
|
170
198
|
// (`session:`), which in a shared working copy need not be the session this
|
|
171
199
|
// record describes — two parallel sessions, or a `/close` run after a
|
|
172
200
|
// foreign `/plan` session left its STATE.md behind (see
|
|
173
201
|
// `.claude/rules/parallel-sessions.md`). Filing session B's waves under
|
|
174
|
-
// session A's profile is exactly the cross-contamination
|
|
175
|
-
//
|
|
176
|
-
// equals this record's `session_id`; on any mismatch (or an
|
|
177
|
-
// ownership, i.e. no `session` key)
|
|
202
|
+
// session A's profile — or A's start ref — is exactly the cross-contamination
|
|
203
|
+
// both fields exist to remove. So they are adopted ONLY when the frontmatter
|
|
204
|
+
// `session` equals this record's `session_id`; on any mismatch (or an
|
|
205
|
+
// unprovable ownership, i.e. no `session` key) they are OMITTED — never guessed.
|
|
178
206
|
//
|
|
179
207
|
// FAIL-SAFE: the read is enrichment; the session record is the load-bearing
|
|
180
208
|
// artefact. Any failure reading or parsing STATE.md (EISDIR when the path is
|
|
181
|
-
// a directory, EACCES/EPERM, a truncated file) omits the
|
|
209
|
+
// a directory, EACCES/EPERM, a truncated file) omits the fields and lets the
|
|
182
210
|
// record through — it must never be the reason `sessions.jsonl` gains no
|
|
183
211
|
// line at all.
|
|
184
|
-
|
|
212
|
+
const hasOwn = (key) => Object.prototype.hasOwnProperty.call(repaired, key);
|
|
213
|
+
const needsProfile = !hasOwn('session_profile');
|
|
214
|
+
const needsStartRef = !hasOwn('session_start_ref');
|
|
215
|
+
if (needsProfile || needsStartRef) {
|
|
185
216
|
let stateMdContents = '';
|
|
186
217
|
try {
|
|
187
218
|
const stateMdPath = resolveStateMdPath(process.cwd());
|
|
188
219
|
if (existsSync(stateMdPath)) stateMdContents = readFileSync(stateMdPath, 'utf8');
|
|
189
220
|
} catch (err) {
|
|
190
221
|
process.stderr.write(
|
|
191
|
-
`emit-session: WARN could not read STATE.md for session_profile
|
|
192
|
-
`(${err?.message ?? err}); omitting
|
|
222
|
+
`emit-session: WARN could not read STATE.md for session_profile/session_start_ref ` +
|
|
223
|
+
`derivation (${err?.message ?? err}); omitting both\n`
|
|
193
224
|
);
|
|
194
225
|
stateMdContents = '';
|
|
195
226
|
}
|
|
196
227
|
// No initialiser: both the try and the catch below assign `profile`.
|
|
197
228
|
let profile;
|
|
229
|
+
let startRef = null;
|
|
198
230
|
let owner = null;
|
|
199
231
|
try {
|
|
200
232
|
profile = readSessionProfile(stateMdContents);
|
|
201
|
-
const
|
|
202
|
-
const rawOwner =
|
|
233
|
+
const fm = parseStateMd(stateMdContents)?.frontmatter;
|
|
234
|
+
const rawOwner = fm?.session;
|
|
203
235
|
owner = typeof rawOwner === 'string' && rawOwner.trim().length > 0 ? rawOwner.trim() : null;
|
|
236
|
+
const rawRef = fm?.['session-start-ref'];
|
|
237
|
+
if (typeof rawRef === 'string' && FULL_SHA_RE.test(rawRef.trim())) {
|
|
238
|
+
startRef = rawRef.trim();
|
|
239
|
+
} else if (needsStartRef && rawRef !== undefined && rawRef !== null && rawRef !== '') {
|
|
240
|
+
process.stderr.write(
|
|
241
|
+
`emit-session: WARN STATE.md session-start-ref=${rawRef} is not a full hex sha; ` +
|
|
242
|
+
`omitting session_start_ref\n`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
204
245
|
} catch (err) {
|
|
205
246
|
process.stderr.write(
|
|
206
|
-
`emit-session: WARN could not parse STATE.md for session_profile
|
|
207
|
-
`(${err?.message ?? err}); omitting
|
|
247
|
+
`emit-session: WARN could not parse STATE.md for session_profile/session_start_ref ` +
|
|
248
|
+
`derivation (${err?.message ?? err}); omitting both\n`
|
|
208
249
|
);
|
|
209
250
|
profile = null;
|
|
251
|
+
startRef = null;
|
|
210
252
|
}
|
|
211
|
-
|
|
253
|
+
const derived = {};
|
|
254
|
+
if (needsProfile && profile !== null) derived.session_profile = profile;
|
|
255
|
+
if (needsStartRef && startRef !== null) derived.session_start_ref = startRef;
|
|
256
|
+
const derivedKeys = Object.keys(derived);
|
|
257
|
+
if (derivedKeys.length > 0) {
|
|
212
258
|
if (owner !== null && owner === repaired.session_id) {
|
|
213
|
-
repaired = { ...repaired,
|
|
259
|
+
repaired = { ...repaired, ...derived };
|
|
214
260
|
} else {
|
|
215
261
|
// A visible omission: silence here is indistinguishable from "STATE.md
|
|
216
|
-
// carries no
|
|
217
|
-
// precisely what the operator wants to know about.
|
|
262
|
+
// carries no such field", and a foreign STATE.md in this working copy
|
|
263
|
+
// is precisely what the operator wants to know about.
|
|
218
264
|
process.stderr.write(
|
|
219
|
-
`emit-session: WARN STATE.md
|
|
220
|
-
`session=${owner ?? '<absent>'}, not session_id=${repaired.session_id ?? '<unknown>'}; ` +
|
|
221
|
-
`omitting
|
|
265
|
+
`emit-session: WARN STATE.md ${derivedKeys.map((k) => `${k}=${derived[k]}`).join(' ')} ` +
|
|
266
|
+
`belongs to session=${owner ?? '<absent>'}, not session_id=${repaired.session_id ?? '<unknown>'}; ` +
|
|
267
|
+
`omitting ${derivedKeys.join(', ')}\n`
|
|
222
268
|
);
|
|
223
269
|
}
|
|
224
270
|
}
|