session-orchestrator 4.2.0 → 5.0.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-start/SKILL.md +1 -1
- package/.agents/skills/ux-grill/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-start/SKILL.md +1 -1
- package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
- package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
- package/.cursor/commands/ux-grill.md +14 -0
- package/.cursor/skills/session-start/SKILL.md +1 -1
- package/.cursor/skills/ux-grill/SKILL.md +13 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +94 -1
- package/README.md +98 -86
- package/agents/dialectic-deriver.md +11 -0
- package/commands/ux-grill.md +51 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/codex-setup.md +8 -0
- package/docs/components.md +7 -7
- package/docs/events-schema.md +9 -5
- package/docs/github-mirror-protection.md +61 -20
- package/docs/migration-v5.md +62 -0
- package/docs/scope-collision-guard.md +14 -0
- package/hooks/_lib/hook-import-set.json +44 -2
- package/hooks/_lib/lock-bootstrap.mjs +84 -1
- package/hooks/_lib/vcs-create-matcher.mjs +190 -3
- package/hooks/enforce-scope.mjs +201 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks.json +7 -2
- package/hooks/on-session-start.mjs +171 -49
- package/hooks/post-bash-issue-budget-refund.mjs +375 -0
- package/hooks/pre-auq-clarity.mjs +70 -18
- package/hooks/pre-bash-issue-budget.mjs +51 -4
- package/package.json +2 -1
- package/pi/prompts/ux-grill.md +12 -0
- package/scripts/ci/assert-vitest-green.mjs +4 -2
- package/scripts/dialectic-deriver.mjs +32 -8
- package/scripts/emit-session.mjs +72 -1
- package/scripts/lib/agent-status.mjs +441 -9
- package/scripts/lib/auq/schema.mjs +10 -3
- package/scripts/lib/ci-status-banner.mjs +29 -6
- package/scripts/lib/claude-md-budget-lint.mjs +52 -2
- package/scripts/lib/config.mjs +12 -1
- package/scripts/lib/eval/engine.mjs +7 -1
- package/scripts/lib/file-lock.mjs +114 -13
- package/scripts/lib/git-porcelain.mjs +113 -0
- package/scripts/lib/instruction-budget-guard.mjs +415 -47
- package/scripts/lib/io.mjs +29 -4
- package/scripts/lib/issue-budget.mjs +336 -6
- package/scripts/lib/learnings/sizing-subject.mjs +44 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
- package/scripts/lib/locks/state-md-lock.mjs +19 -41
- package/scripts/lib/maintenance-due-banner.mjs +11 -1
- package/scripts/lib/peer-cards/merger.mjs +143 -0
- package/scripts/lib/pre-dispatch-check.mjs +20 -14
- package/scripts/lib/project-hygiene.mjs +81 -30
- package/scripts/lib/quality-gate.mjs +14 -65
- package/scripts/lib/reconcile/engine.mjs +19 -1
- package/scripts/lib/reconcile/writer.mjs +278 -11
- package/scripts/lib/scope-echo.mjs +346 -0
- package/scripts/lib/session-lock.mjs +62 -2
- package/scripts/lib/session-record-repair.mjs +91 -0
- package/scripts/lib/session-schema/filters.mjs +26 -1
- package/scripts/lib/session-start-probes.mjs +419 -53
- package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
- package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
- package/scripts/lib/tmux-layout/layouts.mjs +62 -4
- package/scripts/lib/ux-grill/collect.mjs +1163 -0
- package/scripts/lib/ux-grill/compare.mjs +285 -0
- package/scripts/lib/ux-grill/manifest.mjs +618 -0
- package/scripts/lib/ux-grill/measures.mjs +431 -0
- package/scripts/lib/ux-grill/paths.mjs +224 -0
- package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
- package/scripts/lib/ux-grill/reconcile.mjs +344 -0
- package/scripts/lib/ux-grill/run-record.mjs +316 -0
- package/scripts/lib/ux-grill/schema.mjs +321 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
- package/scripts/lib/validate/check-unwired-features.mjs +48 -20
- package/scripts/lib/vault-status/board-lock.mjs +18 -0
- package/scripts/lib/vault-status/board-writer.mjs +8 -0
- package/scripts/mcp-server.sh +16 -1
- package/scripts/release.mjs +7 -2
- package/skills/bootstrap/SKILL.md +12 -209
- package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
- package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -2
- package/skills/claude-md-drift-check/checker.mjs +213 -21
- package/skills/discovery/SKILL.md +6 -173
- package/skills/discovery/probes/vault-staleness.mjs +35 -5
- package/skills/discovery/probes-docs.md +8 -4
- package/skills/discovery/probes-supply-chain.md +4 -2
- package/skills/discovery/probes-ui.md +7 -3
- package/skills/discovery/probes-vault.md +12 -4
- package/skills/discovery/references/discovery-interactive-triage.md +139 -0
- package/skills/discovery/references/discovery-triage-state.md +54 -0
- package/skills/eval/rubric-v1.md +13 -0
- package/skills/evolve/SKILL.md +2 -458
- package/skills/evolve/references/evolve-analyze-mode.md +360 -0
- package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
- package/skills/plan/mode-retro.md +4 -3
- package/skills/reconcile/SKILL.md +10 -0
- package/skills/session-end/drift-operations.md +20 -5
- package/skills/session-end/metrics-collection.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +4 -2
- package/skills/session-end/references/phase-2-quality-gate.md +3 -3
- package/skills/session-end/references/phase-5-issue-cleanup.md +6 -1
- package/skills/session-end/session-metrics-write.md +2 -0
- package/skills/session-plan/SKILL.md +2 -144
- package/skills/session-plan/references/session-plan-task-classification.md +152 -0
- package/skills/session-start/SKILL.md +24 -6
- package/skills/session-start/references/operations-contract.md +114 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +22 -20
- package/skills/session-start/soul.md +2 -2
- package/skills/test-runner/SKILL.md +1 -1
- package/skills/tmux-layout/SKILL.md +3 -1
- package/skills/ux-grill/SKILL.md +211 -0
- package/skills/ux-grill/rubric-v2.md +201 -0
- package/skills/ux-grill/soul.md +76 -0
- package/skills/wave-executor/SKILL.md +3 -128
- package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
- package/skills/wave-executor/references/wave-loop-review.md +18 -5
- package/templates/_shared/ux-manifest.template.md +149 -0
- package/templates/_shared/journey-manifest.md +0 -114
|
@@ -492,6 +492,11 @@ export function readLockDetailed(opts = {}) {
|
|
|
492
492
|
* recorded pids dead, INCLUDING the currently heartbeating session's
|
|
493
493
|
* own lock), so `stale-pid-alive` was unreachable same-host and every
|
|
494
494
|
* stale lock rendered as "confirmed dead" in the recovery AUQ.
|
|
495
|
+
* { ok: false, reason: 'missing-session-id' }
|
|
496
|
+
* — no usable `sessionId` given; NOTHING was written. Same reason string
|
|
497
|
+
* and same predicate as forceAcquire() (see its docblock for why an
|
|
498
|
+
* ownerless lock is worse than no lock). No `exclusivityClass`: the
|
|
499
|
+
* call never reached classification.
|
|
495
500
|
* { ok: false, reason: 'fs-error', error, exclusivityClass? }
|
|
496
501
|
* — filesystem failure
|
|
497
502
|
* { ok: false, reason: 'active-incompatible-exclusive', allActiveSessions, blockingSession, exclusivityClass }
|
|
@@ -509,6 +514,28 @@ export function readLockDetailed(opts = {}) {
|
|
|
509
514
|
* obtaining user consent.
|
|
510
515
|
*/
|
|
511
516
|
export function acquire({ sessionId, mode, ttlHours = DEFAULT_TTL_HOURS, repoRoot, activeSessions, semanticSessionId, quiet = false } = {}) {
|
|
517
|
+
// -------------------------------------------------------------------------
|
|
518
|
+
// Ownerless-lock guard. Measured 2026-09-11 in a tmp repoRoot:
|
|
519
|
+
// acquire({sessionId: undefined}) -> {ok:true}, lockfile written with NO
|
|
520
|
+
// `session_id` key at all; acquire({sessionId: ''}) -> {ok:true}, lockfile
|
|
521
|
+
// written with `"session_id": ""`. Both are the exact "present but owned by
|
|
522
|
+
// nobody" state forceAcquire()'s docblock below describes: every reader
|
|
523
|
+
// compares such a lock against its own id, finds no match, classifies it
|
|
524
|
+
// FOREIGN — and foreign is the classification that SKIPS enforcement.
|
|
525
|
+
// The guard lived only on forceAcquire(), the rare path (explicit
|
|
526
|
+
// stale-lock takeover); acquire() is the primary one. The production
|
|
527
|
+
// caller hooks/_lib/lock-bootstrap.mjs gates empty ids upstream, but the
|
|
528
|
+
// prose path skills/session-start/references/phase-1-2-session-lock.md
|
|
529
|
+
// calls acquire() straight from the coordinator LLM with no gate at all.
|
|
530
|
+
// Placed FIRST, before classifyMode and before any fs work: nothing is
|
|
531
|
+
// written. `exclusivityClass` is deliberately absent from this shape — the
|
|
532
|
+
// caller's class is a property of a call that got far enough to be
|
|
533
|
+
// classified, and this one did not.
|
|
534
|
+
// -------------------------------------------------------------------------
|
|
535
|
+
if (!hasUsableSessionId(sessionId)) {
|
|
536
|
+
return { ok: false, reason: 'missing-session-id' };
|
|
537
|
+
}
|
|
538
|
+
|
|
512
539
|
const lockFile = lockPathFor(repoRoot);
|
|
513
540
|
|
|
514
541
|
// -------------------------------------------------------------------------
|
|
@@ -671,12 +698,45 @@ export function acquire({ sessionId, mode, ttlHours = DEFAULT_TTL_HOURS, repoRoo
|
|
|
671
698
|
* Call only after the user has explicitly authorised stale-lock takeover.
|
|
672
699
|
*
|
|
673
700
|
* Returns:
|
|
674
|
-
* { ok: true, lock, replacedLock? }
|
|
675
|
-
* { ok: false, reason: '
|
|
701
|
+
* { ok: true, lock, replacedLock? } — lock written (replacedLock present if one was overwritten)
|
|
702
|
+
* { ok: false, reason: 'missing-session-id' } — no usable `sessionId` given; NOTHING was written
|
|
703
|
+
* { ok: false, reason: 'fs-error', ... } — filesystem failure
|
|
704
|
+
*
|
|
705
|
+
* The `missing-session-id` guard is not cosmetic. `buildLock` assigns
|
|
706
|
+
* `session_id: sessionId` verbatim, and `JSON.stringify` DROPS an `undefined`
|
|
707
|
+
* value — so a `forceAcquire()` without a sessionId used to write a lock file
|
|
708
|
+
* carrying no `session_id` key at all. Such a lock is present but owned by
|
|
709
|
+
* nobody: every reader compares it against its own id, finds no match, and
|
|
710
|
+
* classifies it as FOREIGN — and foreign is precisely the classification that
|
|
711
|
+
* skips enforcement. The same failure class is why the wave scope manifest
|
|
712
|
+
* forbids `"session_id": ""` and requires the key be omitted instead.
|
|
713
|
+
*
|
|
714
|
+
* The result SHAPE (not a throw) is deliberate and matches the callers: the
|
|
715
|
+
* only production call site, `hooks/_lib/lock-bootstrap.mjs`, branches on
|
|
716
|
+
* `acquireResult.ok !== true` and bails non-blocking; a throw there would be
|
|
717
|
+
* swallowed by its `catch` and reported as the same `null`, i.e. it would add
|
|
718
|
+
* no information while breaking the direct `forceAcquire()` calls documented in
|
|
719
|
+
* `skills/session-start/references/phase-1-2-session-lock.md`.
|
|
676
720
|
*
|
|
677
721
|
* @param {{ sessionId: string, mode: string, ttlHours?: number, repoRoot?: string, semanticSessionId?: string }} args
|
|
678
722
|
*/
|
|
723
|
+
/**
|
|
724
|
+
* A `sessionId` is usable only when it is a non-blank string. ONE predicate,
|
|
725
|
+
* shared by acquire() and forceAcquire(), so the two entry points cannot drift
|
|
726
|
+
* apart again — they did: the guard existed on forceAcquire() alone until
|
|
727
|
+
* 2026-09-11, leaving the primary path able to write an ownerless lock.
|
|
728
|
+
*
|
|
729
|
+
* @param {unknown} sessionId
|
|
730
|
+
* @returns {boolean}
|
|
731
|
+
*/
|
|
732
|
+
function hasUsableSessionId(sessionId) {
|
|
733
|
+
return typeof sessionId === 'string' && sessionId.trim().length > 0;
|
|
734
|
+
}
|
|
735
|
+
|
|
679
736
|
export function forceAcquire({ sessionId, mode, ttlHours = DEFAULT_TTL_HOURS, repoRoot, semanticSessionId } = {}) {
|
|
737
|
+
if (!hasUsableSessionId(sessionId)) {
|
|
738
|
+
return { ok: false, reason: 'missing-session-id' };
|
|
739
|
+
}
|
|
680
740
|
try {
|
|
681
741
|
const replacedLock = readLock({ repoRoot });
|
|
682
742
|
const lock = buildLock({ sessionId, mode, ttlHours, semanticSessionId });
|
|
@@ -42,6 +42,14 @@
|
|
|
42
42
|
* had defaulted, which is what lets a downstream consumer tell a measured zero
|
|
43
43
|
* apart from a repaired-to-zero.
|
|
44
44
|
*
|
|
45
|
+
* It also never DISCARDS what it replaces: any field whose present value is
|
|
46
|
+
* overwritten by a default is first copied to a `_<field>_raw` sidecar
|
|
47
|
+
* (`preserveRaw`, same convention and same non-clobber guard as
|
|
48
|
+
* `session-schema/normalizer.mjs`'s `_express_path_detail`). Without it the
|
|
49
|
+
* repair was itself a data-loss event — 7 of 40 records in the
|
|
50
|
+
* `projects-baseline` ledger (S119, 2026-09-10) lost narrative `agent_summary`
|
|
51
|
+
* strings and `total_files_changed` path lists to `{...0}` / `0`.
|
|
52
|
+
*
|
|
45
53
|
* Plain Node ESM. Named exports. DI-friendly via `deps`.
|
|
46
54
|
*
|
|
47
55
|
* Cross-references:
|
|
@@ -112,6 +120,70 @@ function orderIncompleteFields(fields) {
|
|
|
112
120
|
return [...fields].sort((a, b) => rank(a) - rank(b) || (a < b ? -1 : a > b ? 1 : 0));
|
|
113
121
|
}
|
|
114
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Preserve the pre-repair value of `key` under a `_<key>_raw` sidecar BEFORE a
|
|
125
|
+
* default overwrites it.
|
|
126
|
+
*
|
|
127
|
+
* Convention lifted from `session-schema/normalizer.mjs`
|
|
128
|
+
* (`_express_path_detail`, `if (!('_express_path_detail' in next))`) — but its
|
|
129
|
+
* non-clobber guard is keyed on WHO WROTE the sidecar, not on its mere presence.
|
|
130
|
+
* That distinction is the fix for a measured data-loss bug (2026-09-11): the
|
|
131
|
+
* presence guard read `out`, and `out = { ...record }` carries the INPUT's key
|
|
132
|
+
* space, so a record arriving with its own `_agent_summary_raw` kept that
|
|
133
|
+
* unverifiable value and dropped the real `agent_summary` the repair was about
|
|
134
|
+
* to overwrite:
|
|
135
|
+
*
|
|
136
|
+
* in: agent_summary: 'ECHTE NARRATIVE ZUSAMMENFASSUNG',
|
|
137
|
+
* _agent_summary_raw: 'ANGREIFER-WERT'
|
|
138
|
+
* out: _agent_summary_raw = 'ANGREIFER-WERT' ← the real value was gone
|
|
139
|
+
*
|
|
140
|
+
* That is the mirror image of the promise this module's docblock makes, in the
|
|
141
|
+
* one case where the promise matters. So: `rescued` — a per-`repairRecord()`-call
|
|
142
|
+
* Set of sidecar keys THIS pass has written — replaces `sidecar in out`. A value
|
|
143
|
+
* that is provably real RIGHT NOW always outranks a sidecar of unknown origin.
|
|
144
|
+
*
|
|
145
|
+
* The multi-pass concern the old comment named is not what kept the sidecar
|
|
146
|
+
* safe, and was measured unreachable: after run 1 the field holds a VALID
|
|
147
|
+
* default, so run 2 records no defect for it and never calls preserveRaw at all
|
|
148
|
+
* (measured: `repairRecord(repairRecord(x).record).changed === false`, defects
|
|
149
|
+
* `[]`, sidecar intact). Within ONE pass the Set still gives first-write-wins,
|
|
150
|
+
* which is what the two branches per field (waves, agent_summary) rely on.
|
|
151
|
+
*
|
|
152
|
+
* NAMED CEILING (BV-004): if a record is RE-CORRUPTED with a different real
|
|
153
|
+
* value for the same key BETWEEN two repair runs, run 2's original wins and run
|
|
154
|
+
* 1's rescue is dropped. Reaching it requires corruption after a repair; the
|
|
155
|
+
* alternative — trusting an input-supplied sidecar over a value that is
|
|
156
|
+
* demonstrably real — is the bug above. Revisit if a ledger is ever repaired
|
|
157
|
+
* in a loop that can re-break the same key.
|
|
158
|
+
*
|
|
159
|
+
* Motivation is measured, not hypothetical: 7 of 40 repaired records in the
|
|
160
|
+
* `projects-baseline` ledger (S119, 2026-09-10) carried narrative strings in
|
|
161
|
+
* `agent_summary` and path lists in `total_files_changed`; both were replaced
|
|
162
|
+
* by `{...0}` / `0` and had to be restored by hand. This module's own docblock
|
|
163
|
+
* promises "nothing is invented" — discarding an unreadable original is the
|
|
164
|
+
* mirror-image violation of that promise.
|
|
165
|
+
*
|
|
166
|
+
* `undefined` and `null` are NOT preserved: an absent field has nothing to
|
|
167
|
+
* lose (the defect classes are named `*_absent` / `*_missing` for exactly that
|
|
168
|
+
* reason), and `JSON.stringify` drops an `undefined` sidecar anyway.
|
|
169
|
+
*
|
|
170
|
+
* The sidecars are schema-safe: `validateSession` has no unknown-key rejection
|
|
171
|
+
* (see `validator.mjs` — every `_validate*` helper checks named fields only),
|
|
172
|
+
* and `serializeSessionLineChecked` round-trips extra keys untouched.
|
|
173
|
+
*
|
|
174
|
+
* @param {Record<string, any>} out — the mutable repaired copy
|
|
175
|
+
* @param {string} key — the field about to be defaulted
|
|
176
|
+
* @param {any} rawValue — its ORIGINAL value (read from the untouched input)
|
|
177
|
+
* @param {Set<string>} rescued — sidecar keys already written by THIS call
|
|
178
|
+
*/
|
|
179
|
+
function preserveRaw(out, key, rawValue, rescued) {
|
|
180
|
+
if (rawValue === undefined || rawValue === null) return;
|
|
181
|
+
const sidecar = `_${key}_raw`;
|
|
182
|
+
if (rescued.has(sidecar)) return;
|
|
183
|
+
out[sidecar] = rawValue;
|
|
184
|
+
rescued.add(sidecar);
|
|
185
|
+
}
|
|
186
|
+
|
|
115
187
|
/** Compact ISO stamp for backup filenames: `20260805T091500Z`. */
|
|
116
188
|
export function backupStamp(date = new Date()) {
|
|
117
189
|
return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
|
@@ -143,6 +215,9 @@ export function repairRecord(record) {
|
|
|
143
215
|
const out = { ...record };
|
|
144
216
|
const defects = [];
|
|
145
217
|
const incomplete = new Set();
|
|
218
|
+
// Sidecar keys written by THIS pass — the provenance the non-clobber guard
|
|
219
|
+
// needs. `out`'s own key space cannot serve: it is the INPUT's. See preserveRaw.
|
|
220
|
+
const rescued = new Set();
|
|
146
221
|
|
|
147
222
|
// -- waves ----------------------------------------------------------------
|
|
148
223
|
// A NUMBER here is not garbage: it IS the wave count, written by an older
|
|
@@ -158,6 +233,7 @@ export function repairRecord(record) {
|
|
|
158
233
|
} else {
|
|
159
234
|
defects.push('waves_not_array');
|
|
160
235
|
}
|
|
236
|
+
preserveRaw(out, 'waves', record.waves, rescued);
|
|
161
237
|
out.waves = [];
|
|
162
238
|
incomplete.add('waves');
|
|
163
239
|
}
|
|
@@ -173,6 +249,11 @@ export function repairRecord(record) {
|
|
|
173
249
|
if (out.waves.length > 0 && out.waves.every(isPlainObject)) {
|
|
174
250
|
const needsRenumber = out.waves.some((w) => !isCount(w.wave) || w.wave < 1);
|
|
175
251
|
if (needsRenumber) {
|
|
252
|
+
// The ORIGINAL ordinals are not recoverable from the renumbered output
|
|
253
|
+
// (the input may mix valid, absent and 0-based `wave` values), so the
|
|
254
|
+
// pre-renumber array is preserved whole — same sidecar as the
|
|
255
|
+
// not-an-array branch above, which cannot have fired on this path.
|
|
256
|
+
preserveRaw(out, 'waves', record.waves, rescued);
|
|
176
257
|
out.waves = out.waves.map((w, i) => ({ ...w, wave: i + 1 }));
|
|
177
258
|
defects.push('wave_index_invalid');
|
|
178
259
|
incomplete.add('waves[].wave');
|
|
@@ -181,6 +262,7 @@ export function repairRecord(record) {
|
|
|
181
262
|
|
|
182
263
|
// -- total_waves ----------------------------------------------------------
|
|
183
264
|
if (!isCount(out.total_waves)) {
|
|
265
|
+
preserveRaw(out, 'total_waves', record.total_waves, rescued);
|
|
184
266
|
out.total_waves = wavesNumber !== null ? wavesNumber : out.waves.length;
|
|
185
267
|
defects.push('total_waves_missing');
|
|
186
268
|
incomplete.add('total_waves');
|
|
@@ -188,12 +270,18 @@ export function repairRecord(record) {
|
|
|
188
270
|
|
|
189
271
|
// -- agent_summary --------------------------------------------------------
|
|
190
272
|
if (!isPlainObject(out.agent_summary)) {
|
|
273
|
+
preserveRaw(out, 'agent_summary', record.agent_summary, rescued);
|
|
191
274
|
out.agent_summary = { complete: 0, partial: 0, failed: 0, spiral: 0 };
|
|
192
275
|
defects.push('agent_summary_absent');
|
|
193
276
|
incomplete.add('agent_summary');
|
|
194
277
|
} else {
|
|
195
278
|
const missing = AGENT_SUMMARY_FIELDS.filter((f) => !isCount(out.agent_summary[f]));
|
|
196
279
|
if (missing.length > 0) {
|
|
280
|
+
// Preserve the WHOLE original summary object rather than one sidecar per
|
|
281
|
+
// defaulted counter: it carries every original field value at once and
|
|
282
|
+
// keeps the sidecar namespace flat (`_agent_summary.spiral_raw` would be
|
|
283
|
+
// a second, uglier convention for the same job).
|
|
284
|
+
preserveRaw(out, 'agent_summary', record.agent_summary, rescued);
|
|
197
285
|
out.agent_summary = { ...out.agent_summary };
|
|
198
286
|
for (const f of missing) {
|
|
199
287
|
out.agent_summary[f] = 0;
|
|
@@ -209,6 +297,7 @@ export function repairRecord(record) {
|
|
|
209
297
|
|
|
210
298
|
// -- total_agents ---------------------------------------------------------
|
|
211
299
|
if (!isCount(out.total_agents)) {
|
|
300
|
+
preserveRaw(out, 'total_agents', record.total_agents, rescued);
|
|
212
301
|
// Prefer the record's own evidence: an agent_summary PRESENT in the
|
|
213
302
|
// original sums to the real agent count (live line 71 sums to 30 where
|
|
214
303
|
// waves.length is 5 — W2/A4 review finding). Fall back to waves.length
|
|
@@ -226,6 +315,7 @@ export function repairRecord(record) {
|
|
|
226
315
|
|
|
227
316
|
// -- total_files_changed --------------------------------------------------
|
|
228
317
|
if (!isCount(out.total_files_changed)) {
|
|
318
|
+
preserveRaw(out, 'total_files_changed', record.total_files_changed, rescued);
|
|
229
319
|
out.total_files_changed = 0;
|
|
230
320
|
defects.push('total_files_changed_missing');
|
|
231
321
|
incomplete.add('total_files_changed');
|
|
@@ -239,6 +329,7 @@ export function repairRecord(record) {
|
|
|
239
329
|
// i.e. "unknown", and flagged as such. NEVER the next record's timestamp:
|
|
240
330
|
// the ledger is not chronologically ordered (see the module docblock).
|
|
241
331
|
if (typeof out.completed_at !== 'string' && typeof out.started_at === 'string') {
|
|
332
|
+
preserveRaw(out, 'completed_at', record.completed_at, rescued);
|
|
242
333
|
const endedMs = typeof out.ended_at === 'string' ? Date.parse(out.ended_at) : NaN;
|
|
243
334
|
const startedMs = Date.parse(out.started_at);
|
|
244
335
|
out.completed_at =
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
* schema migrations and dedup checks all legitimately need the phantoms. Use
|
|
34
34
|
* these helpers only where the window is meant to represent REAL WORK.
|
|
35
35
|
*
|
|
36
|
-
* Exports: isRealSession, filterRealSessions, tailRealSessions
|
|
36
|
+
* Exports: isRealSession, filterRealSessions, tailRealSessions,
|
|
37
|
+
* isCoordinatorDirectHousekeeping
|
|
37
38
|
*/
|
|
38
39
|
|
|
39
40
|
// ---------------------------------------------------------------------------
|
|
@@ -86,3 +87,27 @@ export function tailRealSessions(records, n) {
|
|
|
86
87
|
if (typeof n !== 'number' || !Number.isFinite(n) || n <= 0) return [];
|
|
87
88
|
return real.slice(-Math.floor(n));
|
|
88
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* True when a record's waves are ALL coordinator-direct `Housekeeping` waves —
|
|
93
|
+
* the shape the session-end writer rule (#1321) records for a session that ran
|
|
94
|
+
* no dispatched waves. The single definition of that shape: consumers treat such
|
|
95
|
+
* a record as "no waves ran" (eval gate-health) and exclude its 0-agent waves
|
|
96
|
+
* from agents-per-wave ratios.
|
|
97
|
+
*
|
|
98
|
+
* Keys on the wave SHAPE only, never on `session_type`: a housekeeping session
|
|
99
|
+
* that ran real waves is not this shape. Nor on `coordinator_direct` alone —
|
|
100
|
+
* whole multi-wave feature sessions carry it too.
|
|
101
|
+
*
|
|
102
|
+
* @param {unknown} record — a parsed sessions.jsonl entry
|
|
103
|
+
* @returns {boolean} — false for empty/absent `waves`
|
|
104
|
+
*/
|
|
105
|
+
export function isCoordinatorDirectHousekeeping(record) {
|
|
106
|
+
if (record === null || typeof record !== 'object' || Array.isArray(record)) return false;
|
|
107
|
+
const { waves } = record;
|
|
108
|
+
return (
|
|
109
|
+
Array.isArray(waves) &&
|
|
110
|
+
waves.length > 0 &&
|
|
111
|
+
waves.every((w) => w?.role === 'Housekeeping' && w?.coordinator_direct === true)
|
|
112
|
+
);
|
|
113
|
+
}
|