worktrust 0.8.2 → 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 +3 -1
- package/log-session.mjs +26 -4
- 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 +30 -3
package/README.md
CHANGED
|
@@ -169,6 +169,8 @@ nothing it does not:
|
|
|
169
169
|
| `tools`, `complexity` | how many different tools the agent used, and the stretch's task complexity C1 to C5 by the rule complexity/1 (points for layers touched, tools, subagents, duration, a recovered failure, kinds of check and a delivery; the rule is in stretch-evidence.mjs), with its points and the rule's version (0.7.6) |
|
|
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
|
+
| `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) |
|
|
172
174
|
| `collector_version` | the CLI version that measured it |
|
|
173
175
|
| `seq`, `prev`, `hash` | the line's place, the hash of the line before it, and its own hash |
|
|
174
176
|
|
|
@@ -257,7 +259,7 @@ which would delete every password on the computer, and many people rightly keep
|
|
|
257
259
|
| **Reads** | Which AI apps are installed (the settings folders of Claude Code, Codex, Cursor, Gemini CLI, Antigravity, VS Code, Windsurf, Hermes Agent, Goose, OpenCode, OpenClaw), and the chats Cursor and Copilot keep (clocks, models and token counts only). Afterwards, the session hook reads Claude Code's, Codex's and Antigravity's session files, the session databases of Hermes, Goose, OpenCode and OpenClaw, and the chats Cursor and Copilot keep, on this computer to measure durations and token counts. |
|
|
258
260
|
| **Writes** | The key file (above). In each AI app's MCP settings (`~/.claude.json` through `claude mcp`, `~/.cursor/mcp.json`, `~/.codex/config.toml`, `~/.gemini/settings.json`, `~/.gemini/config/mcp_config.json` (Antigravity), VS Code's `mcp.json`, Windsurf's `mcp_config.json`, Hermes's and Goose's `config.yaml`, OpenCode's `opencode.json`, OpenClaw through `openclaw mcp set`) a WorkTrust entry that runs the local bridge, with no key in it; three hooks in `~/.claude/settings.json`; the bridge, the hook and the counter in `~/.worktrust/`. |
|
|
259
261
|
| **Sends, to pair** | This computer's system (macOS, Windows, Linux) and host name. WorkTrust also records the network address the request arrives from, as a security record kept 90 days and seen only by WorkTrust staff (to answer a theft or fraud report). |
|
|
260
|
-
| **Sends, afterwards** | Per measured stretch of work, and nothing else: its durations (`seconds`, `duration_basis`, `model_seconds`, `tool_seconds`, `human_seconds`, `idle_seconds`), token counts (`tokens_in`, `tokens_out`, `tokens_cache_read`, `tokens_cache_write`, `token_basis`), the model's name (`model`), the kind of work and one layer keyword such as frontend or backend (`title`, `kind`, `layer`, `layers`), counts of how you work (`exchanges`, `turn_basis`, `agent_runs`, `agent_seconds`, `agent_peak`, `interrupts`, `steers`), when it began and ended and your computer's offset from UTC (`started_at`, `at`, `utc_offset`), a hash of the session and the day (`stretch_ref`), `steered_from` when it ran over SSH, and, so the work can be matched to GitHub, the repository as its remote's owner/name (`repo`) and the hashes of the commits made in the stretch (`commits`, never their messages). |
|
|
262
|
+
| **Sends, afterwards** | Per measured stretch of work, and nothing else: its durations (`seconds`, `duration_basis`, `model_seconds`, `tool_seconds`, `human_seconds`, `idle_seconds`), token counts (`tokens_in`, `tokens_out`, `tokens_cache_read`, `tokens_cache_write`, `token_basis`), the model's name (`model`), the kind of work and one layer keyword such as frontend or backend (`title`, `kind`, `layer`, `layers`), counts of how you work (`exchanges`, `turn_basis`, `agent_runs`, `agent_seconds`, `agent_peak`, `interrupts`, `steers`), when it began and ended and your computer's offset from UTC (`started_at`, `at`, `utc_offset`), what the CLI derived from the stretch as kinds and counts (`evidence`: checks run and failed, delivery steps and whether a check passed first, recoveries, delegation, context, routing, steering, refusals, planning, files and test files changed, the complexity class; never a word, a command or a path; since 0.8.3), a hash of the session and the day (`stretch_ref`), `steered_from` when it ran over SSH, and, so the work can be matched to GitHub, the repository as its remote's owner/name (`repo`) and the hashes of the commits made in the stretch (`commits`, never their messages). |
|
|
261
263
|
| **Never sends** | Prompts, answers, code, file names or paths, commit messages, branch names, or the name of your local folder (a repository is named only as its remote's owner/name, above). The door has no field for them and refuses a submission that carries them. |
|
|
262
264
|
|
|
263
265
|
## Why you can check it
|
package/log-session.mjs
CHANGED
|
@@ -593,6 +593,10 @@ function payloadFor(stretch) {
|
|
|
593
593
|
// The seventh kind, used as it is defined: a stretch that changed no artefact is knowing
|
|
594
594
|
// something, not building something, and it earns neither an artefact's nor a validation's credit.
|
|
595
595
|
const kind = touched.paths.length === 0 ? "researched" : touched.committed ? "changed" : "built";
|
|
596
|
+
// 0.8.1: what the stretch changed, as counts of files (never their paths): how many, and how many of them tests.
|
|
597
|
+
// 0.7.6: and the stretch's complexity class, read from what the hook knows here (layers, subagents, duration) and the record.
|
|
598
|
+
const changed = [...new Set(touched.paths)], tests = changed.filter((path) => TEST_PATH.test(path)).length;
|
|
599
|
+
const derived = stretch.derived ? { ...stretch.derived, ...(changed.length > 0 ? { changes: { files: changed.length, test_files: tests } } : {}), ...(complexityOf ? { complexity: complexityOf(stretch.derived, { layers: Math.max(layers.length, layer ? 1 : 0), agentRuns: stretch.agents?.runs ?? 0, agentPeak: stretch.agents?.peak ?? 0, seconds: stretch.seconds }) } : {}) } : null;
|
|
596
600
|
const payload = {
|
|
597
601
|
title: layer ? `AI-assisted work · ${layer}` : "AI-assisted work",
|
|
598
602
|
kind,
|
|
@@ -622,13 +626,13 @@ function payloadFor(stretch) {
|
|
|
622
626
|
...(stretch.interrupts > 0 ? { interrupts: Math.min(10000, stretch.interrupts) } : {}),
|
|
623
627
|
...(stretch.steers > 0 ? { steers: Math.min(10000, stretch.steers) } : {}),
|
|
624
628
|
...(stretch.agents && stretch.agents.runs > 0 ? { agent_runs: Math.min(AGENT_RUNS_MAX, stretch.agents.runs), agent_seconds: Math.min(AGENT_SECONDS_MAX, stretch.agents.seconds), agent_peak: Math.min(AGENT_RUNS_MAX, stretch.agents.peak) } : {}),
|
|
629
|
+
// THE STRETCH'S EVIDENCE (0.8.3): what the CLI derived from it here, kinds and counts only (stretch-evidence.mjs); the door keeps it from a CLI key alone.
|
|
630
|
+
...(derived ? { evidence: derived } : {}),
|
|
625
631
|
at: new Date(stretch.to).toISOString(),
|
|
626
632
|
};
|
|
627
633
|
// WHAT STAYS ON THIS COMPUTER (0.7.1): the stretch's derived record rides beside the payload for the local archive, never in it.
|
|
628
|
-
//
|
|
629
|
-
|
|
630
|
-
const changed = [...new Set(touched.paths)], tests = changed.filter((path) => TEST_PATH.test(path)).length;
|
|
631
|
-
if (stretch.derived) DERIVED.set(payload, { ...stretch.derived, ...(changed.length > 0 ? { changes: { files: changed.length, test_files: tests } } : {}), ...(complexityOf ? { complexity: complexityOf(stretch.derived, { layers: Math.max(layers.length, layer ? 1 : 0), agentRuns: stretch.agents?.runs ?? 0, agentPeak: stretch.agents?.peak ?? 0, seconds: stretch.seconds }) } : {}) });
|
|
634
|
+
// The same record rides beside the payload for the local archive (preserve --archive).
|
|
635
|
+
if (derived) DERIVED.set(payload, derived);
|
|
632
636
|
return payload;
|
|
633
637
|
}
|
|
634
638
|
|
|
@@ -934,6 +938,24 @@ const doorFor = (entry) => (String(entry?.id ?? "").startsWith("codex:") ? codex
|
|
|
934
938
|
const entrySource = new WeakMap(); // an entry → "codex" or "antigravity" by the client that wrote it; the sends route and name by it
|
|
935
939
|
const entryFile = new WeakMap(); // a history entry → its transcript (`--summary` counts sessions), first moment and folder name (`--archive-lines`)
|
|
936
940
|
// THE LOCAL ARCHIVE (`npx worktrust preserve --archive`, 2026-10-06): every measured day, live window and floors aside, as JSON lines to preserve.mjs on this computer, which keeps metadata only; nothing is sent.
|
|
941
|
+
// WHAT THIS COMPUTER MEASURES (0.8.3, for `npx worktrust status`): the last thirty days of its own sessions, summed from
|
|
942
|
+
// the same derived record the hook sends, counts only; reads, prints and exits, nothing sent.
|
|
943
|
+
if (flag("evidence-summary")) {
|
|
944
|
+
const since = Date.now() - 30 * 86400_000, rows = historyEntries(null, true).filter((entry) => Date.parse(entry.at) >= since);
|
|
945
|
+
const sum = (pick) => rows.reduce((total, entry) => total + (pick(DERIVED.get(entry) ?? {}) ?? 0), 0);
|
|
946
|
+
const complexity = {};
|
|
947
|
+
for (const entry of rows) { const tier = DERIVED.get(entry)?.complexity?.class; if (tier) complexity[tier] = (complexity[tier] ?? 0) + 1; }
|
|
948
|
+
const summary = {
|
|
949
|
+
stretches: rows.length, hours: Math.round(rows.reduce((total, entry) => total + (entry.seconds ?? 0), 0) / 360) / 10,
|
|
950
|
+
delivered: sum((d) => (d.delivery ? 1 : 0)), verified_first: sum((d) => (d.delivery?.verified_first ? 1 : 0)),
|
|
951
|
+
failures: sum((d) => d.recovery?.failures), recovered: sum((d) => d.recovery?.recovered), blind_retries: sum((d) => d.recovery?.blind_retries),
|
|
952
|
+
steers: sum((d) => d.steering?.moments), effective_steers: sum((d) => d.steering?.effective),
|
|
953
|
+
refused: sum((d) => (d.oversight ? d.oversight.refused + d.oversight.plans_rejected : 0)), guard_denied: sum((d) => d.oversight?.guard_denied),
|
|
954
|
+
planned: sum((d) => (d.planning ? 1 : 0)), files: sum((d) => d.changes?.files), test_files: sum((d) => d.changes?.test_files), complexity,
|
|
955
|
+
};
|
|
956
|
+
process.stdout.write(`${JSON.stringify(summary)}\n`);
|
|
957
|
+
await exitFlushed(0);
|
|
958
|
+
}
|
|
937
959
|
if (flag("archive-lines")) { const lines = historyEntries(null, true).map((entry) => JSON.stringify({ ...entry, ...(DERIVED.get(entry) ?? {}), client: entrySource.get(entry) ?? "claude", started_at: new Date(entryFile.get(entry).from).toISOString(), folder: entryFile.get(entry).folder })); process.stdout.write(`${[...lines, JSON.stringify({ archive_end: lines.length })].join("\n")}\n`); await exitFlushed(0); } // one write and a count, so the reader tells a whole answer from a cut one
|
|
938
960
|
if (!door && !codexDoor && !DRY) { process.exit(0); } // Not coupled on this machine: nothing to do, quietly.
|
|
939
961
|
|
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");
|
|
@@ -844,7 +844,7 @@ async function disconnect() {
|
|
|
844
844
|
say(" Done. Revoke the key in WorkTrust to finish.");
|
|
845
845
|
}
|
|
846
846
|
|
|
847
|
-
function status() {
|
|
847
|
+
async function status() {
|
|
848
848
|
const servers = {};
|
|
849
849
|
try {
|
|
850
850
|
const config = JSON.parse(readFileSync(join(homedir(), ".claude.json"), "utf8"));
|
|
@@ -863,6 +863,33 @@ function status() {
|
|
|
863
863
|
if (existsSync(join(homedir(), ".hermes"))) { let text = ""; try { text = readFileSync(join(homedir(), ".hermes", "config.yaml"), "utf8"); } catch { /* none */ } say(/worktrust\.mjs\\?" hook --hermes/.test(text) ? " Hermes Agent: wakes the session hook" : " Hermes Agent: does not wake the session hook (npx worktrust@latest update writes it)"); }
|
|
864
864
|
if (antigravityHere()) say(agyHooks()?.worktrust ? " Antigravity: wakes the session hook" : " Antigravity: does not wake the session hook (npx worktrust@latest update asks)");
|
|
865
865
|
say(existsSync(join(HOME_DIR, "pinned")) ? " Counter: from the package, pinned" : existsSync(join(HOME_DIR, "count-behaviour.mjs")) ? " Counter: follows the site" : " Counter: not installed");
|
|
866
|
+
await measuredHere();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* WHAT THIS COMPUTER MEASURES (0.8.3): the last thirty days of its own sessions, as the hook derives them and sends them
|
|
871
|
+
* with each stretch (kinds and counts, never text). Read here by the hook beside this file; nothing is sent to show it.
|
|
872
|
+
*/
|
|
873
|
+
async function measuredHere() {
|
|
874
|
+
const hook = [join(HERE, "log-session.mjs"), join(HOME_DIR, "log-session.mjs")].find((path) => existsSync(path));
|
|
875
|
+
if (!hook) return;
|
|
876
|
+
// Through the CLI's one way of starting a script (never a synchronous child: check-computer-pairing), with no door to send to.
|
|
877
|
+
const read = await run(hook, ["--evidence-summary"], "", true, { WORKTRUST_MCP_URL: "", WORKTRUST_HOOK_INPUT: "" });
|
|
878
|
+
let m = null;
|
|
879
|
+
try { m = JSON.parse(read.out.trim().split("\n").filter((text) => text.startsWith("{")).pop()); } catch { return; }
|
|
880
|
+
if (!m || !m.stretches) { say(" Measured here, last 30 days: no finished work stretch yet."); return; }
|
|
881
|
+
const of = (part, whole, unit) => `${part} of ${whole} ${unit}`;
|
|
882
|
+
say();
|
|
883
|
+
say(` Measured here, last 30 days: ${m.stretches} work stretches · ${m.hours} h`);
|
|
884
|
+
if (m.delivered) say(` verified before delivery ${of(m.verified_first, m.delivered, "stretches that delivered")}`);
|
|
885
|
+
if (m.failures) say(` recovered failures ${of(m.recovered, m.failures, "failed tool calls")} (${m.blind_retries} retried unchanged)`);
|
|
886
|
+
if (m.steers) say(` steering that worked ${of(m.effective_steers, m.steers, "times you stepped in")}`);
|
|
887
|
+
if (m.refused || m.guard_denied) say(` refused ${m.refused} by you · ${m.guard_denied} by a guard`);
|
|
888
|
+
if (m.planned) say(` planned ${m.planned} stretches with a to-do list`);
|
|
889
|
+
if (m.files) say(` tests among changed files ${of(m.test_files, m.files, "files")}`);
|
|
890
|
+
const tiers = Object.entries(m.complexity ?? {}).sort();
|
|
891
|
+
if (tiers.length) say(` task complexity ${tiers.map(([tier, n]) => `${tier} ${n}`).join(" · ")}`);
|
|
892
|
+
say(" Sent with each stretch as kinds and counts, never text. Keep your own copy: npx worktrust@latest preserve");
|
|
866
893
|
}
|
|
867
894
|
|
|
868
895
|
/** Claude Code's `cleanupPeriodDays` (its default 30), or null without Claude Code here. Read only: preserve.mjs changes it, on a yes. */
|
|
@@ -928,6 +955,6 @@ else if (command === "sources" || command === "web" || command === "import") { s
|
|
|
928
955
|
else if (command === "codex") await offerCodex();
|
|
929
956
|
else if (command === "antigravity") await offerAntigravity();
|
|
930
957
|
else if (command === "disconnect") await disconnect();
|
|
931
|
-
else if (command === "status") status();
|
|
958
|
+
else if (command === "status") await status();
|
|
932
959
|
else if (command === "preserve") await preserve();
|
|
933
960
|
else help();
|