worktrust 0.8.6 → 0.8.8
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 +2 -0
- package/log-session.mjs +21 -1
- package/package.json +1 -1
- package/preserve-lines.mjs +19 -2
- package/stretch-evidence.mjs +86 -3
- package/transcript-readers.mjs +9 -3
- package/worktrust.mjs +1 -1
package/README.md
CHANGED
|
@@ -171,6 +171,8 @@ nothing it does not:
|
|
|
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
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) |
|
|
174
|
+
| `autonomy`, `authorship` | the permission mode the AI app ran in, on one scale (ask, edits, plan, auto, full), how often it changed and the turns in plan mode; of the stretch's commits, how many name an AI as co-author (the trailer is read here, its names never leave) (0.8.7) |
|
|
175
|
+
| `framing`, `quality`, `risk`, `tool_mix`, `reads`, `context_files` | how the work was framed (your turns before the agent's first action, a plan first or not), checked (a check passing after the last change, the failures before a check passed, the change looked at before delivery; security scans count as checks), risked (destructive commands proposed, refused, run), which kinds of tools were used, files read again unchanged, and which of the AI app's own context files (instructions, skills, agents, commands, settings, MCP) the commits changed; kinds and counts only (0.8.8) |
|
|
174
176
|
| `collector_version` | the CLI version that measured it |
|
|
175
177
|
| `seq`, `prev`, `hash` | the line's place, the hash of the line before it, and its own hash |
|
|
176
178
|
|
package/log-session.mjs
CHANGED
|
@@ -135,6 +135,22 @@ const DAY_SECONDS = 86400;
|
|
|
135
135
|
const TOOL_CAP = 1800;
|
|
136
136
|
/** Tools whose result waits for the person: the gap to it is the person's time. Names only; nothing of the call is read. */
|
|
137
137
|
const WAITS_FOR_PERSON = new Set(["AskUserQuestion", "ExitPlanMode"]);
|
|
138
|
+
/**
|
|
139
|
+
* CONTEXT ENGINEERING (0.8.8, framework L6): which of an AI app's own context files the stretch changed, by category,
|
|
140
|
+
* counted; read from the changed paths here, a path never leaves. Instructions (CLAUDE.md, AGENTS.md, GEMINI.md, Cursor
|
|
141
|
+
* rules, Copilot instructions), skills, subagents, commands, the client's settings (permissions, hooks), MCP servers.
|
|
142
|
+
*/
|
|
143
|
+
const CONTEXT_FILES = [
|
|
144
|
+
["instructions", /(^|[\\/])(CLAUDE|AGENTS|GEMINI)\.md$|(^|[\\/])\.cursorrules$|(^|[\\/])\.cursor[\\/]rules[\\/]|copilot-instructions\.md$|\.instructions\.md$|(^|[\\/])\.windsurfrules$/i],
|
|
145
|
+
["skills", /(^|[\\/])\.claude[\\/]skills[\\/]|(^|[\\/])SKILL\.md$/],
|
|
146
|
+
["agents", /(^|[\\/])\.claude[\\/]agents[\\/]|(^|[\\/])\.github[\\/]agents[\\/]/],
|
|
147
|
+
["commands", /(^|[\\/])\.claude[\\/]commands[\\/]|(^|[\\/])\.github[\\/]prompts[\\/]/],
|
|
148
|
+
["settings", /(^|[\\/])\.claude[\\/]settings(\.local)?\.json$|(^|[\\/])\.codex[\\/]config\.toml$/],
|
|
149
|
+
["mcp", /(^|[\\/])\.?mcp\.json$|(^|[\\/])\.cursor[\\/]mcp\.json$|(^|[\\/])\.vscode[\\/]mcp\.json$/],
|
|
150
|
+
];
|
|
151
|
+
const contextFilesOf = (paths) => { const out = {}; for (const path of paths) for (const [kind, pattern] of CONTEXT_FILES) if (pattern.test(path)) { out[kind] = (out[kind] ?? 0) + 1; break; } return Object.keys(out).length ? out : null; };
|
|
152
|
+
/** An AI named as a commit's co-author by the tools that sign their work that way (read locally; the name never leaves). */
|
|
153
|
+
const AI_COAUTHOR = /\b(claude|anthropic|codex|openai|chatgpt|copilot|cursor|devin|gemini|jules|aider|windsurf|cline)\b/i;
|
|
138
154
|
/** A path that holds tests, by the conventions of the common test runners (read here; the path never leaves). */
|
|
139
155
|
const TEST_PATH = /(^|[\\/])(__tests__|tests?|spec|e2e)[\\/]|\.(test|spec)\.[cm]?[jt]sx?$|_test\.(go|py|rb)$|(^|[\\/])test_[^\\/]+\.py$|Tests?\.(java|kt|cs|swift)$/;
|
|
140
156
|
/** The derived record of a stretch, kept for the local archive only (see payloadFor). */
|
|
@@ -397,6 +413,8 @@ const messageOf = (line, at) => {
|
|
|
397
413
|
// out, the hook counted it as a turn and the gap before it as the person's time. It is the loop's own, like a meta line.
|
|
398
414
|
at, type: line.type, usage: line.message?.usage ?? null, model: line.message?.model ?? null, messageId: typeof line.message?.id === "string" ? line.message.id : null, meta: line.isMeta === true || line.isCompactSummary === true, content,
|
|
399
415
|
compacted: line.isCompactSummary === true,
|
|
416
|
+
// 0.8.7: the permission mode the client recorded for this turn (Claude Code's own word; a reader's for Codex), never more.
|
|
417
|
+
permissionMode: typeof line.permissionMode === "string" ? line.permissionMode : null,
|
|
400
418
|
// A tool's answer comes back on the user role; a line a reader marks `agentic` is an agent run (Copilot's asked → completed).
|
|
401
419
|
kind: line.type === "assistant" ? "assistant" : blocks.some((block) => block && block.type === "tool_result") ? "tool_result" : "user",
|
|
402
420
|
toolUse: line.type === "assistant" && blocks.some((block) => block && block.type === "tool_use"),
|
|
@@ -602,7 +620,9 @@ function payloadFor(stretch) {
|
|
|
602
620
|
// 0.8.1: what the stretch changed, as counts of files (never their paths): how many, and how many of them tests.
|
|
603
621
|
// 0.7.6: and the stretch's complexity class, read from what the hook knows here (layers, subagents, duration) and the record.
|
|
604
622
|
const changed = [...new Set(touched.paths)], tests = changed.filter((path) => TEST_PATH.test(path)).length;
|
|
605
|
-
|
|
623
|
+
// 0.8.7: of the stretch's commits, how many name an AI as co-author (the Co-authored-by trailer, read here, never kept).
|
|
624
|
+
const coauthored = cwd && touched.shas.length > 0 ? git(cwd, "log", "--no-walk", "--format=%(trailers:key=Co-authored-by,valueonly,separator=%x01)%x00", ...touched.shas.slice(0, 50)).split("\0").filter((entry, index) => index < Math.min(50, touched.shas.length) && AI_COAUTHOR.test(entry)).length : 0;
|
|
625
|
+
const derived = stretch.derived ? { ...stretch.derived, ...(changed.length > 0 ? { changes: { files: changed.length, test_files: tests } } : {}), ...(touched.shas.length > 0 ? { authorship: { commits: Math.min(50, touched.shas.length), ai_coauthored: coauthored } } : {}), ...(contextFilesOf(changed) ? { context_files: contextFilesOf(changed) } : {}), ...(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;
|
|
606
626
|
const payload = {
|
|
607
627
|
title: layer ? `AI-assisted work · ${layer}` : "AI-assisted work",
|
|
608
628
|
kind,
|
package/package.json
CHANGED
package/preserve-lines.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
|
|
11
11
|
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
12
|
-
const CHECK_KINDS = ["test", "typecheck", "lint", "build", "gate", "ci"], DELIVERY_KINDS = ["commit", "pr", "push", "deploy"];
|
|
12
|
+
const CHECK_KINDS = ["test", "typecheck", "lint", "build", "gate", "ci", "security"], DELIVERY_KINDS = ["commit", "pr", "push", "deploy"];
|
|
13
13
|
|
|
14
14
|
/** THE ALLOWLIST: what a line may carry, each value checked for its type. A key not named here never reaches the archive. */
|
|
15
15
|
export const COUNTS = ["seconds", "tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write", "model_seconds", "tool_seconds", "human_seconds", "idle_seconds", "agent_runs", "agent_seconds", "agent_peak", "interrupts", "steers"];
|
|
@@ -98,6 +98,23 @@ export function archiveLine(entry, behaviour = null, { ids = {}, collector, salt
|
|
|
98
98
|
if (planning && planning.lists > 0 && planning.done_last <= planning.items_last && planning.items_last <= planning.items_max) line.planning = planning;
|
|
99
99
|
const changes = pick(entry.changes, ["files", "test_files"]);
|
|
100
100
|
if (changes && changes.test_files <= changes.files) line.changes = changes;
|
|
101
|
+
// AUTONOMY AND AUTHORSHIP (0.8.7): the permission mode on one scale; commits and those naming an AI as co-author.
|
|
102
|
+
if (entry.autonomy && ["ask", "edits", "plan", "auto", "full"].includes(entry.autonomy.mode) && whole(entry.autonomy.switches) && whole(entry.autonomy.plan_turns)) line.autonomy = { mode: entry.autonomy.mode, switches: entry.autonomy.switches, plan_turns: entry.autonomy.plan_turns };
|
|
103
|
+
const authorship = pick(entry.authorship, ["commits", "ai_coauthored"]);
|
|
104
|
+
if (authorship && authorship.commits > 0 && authorship.ai_coauthored <= authorship.commits) line.authorship = authorship;
|
|
105
|
+
// PRACTICE (0.8.8): framing, verification quality, risk, tool mix, re-reads, context files; whole counts and flags only.
|
|
106
|
+
const bool = (value) => value === true || value === false;
|
|
107
|
+
if (entry.framing && whole(entry.framing.turns_before_action) && bool(entry.framing.planned_first)) line.framing = { turns_before_action: entry.framing.turns_before_action, planned_first: entry.framing.planned_first };
|
|
108
|
+
if (entry.quality && whole(entry.quality.fix_cycles_max) && (bool(entry.quality.checked_after_change) || entry.quality.checked_after_change === null) && (bool(entry.quality.inspected_first) || entry.quality.inspected_first === null)) line.quality = { checked_after_change: entry.quality.checked_after_change, fix_cycles_max: entry.quality.fix_cycles_max, inspected_first: entry.quality.inspected_first };
|
|
109
|
+
const risk = pick(entry.risk, ["proposed", "refused", "run"]);
|
|
110
|
+
if (risk && risk.proposed > 0 && risk.refused + risk.run <= risk.proposed) line.risk = risk;
|
|
111
|
+
const keyed = (record, keys) => { const kept = Object.entries(record && typeof record === "object" ? record : {}).filter(([key, n]) => keys.includes(key) && whole(n) && n > 0).sort(); return kept.length ? Object.fromEntries(kept) : null; };
|
|
112
|
+
const mix = keyed(entry.tool_mix, ["search", "read", "edit", "shell", "agent", "web", "data", "mcp", "plan", "other"]);
|
|
113
|
+
if (mix) line.tool_mix = mix;
|
|
114
|
+
const reads = pick(entry.reads, ["repeated"]);
|
|
115
|
+
if (reads) line.reads = reads;
|
|
116
|
+
const files = keyed(entry.context_files, ["instructions", "skills", "agents", "commands", "settings", "mcp"]);
|
|
117
|
+
if (files) line.context_files = files;
|
|
101
118
|
// The behaviour signals of this stretch (0.7.0): keys of the counter's rubric and whole counts, never a word of a turn.
|
|
102
119
|
if (behaviour && behaviour.signals && typeof behaviour.analyzer_version === "string" && /^counter@\d+\.\d+\.\d+$/.test(behaviour.analyzer_version)) {
|
|
103
120
|
const kept = Object.entries(behaviour.signals).filter(([key, count]) => SIGNAL.test(key) && Number.isInteger(count) && count > 0).sort();
|
|
@@ -145,7 +162,7 @@ export function archiveEntries(dir, ownKey) {
|
|
|
145
162
|
* a line of their own that names the stretch it adds to (`supplements`) and carries only what no earlier line of that
|
|
146
163
|
* stretch carries, in the same chain, under the same day's root. Null when there is nothing new.
|
|
147
164
|
*/
|
|
148
|
-
export const DERIVED_KEYS = ["signals", "analyzer_version", "verification", "unconfirmed", "delivery", "recovery", "delegation", "context", "routing", "steering", "tools", "complexity", "oversight", "planning", "changes", "interrupts", "steers", "utc_offset"];
|
|
165
|
+
export const DERIVED_KEYS = ["signals", "analyzer_version", "verification", "unconfirmed", "delivery", "recovery", "delegation", "context", "routing", "steering", "tools", "complexity", "oversight", "planning", "changes", "autonomy", "authorship", "framing", "quality", "risk", "tool_mix", "reads", "context_files", "interrupts", "steers", "utc_offset"];
|
|
149
166
|
export function supplementFor(line, earlier) {
|
|
150
167
|
const missing = DERIVED_KEYS.filter((key) => line[key] !== undefined && !earlier.some((old) => old[key] !== undefined));
|
|
151
168
|
if (missing.length === 0) return null;
|
package/stretch-evidence.mjs
CHANGED
|
@@ -25,8 +25,12 @@ const CALL_KINDS = [
|
|
|
25
25
|
["pr", /\bgh\s+pr\s+(create|merge)\b/],
|
|
26
26
|
["push", /\bgit\s+push\b/],
|
|
27
27
|
["deploy", /\b(vercel(\s+deploy)?\s+--prod|fly\s+deploy|netlify\s+deploy|wrangler\s+deploy)\b/],
|
|
28
|
+
// 0.8.8: a security scan is a check of its own; looking at the change before delivering it; and a destructive command.
|
|
29
|
+
["security", /\b(gitleaks|trufflehog|semgrep|snyk|osv-scanner|trivy|bandit)\b|\b(npm|pnpm|yarn)\s+audit\b/],
|
|
30
|
+
["inspect", /\bgit\s+(diff|show|status)\b/],
|
|
31
|
+
["destructive", /\brm\s+-[a-z]*r[a-z]*f|\brm\s+-[a-z]*f[a-z]*r|\bgit\s+(reset\s+--hard|push\s+(-f\b|--force)|clean\s+-[a-z]*f)|\bdrop\s+(table|database|schema)\b|\btruncate\s+table\b|\bkubectl\s+delete\b|\bterraform\s+destroy\b/i],
|
|
28
32
|
];
|
|
29
|
-
const CHECK_KINDS = ["test", "typecheck", "lint", "build", "gate", "ci"];
|
|
33
|
+
const CHECK_KINDS = ["test", "typecheck", "lint", "build", "gate", "ci", "security"];
|
|
30
34
|
const DELIVERY_KINDS = ["commit", "pr", "push", "deploy"];
|
|
31
35
|
export const callKindsOf = (block) => {
|
|
32
36
|
const command = block && SHELL_TOOLS.has(String(block.name)) ? block.input?.command ?? block.input?.CommandLine ?? block.input?.cmd : null;
|
|
@@ -237,9 +241,88 @@ function planningOf(messages) {
|
|
|
237
241
|
return { lists: lists.length, items_max: Math.max(...lists.map((list) => list.items)), items_last: last.items, done_last: last.done };
|
|
238
242
|
}
|
|
239
243
|
|
|
244
|
+
/**
|
|
245
|
+
* THE AUTONOMY THE PERSON GAVE (0.8.7; framework L5, "autonomy calibration"): the permission mode the client ran in, on
|
|
246
|
+
* one common scale (ask, edits, plan, auto, full), as the mode most turns ran in, how often it changed, and how many
|
|
247
|
+
* turns ran in plan mode. Claude Code records its own word per turn; the Codex reader maps its approval policy and
|
|
248
|
+
* sandbox. Null where no client recorded one.
|
|
249
|
+
*/
|
|
250
|
+
const MODE_SCALE = { default: "ask", ask: "ask", acceptEdits: "edits", edits: "edits", plan: "plan", auto: "auto", bypassPermissions: "full", dontAsk: "full", full: "full" };
|
|
251
|
+
function autonomyOf(messages) {
|
|
252
|
+
const modes = [];
|
|
253
|
+
for (const message of messages) if (!message.bridge && message.permissionMode && MODE_SCALE[message.permissionMode]) modes.push(MODE_SCALE[message.permissionMode]);
|
|
254
|
+
if (modes.length === 0) return null;
|
|
255
|
+
const tally = {};
|
|
256
|
+
for (const mode of modes) tally[mode] = (tally[mode] ?? 0) + 1;
|
|
257
|
+
const mode = Object.entries(tally).sort((a, b) => b[1] - a[1])[0][0];
|
|
258
|
+
let switches = 0;
|
|
259
|
+
for (let i = 1; i < modes.length; i += 1) if (modes[i] !== modes[i - 1]) switches += 1;
|
|
260
|
+
return { mode, switches, plan_turns: tally.plan ?? 0 };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* HOW THE WORK WAS FRAMED, CHECKED AND RISKED (0.8.8; the owner's capability list: problem framing, verification quality,
|
|
265
|
+
* acceptance discipline, risk awareness, tool selection, context efficiency, recovery cycles). All structural, read from
|
|
266
|
+
* the order of turns and calls; nothing of their content.
|
|
267
|
+
* framing: the person's turns before the agent's first action (an edit, a write or a shell command), and whether a plan
|
|
268
|
+
* (plan mode, a plan approved, a to-do list) came before it.
|
|
269
|
+
* quality: whether a check passed AFTER the stretch's last change; the most failures one check family took before it
|
|
270
|
+
* passed; whether the change was looked at (git diff, show, status) before the first delivery step.
|
|
271
|
+
* risk: destructive commands proposed, refused (by the person or a guard) and run.
|
|
272
|
+
* tool_mix: calls per kind of tool (search, read, edit, shell, agent, web, data, mcp, plan).
|
|
273
|
+
* reads: files read again unchanged (the same read twice), the cost of context that did not hold.
|
|
274
|
+
*/
|
|
275
|
+
const ACTION_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "Bash"]);
|
|
276
|
+
const EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
277
|
+
const PLAN_TOOLS = new Set(["TodoWrite", "ExitPlanMode", "AskUserQuestion"]);
|
|
278
|
+
const toolKind = (tool) => EDIT_TOOLS.has(tool) ? "edit" : tool === "Read" ? "read" : ["Grep", "Glob", "WebSearch", "ToolSearch"].includes(tool) ? "search" : tool === "Bash" ? "shell" : ["Agent", "Task"].includes(tool) ? "agent" : /^(WebFetch|browser|mcp__.*(playwright|browser|chrome))/i.test(tool) ? "web" : /sql|postgres|supabase|database|bigquery|snowflake/i.test(tool) ? "data" : /^mcp__/.test(tool) ? "mcp" : PLAN_TOOLS.has(tool) ? "plan" : "other";
|
|
279
|
+
function practiceOf(messages, { isHumanTurn }) {
|
|
280
|
+
const results = new Map();
|
|
281
|
+
for (const message of messages) for (const result of message.results ?? []) results.set(result.id, result);
|
|
282
|
+
let turns = 0, acted = false, planned = false, turnsBefore = 0, plannedFirst = false;
|
|
283
|
+
let lastChange = -1, passedAfter = false, delivered = false, inspectedFirst = false;
|
|
284
|
+
const failuresBefore = new Map(); let cyclesMax = 0;
|
|
285
|
+
const risk = { proposed: 0, refused: 0, run: 0 }, mix = {}, reads = new Map();
|
|
286
|
+
let index = 0;
|
|
287
|
+
for (const message of messages) {
|
|
288
|
+
if (message.bridge) continue;
|
|
289
|
+
if (message.type === "user" && !message.meta && message.kind !== "tool_result" && isHumanTurn(message.content)) turns += 1;
|
|
290
|
+
if (message.permissionMode === "plan") planned = true;
|
|
291
|
+
for (const call of message.calls ?? []) {
|
|
292
|
+
index += 1;
|
|
293
|
+
const tool = call.family.split("|")[0], result = results.get(call.id);
|
|
294
|
+
mix[toolKind(tool)] = (mix[toolKind(tool)] ?? 0) + 1;
|
|
295
|
+
if (PLAN_TOOLS.has(tool) && tool !== "AskUserQuestion") planned = true;
|
|
296
|
+
if (!acted && ACTION_TOOLS.has(tool)) { acted = true; turnsBefore = turns; plannedFirst = planned; }
|
|
297
|
+
if (EDIT_TOOLS.has(tool)) { lastChange = index; passedAfter = false; }
|
|
298
|
+
if (tool === "Read") reads.set(call.digest, (reads.get(call.digest) ?? 0) + 1);
|
|
299
|
+
const checks = call.kinds.filter((kind) => CHECK_KINDS.includes(kind));
|
|
300
|
+
for (const kind of checks) {
|
|
301
|
+
if (result?.failed === true) failuresBefore.set(kind, (failuresBefore.get(kind) ?? 0) + 1);
|
|
302
|
+
if (result?.failed === false) { cyclesMax = Math.max(cyclesMax, failuresBefore.get(kind) ?? 0); failuresBefore.set(kind, 0); if (index > lastChange) passedAfter = true; }
|
|
303
|
+
}
|
|
304
|
+
if (call.kinds.includes("inspect") && !delivered) inspectedFirst = true;
|
|
305
|
+
if (call.kinds.some((kind) => DELIVERY_KINDS.includes(kind)) && result?.failed === false) delivered = true;
|
|
306
|
+
if (call.kinds.includes("destructive")) {
|
|
307
|
+
risk.proposed += 1;
|
|
308
|
+
if (result?.refusal) risk.refused += 1; else if (result?.failed === false) risk.run += 1;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (index === 0) return null;
|
|
313
|
+
const repeated = [...reads.values()].reduce((sum, n) => sum + Math.max(0, n - 1), 0);
|
|
314
|
+
return {
|
|
315
|
+
framing: { turns_before_action: acted ? turnsBefore : turns, planned_first: acted ? plannedFirst : planned },
|
|
316
|
+
quality: { checked_after_change: lastChange > 0 ? passedAfter : null, fix_cycles_max: cyclesMax, inspected_first: delivered ? inspectedFirst : null },
|
|
317
|
+
...(risk.proposed > 0 ? { risk } : {}),
|
|
318
|
+
tool_mix: mix,
|
|
319
|
+
reads: { repeated },
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
240
323
|
/** The stretch's whole derived record; `helpers` are the hook's own readers of a human turn, so both read it the same way. */
|
|
241
324
|
export function deriveStretch(messages, helpers) {
|
|
242
325
|
const context = contextOf(messages, helpers), routing = routingOf(messages), steering = steeringOf(messages, helpers);
|
|
243
|
-
const tools = toolsOf(messages), oversight = oversightOf(messages), planning = planningOf(messages);
|
|
244
|
-
return { ...(oversight ? { oversight } : {}), ...(planning ? { planning } : {}), ...verificationOf(messages, helpers), ...(context ? { context } : {}), ...(routing ? { routing } : {}), ...(steering ? { steering } : {}), ...(tools ? { tools } : {}) };
|
|
326
|
+
const tools = toolsOf(messages), oversight = oversightOf(messages), planning = planningOf(messages), autonomy = autonomyOf(messages), practice = practiceOf(messages, helpers);
|
|
327
|
+
return { ...(practice ?? {}), ...(oversight ? { oversight } : {}), ...(planning ? { planning } : {}), ...(autonomy ? { autonomy } : {}), ...verificationOf(messages, helpers), ...(context ? { context } : {}), ...(routing ? { routing } : {}), ...(steering ? { steering } : {}), ...(tools ? { tools } : {}) };
|
|
245
328
|
}
|
package/transcript-readers.mjs
CHANGED
|
@@ -56,7 +56,7 @@ export const codexToolInput = (name, raw) => {
|
|
|
56
56
|
export const codexOutputText = (output) => (typeof output === "string" ? output : Array.isArray(output) ? output.map((part) => (typeof part === "string" ? part : part?.text ?? "")).join("\n") : "");
|
|
57
57
|
/** The rollout's raw JSON lines → transcript-shaped line objects, in file order. */
|
|
58
58
|
export function* codexLines(records) {
|
|
59
|
-
let cwd = null, model = null;
|
|
59
|
+
let cwd = null, model = null, mode = null;
|
|
60
60
|
const seenResponses = new Set();
|
|
61
61
|
for (const raw of records) {
|
|
62
62
|
if (!raw || !String(raw).trim()) continue;
|
|
@@ -64,7 +64,13 @@ export function* codexLines(records) {
|
|
|
64
64
|
const timestamp = typeof record?.timestamp === "string" ? record.timestamp : null;
|
|
65
65
|
const payload = record?.payload && typeof record.payload === "object" ? record.payload : {};
|
|
66
66
|
if (record.type === "session_meta") { if (typeof payload.cwd === "string") cwd = payload.cwd; continue; }
|
|
67
|
-
if (record.type === "turn_context") {
|
|
67
|
+
if (record.type === "turn_context") {
|
|
68
|
+
if (typeof payload.cwd === "string") cwd = payload.cwd; if (typeof payload.model === "string") model = payload.model;
|
|
69
|
+
// 0.8.7: Codex's approval policy and sandbox, as one word on the common scale (never asks and full access is "full").
|
|
70
|
+
const approval = String(payload.approval_policy ?? ""), sandbox = String(payload.sandbox_policy?.type ?? payload.sandbox_policy ?? payload.sandbox_mode ?? "");
|
|
71
|
+
mode = approval === "never" ? (sandbox === "danger-full-access" ? "full" : "auto") : approval === "on-failure" ? "edits" : approval ? "ask" : mode;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
68
74
|
if (record.type === "token_usage_record") {
|
|
69
75
|
const id = String(payload.response_id ?? "");
|
|
70
76
|
const usage = payload.usage;
|
|
@@ -80,7 +86,7 @@ export function* codexLines(records) {
|
|
|
80
86
|
if (payload.role !== "user" && payload.role !== "assistant") continue;
|
|
81
87
|
const text = Array.isArray(payload.content) ? payload.content.filter((part) => typeof part?.text === "string").map((part) => part.text).join("\n") : typeof payload.content === "string" ? payload.content : "";
|
|
82
88
|
if (payload.role === "user" && CODEX_HARNESS_TURN.test(text)) continue;
|
|
83
|
-
yield { type: payload.role, timestamp, cwd, uuid: payload.id ? `codex-${payload.id}` : undefined, message: { ...(payload.role === "assistant" && model ? { model } : {}), content: [{ type: "text", text }] } };
|
|
89
|
+
yield { type: payload.role, timestamp, cwd, ...(mode ? { permissionMode: mode } : {}), uuid: payload.id ? `codex-${payload.id}` : undefined, message: { ...(payload.role === "assistant" && model ? { model } : {}), content: [{ type: "text", text }] } };
|
|
84
90
|
continue;
|
|
85
91
|
}
|
|
86
92
|
if (kind === "custom_tool_call" || kind === "function_call") {
|
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.8";
|
|
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");
|