worktrust 0.8.4 → 0.8.5
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 +1 -0
- package/package.json +1 -1
- package/preserve-lines.mjs +6 -1
- package/session-databases.mjs +26 -3
- package/stretch-evidence.mjs +7 -2
- package/transcript-readers.mjs +43 -2
- package/worktrust.mjs +1 -1
package/README.md
CHANGED
|
@@ -170,6 +170,7 @@ nothing it does not:
|
|
|
170
170
|
| `oversight`, `planning`, `changes` | how often you refused an action or a plan the agent proposed, and how often a guard (the client's safety layer) refused one; how often the agent wrote its to-do list, the most items, and of the last list how many were done (never what they say); how many files the stretch's commits changed and how many of them tests (never a path) (0.8.1) |
|
|
171
171
|
| `supplements` | a supplement line: when a newer CLI measures more of a stretch already archived, the new fields go into a line of their own that names that stretch; nothing earlier is rewritten, and the chain and the day's root cover it (0.8.2) |
|
|
172
172
|
| `evidence` (sent) | the same derived record now also travels with each stretch to WorkTrust, kinds and counts only, and `npx worktrust status` shows what this computer measured in the last thirty days (0.8.3) |
|
|
173
|
+
| `unconfirmed` | checks the stretch ran whose outcome no reader could see (Codex's code mode prints no exit code; a background terminal), per kind: counted, never as a pass (0.8.5) |
|
|
173
174
|
| `collector_version` | the CLI version that measured it |
|
|
174
175
|
| `seq`, `prev`, `hash` | the line's place, the hash of the line before it, and its own hash |
|
|
175
176
|
|
package/package.json
CHANGED
package/preserve-lines.mjs
CHANGED
|
@@ -54,6 +54,11 @@ export function archiveLine(entry, behaviour = null, { ids = {}, collector } = {
|
|
|
54
54
|
const kept = Object.entries(entry.verification).filter(([kind, tally]) => CHECK_KINDS.includes(kind) && Array.isArray(tally) && tally.length === 2 && tally.every(whole) && tally[1] <= tally[0] && tally[0] > 0).sort();
|
|
55
55
|
if (kept.length > 0) line.verification = Object.fromEntries(kept);
|
|
56
56
|
}
|
|
57
|
+
// UNCONFIRMED (0.8.5): checks run whose outcome no reader could see, per kind, whole counts.
|
|
58
|
+
if (entry.unconfirmed && typeof entry.unconfirmed === "object") {
|
|
59
|
+
const kept = Object.entries(entry.unconfirmed).filter(([kind, count]) => CHECK_KINDS.includes(kind) && whole(count) && count > 0).sort();
|
|
60
|
+
if (kept.length > 0) line.unconfirmed = Object.fromEntries(kept);
|
|
61
|
+
}
|
|
57
62
|
if (entry.delivery && typeof entry.delivery === "object") {
|
|
58
63
|
const kept = Object.entries(entry.delivery).filter(([kind, count]) => DELIVERY_KINDS.includes(kind) && whole(count) && count > 0).sort();
|
|
59
64
|
if (kept.length > 0) line.delivery = { ...Object.fromEntries(kept), verified_first: entry.delivery.verified_first === true };
|
|
@@ -138,7 +143,7 @@ export function archiveEntries(dir, ownKey) {
|
|
|
138
143
|
* a line of their own that names the stretch it adds to (`supplements`) and carries only what no earlier line of that
|
|
139
144
|
* stretch carries, in the same chain, under the same day's root. Null when there is nothing new.
|
|
140
145
|
*/
|
|
141
|
-
export const DERIVED_KEYS = ["signals", "analyzer_version", "verification", "delivery", "recovery", "delegation", "context", "routing", "steering", "tools", "complexity", "oversight", "planning", "changes", "interrupts", "steers", "utc_offset"];
|
|
146
|
+
export const DERIVED_KEYS = ["signals", "analyzer_version", "verification", "unconfirmed", "delivery", "recovery", "delegation", "context", "routing", "steering", "tools", "complexity", "oversight", "planning", "changes", "interrupts", "steers", "utc_offset"];
|
|
142
147
|
export function supplementFor(line, earlier) {
|
|
143
148
|
const missing = DERIVED_KEYS.filter((key) => line[key] !== undefined && !earlier.some((old) => old[key] !== undefined));
|
|
144
149
|
if (missing.length === 0) return null;
|
package/session-databases.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
|
12
12
|
import * as zlib from "node:zlib"; // zstd since Node 22.15; older Node reads OpenClaw's uncompressed rows only
|
|
13
13
|
import { homedir, platform } from "node:os";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
-
import { cleanModel } from "./transcript-readers.mjs";
|
|
15
|
+
import { cleanModel, cursorCall, copilotCall } from "./transcript-readers.mjs";
|
|
16
16
|
|
|
17
17
|
// ── hermes session reader ──
|
|
18
18
|
/**
|
|
@@ -344,18 +344,34 @@ export function cursorLines(pseudo) {
|
|
|
344
344
|
const chat = db.prepare("select json_extract(value, '$.modelConfig.modelName') as model from cursorDiskKV where key = ?").get(`composerData:${id}`);
|
|
345
345
|
const bubbles = db.prepare(`select json_extract(value, '$.type') as type, json_extract(value, '$.createdAt') as at,
|
|
346
346
|
json_extract(value, '$.tokenCount.inputTokens') as input, json_extract(value, '$.tokenCount.outputTokens') as output,
|
|
347
|
-
json_extract(value, '$.toolFormerData') is not null as tool
|
|
347
|
+
json_extract(value, '$.toolFormerData') is not null as tool,
|
|
348
|
+
json_extract(value, '$.toolFormerData.name') as toolName, json_extract(value, '$.toolFormerData.toolCallId') as callId,
|
|
349
|
+
json_extract(value, '$.toolFormerData.status') as status, json_extract(value, '$.toolFormerData.userDecision') as decision,
|
|
350
|
+
json_extract(value, '$.toolFormerData.additionalData.status') as outcome,
|
|
351
|
+
json_extract(json_extract(value, '$.toolFormerData.params'), '$.command') as command,
|
|
352
|
+
json_extract(json_extract(value, '$.toolFormerData.result'), '$.exitCode') as exitCode,
|
|
353
|
+
json_extract(json_extract(value, '$.toolFormerData.result'), '$.rejected') as rejected,
|
|
354
|
+
json_extract(json_extract(value, '$.toolFormerData.result'), '$.backgroundShellId') is not null as background,
|
|
355
|
+
json_type(json_extract(value, '$.toolFormerData.result'), '$.output') = 'text' as hasOutput,
|
|
356
|
+
(select json_group_array(json_extract(todo.value, '$.status')) from json_each(json_extract(cursorDiskKV.value, '$.toolFormerData.result'), '$.finalTodos') as todo) as todoStatuses
|
|
348
357
|
from cursorDiskKV where key > ? and key < ? and json_extract(value, '$.createdAt') is not null`).all(`bubbleId:${id}:`, `bubbleId:${id};`);
|
|
349
358
|
const model = cleanModel(chat?.model);
|
|
350
359
|
const lines = [];
|
|
360
|
+
let pending = null;
|
|
351
361
|
for (const bubble of bubbles.sort((a, b) => String(a.at).localeCompare(String(b.at)))) {
|
|
352
362
|
const timestamp = isoOf(bubble.at); if (!timestamp) continue;
|
|
363
|
+
if (pending) { lines.push({ ...pending, timestamp }); pending = null; }
|
|
353
364
|
if (Number(bubble.type) === 1) lines.push({ type: "user", timestamp, message: { content: [{ type: "text" }] } });
|
|
354
365
|
else if (Number(bubble.type) === 2) {
|
|
355
366
|
const usage = Number(bubble.input) || Number(bubble.output) ? { usage: { input_tokens: Number(bubble.input) || 0, output_tokens: Number(bubble.output) || 0 } } : {};
|
|
356
|
-
|
|
367
|
+
const call = bubble.tool ? cursorCall(bubble) : null;
|
|
368
|
+
lines.push({ type: "assistant", timestamp, message: { model, ...usage, content: call ? [call.use] : [] } });
|
|
369
|
+
// THE CALL'S RESULT (0.8.5) lands at the NEXT bubble's moment: the gap after a call stays tool time and no gap is
|
|
370
|
+
// added, so the stretch's seconds and layers are exactly as before; a call that ends the session closes on its own moment.
|
|
371
|
+
if (call) pending = { type: "user", timestamp, message: { content: [call.result] } };
|
|
357
372
|
}
|
|
358
373
|
}
|
|
374
|
+
if (pending) lines.push(pending);
|
|
359
375
|
return lines;
|
|
360
376
|
} catch { return null; } finally { db.close(); }
|
|
361
377
|
}
|
|
@@ -421,6 +437,13 @@ export function copilotLines(pseudo) {
|
|
|
421
437
|
const meta = request?.result?.metadata ?? {};
|
|
422
438
|
const usage = Number(meta.promptTokens) || Number(meta.outputTokens) ? { usage: { input_tokens: Number(meta.promptTokens) || 0, output_tokens: Number(meta.outputTokens) || 0 } } : {};
|
|
423
439
|
// `agentic` (owner, 2026-10-06): the request's own run, asked → completed, is the agent's time; the hook files that gap as tool time, uncapped.
|
|
440
|
+
// THE REQUEST'S TOOL CALLS (0.8.5), all at the moment it was asked: no gap is added, so its seconds and layers are as before.
|
|
441
|
+
for (const part of Array.isArray(request?.response) ? request.response : []) {
|
|
442
|
+
const call = part && part.kind === "toolInvocationSerialized" ? copilotCall(part) : null;
|
|
443
|
+
if (!call) continue;
|
|
444
|
+
lines.push({ type: "assistant", timestamp: new Date(asked).toISOString(), message: { content: [call.use] } });
|
|
445
|
+
lines.push({ type: "user", timestamp: new Date(asked).toISOString(), message: { content: [call.result] } });
|
|
446
|
+
}
|
|
424
447
|
lines.push({ type: "assistant", timestamp: new Date(done).toISOString(), agentic: true, message: { model: cleanModel(String(meta.resolvedModel ?? request?.modelId ?? "").replace(/^copilot\//, "")), ...usage, content: [] } });
|
|
425
448
|
}
|
|
426
449
|
return lines;
|
package/stretch-evidence.mjs
CHANGED
|
@@ -42,6 +42,8 @@ const GUARD_REFUSED = /^\s*Permission for this (command|action) was denied by/i;
|
|
|
42
42
|
export const resultOf = (block) => {
|
|
43
43
|
const text = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((part) => (part && typeof part.text === "string" ? part.text : "")).join("") : "";
|
|
44
44
|
const failed = block.is_error === true;
|
|
45
|
+
// 0.8.5: a reader that could not see the outcome (Codex's code mode, a background terminal) says so; unknown is never a pass.
|
|
46
|
+
if (!failed && block.outcome === "unknown") return { id: block.tool_use_id, failed: null, refusal: null };
|
|
45
47
|
return { id: block.tool_use_id, failed, refusal: failed && PERSON_REFUSED.test(text) ? "person" : failed && GUARD_REFUSED.test(text) ? "guard" : null };
|
|
46
48
|
};
|
|
47
49
|
/** A to-do list the agent wrote (TodoWrite): how many items and how many done, never what they say. */
|
|
@@ -58,17 +60,20 @@ export const callOf = (block) => { const kinds = callKindsOf(block), plan = plan
|
|
|
58
60
|
function verificationOf(messages, helpers) {
|
|
59
61
|
const kinds = new Map();
|
|
60
62
|
for (const message of messages) for (const call of message.calls ?? []) kinds.set(call.id, call.kinds);
|
|
61
|
-
const checks = {}, delivered = {};
|
|
63
|
+
const checks = {}, delivered = {}, unconfirmed = {};
|
|
62
64
|
let deliveredAt = null, passedFirst = false;
|
|
63
65
|
for (const message of messages) for (const result of message.results ?? []) {
|
|
64
66
|
for (const kind of kinds.get(result.id) ?? []) {
|
|
67
|
+
// A check whose outcome nobody could see is counted as run and unconfirmed, apart from the known tallies (0.8.5).
|
|
68
|
+
if (CHECK_KINDS.includes(kind) && result.failed === null) { unconfirmed[kind] = (unconfirmed[kind] ?? 0) + 1; continue; }
|
|
69
|
+
if (DELIVERY_KINDS.includes(kind) && result.failed === null) continue;
|
|
65
70
|
if (CHECK_KINDS.includes(kind)) { const tally = (checks[kind] ??= [0, 0]); tally[0] += 1; if (result.failed) tally[1] += 1; else if (deliveredAt === null) passedFirst = true; }
|
|
66
71
|
else if (DELIVERY_KINDS.includes(kind) && !result.failed) { delivered[kind] = (delivered[kind] ?? 0) + 1; if (deliveredAt === null) deliveredAt = message.at; }
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
const any = (record) => Object.keys(record).length > 0;
|
|
70
75
|
const recovery = recoveryOf(messages), delegation = delegationOf(messages, helpers);
|
|
71
|
-
return { ...(any(checks) ? { verification: checks } : {}), ...(any(delivered) ? { delivery: { ...delivered, verified_first: passedFirst } } : {}), ...(recovery ? { recovery } : {}), ...(delegation ? { delegation } : {}) };
|
|
76
|
+
return { ...(any(checks) ? { verification: checks } : {}), ...(any(unconfirmed) ? { unconfirmed } : {}), ...(any(delivered) ? { delivery: { ...delivered, verified_first: passedFirst } } : {}), ...(recovery ? { recovery } : {}), ...(delegation ? { delegation } : {}) };
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
/**
|
package/transcript-readers.mjs
CHANGED
|
@@ -43,7 +43,7 @@ export const CODEX_ROLLOUT = /(^|[\\/])rollout-\d{4}-\d{2}-\d{2}T[\d-]+-[0-9a-f-
|
|
|
43
43
|
/** A rollout's thread id: its file's name without .jsonl, split on either separator. */
|
|
44
44
|
export const codexIdOf = (file) => String(file).split(/[\\/]/).at(-1).replace(/\.jsonl$/, "");
|
|
45
45
|
export const CODEX_HARNESS_TURN = /^\s*<[a-z][a-z_]*[\s>]/i;
|
|
46
|
-
export const codexToolName = (name) => (name === "exec" || name === "shell" || name === "container.exec" || name === "local_shell" ? "Bash" : name === "apply_patch" ? "Edit" : name === "spawn_agent" ? "Agent" : String(name ?? "tool"));
|
|
46
|
+
export const codexToolName = (name) => (name === "exec" || name === "shell" || name === "container.exec" || name === "local_shell" ? "Bash" : name === "apply_patch" ? "Edit" : name === "spawn_agent" || name === "followup_task" ? "Agent" : name === "request_user_input" || name === "request_user_input_async" ? "AskUserQuestion" : String(name ?? "tool"));
|
|
47
47
|
export const codexToolInput = (name, raw) => {
|
|
48
48
|
const mapped = codexToolName(name);
|
|
49
49
|
if (mapped === "Bash") {
|
|
@@ -91,7 +91,7 @@ export function* codexLines(records) {
|
|
|
91
91
|
if (kind === "custom_tool_call_output" || kind === "function_call_output") {
|
|
92
92
|
const text = codexOutputText(payload.output);
|
|
93
93
|
const exit = /(?:failed with|exited with|exit code)[:\s]+(-?\d+)/i.exec(text.slice(-400));
|
|
94
|
-
yield { type: "user", timestamp, cwd, uuid: payload.id ? `codex-${payload.id}` : undefined, message: { content: [{ type: "tool_result", ...(payload.call_id ? { tool_use_id: String(payload.call_id) } : {}), content: text, is_error: exit ? exit[1] !== "0" : false }] } };
|
|
94
|
+
yield { type: "user", timestamp, cwd, uuid: payload.id ? `codex-${payload.id}` : undefined, message: { content: [{ type: "tool_result", ...(payload.call_id ? { tool_use_id: String(payload.call_id) } : {}), content: text, is_error: exit ? exit[1] !== "0" : false, ...(exit ? {} : { outcome: "unknown" }) }] } }; // 0.8.5: no exit code (Codex code mode never prints one) is an unknown outcome, never a pass
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -220,3 +220,44 @@ export function rememberAntigravity(input) {
|
|
|
220
220
|
return id;
|
|
221
221
|
}
|
|
222
222
|
// ── end antigravity transcript reader ──
|
|
223
|
+
|
|
224
|
+
// ── the database clients' tool calls (0.8.5), read by session-databases.mjs ──
|
|
225
|
+
/**
|
|
226
|
+
* A CURSOR TOOL CALL AS THE STRETCH READS IT (0.8.5): its name mapped to the common one (a terminal command is Bash,
|
|
227
|
+
* its to-do list is TodoWrite), the command for the CLI to name a KIND of (never kept), the to-do statuses (never their
|
|
228
|
+
* text), and the outcome Cursor recorded: an exit code only when the command failed, an error status, or the person's
|
|
229
|
+
* rejection; a background command, or a call with no outcome, is unknown, never a pass.
|
|
230
|
+
*/
|
|
231
|
+
const CURSOR_NAMES = { run_terminal_command_v2: "Bash", run_terminal_cmd: "Bash", todo_write: "TodoWrite", edit_file_v2: "Edit", edit_file: "Edit", read_file_v2: "Read", read_file: "Read", ripgrep_raw_search: "Grep", glob_file_search: "Glob", delete_file: "Edit" };
|
|
232
|
+
const json = (text) => { if (typeof text !== "string") return null; try { return JSON.parse(text); } catch { return null; } };
|
|
233
|
+
export function cursorCall(bubble) {
|
|
234
|
+
const id = typeof bubble.callId === "string" && bubble.callId ? bubble.callId : null;
|
|
235
|
+
if (!id) return null;
|
|
236
|
+
const raw = String(bubble.toolName ?? "tool"), name = CURSOR_NAMES[raw] ?? raw;
|
|
237
|
+
const todos = json(bubble.todoStatuses);
|
|
238
|
+
const input = name === "Bash" ? { command: typeof bubble.command === "string" ? bubble.command : "" } : name === "TodoWrite" && Array.isArray(todos) && todos.length > 0 ? { todos: todos.map((status) => ({ status: status === "completed" ? "completed" : "pending" })) } : {};
|
|
239
|
+
const rejected = Number(bubble.rejected) === 1 || bubble.rejected === true || bubble.decision === "rejected";
|
|
240
|
+
const exit = bubble.exitCode === null || bubble.exitCode === undefined ? null : Number(bubble.exitCode);
|
|
241
|
+
const failed = rejected || (exit !== null && exit !== 0) || bubble.outcome === "error" || bubble.status === "error";
|
|
242
|
+
const known = failed || (name === "Bash" ? Number(bubble.hasOutput) === 1 && Number(bubble.background) !== 1 : bubble.status === "completed" || bubble.outcome === "success");
|
|
243
|
+
return {
|
|
244
|
+
use: { type: "tool_use", id, name, input },
|
|
245
|
+
result: { type: "tool_result", tool_use_id: id, is_error: failed, ...(known ? {} : { outcome: "unknown" }), content: rejected ? "User rejected the tool call." : "" },
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* A COPILOT TOOL CALL AS THE STRETCH READS IT (0.8.5): a terminal command is Bash (its command line for the CLI to name
|
|
250
|
+
* a kind of, never kept) with the exit code VS Code recorded; a missing exit code is unknown. Its to-do list is TodoWrite
|
|
251
|
+
* (statuses only), its subagents are Agent. What a confirmation type means is not documented, so no refusal is read.
|
|
252
|
+
*/
|
|
253
|
+
const COPILOT_NAMES = { run_in_terminal: "Bash", manage_todo_list: "TodoWrite", execution_subagent: "Agent", runSubagent: "Agent", copilot_replaceString: "Edit", copilot_createFile: "Write", copilot_readFile: "Read", copilot_findTextInFiles: "Grep", copilot_findFiles: "Glob" };
|
|
254
|
+
export function copilotCall(part) {
|
|
255
|
+
const id = typeof part.toolCallId === "string" && part.toolCallId ? part.toolCallId : null;
|
|
256
|
+
if (!id) return null;
|
|
257
|
+
const raw = String(part.toolId ?? "tool"), name = COPILOT_NAMES[raw] ?? raw, data = part.toolSpecificData ?? {};
|
|
258
|
+
const line = data.commandLine && typeof data.commandLine === "object" ? data.commandLine.toolEdited ?? data.commandLine.original : null;
|
|
259
|
+
const input = name === "Bash" ? { command: typeof line === "string" ? line : "" } : name === "TodoWrite" && Array.isArray(data.todoList) ? { todos: data.todoList.map((todo) => ({ status: todo?.status === "completed" ? "completed" : "pending" })) } : {};
|
|
260
|
+
const exit = Number.isInteger(data.terminalCommandState?.exitCode) ? data.terminalCommandState.exitCode : null;
|
|
261
|
+
const known = name === "Bash" ? exit !== null : false;
|
|
262
|
+
return { use: { type: "tool_use", id, name, input }, result: { type: "tool_result", tool_use_id: id, is_error: exit !== null && exit !== 0, ...(known ? {} : { outcome: "unknown" }), content: "" } };
|
|
263
|
+
}
|
package/worktrust.mjs
CHANGED
|
@@ -73,7 +73,7 @@ const command = args.find((arg, at) => !arg.startsWith("--") && !(at > 0 && VALU
|
|
|
73
73
|
const flag = (name) => { const at = args.indexOf(`--${name}`); return at >= 0 ? args[at + 1] : undefined; };
|
|
74
74
|
const has = (name) => args.includes(`--${name}`);
|
|
75
75
|
/** This CLI's version, said to the door so the app can tell which computer runs an old one (check-cli-package holds it equal to package.json). */
|
|
76
|
-
const CLI_VERSION = "0.8.
|
|
76
|
+
const CLI_VERSION = "0.8.5";
|
|
77
77
|
const ORIGIN = (flag("origin") ?? process.env.WORKTRUST_ORIGIN ?? "https://app.worktrust.io").replace(/\/$/, "");
|
|
78
78
|
const MCP = flag("url") ?? process.env.WORKTRUST_MCP_URL ?? `${ORIGIN}/api/mcp`;
|
|
79
79
|
const HOME_DIR = join(homedir(), ".worktrust");
|