mover-os 4.7.8 → 4.7.9
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/README.md +34 -24
- package/install.js +2197 -200
- package/package.json +1 -1
- package/src/dashboard/build.js +41 -3
- package/src/dashboard/lib/active-context-parser.js +16 -4
- package/src/dashboard/lib/agent-session.js +70 -49
- package/src/dashboard/lib/approval-registry.js +170 -0
- package/src/dashboard/lib/daily-note-resolver.js +20 -3
- package/src/dashboard/lib/date-utils.js +9 -1
- package/src/dashboard/lib/distribution-parser.js +69 -10
- package/src/dashboard/lib/drift-history.js +6 -1
- package/src/dashboard/lib/engine-default-fingerprints.json +53 -0
- package/src/dashboard/lib/engine-health.js +4 -1
- package/src/dashboard/lib/engine-writer.js +157 -11
- package/src/dashboard/lib/goal-forecast.js +154 -31
- package/src/dashboard/lib/library-indexer-v2.js +14 -0
- package/src/dashboard/lib/library-search.js +290 -0
- package/src/dashboard/lib/log-activation.sh +0 -0
- package/src/dashboard/lib/memory-index.js +61 -12
- package/src/dashboard/lib/pid-markers.js +80 -0
- package/src/dashboard/lib/regenerate-manifest.js +0 -0
- package/src/dashboard/lib/run-registry.js +75 -15
- package/src/dashboard/lib/state-core/backfill.js +298 -0
- package/src/dashboard/lib/state-core/dryrun.js +188 -0
- package/src/dashboard/lib/state-core/event-log.js +615 -0
- package/src/dashboard/lib/state-core/events.js +265 -0
- package/src/dashboard/lib/state-core/projections.js +376 -0
- package/src/dashboard/lib/state-core/start-close.js +162 -0
- package/src/dashboard/lib/state-core/trial.js +96 -0
- package/src/dashboard/lib/strategy-parser.js +3 -0
- package/src/dashboard/lib/suggested-now.js +2 -2
- package/src/dashboard/lib/transcript-parser.js +48 -8
- package/src/dashboard/server.js +422 -44
- package/src/dashboard/shortcut.js +0 -0
- package/src/dashboard/ui/dist/assets/index-ByVKPvLf.js +34 -0
- package/src/dashboard/ui/dist/assets/index-CCoKjUcC.js +161 -0
- package/src/dashboard/ui/dist/assets/index-CZWNQDt5.css +1 -0
- package/src/dashboard/ui/dist/index.html +2 -2
- package/src/dashboard/ui/dist/sw.js +1 -1
- package/src/dashboard/ui/dist/assets/index-598CSGOZ.js +0 -157
- package/src/dashboard/ui/dist/assets/index-BP--M69H.css +0 -1
- package/src/dashboard/ui/dist/assets/index-CidzmwSW.js +0 -34
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// start-close.js — CORE-LOOP PHASE 2b (SCHEMA-SPEC v0.2 section 3, owner
|
|
4
|
+
// default #4). The two verbs the whole loop collapses into, as LIBRARY
|
|
5
|
+
// functions only: nothing here is wired to a workflow, hook, or server
|
|
6
|
+
// route yet. Wiring comes after the owner inspects the migration manifest.
|
|
7
|
+
//
|
|
8
|
+
// start(vaultRoot, ...) = reap orphaned sessions from before, write
|
|
9
|
+
// session.started, and (optionally) inventory
|
|
10
|
+
// today's plan (plan.observed + plan.ticked from
|
|
11
|
+
// the note's checkboxes). No other side effects.
|
|
12
|
+
// close(vaultRoot, ...) = write the session's gathered facts +
|
|
13
|
+
// session.closed, then rebuild and write the four
|
|
14
|
+
// projection files beside the legacy ones.
|
|
15
|
+
//
|
|
16
|
+
// Projections are rebuildable, never hand-edited (spec section 2), so
|
|
17
|
+
// writeProjections overwrites them wholesale on every Close. The file bytes
|
|
18
|
+
// use the SAME assembly as dryrun.js's manifest (projectionFileText), so a
|
|
19
|
+
// Close over an unchanged log at the same clock reading reproduces the
|
|
20
|
+
// migration's bytes exactly — tested in state-core-startclose.test.mjs.
|
|
21
|
+
|
|
22
|
+
const fs = require("fs");
|
|
23
|
+
const path = require("path");
|
|
24
|
+
const events = require("./events");
|
|
25
|
+
const { appendEvent, readEvents, acquireLock } = require("./event-log");
|
|
26
|
+
|
|
27
|
+
// A session younger than this is NEVER reaped by another Start's sweep:
|
|
28
|
+
// terra attack 5 reproduced session B (started 1ms after A) reaping live
|
|
29
|
+
// session A. The grace window makes parallel sessions safe without a
|
|
30
|
+
// liveness lease; a genuinely crashed session is reaped by the first Start
|
|
31
|
+
// that runs after the window. Wiring may later pass real liveness (agent
|
|
32
|
+
// pids) to tighten this.
|
|
33
|
+
const DEFAULT_REAP_GRACE_MS = 24 * 3600 * 1000;
|
|
34
|
+
const { rebuildProjections } = require("./projections");
|
|
35
|
+
const { projectionFileText, PROJECTION_NAMES, STATE_DIR, LOG_NAME } = require("./dryrun");
|
|
36
|
+
const { backfillDailyTasks } = require("./backfill");
|
|
37
|
+
|
|
38
|
+
function logPathFor(vaultRoot) {
|
|
39
|
+
return path.join(vaultRoot, STATE_DIR, LOG_NAME);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Rebuilds from the log and overwrites the four projection files. Holds the
|
|
43
|
+
// log's advisory lock across the read-reduce-write so a concurrent append
|
|
44
|
+
// cannot land between the rebuild and the file writes (terra attack 6
|
|
45
|
+
// reproduced a stored projection with a stale watermark that way), and each
|
|
46
|
+
// file goes through a same-dir temp+rename so a crash never leaves a
|
|
47
|
+
// half-written projection. Returns the rebuild result.
|
|
48
|
+
function writeProjections(vaultRoot, { now, lockWaitMs } = {}) {
|
|
49
|
+
const nowMs = typeof now === "number" ? now : Date.now();
|
|
50
|
+
const logPath = logPathFor(vaultRoot);
|
|
51
|
+
const lock = acquireLock(logPath, lockWaitMs != null ? { lockWaitMs } : {});
|
|
52
|
+
try {
|
|
53
|
+
const rebuilt = rebuildProjections(logPath, { now: () => nowMs });
|
|
54
|
+
const watermark = { id: rebuilt.body.throughEventId, ts: rebuilt.body.throughEventTs };
|
|
55
|
+
for (const [kind, name] of Object.entries(PROJECTION_NAMES)) {
|
|
56
|
+
const dest = path.join(vaultRoot, STATE_DIR, name);
|
|
57
|
+
const tmp = `${dest}.tmp-${process.pid}`;
|
|
58
|
+
fs.writeFileSync(tmp, projectionFileText(kind, rebuilt.body[kind], watermark, rebuilt.body.eventCount, nowMs), "utf8");
|
|
59
|
+
fs.renameSync(tmp, dest);
|
|
60
|
+
}
|
|
61
|
+
return rebuilt;
|
|
62
|
+
} finally {
|
|
63
|
+
lock.release();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Start = session.started + orphan sweep + (optional) plan inventory.
|
|
68
|
+
// The sweep runs BEFORE this session's own started event is appended, so a
|
|
69
|
+
// session can never reap itself. planNote is today's Daily Note content:
|
|
70
|
+
// its checkboxes become the honest denominator (plan.observed) plus a
|
|
71
|
+
// plan.ticked for boxes already [x] — both dedupe on identical re-runs, so
|
|
72
|
+
// a double Start is harmless.
|
|
73
|
+
// One TOTAL wall-clock budget across a whole start()/close() call (sol #8
|
|
74
|
+
// partial refutation 2: lockWaitMs bounded each acquisition, but a call
|
|
75
|
+
// performs one acquisition per orphan/fact, so a hook child had no single
|
|
76
|
+
// deadline). Returns append options for the NEXT acquisition: the per-append
|
|
77
|
+
// wait shrinks to the remaining budget; when the budget is spent it throws,
|
|
78
|
+
// and the caller's partial work stands (appends are individually atomic and
|
|
79
|
+
// deduped, so the next session's Start/Close catches up losslessly).
|
|
80
|
+
function budgetedOpts(deadlineAt, lockWaitMs) {
|
|
81
|
+
if (deadlineAt == null) return lockWaitMs != null ? { lockWaitMs } : {};
|
|
82
|
+
const remaining = deadlineAt - Date.now();
|
|
83
|
+
if (remaining <= 0) throw new Error("start-close: total budget exhausted (partial work stands; the next session catches up)");
|
|
84
|
+
return { lockWaitMs: lockWaitMs != null ? Math.min(lockWaitMs, remaining) : remaining };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function start(vaultRoot, { agent, project, focus, sessionId, planNote, now, reapGraceMs, lockWaitMs, totalBudgetMs } = {}) {
|
|
88
|
+
const nowMs = typeof now === "number" ? now : Date.now();
|
|
89
|
+
const graceMs = typeof reapGraceMs === "number" ? reapGraceMs : DEFAULT_REAP_GRACE_MS;
|
|
90
|
+
const logPath = logPathFor(vaultRoot);
|
|
91
|
+
// lockWaitMs bounds EACH append's wait for a contended lock (terra wiring
|
|
92
|
+
// finding 5); totalBudgetMs bounds the WHOLE call (sol #8 refutation 2).
|
|
93
|
+
// The deadline is real wall clock, never the caller's logical `now`.
|
|
94
|
+
const deadlineAt = typeof totalBudgetMs === "number" ? Date.now() + totalBudgetMs : null;
|
|
95
|
+
|
|
96
|
+
const { events: existing } = readEvents(logPath);
|
|
97
|
+
// Only sessions older than the grace window are treated as crashed; a
|
|
98
|
+
// parallel session that started recently is LIVE, not an orphan.
|
|
99
|
+
const orphans = events.findOrphanSessions(existing).filter((o) => nowMs - o.ts > graceMs);
|
|
100
|
+
let reaped = 0;
|
|
101
|
+
for (const orphan of orphans) {
|
|
102
|
+
const r = appendEvent(logPath, events.reapSession(orphan, { ts: nowMs }), budgetedOpts(deadlineAt, lockWaitMs));
|
|
103
|
+
if (r.appended) reaped += 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const startedPayload = { agent, project };
|
|
107
|
+
if (sessionId) startedPayload.sessionId = sessionId;
|
|
108
|
+
if (focus) startedPayload.focus = focus;
|
|
109
|
+
const started = events.sessionStarted(startedPayload, { ts: nowMs });
|
|
110
|
+
appendEvent(logPath, started, budgetedOpts(deadlineAt, lockWaitMs));
|
|
111
|
+
|
|
112
|
+
let planEvents = 0;
|
|
113
|
+
const skipped = [];
|
|
114
|
+
if (planNote && typeof planNote.text === "string") {
|
|
115
|
+
const r = backfillDailyTasks(planNote.text, {
|
|
116
|
+
noteDate: planNote.noteDate,
|
|
117
|
+
relPath: planNote.relPath || `Dailies/${planNote.noteDate}.md`,
|
|
118
|
+
});
|
|
119
|
+
for (const e of r.events) {
|
|
120
|
+
if (appendEvent(logPath, e, budgetedOpts(deadlineAt, lockWaitMs)).appended) planEvents += 1;
|
|
121
|
+
}
|
|
122
|
+
skipped.push(...r.skipped);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { sessionId: started.payload.sessionId, reaped, planEvents, skipped };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Close = the session's gathered facts + session.closed + projection
|
|
129
|
+
// rebuild. `facts` are prebuilt envelopes (events.js constructors) — Close
|
|
130
|
+
// appends them verbatim; duplicates dedupe by key, so retrying a crashed
|
|
131
|
+
// Close never double-counts.
|
|
132
|
+
function close(vaultRoot, { sessionId, agent, project, summary, mins, dateKey, facts = [], now, lockWaitMs, totalBudgetMs } = {}) {
|
|
133
|
+
const nowMs = typeof now === "number" ? now : Date.now();
|
|
134
|
+
const logPath = logPathFor(vaultRoot);
|
|
135
|
+
const deadlineAt = typeof totalBudgetMs === "number" ? Date.now() + totalBudgetMs : null;
|
|
136
|
+
const appendOpts = () => budgetedOpts(deadlineAt, lockWaitMs);
|
|
137
|
+
|
|
138
|
+
let appended = 0;
|
|
139
|
+
let duplicates = 0;
|
|
140
|
+
for (const fact of facts) {
|
|
141
|
+
const r = appendEvent(logPath, fact, appendOpts());
|
|
142
|
+
if (r.appended) appended += 1;
|
|
143
|
+
else if (r.duplicate) duplicates += 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const closedPayload = { sessionId, agent, project, summary };
|
|
147
|
+
if (typeof mins === "number") closedPayload.mins = mins;
|
|
148
|
+
// Vault-zone policy (SCHEMA-SPEC v0.2.7): live wiring passes the
|
|
149
|
+
// product-local day; events.sessionClosed validates the shape.
|
|
150
|
+
if (dateKey !== undefined) closedPayload.dateKey = dateKey;
|
|
151
|
+
const r = appendEvent(logPath, events.sessionClosed(closedPayload, { ts: nowMs }), appendOpts());
|
|
152
|
+
if (r.appended) appended += 1;
|
|
153
|
+
else if (r.duplicate) duplicates += 1;
|
|
154
|
+
|
|
155
|
+
// The projection rebuild is the one step that must not be skipped mid-call:
|
|
156
|
+
// it gets whatever budget remains (or the per-lock default when unbudgeted).
|
|
157
|
+
const rebuildOpts = appendOpts();
|
|
158
|
+
const rebuilt = writeProjections(vaultRoot, { now: nowMs, lockWaitMs: rebuildOpts.lockWaitMs });
|
|
159
|
+
return { appended, duplicates, hash: rebuilt.hash, eventCount: rebuilt.body.eventCount };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { start, close, writeProjections, logPathFor };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// trial.js — CORE-LOOP TRIAL MODE (SCHEMA-SPEC v0.2 section 2): "If a
|
|
4
|
+
// projection disagrees with its legacy sibling during the trial, the
|
|
5
|
+
// DASHBOARD shows the legacy value and logs the delta — discrepancies
|
|
6
|
+
// surface, never silently switch."
|
|
7
|
+
//
|
|
8
|
+
// Scope discipline: only pairs with IDENTICAL semantics are compared.
|
|
9
|
+
// Overrides qualify (pattern-manifest override_history and override.logged
|
|
10
|
+
// events share one source since the migration). Streak deliberately does
|
|
11
|
+
// NOT: the legacy streak counts Daily-Note-existence days, the projection
|
|
12
|
+
// counts session.closed days — different definitions, and logging their
|
|
13
|
+
// disagreement would be false-equivalence noise, not signal. When more
|
|
14
|
+
// event writers land (override capture at Close), this file grows pairs.
|
|
15
|
+
//
|
|
16
|
+
// The delta log is the honest gap meter: today nothing writes
|
|
17
|
+
// override.logged forward, so `legacy - events` is exactly the wiring
|
|
18
|
+
// backlog. Watching it shrink to zero across releases IS the trial.
|
|
19
|
+
|
|
20
|
+
const fs = require("fs");
|
|
21
|
+
const path = require("path");
|
|
22
|
+
|
|
23
|
+
const STATE_DIR = path.join("02_Areas", "Engine", "State");
|
|
24
|
+
const LOG_NAME = "events.jsonl";
|
|
25
|
+
const DELTA_NAME = "trial-deltas.jsonl";
|
|
26
|
+
const MAX_DELTA_BYTES = 512 * 1024; // rotate once, keep the tail honest
|
|
27
|
+
|
|
28
|
+
// Compares override counts. Returns null when the vault is unmigrated
|
|
29
|
+
// (beside-mode: no State dir means no trial), else a comparison record.
|
|
30
|
+
function checkOverrides({ vault, manifestOverrideCount }) {
|
|
31
|
+
const logPath = path.join(vault, STATE_DIR, LOG_NAME);
|
|
32
|
+
if (!fs.existsSync(logPath)) return null;
|
|
33
|
+
const { readEvents } = require("./event-log");
|
|
34
|
+
const { events } = readEvents(logPath);
|
|
35
|
+
const eventCount = events.filter((e) => e.type === "override.logged").length;
|
|
36
|
+
const legacy = Number.isFinite(manifestOverrideCount) ? manifestOverrideCount : 0;
|
|
37
|
+
return {
|
|
38
|
+
kind: "overrides",
|
|
39
|
+
legacy,
|
|
40
|
+
events: eventCount,
|
|
41
|
+
agrees: legacy === eventCount,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Appends a record when the state MOVES: a new disagreement, a change in a
|
|
46
|
+
// standing disagreement, or a disagreement RESOLVING back to agreement (sol
|
|
47
|
+
// #7 partial refutation: returning on agrees left the last durable record
|
|
48
|
+
// claiming an open gap forever — the resolution is exactly the event the
|
|
49
|
+
// 14-day zero-delta switch criterion needs on the record). Standing states,
|
|
50
|
+
// agreeing or not, never re-log: the dashboard rebuilds often and spam
|
|
51
|
+
// would bury real movement.
|
|
52
|
+
function recordDelta(vault, record, { now } = {}) {
|
|
53
|
+
if (!record) return { logged: false, reason: "unmigrated" };
|
|
54
|
+
const deltaPath = path.join(vault, STATE_DIR, DELTA_NAME);
|
|
55
|
+
try {
|
|
56
|
+
let last = null;
|
|
57
|
+
if (fs.existsSync(deltaPath)) {
|
|
58
|
+
const stat = fs.statSync(deltaPath);
|
|
59
|
+
if (stat.size > MAX_DELTA_BYTES) {
|
|
60
|
+
fs.renameSync(deltaPath, deltaPath + ".1"); // one rotation, oldest history discarded
|
|
61
|
+
}
|
|
62
|
+
const lines = fs.readFileSync(deltaPath, "utf8").trim().split("\n").filter(Boolean);
|
|
63
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(lines[i]);
|
|
66
|
+
if (parsed.kind === record.kind) { last = parsed; break; }
|
|
67
|
+
} catch { continue; }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (record.agrees) {
|
|
71
|
+
// Agreement is durable news only when it RESOLVES a recorded gap.
|
|
72
|
+
if (!last || last.agrees) return { logged: false, reason: "agrees (no open gap on record)" };
|
|
73
|
+
} else if (last && !last.agrees && last.legacy === record.legacy && last.events === record.events) {
|
|
74
|
+
return { logged: false, reason: "unchanged since last record" };
|
|
75
|
+
}
|
|
76
|
+
const entry = { ts: typeof now === "number" ? now : Date.now(), ...record };
|
|
77
|
+
fs.appendFileSync(deltaPath, JSON.stringify(entry) + "\n", "utf8");
|
|
78
|
+
return { logged: true };
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return { logged: false, reason: `delta log write failed: ${e.message}` };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// One-call convenience for the build path: best-effort, never throws.
|
|
85
|
+
function runTrial({ vault, manifestOverrideCount }) {
|
|
86
|
+
try {
|
|
87
|
+
const overrides = checkOverrides({ vault, manifestOverrideCount });
|
|
88
|
+
if (!overrides) return { available: false, reason: "vault not migrated" };
|
|
89
|
+
const logged = recordDelta(vault, overrides);
|
|
90
|
+
return { available: true, pairs: [overrides], logged: logged.logged };
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return { available: false, reason: e.message };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { checkOverrides, recordDelta, runTrial, DELTA_NAME };
|
|
@@ -37,6 +37,9 @@ function parseStrategy(vault) {
|
|
|
37
37
|
out.primeObjective = out.fields.prime_objective || null;
|
|
38
38
|
out.stretchTarget = out.fields.stretch_target || null;
|
|
39
39
|
out.currentState = out.fields.current_state || null;
|
|
40
|
+
// The input commitment (daily quota) is SEPARATE from the Single Test (the
|
|
41
|
+
// outcome bet). The hero renders the bet; Today renders the commitment.
|
|
42
|
+
out.commitment = out.fields.commitment || null;
|
|
40
43
|
|
|
41
44
|
// Single Test extraction — V4 uses **Mid-term Milestone:**, older versions
|
|
42
45
|
// used **Single Test:** / **The Bet:**. Try V4 first, fall back to legacy
|
|
@@ -156,8 +156,8 @@ function computeSuggestions(input, options = {}) {
|
|
|
156
156
|
return [{
|
|
157
157
|
workflow: strategyWritten ? "/morning" : "/setup",
|
|
158
158
|
reason: strategyWritten
|
|
159
|
-
? "Your
|
|
160
|
-
: "Nothing is on the record yet. /setup builds your Identity, Strategy, and Goals, the
|
|
159
|
+
? "Your record is set up but no day is on it yet. /morning starts the first one."
|
|
160
|
+
: "Nothing is on the record yet. /setup builds your Identity, Strategy, and Goals, the files everything here runs on.",
|
|
161
161
|
score: 100,
|
|
162
162
|
urgency: "INFO",
|
|
163
163
|
signal: "freshVault",
|
|
@@ -60,6 +60,20 @@ function deriveProject(dirName) {
|
|
|
60
60
|
return dirName.replace(/^-+/, "").replace(/-/g, " ").trim();
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function projectFromCwd(cwd) {
|
|
64
|
+
const parts = String(cwd || "").split(/[\\/]01_Projects[\\/]/);
|
|
65
|
+
if (parts.length < 2) return null;
|
|
66
|
+
const project = parts[1].split(/[\\/]/)[0];
|
|
67
|
+
return project ? project.trim() : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isBootstrapText(text) {
|
|
71
|
+
const s = String(text || "").trim();
|
|
72
|
+
return /^# AGENTS\.md instructions\b/i.test(s) ||
|
|
73
|
+
/^<environment_context\b/i.test(s) ||
|
|
74
|
+
/^<hook_prompt\b/i.test(s);
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
function detectAgent(dirName) {
|
|
64
78
|
const d = dirName.toLowerCase();
|
|
65
79
|
if (d.includes("gemini") || d.includes("antigravity")) return "gemini";
|
|
@@ -160,6 +174,9 @@ function parseTranscript(file, opts = {}) {
|
|
|
160
174
|
const errors = [];
|
|
161
175
|
const decisions = [];
|
|
162
176
|
let firstTs = null, lastTs = null;
|
|
177
|
+
let detectedProject = opts.project || null;
|
|
178
|
+
let detectedAgent = opts.agent || null;
|
|
179
|
+
let detectedSessionId = opts.sessionId || null;
|
|
163
180
|
// C1 — workflow attribution. currentCommand = the most recent slash-command seen; on each
|
|
164
181
|
// error we snapshot it into lastCommand so a digest carries WHICH workflow was running when
|
|
165
182
|
// things broke. Self-improvement (GEPA/§5.5) reads this to name the workflow to fix.
|
|
@@ -189,25 +206,48 @@ function parseTranscript(file, opts = {}) {
|
|
|
189
206
|
// partial writes). `null.message` below would throw UNCAUGHT in this readline handler and kill
|
|
190
207
|
// an entire backfill (rl.on("error") only catches stream errors, not handler throws). Skip it.
|
|
191
208
|
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return;
|
|
192
|
-
|
|
209
|
+
|
|
210
|
+
// Codex rollout transcripts begin with session_meta and then wrap messages in
|
|
211
|
+
// {type:"response_item", payload:{type:"message", ...}}. Capture the cwd here because
|
|
212
|
+
// Codex stores rollouts under date folders, so the transcript path cannot identify the project.
|
|
213
|
+
if (obj.type === "session_meta" && obj.payload && typeof obj.payload === "object") {
|
|
214
|
+
detectedSessionId = detectedSessionId || obj.payload.session_id || obj.payload.id || null;
|
|
215
|
+
detectedProject = detectedProject || projectFromCwd(obj.payload.cwd);
|
|
216
|
+
if (/codex/i.test(String(obj.payload.originator || obj.payload.source || ""))) detectedAgent = "codex";
|
|
217
|
+
const metaTs = obj.timestamp || obj.payload.timestamp || "";
|
|
218
|
+
if (metaTs) { if (!firstTs) firstTs = metaTs; lastTs = metaTs; }
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const isCodexMessage = obj.type === "response_item" && obj.payload && obj.payload.type === "message";
|
|
223
|
+
const msg = isCodexMessage ? obj.payload : (obj.message || obj);
|
|
193
224
|
const role = msg.role || obj.type || "";
|
|
194
225
|
const ts = obj.timestamp || obj.created_at || msg.timestamp || "";
|
|
195
226
|
if (ts) { if (!firstTs) firstTs = ts; lastTs = ts; }
|
|
196
|
-
const content = msg.content
|
|
227
|
+
const content = isCodexMessage && Array.isArray(msg.content)
|
|
228
|
+
? msg.content.map((c) => c && (c.type === "input_text" || c.type === "output_text")
|
|
229
|
+
? { ...c, type: "text" }
|
|
230
|
+
: c)
|
|
231
|
+
: msg.content;
|
|
232
|
+
|
|
233
|
+
if (role === "user" && Array.isArray(content)) {
|
|
234
|
+
const textBlocks = content.filter((c) => c && c.type === "text" && c.text);
|
|
235
|
+
if (textBlocks.length && textBlocks.every((c) => isBootstrapText(c.text))) return;
|
|
236
|
+
}
|
|
197
237
|
|
|
198
238
|
if (role === "user") {
|
|
199
239
|
turns.user++;
|
|
200
240
|
if (typeof content === "string") {
|
|
201
241
|
const cmd = detectCommand(content); // scan even <command-name> turns (not pushed as prose)
|
|
202
242
|
if (cmd) currentCommand = cmd;
|
|
203
|
-
if (!content.startsWith("<")) push(content);
|
|
243
|
+
if (!content.startsWith("<") && !isBootstrapText(content)) push(content);
|
|
204
244
|
} else if (Array.isArray(content)) {
|
|
205
245
|
for (const c of content) {
|
|
206
246
|
if (!c) continue;
|
|
207
247
|
if (c.type === "text" && c.text) {
|
|
208
248
|
const cmd = detectCommand(c.text);
|
|
209
249
|
if (cmd) currentCommand = cmd;
|
|
210
|
-
if (!c.text.startsWith("<")) push(c.text);
|
|
250
|
+
if (!c.text.startsWith("<") && !isBootstrapText(c.text)) push(c.text);
|
|
211
251
|
}
|
|
212
252
|
if (c.type === "tool_result" && c.is_error) {
|
|
213
253
|
if (currentCommand) lastCommand = currentCommand; // attribute this error to its workflow
|
|
@@ -277,9 +317,9 @@ function parseTranscript(file, opts = {}) {
|
|
|
277
317
|
}
|
|
278
318
|
resolve({
|
|
279
319
|
file,
|
|
280
|
-
sessionId:
|
|
281
|
-
project:
|
|
282
|
-
agent:
|
|
320
|
+
sessionId: detectedSessionId || path.basename(file).replace(/\.jsonl$/, ""),
|
|
321
|
+
project: detectedProject,
|
|
322
|
+
agent: detectedAgent || (path.basename(file).startsWith("rollout-") ? "codex" : null),
|
|
283
323
|
firstTs, lastTs,
|
|
284
324
|
durationMin: clampDuration(firstTs, lastTs),
|
|
285
325
|
turns,
|
|
@@ -307,7 +347,7 @@ function parseTranscript(file, opts = {}) {
|
|
|
307
347
|
|
|
308
348
|
function dedupe(a) { return [...new Set(a)]; }
|
|
309
349
|
|
|
310
|
-
module.exports = { enumerateSessions, parseTranscript, deriveProject, extractCommitSubject, DEFAULT_PROJECTS_DIR };
|
|
350
|
+
module.exports = { enumerateSessions, parseTranscript, deriveProject, projectFromCwd, extractCommitSubject, DEFAULT_PROJECTS_DIR };
|
|
311
351
|
|
|
312
352
|
// ── CLI (standalone testing) ──
|
|
313
353
|
if (require.main === module) {
|