session-orchestrator 4.0.1 → 4.2.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/session-plan/SKILL.md +1 -1
- 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/session-plan/SKILL.md +1 -1
- package/.cursor/skills/session-plan/SKILL.md +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +57 -0
- package/README.md +55 -51
- package/agents/ux-evaluator.md +1 -1
- package/commands/close.md +3 -3
- package/commands/go.md +2 -0
- package/commands/memory-cleanup.md +4 -3
- package/commands/persona-panel.md +1 -1
- package/commands/release.md +4 -4
- package/commands/session.md +3 -2
- package/docs/README.md +4 -4
- package/docs/USER-GUIDE.md +115 -48
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +55 -1
- package/docs/ci-setup.md +1 -1
- package/docs/codex-setup.md +1 -0
- package/docs/components.md +2 -2
- package/docs/cursor-setup.md +1 -0
- package/docs/events-schema.md +4 -1
- package/docs/instruction-delivery.md +1 -1
- package/docs/memory-proposal-flow.md +3 -3
- package/docs/migration-v4.md +2 -2
- package/docs/owner-config-schema.md +74 -90
- package/docs/persona-panel.md +4 -4
- package/docs/pi-setup.md +1 -0
- package/docs/rule-authoring.md +13 -6
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +55 -22
- package/docs/session-config-template.md +9 -5
- package/docs/vault-docs-architecture.md +4 -2
- package/hooks/_lib/hook-import-set.json +28 -3
- package/hooks/_lib/vcs-create-matcher.mjs +214 -16
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/pre-bash-issue-budget.mjs +123 -26
- package/hooks/subagent-telemetry.mjs +106 -20
- package/package.json +4 -4
- package/scripts/baseline-archetypes.mjs +28 -0
- package/scripts/ci/assert-coverage-green.mjs +100 -0
- package/scripts/lib/auto-dialectic.mjs +0 -68
- package/scripts/lib/baseline-archetypes.mjs +439 -0
- package/scripts/lib/build-live-signals.mjs +5 -6
- package/scripts/lib/config/issue-budget.mjs +68 -8
- package/scripts/lib/config/private-config-dir.mjs +3 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config-schema.mjs +79 -0
- package/scripts/lib/events.mjs +3 -3
- package/scripts/lib/file-lock.mjs +47 -5
- package/scripts/lib/issue-budget-reconcile.mjs +392 -0
- package/scripts/lib/issue-budget.mjs +76 -3
- package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
- package/scripts/lib/maintenance-due-banner.mjs +440 -0
- package/scripts/lib/owner-config.example.yaml +29 -46
- package/scripts/lib/owner-yaml.mjs +14 -13
- package/scripts/lib/project-hygiene.mjs +182 -6
- package/scripts/lib/quality-gate.mjs +13 -6
- package/scripts/lib/resource-probe/evaluate.mjs +19 -21
- package/scripts/lib/rules-sync.mjs +34 -4
- package/scripts/lib/session-close-backfill.mjs +182 -40
- package/scripts/lib/session-end/phase-skip.mjs +85 -86
- package/scripts/lib/session-end/tail-runner.mjs +178 -0
- package/scripts/lib/session-identity/own-session.mjs +24 -13
- package/scripts/lib/session-schema/constants.mjs +6 -0
- package/scripts/lib/session-schema/validator.mjs +20 -0
- package/scripts/lib/session-shape.mjs +558 -0
- package/scripts/lib/session-start-probes.mjs +10 -3
- package/scripts/lib/session-token-rollup.mjs +95 -10
- package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
- package/scripts/lib/state-md.mjs +1 -0
- package/scripts/lib/subagents-schema.mjs +77 -9
- package/scripts/lib/telemetry/pricing.mjs +197 -0
- package/scripts/lib/telemetry/sync.mjs +50 -1
- package/scripts/lib/validate/check-owner-leakage.mjs +17 -8
- package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
- package/scripts/lib/validate/check-unwired-features.mjs +8 -7
- package/scripts/lib/vault-mirror/process.mjs +2 -1
- package/scripts/lib/vault-mirror/render-sessions.mjs +8 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
- package/scripts/lib/wave-resource-gate.mjs +23 -27
- package/scripts/lib/wave-sizing.mjs +10 -3
- package/scripts/materialize-wave-scope.mjs +68 -14
- package/scripts/print-applicable-rules.mjs +7 -6
- package/scripts/print-learnings-index.mjs +3 -2
- package/scripts/release.mjs +32 -11
- package/scripts/session-shape.mjs +266 -0
- package/skills/_shared/config-reading.md +15 -9
- package/skills/_shared/private-capability-context.md +89 -0
- package/skills/bootstrap/SKILL.md +61 -13
- package/skills/bootstrap/_shared-template.md +99 -14
- package/skills/bootstrap/deep-template.md +36 -26
- package/skills/bootstrap/fast-template.md +44 -8
- package/skills/bootstrap/intensity-heuristic.md +10 -4
- package/skills/bootstrap/private-contract.md +119 -0
- package/skills/bootstrap/public-fallback.md +30 -18
- package/skills/bootstrap/standard-template.md +39 -24
- package/skills/discovery/probes-ui.md +1 -1
- package/skills/docs-orchestrator/audience-mapping.md +1 -1
- package/skills/evolve/SKILL.md +2 -2
- package/skills/gitlab-ops/SKILL.md +3 -3
- package/skills/grill/SKILL.md +1 -1
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/plan/mode-new.md +9 -0
- package/skills/reconcile/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +3 -2
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +23 -65
- package/skills/session-end/phase-3-7a-recommendations.md +2 -2
- package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
- package/skills/session-end/references/phase-5-issue-cleanup.md +26 -0
- package/skills/session-end/session-metrics-write.md +31 -12
- package/skills/session-plan/SKILL.md +56 -48
- package/skills/session-plan/wave-template.md +8 -15
- package/skills/session-start/SKILL.md +18 -2
- package/skills/session-start/phase-2-5-docs-planning.md +1 -1
- package/skills/session-start/phase-8-5-express-path.md +12 -9
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +21 -5
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
- package/skills/test-runner/rubric-v1.md +2 -2
- package/skills/wave-executor/SKILL.md +42 -12
- package/skills/wave-executor/circuit-breaker.md +3 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +4 -2
- package/skills/wave-executor/references/wave-loop-review.md +1 -1
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
- package/templates/nextjs-minimal/package.json +1 -1
- package/templates/node-minimal/package.json +1 -1
- package/scripts/lib/multi-provider-build/providers.mjs +0 -64
- package/scripts/lib/multi-provider-build/templating.mjs +0 -130
- package/scripts/lib/owner-config/coerce.mjs +0 -29
- package/scripts/lib/owner-config/constants.mjs +0 -21
- package/scripts/lib/owner-config/defaults.mjs +0 -50
- package/scripts/lib/owner-config/error.mjs +0 -19
- package/scripts/lib/owner-config/index.mjs +0 -13
- package/scripts/lib/owner-config/merge.mjs +0 -52
- package/scripts/lib/owner-config/validate.mjs +0 -259
- package/scripts/lib/owner-config-loader.mjs +0 -170
- package/scripts/lib/owner-config.mjs +0 -28
- package/scripts/lib/soul-resolve.mjs +0 -130
- package/scripts/lib/vault-mirror/render.mjs +0 -8
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-end/tail-runner.mjs — the APPLY half of the Phase 3.6.x tail.
|
|
3
|
+
*
|
|
4
|
+
* `phase-skip.mjs` decides WHICH tail phases should run and is side-effect-free
|
|
5
|
+
* by contract. Until now nothing was the other half for 3.6.4: the
|
|
6
|
+
* Expired-Learnings Sweep was coordinator PROSE, so the dry-run decision was
|
|
7
|
+
* computed on every close and the apply path was never mechanically reached.
|
|
8
|
+
* Census (2026-09-09, `rg -n sweepExpiredLearnings scripts/ hooks/ skills/
|
|
9
|
+
* tests/`): the only call sites were the definition itself, the `dryRun: true`
|
|
10
|
+
* probe in `phase-skip.mjs`, the standalone CLI, and tests — no session-end
|
|
11
|
+
* caller applied anything. Measured consequence across three consumer repos:
|
|
12
|
+
* 0 sweeps ever applied, 628 learnings resident in the active stores.
|
|
13
|
+
*
|
|
14
|
+
* This module closes that gap and nothing else. It is deliberately NOT a
|
|
15
|
+
* general tail executor:
|
|
16
|
+
*
|
|
17
|
+
* - **3.6.3 Memory-Proposals** — AUQ-gated (the operator approves each
|
|
18
|
+
* proposal before it is written); `.claude/rules/ask-via-tool.md` AUQ-004
|
|
19
|
+
* puts the tool out of reach of anything but the coordinator thread.
|
|
20
|
+
* - **3.6.5 Auto-Dream** and **3.6.7 Auto-Dialectic** — both NUDGES that end
|
|
21
|
+
* in a subagent dispatch (memory-cleanup, `dialectic-deriver`); a library
|
|
22
|
+
* function cannot dispatch an agent.
|
|
23
|
+
* - **3.6.6 Skill-Judge** — needs a live LLM dispatch for the judgement.
|
|
24
|
+
* - **3.6.8 Reconcile** — the write is operator-approved per proposal (AUQ),
|
|
25
|
+
* which is the whole never-always-on firewall of Epic #693.
|
|
26
|
+
*
|
|
27
|
+
* So {@link runTailPhases} exists as the SEAM (one dispatch table, one result
|
|
28
|
+
* shape) rather than as an abstraction over six phases that will never all be
|
|
29
|
+
* mechanical. A future phase that becomes mechanical is added here; the five
|
|
30
|
+
* above stay coordinator-executed by design, not by omission.
|
|
31
|
+
*
|
|
32
|
+
* Never-throws contract, and its DIRECTION is the opposite of `phase-skip.mjs`.
|
|
33
|
+
* The planner fails OPEN (probe error → run the phase, because losing a phase
|
|
34
|
+
* silently is worse than running it needlessly). A runner that writes to disk
|
|
35
|
+
* must fail CLOSED: any error yields `{ran: false, reason: 'error'}` and the
|
|
36
|
+
* close proceeds. Session close is the one thing that must never be blocked by
|
|
37
|
+
* a best-effort maintenance sweep — a store that stays unswept for one more
|
|
38
|
+
* session costs nothing; a close that aborts loses the session record.
|
|
39
|
+
*
|
|
40
|
+
* Plain Node ESM, no external deps.
|
|
41
|
+
*
|
|
42
|
+
* @typedef {Object} SweepResult
|
|
43
|
+
* @property {boolean} ran - true only when the store was actually rewritten.
|
|
44
|
+
* @property {string} [reason] - why it did not run ('plan-skip' | 'no-plan' | 'error').
|
|
45
|
+
* @property {string} [error] - error message, `reason === 'error'` only.
|
|
46
|
+
* @property {number} [scanned] - entries read from the active store (ran only).
|
|
47
|
+
* @property {number} [archived] - entries moved to the archive sidecar (ran only).
|
|
48
|
+
* @property {string} [archivePath] - the archive sidecar written to (ran only).
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { sweepExpiredLearnings } from '../learnings/expiry-sweep.mjs';
|
|
52
|
+
import { resolveLearningsPaths } from './phase-skip.mjs';
|
|
53
|
+
|
|
54
|
+
/** Event name — a plain string literal so the events-schema census can see it. */
|
|
55
|
+
const SWEEP_EVENT = 'orchestrator.learnings.sweep_applied';
|
|
56
|
+
|
|
57
|
+
/** Payload marker for the producer, so the ledger separates it from the CLI. */
|
|
58
|
+
const SWEEP_SOURCE = 'session-end-3.6.4';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pull the decision for one phase id out of a `planTailPhases()` result.
|
|
62
|
+
*
|
|
63
|
+
* Accepts three shapes so callers need no adapter: the full
|
|
64
|
+
* `{plan, skippedReport}` envelope, a bare `PhaseDecision[]`, or a single
|
|
65
|
+
* `PhaseDecision` object. Anything else → `null` (treated as "no plan").
|
|
66
|
+
*
|
|
67
|
+
* @param {object|Array|undefined} plan
|
|
68
|
+
* @param {string} phase
|
|
69
|
+
* @returns {object|null}
|
|
70
|
+
*/
|
|
71
|
+
function findPhaseDecision(plan, phase) {
|
|
72
|
+
if (!plan) return null;
|
|
73
|
+
const list = Array.isArray(plan) ? plan : Array.isArray(plan.plan) ? plan.plan : null;
|
|
74
|
+
if (list) return list.find((d) => d && d.phase === phase) ?? null;
|
|
75
|
+
return plan.phase === phase ? plan : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Emit the one sweep record. Best-effort by construction: a telemetry failure
|
|
80
|
+
* never changes the caller's return value.
|
|
81
|
+
*
|
|
82
|
+
* The shape (lazy `events.mjs` import, explicit `repoRoot`, silent catch) is
|
|
83
|
+
* copied from `_emitEvaluated` in `scripts/lib/express-path.mjs`. Both halves
|
|
84
|
+
* are load-bearing: the lazy import keeps `platform.mjs`'s load-time filesystem
|
|
85
|
+
* walk-up out of the static graph of a module the planner imports, and the
|
|
86
|
+
* refusal to emit WITHOUT an explicit `repoRoot` prevents the `SO_PROJECT_DIR`
|
|
87
|
+
* fallback from writing a synthetic record into whatever tree the ambient env
|
|
88
|
+
* happens to resolve to (#941 — that mistake once landed a test record in the
|
|
89
|
+
* operator's real fleet ledger).
|
|
90
|
+
*
|
|
91
|
+
* @param {{repoRoot: string, scanned: number, archived: number}} ctx
|
|
92
|
+
* @returns {Promise<void>}
|
|
93
|
+
*/
|
|
94
|
+
async function emitSweepApplied({ repoRoot, scanned, archived }) {
|
|
95
|
+
if (typeof repoRoot !== 'string' || repoRoot.trim() === '') {
|
|
96
|
+
process.stderr.write(
|
|
97
|
+
`tail-runner: skipped ${SWEEP_EVENT} — no repoRoot given; ` +
|
|
98
|
+
'refusing the ambient SO_PROJECT_DIR destination (#941).\n',
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const { emitEvent, sessionAttribution } = await import('../events.mjs');
|
|
104
|
+
await emitEvent(
|
|
105
|
+
SWEEP_EVENT,
|
|
106
|
+
{ scanned, archived, source: SWEEP_SOURCE, ...sessionAttribution(repoRoot) },
|
|
107
|
+
{ repoRoot },
|
|
108
|
+
);
|
|
109
|
+
} catch {
|
|
110
|
+
// Best-effort telemetry — the sweep already happened and its result is
|
|
111
|
+
// authoritative whether or not the ledger accepted the record.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Phase 3.6.4 — apply the Expired-Learnings Sweep the planner decided on.
|
|
117
|
+
*
|
|
118
|
+
* Runs ONLY when the plan's 3.6.4 decision says `run === true`. Any other
|
|
119
|
+
* shape (skip decision, phase absent, no plan at all) returns without touching
|
|
120
|
+
* disk — the planner owns the decision, this function owns the write, and a
|
|
121
|
+
* runner that re-derives the decision would be free to disagree with it.
|
|
122
|
+
*
|
|
123
|
+
* @param {object} args
|
|
124
|
+
* @param {string} args.repoRoot Absolute repo root.
|
|
125
|
+
* @param {object|Array} [args.plan] `planTailPhases()` result, its `plan`
|
|
126
|
+
* array, or the bare 3.6.4 decision.
|
|
127
|
+
* @param {Date|number} [args.now] Injectable clock (grace-window maths).
|
|
128
|
+
* @param {number} [args.graceDays] Override the 14-day grace window.
|
|
129
|
+
* @param {boolean} [args.emit=true] Set false to suppress the ledger record.
|
|
130
|
+
* @returns {Promise<SweepResult>} Never throws.
|
|
131
|
+
*/
|
|
132
|
+
export async function runExpiredSweep({ repoRoot, plan, now, graceDays, emit = true } = {}) {
|
|
133
|
+
try {
|
|
134
|
+
const decision = findPhaseDecision(plan, '3.6.4');
|
|
135
|
+
if (!decision) return { ran: false, reason: 'no-plan' };
|
|
136
|
+
if (decision.run !== true) {
|
|
137
|
+
return { ran: false, reason: 'plan-skip', planReason: decision.reason };
|
|
138
|
+
}
|
|
139
|
+
if (typeof repoRoot !== 'string' || repoRoot.trim() === '') {
|
|
140
|
+
return { ran: false, reason: 'error', error: 'runExpiredSweep: repoRoot is required' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const { filePath, archivePath } = resolveLearningsPaths(repoRoot);
|
|
144
|
+
const res = await sweepExpiredLearnings({
|
|
145
|
+
filePath,
|
|
146
|
+
archivePath,
|
|
147
|
+
now,
|
|
148
|
+
dryRun: false,
|
|
149
|
+
...(graceDays === undefined ? {} : { graceDays }),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const scanned = res?.scanned ?? 0;
|
|
153
|
+
const archived = res?.archived ?? 0;
|
|
154
|
+
if (emit) await emitSweepApplied({ repoRoot, scanned, archived });
|
|
155
|
+
|
|
156
|
+
return { ran: true, scanned, archived, archivePath: res?.archivePath ?? archivePath };
|
|
157
|
+
} catch (err) {
|
|
158
|
+
// Fail CLOSED (see the module header): a maintenance sweep must never be
|
|
159
|
+
// able to block a session close.
|
|
160
|
+
return { ran: false, reason: 'error', error: err && err.message ? err.message : String(err) };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Run every MECHANICAL phase of the session-end tail.
|
|
166
|
+
*
|
|
167
|
+
* Today that is exactly one — 3.6.4. See the module header for why 3.6.3 and
|
|
168
|
+
* 3.6.5–3.6.8 stay coordinator-executed (AUQ-gated per
|
|
169
|
+
* `.claude/rules/ask-via-tool.md` AUQ-004, or requiring a subagent dispatch a
|
|
170
|
+
* library function cannot make). The keyed return shape is the seam: a caller
|
|
171
|
+
* reads `result['3.6.4']` today and keeps compiling when a second phase lands.
|
|
172
|
+
*
|
|
173
|
+
* @param {object} args — forwarded verbatim to {@link runExpiredSweep}.
|
|
174
|
+
* @returns {Promise<Record<string, SweepResult>>} Never throws.
|
|
175
|
+
*/
|
|
176
|
+
export async function runTailPhases({ repoRoot, plan, now, graceDays, emit } = {}) {
|
|
177
|
+
return { '3.6.4': await runExpiredSweep({ repoRoot, plan, now, graceDays, emit }) };
|
|
178
|
+
}
|
|
@@ -80,8 +80,8 @@ function readLockIds(repoRoot) {
|
|
|
80
80
|
* handled right now (`session_id` / `sessionId`, plus `parent_session_id`
|
|
81
81
|
* for a sub-agent invocation, whose coordinator is equally us). The only
|
|
82
82
|
* tier that is per-INVOCATION rather than per-working-copy.
|
|
83
|
-
* 2.
|
|
84
|
-
*
|
|
83
|
+
* 2. The native process-scoped id (`CLAUDE_CODE_SESSION_ID` or
|
|
84
|
+
* `CODEX_THREAD_ID`), selected by {@link readProcessLocalSessionIds}.
|
|
85
85
|
* 3. `session.lock` `session_id` / `semantic_session_id` — repo-GLOBAL, and
|
|
86
86
|
* the identity the WRITER of a manifest uses: `wave-scope.json`'s
|
|
87
87
|
* `session` field comes from `sessionAttribution()`, which reads this same
|
|
@@ -138,20 +138,12 @@ function readLockIds(repoRoot) {
|
|
|
138
138
|
* never as a mismatch.
|
|
139
139
|
*/
|
|
140
140
|
export function readOwnSessionIds(repoRoot, { hookInput = null } = {}) {
|
|
141
|
-
const ids = new Set();
|
|
141
|
+
const ids = new Set(readProcessLocalSessionIds({ hookInput }));
|
|
142
142
|
const add = (value) => {
|
|
143
143
|
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
144
144
|
if (trimmed) ids.add(trimmed);
|
|
145
145
|
};
|
|
146
146
|
|
|
147
|
-
// Source 1 — the harness's statement about THIS invocation.
|
|
148
|
-
if (hookInput && typeof hookInput === 'object') {
|
|
149
|
-
for (const key of ['session_id', 'sessionId', 'parent_session_id']) add(hookInput[key]);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Source 2 — process-scoped env var.
|
|
153
|
-
add(process.env.CLAUDE_CODE_SESSION_ID);
|
|
154
|
-
|
|
155
147
|
// Source 3 — repo-global lock file (the manifest writer's own identity).
|
|
156
148
|
try {
|
|
157
149
|
const lock = readLockIds(repoRoot);
|
|
@@ -190,6 +182,14 @@ export function readOwnSessionIds(repoRoot, { hookInput = null } = {}) {
|
|
|
190
182
|
* rather than unioning is the fix — a better signal REPLACES a worse one
|
|
191
183
|
* (`.claude/rules/host-resources.md` § HR-102).
|
|
192
184
|
*
|
|
185
|
+
* Native env identity (#1274): an explicit SO_PLATFORM selects ONLY its own
|
|
186
|
+
* variable (claude → CLAUDE_CODE_SESSION_ID, codex → CODEX_THREAD_ID). Cursor
|
|
187
|
+
* and Pi have no native env source here. Without a valid explicit selection,
|
|
188
|
+
* a single non-empty native id, or two equal ids, is usable; conflicting ids
|
|
189
|
+
* are ambiguous and contribute nothing. This prevents an inherited parent
|
|
190
|
+
* harness's id from confirming a peer lock. Hook invocation/parent ids remain
|
|
191
|
+
* valid independently. No filesystem/platform detection is an identity proof.
|
|
192
|
+
*
|
|
193
193
|
* Never throws.
|
|
194
194
|
*
|
|
195
195
|
* @param {{ env?: object, hookInput?: object|null }} [opts]
|
|
@@ -211,8 +211,19 @@ export function readProcessLocalSessionIds({ env = process.env, hookInput = null
|
|
|
211
211
|
if (hookInput && typeof hookInput === 'object') {
|
|
212
212
|
for (const key of ['session_id', 'sessionId', 'parent_session_id']) add(hookInput[key]);
|
|
213
213
|
}
|
|
214
|
-
// Tier 2 — process
|
|
215
|
-
|
|
214
|
+
// Tier 2 — select a native process id without importing the platform module
|
|
215
|
+
// into this hook-safe leaf or using shared repository markers as evidence.
|
|
216
|
+
const trim = (value) => typeof value === 'string' ? value.trim() : '';
|
|
217
|
+
const platform = trim(env?.SO_PLATFORM);
|
|
218
|
+
const claudeId = trim(env?.CLAUDE_CODE_SESSION_ID);
|
|
219
|
+
const codexId = trim(env?.CODEX_THREAD_ID);
|
|
220
|
+
if (platform === 'claude') add(claudeId);
|
|
221
|
+
else if (platform === 'codex') add(codexId);
|
|
222
|
+
else if (platform !== 'cursor' && platform !== 'pi') {
|
|
223
|
+
// Invalid/absent SO_PLATFORM is not an explicit choice (platform.mjs uses
|
|
224
|
+
// the same trimmed allowlist). Disagreement must never become a union.
|
|
225
|
+
if (!claudeId || !codexId || claudeId === codexId) add(claudeId || codexId);
|
|
226
|
+
}
|
|
216
227
|
|
|
217
228
|
return ids;
|
|
218
229
|
}
|
|
@@ -161,6 +161,12 @@ export const OPTIONAL_FIELDS = Object.freeze([
|
|
|
161
161
|
'total_token_input',
|
|
162
162
|
'total_token_output',
|
|
163
163
|
'subagents_with_tokens',
|
|
164
|
+
// #1244 — cache buckets + cost estimate (additive, optional).
|
|
165
|
+
'total_token_input_uncached',
|
|
166
|
+
'total_token_cache_read',
|
|
167
|
+
'total_token_cache_creation',
|
|
168
|
+
'total_cost_usd',
|
|
169
|
+
'_token_schema',
|
|
164
170
|
// Epic #724 C1 — SessionEnd close-through backfill provenance fields.
|
|
165
171
|
// `status` distinguishes normally-closed ('completed') from hook-backfilled
|
|
166
172
|
// ('abandoned') records. The `_backfill_*` markers record how a stub was
|
|
@@ -384,6 +384,26 @@ function _validateOptionalFields(entry) {
|
|
|
384
384
|
);
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
|
+
// #1244 — cache buckets, cost estimate and the token-contract marker.
|
|
388
|
+
// Same non-negative-finite-or-null contract as the two totals above;
|
|
389
|
+
// total_cost_usd is fractional and null means "unknown model", never 0.
|
|
390
|
+
for (const field of [
|
|
391
|
+
'total_token_input_uncached',
|
|
392
|
+
'total_token_cache_read',
|
|
393
|
+
'total_token_cache_creation',
|
|
394
|
+
'total_cost_usd',
|
|
395
|
+
'_token_schema',
|
|
396
|
+
]) {
|
|
397
|
+
const value = entry[field];
|
|
398
|
+
if (value !== undefined && value !== null) {
|
|
399
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
400
|
+
throw new ValidationError(
|
|
401
|
+
`${field} must be a non-negative finite number or null, got: ${value}`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
387
407
|
// subagents_with_tokens: non-negative integer (never null — it is always a count, defaulting to 0).
|
|
388
408
|
if (entry.subagents_with_tokens !== undefined && entry.subagents_with_tokens !== null) {
|
|
389
409
|
if (
|