worktrust 0.7.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/log-session.mjs +44 -4
- package/package.json +1 -1
- package/preserve-lines.mjs +35 -1
- package/stretch-evidence.mjs +25 -1
- package/worktrust.mjs +23 -1
package/README.md
CHANGED
|
@@ -81,6 +81,8 @@ yourself run a command that sends. See [Keep your history](#keep-your-history).
|
|
|
81
81
|
npx worktrust@latest history send this computer's earlier sessions later (asks first)
|
|
82
82
|
npx worktrust@latest history --rebuild re-measure them with the current rules; WorkTrust replaces this computer's
|
|
83
83
|
earlier lines day by day, never counting a day twice (shows the plan, asks first)
|
|
84
|
+
npx worktrust@latest history --from-archive [dir] send what your local archive holds of sessions your AI apps
|
|
85
|
+
already deleted, marked as from your archive (shows the plan, asks first)
|
|
84
86
|
npx worktrust codex let Codex wake the session hook (asks first)
|
|
85
87
|
npx worktrust antigravity let Antigravity wake the session hook (asks first)
|
|
86
88
|
npx worktrust@latest status what is coupled here
|
|
@@ -164,6 +166,7 @@ nothing it does not:
|
|
|
164
166
|
| `delegation` | the longest and the middle chain of actions the agent took on its own between your turns (or your stopping it), and how often it stopped to ask you a question or to have a plan approved (0.7.3) |
|
|
165
167
|
| `context`, `routing` | how often the context was compacted and which context commands you used (compact, clear, resume, context, model, memory; never a command of your own), how many models answered and how often the model changed (0.7.4) |
|
|
166
168
|
| `steering` | the moments you stepped in while the agent worked (stopping it, or a message mid-task), how many changed what the agent did next, and how many were followed by a passing check (0.7.5) |
|
|
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) |
|
|
167
170
|
| `collector_version` | the CLI version that measured it |
|
|
168
171
|
| `seq`, `prev`, `hash` | the line's place, the hash of the line before it, and its own hash |
|
|
169
172
|
|
|
@@ -197,6 +200,13 @@ and more AI apps (Cursor, Copilot, Hermes, Goose, OpenCode, OpenClaw, Codex, Ant
|
|
|
197
200
|
sent stand as they were measured then, and sending the sessions again would count those days twice: an old line
|
|
198
201
|
does not name its stretch, so WorkTrust cannot tell it from the one that arrives.
|
|
199
202
|
|
|
203
|
+
`npx worktrust@latest history --from-archive` (0.8.0) is for the sessions an AI app has already deleted (Claude
|
|
204
|
+
Code's default is 30 days). It sends what your local archive (`preserve --archive`) holds of them, and only that: days
|
|
205
|
+
this computer signed with its own key, of sessions no longer on the computer, each line marked as coming from your
|
|
206
|
+
archive. It refuses, and sends nothing, when the computer has no device key, when the archive does not verify, or when
|
|
207
|
+
WorkTrust does not yet accept the mark. It shows the plan first and asks once; Enter is No. Such lines are history,
|
|
208
|
+
like any import: never verified hours.
|
|
209
|
+
|
|
200
210
|
`npx worktrust@latest history --rebuild` re-measures every session still on this computer with the current rules and
|
|
201
211
|
sends them with one difference: WorkTrust **replaces** this computer's earlier lines, day by day.
|
|
202
212
|
|
package/log-session.mjs
CHANGED
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
* CONFIDENTIAL PROJECTS never open: `~/.worktrust-counter.json` { "exclude": ["client-x"] } is
|
|
55
55
|
* honoured here too, matched against the transcript's project directory, before a file is read.
|
|
56
56
|
*/
|
|
57
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
57
|
+
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
58
58
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
59
59
|
import { homedir } from "node:os";
|
|
60
60
|
import { dirname, join } from "node:path";
|
|
61
61
|
import { fileURLToPath } from "node:url";
|
|
62
|
-
import { createHash, randomBytes, sign as cryptoSign } from "node:crypto";
|
|
62
|
+
import { createHash, createPrivateKey, createPublicKey, randomBytes, sign as cryptoSign } from "node:crypto";
|
|
63
63
|
|
|
64
64
|
const args = process.argv.slice(2);
|
|
65
65
|
const flag = (name) => args.includes(`--${name}`), value = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : undefined; }, DRY = flag("dry-run");
|
|
@@ -211,7 +211,7 @@ const excluded = (path) => excludes.some((text) => text && path.includes(text));
|
|
|
211
211
|
const { CODEX_ROLLOUT = /(?!)/, codexIdOf = (file) => String(file).split(/[\\/]/).at(-1).replace(/\.jsonl$/, ""), antigravityIdOf = (file) => String(file).split(/[\\/]/).at(-4), codexLines, codexRolloutFiles = function* () {}, codexCwd = () => null, ANTIGRAVITY_TRANSCRIPT = /(?!)/, antigravityLines, antigravityRoots = () => [], antigravityTranscripts = function* () {}, antigravityContext = () => ({}), rememberAntigravity = () => null } = (await import("./transcript-readers.mjs").catch(() => null)) ?? {};
|
|
212
212
|
// THE STRETCH'S DERIVED RECORD (0.7.4: split from this file): tool calls named by kind, verification, recovery, delegation,
|
|
213
213
|
// context and routing, for the local archive only. A copy without the module still measures and sends exactly as before.
|
|
214
|
-
const { callOf = (block) => ({ id: block.id, kinds: [], family: String(block.name), digest: "" }), deriveStretch = () => ({}) } = (await import("./stretch-evidence.mjs").catch(() => null)) ?? {};
|
|
214
|
+
const { callOf = (block) => ({ id: block.id, kinds: [], family: String(block.name), digest: "" }), deriveStretch = () => ({}), complexityOf = null } = (await import("./stretch-evidence.mjs").catch(() => null)) ?? {};
|
|
215
215
|
const { DATABASE_SESSION = /(?!)/, databaseLines = () => null, databaseSessions = function* () {} } = (await import("./session-databases.mjs").catch(() => null)) ?? {};
|
|
216
216
|
const DATABASE_CLIENTS = ["hermes", "goose", "opencode", "openclaw", "cursor", "copilot"]; // the registry's keys, the name each line carries
|
|
217
217
|
const LIVE_SOURCE = new RegExp(`^(codex|antigravity|${DATABASE_CLIENTS.join("|")}):`); // a sweep entry's id → the client it names (every database client, 0.6.16)
|
|
@@ -623,7 +623,8 @@ function payloadFor(stretch) {
|
|
|
623
623
|
at: new Date(stretch.to).toISOString(),
|
|
624
624
|
};
|
|
625
625
|
// WHAT STAYS ON THIS COMPUTER (0.7.1): the stretch's derived record rides beside the payload for the local archive, never in it.
|
|
626
|
-
|
|
626
|
+
// 0.7.6: and the stretch's complexity class, read from what the hook knows here (layers, subagents, duration) and the record.
|
|
627
|
+
if (stretch.derived) DERIVED.set(payload, { ...stretch.derived, ...(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 }) } : {}) });
|
|
627
628
|
return payload;
|
|
628
629
|
}
|
|
629
630
|
|
|
@@ -657,6 +658,19 @@ async function sendHistory(door, entries, announce = false, rebuild = false) {
|
|
|
657
658
|
return accepted;
|
|
658
659
|
}
|
|
659
660
|
|
|
661
|
+
/** Whether the door's import_history takes `from_archive` (0.8.0): asked with tools/list, never assumed. */
|
|
662
|
+
async function doorKnowsArchive(target) {
|
|
663
|
+
if (!target) return false;
|
|
664
|
+
try {
|
|
665
|
+
const body = JSON.stringify({ jsonrpc: "2.0", id: 0, method: "tools/list" });
|
|
666
|
+
const response = await fetch(target.url, { method: "POST", headers: { "content-type": "application/json", accept: "application/json, text/event-stream", authorization: `Bearer ${target.token}`, ...proofFor(target.url, body) }, body });
|
|
667
|
+
const text = await response.text();
|
|
668
|
+
const json = JSON.parse(text.trim().startsWith("{") ? text : text.split("\n").find((row) => row.startsWith("data:"))?.slice(5) ?? "null");
|
|
669
|
+
const tool = json?.result?.tools?.find((one) => one.name === "import_history");
|
|
670
|
+
return Boolean(tool?.inputSchema?.properties?.entries?.items?.properties?.from_archive);
|
|
671
|
+
} catch { return false; }
|
|
672
|
+
}
|
|
673
|
+
|
|
660
674
|
/** DEVICE PROOF (2026-10-03): through `worktrust hook` a bound computer hands over WORKTRUST_DEVICE_KEY; every call is then signed. */
|
|
661
675
|
const proofFor = (url, body) => {
|
|
662
676
|
const device = process.env.WORKTRUST_DEVICE_KEY;
|
|
@@ -919,6 +933,32 @@ const entryFile = new WeakMap(); // a history entry → its transcript (`--summa
|
|
|
919
933
|
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
|
|
920
934
|
if (!door && !codexDoor && !DRY) { process.exit(0); } // Not coupled on this machine: nothing to do, quietly.
|
|
921
935
|
|
|
936
|
+
/**
|
|
937
|
+
* HISTORY FROM THE ARCHIVE (0.8.0; framework §22). When an AI app has deleted its sessions, the stretches this computer
|
|
938
|
+
* archived (`preserve --archive`) can still be filed, marked `from_archive`. Five conditions, each refused by name: a
|
|
939
|
+
* device key (only lines this computer signed travel), an archive that verifies, a stretch whose session is gone from
|
|
940
|
+
* this computer (one still here goes through the ordinary history), a door that knows the mark, and the person asking.
|
|
941
|
+
*/
|
|
942
|
+
if (flag("history") && flag("from-archive")) {
|
|
943
|
+
const named = value("from-archive"), dir = named && !named.startsWith("--") ? named.replace(/^~(?=\/|$)/, homedir()) : join(homedir(), "AI-Evidence");
|
|
944
|
+
const say = (text) => console.log(text);
|
|
945
|
+
const pem = process.env.WORKTRUST_DEVICE_KEY;
|
|
946
|
+
let ownKey = null;
|
|
947
|
+
try { ownKey = pem ? createPublicKey(createPrivateKey(pem)).export({ format: "jwk" }).x : null; } catch { ownKey = null; }
|
|
948
|
+
if (!ownKey) { say("No device key on this computer: only lines this computer signed can be sent from its archive. Nothing was sent."); await exitFlushed(1); }
|
|
949
|
+
const verified = spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), "preserve.mjs"), "--verify", dir], { encoding: "utf8" });
|
|
950
|
+
if (verified.status !== 0) { say(`The archive in ${dir} does not verify, so nothing was sent: ${(verified.stdout || verified.stderr || "").trim().split("\n").pop()}`); await exitFlushed(1); }
|
|
951
|
+
const { archiveEntries = null } = (await import("./preserve-lines.mjs").catch(() => null)) ?? {};
|
|
952
|
+
if (!archiveEntries) { say("preserve-lines.mjs is not beside this file. Nothing was sent."); await exitFlushed(1); }
|
|
953
|
+
const present = new Set(historyEntries(null, true).map((entry) => entry.stretch_ref));
|
|
954
|
+
const signed = archiveEntries(dir, ownKey), gone = signed.filter(({ entry }) => !present.has(entry.stretch_ref));
|
|
955
|
+
const entries = gone.map(({ entry, client }) => { if (client !== "claude") entrySource.set(entry, client); return entry; });
|
|
956
|
+
say(`${signed.length} stretches in the archive signed by this computer · ${entries.length} of them no longer in any AI app here`);
|
|
957
|
+
if (DRY || entries.length === 0) { if (entries[0]) say(`\nthe oldest of them, in full:\n${JSON.stringify(entries[0], null, 1)}`); say(entries.length === 0 ? "Nothing to send." : "\ndry run — nothing sent."); await exitFlushed(0); }
|
|
958
|
+
if (!(await doorKnowsArchive(door))) { say("WorkTrust does not accept history from an archive yet (the door does not know the mark). Nothing was sent."); await exitFlushed(1); }
|
|
959
|
+
await exitFlushed((await sendHistory(door, entries, true)) === entries.length ? 0 : 1);
|
|
960
|
+
}
|
|
961
|
+
|
|
922
962
|
if (flag("history")) {
|
|
923
963
|
const historyFloor = floorOf(readState(), "__historyFloor");
|
|
924
964
|
const entries = historyEntries(historyFloor);
|
package/package.json
CHANGED
package/preserve-lines.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
8
|
+
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");
|
|
@@ -80,6 +80,9 @@ export function archiveLine(entry, behaviour = null, { ids = {}, collector } = {
|
|
|
80
80
|
const { moments, changed_course: changed, effective } = entry.steering;
|
|
81
81
|
if ([moments, changed, effective].every(whole) && moments > 0 && changed <= moments && effective <= changed) line.steering = { moments, changed_course: changed, effective };
|
|
82
82
|
}
|
|
83
|
+
// TOOL BREADTH AND COMPLEXITY (0.7.6): distinct tools; a class C1–C5 with its points and the rule's version.
|
|
84
|
+
if (whole(entry.tools) && entry.tools > 0) line.tools = entry.tools;
|
|
85
|
+
if (entry.complexity && /^C[1-5]$/.test(entry.complexity.class ?? "") && whole(entry.complexity.points) && /^complexity\/\d+$/.test(entry.complexity.rule ?? "")) line.complexity = { class: entry.complexity.class, points: entry.complexity.points, rule: entry.complexity.rule };
|
|
83
86
|
// The behaviour signals of this stretch (0.7.0): keys of the counter's rubric and whole counts, never a word of a turn.
|
|
84
87
|
if (behaviour && behaviour.signals && typeof behaviour.analyzer_version === "string" && /^counter@\d+\.\d+\.\d+$/.test(behaviour.analyzer_version)) {
|
|
85
88
|
const kept = Object.entries(behaviour.signals).filter(([key, count]) => SIGNAL.test(key) && Number.isInteger(count) && count > 0).sort();
|
|
@@ -88,3 +91,34 @@ export function archiveLine(entry, behaviour = null, { ids = {}, collector } = {
|
|
|
88
91
|
return { ...line, stretch_ref: entry.stretch_ref, ...ids, collector_version: collector };
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* THE ARCHIVE AS HISTORY (0.8.0; framework §22: "preserve as evidence continuity"). The lines of an archive this
|
|
97
|
+
* computer wrote AND signed (a day whose proof carries this computer's key), as `import_history` entries, each marked
|
|
98
|
+
* `from_archive` so the record shows it came from the person's own archive after the AI app had deleted the session.
|
|
99
|
+
* The caller verifies the archive first, sends only stretches whose session is gone, and only to a door that knows the
|
|
100
|
+
* mark. Nothing here sends.
|
|
101
|
+
*/
|
|
102
|
+
const read = (file) => { try { return JSON.parse(readFileSync(file, "utf8")); } catch { return null; } };
|
|
103
|
+
const readdir = (dir) => { try { return readdirSync(dir); } catch { return []; } };
|
|
104
|
+
export function archiveEntries(dir, ownKey) {
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const year of readdir(dir).filter((name) => /^\d{4}$/.test(name)).sort()) for (const month of readdir(join(dir, year)).filter((name) => /^\d{2}$/.test(name)).sort()) {
|
|
107
|
+
const file = join(dir, year, month, "stretches.jsonl");
|
|
108
|
+
if (!existsSync(file)) continue;
|
|
109
|
+
for (const text of readFileSync(file, "utf8").split("\n").filter(Boolean)) {
|
|
110
|
+
let line; try { line = JSON.parse(text); } catch { continue; }
|
|
111
|
+
const proof = read(join(dir, "proofs", `${line.day}.json`));
|
|
112
|
+
if (!ownKey || !proof || typeof proof.signature !== "string" || proof.public_key !== ownKey) continue;
|
|
113
|
+
const entry = {
|
|
114
|
+
title: line.layer ? `AI-assisted work · ${line.layer}` : "AI-assisted work",
|
|
115
|
+
...(line.kind ? { kind: line.kind } : {}), ...(line.layer ? { layer: line.layer } : {}), ...(line.layers ? { layers: line.layers } : {}),
|
|
116
|
+
...(typeof line.seconds === "number" ? { seconds: line.seconds, duration_basis: line.duration_basis ?? "measured" } : {}),
|
|
117
|
+
...Object.fromEntries(["tokens_in", "tokens_out", "token_basis", "tokens_cache_read", "tokens_cache_write", "exchanges", "turn_basis", "model", "steered_from", "model_seconds", "tool_seconds", "human_seconds", "idle_seconds", "agent_runs", "agent_seconds", "agent_peak", "interrupts", "steers", "utc_offset"].filter((key) => line[key] !== undefined).map((key) => [key, line[key]])),
|
|
118
|
+
stretch_ref: line.stretch_ref, started_at: line.started_at, at: line.ended_at, from_archive: true,
|
|
119
|
+
};
|
|
120
|
+
out.push({ entry, client: line.client ?? "claude" });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
package/stretch-evidence.mjs
CHANGED
|
@@ -171,8 +171,32 @@ function steeringOf(messages, { isHumanTurn, INTERRUPTED, textOf }) {
|
|
|
171
171
|
return { moments: moments.length, changed_course: changed.length, effective: changed.filter((moment) => moment.effective).length };
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* TASK COMPLEXITY C1 TO C5 (0.7.6; framework §14 "Task complexity C1-C5"), by a VERSIONED rule, `complexity/1`, every
|
|
176
|
+
* point of it stated here and in the README so anyone can recount a class. Proposed, not calibrated: it weighs evidence,
|
|
177
|
+
* it is not a score of anyone.
|
|
178
|
+
* layers touched ≤1: 0 · 2: 1 · 3+: 2 distinct tools used <3: 0 · 3–6: 1 · 7+: 2
|
|
179
|
+
* subagents none: 0 · ran: 1 · 3+ at once: 2
|
|
180
|
+
* measured duration <30 min: 0 · 30–120 min: 1 · >120 min: 2
|
|
181
|
+
* a failure recovered 1 kinds of check run 0: 0 · 1–2: 1 · 3+: 2
|
|
182
|
+
* something delivered 1
|
|
183
|
+
* points 0–1 → C1 · 2–3 → C2 · 4–6 → C3 · 7–9 → C4 · 10+ → C5
|
|
184
|
+
*/
|
|
185
|
+
export const COMPLEXITY_RULE = "complexity/1";
|
|
186
|
+
export function complexityOf(derived, { layers = 0, agentRuns = 0, agentPeak = 0, seconds = null } = {}) {
|
|
187
|
+
const step = (value, low, high) => (value >= high ? 2 : value >= low ? 1 : 0);
|
|
188
|
+
const points = step(layers, 2, 3) + step(derived.tools ?? 0, 3, 7) + (agentPeak >= 3 ? 2 : agentRuns > 0 ? 1 : 0)
|
|
189
|
+
+ (seconds === null ? 0 : seconds > 7200 ? 2 : seconds >= 1800 ? 1 : 0) + ((derived.recovery?.recovered ?? 0) > 0 ? 1 : 0)
|
|
190
|
+
+ step(Object.keys(derived.verification ?? {}).length, 1, 3) + (derived.delivery ? 1 : 0);
|
|
191
|
+
const tier = points >= 10 ? 5 : points >= 7 ? 4 : points >= 4 ? 3 : points >= 2 ? 2 : 1;
|
|
192
|
+
return { class: `C${tier}`, points, rule: COMPLEXITY_RULE };
|
|
193
|
+
}
|
|
194
|
+
/** How many different tools the stretch's agent used (framework L11: tool breadth); null without a tool call. */
|
|
195
|
+
const toolsOf = (messages) => { const names = new Set(); for (const message of messages) for (const call of message.calls ?? []) names.add(call.family.split("|")[0]); return names.size > 0 ? names.size : null; };
|
|
196
|
+
|
|
174
197
|
/** The stretch's whole derived record; `helpers` are the hook's own readers of a human turn, so both read it the same way. */
|
|
175
198
|
export function deriveStretch(messages, helpers) {
|
|
176
199
|
const context = contextOf(messages, helpers), routing = routingOf(messages), steering = steeringOf(messages, helpers);
|
|
177
|
-
|
|
200
|
+
const tools = toolsOf(messages);
|
|
201
|
+
return { ...verificationOf(messages, helpers), ...(context ? { context } : {}), ...(routing ? { routing } : {}), ...(steering ? { steering } : {}), ...(tools ? { tools } : {}) };
|
|
178
202
|
}
|
package/worktrust.mjs
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* npx worktrust@latest history send this computer's earlier sessions as history (asks first)
|
|
13
13
|
* npx worktrust@latest history --rebuild re-measure them with the current rules; WorkTrust replaces this computer's
|
|
14
14
|
* earlier lines day by day, never counting a day twice (shows the plan, asks first)
|
|
15
|
+
* npx worktrust@latest history --from-archive [dir] send what your local archive holds of sessions your AI apps have
|
|
16
|
+
* already deleted, marked as from your archive (shows the plan, asks first)
|
|
15
17
|
* npx worktrust@latest disconnect show what comes out, ask, take it out again
|
|
16
18
|
* npx worktrust@latest status what is coupled here
|
|
17
19
|
* npx worktrust@latest preserve how long each AI app here keeps its sessions (--apply keeps them,
|
|
@@ -71,7 +73,7 @@ const command = args.find((arg, at) => !arg.startsWith("--") && !(at > 0 && VALU
|
|
|
71
73
|
const flag = (name) => { const at = args.indexOf(`--${name}`); return at >= 0 ? args[at + 1] : undefined; };
|
|
72
74
|
const has = (name) => args.includes(`--${name}`);
|
|
73
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). */
|
|
74
|
-
const CLI_VERSION = "0.
|
|
76
|
+
const CLI_VERSION = "0.8.0";
|
|
75
77
|
const ORIGIN = (flag("origin") ?? process.env.WORKTRUST_ORIGIN ?? "https://app.worktrust.io").replace(/\/$/, "");
|
|
76
78
|
const MCP = flag("url") ?? process.env.WORKTRUST_MCP_URL ?? `${ORIGIN}/api/mcp`;
|
|
77
79
|
const HOME_DIR = join(homedir(), ".worktrust");
|
|
@@ -681,9 +683,27 @@ async function offerRebuild(previousVersion, paths, key) {
|
|
|
681
683
|
async function history() {
|
|
682
684
|
const key = await freshKey();
|
|
683
685
|
if (!key) fail("this computer is not coupled through the key file. Run npx worktrust first.");
|
|
686
|
+
if (has("from-archive")) return offerArchive(await scripts(), key);
|
|
684
687
|
await offerHistory(await scripts(), key.token, key.device ?? null, key.url, has("rebuild"));
|
|
685
688
|
}
|
|
686
689
|
|
|
690
|
+
/**
|
|
691
|
+
* THE ARCHIVE AS HISTORY (0.8.0): the plan first (how many signed stretches, how many of them gone from the AI apps
|
|
692
|
+
* here), then one question, Enter is No; the hook refuses by name whatever does not hold (no device key, an archive
|
|
693
|
+
* that does not verify, a door that does not know the mark).
|
|
694
|
+
*/
|
|
695
|
+
async function offerArchive(paths, key) {
|
|
696
|
+
const at = process.argv.indexOf("--from-archive"), named = process.argv[at + 1], dir = named && !named.startsWith("--") ? [named] : [];
|
|
697
|
+
const env = { WORKTRUST_MCP_URL: key.url, ...(key.device ? { WORKTRUST_DEVICE_KEY: key.device } : {}) };
|
|
698
|
+
const plan = await run(paths["log-session.mjs"], ["--history", "--from-archive", ...dir, "--dry-run"], key.token, true, env);
|
|
699
|
+
say();
|
|
700
|
+
for (const line of plan.out.trim().split("\n").filter((text) => !text.startsWith(" ") && !text.startsWith("{") && !text.startsWith("}") && text.trim())) say(` ${line}`);
|
|
701
|
+
if (plan.code !== 0 || /Nothing to send/.test(plan.out) || has("dry-run")) return;
|
|
702
|
+
if (!(await ask("Send them as history, marked as from your archive?", false))) { say(" Not sent."); return; }
|
|
703
|
+
const sent = await run(paths["log-session.mjs"], ["--history", "--from-archive", ...dir], key.token, true, env);
|
|
704
|
+
say(sent.code === 0 ? " ✓ Sent as history from your archive. It shows in WorkTrust as earlier work, marked as such, never as verified hours." : ` Not sent: ${sent.out.trim().split("\n").pop()}`);
|
|
705
|
+
}
|
|
706
|
+
|
|
687
707
|
/**
|
|
688
708
|
* CODEX WAKES THE HOOK ON A YES (owner, 2026-10-04). Codex has no hook settings: it runs ONE `notify`
|
|
689
709
|
* program at the end of every turn, named on a top-level line of ~/.codex/config.toml. Without it
|
|
@@ -878,6 +898,8 @@ function help() {
|
|
|
878
898
|
say(" worktrust history send this computer's earlier sessions as history (asks first)");
|
|
879
899
|
say(" --rebuild re-measure them with the current rules; WorkTrust replaces this computer's earlier lines,");
|
|
880
900
|
say(" day by day, never counting a day twice (shows the plan per AI app, asks first; --dry-run: the plan only)");
|
|
901
|
+
say(" --from-archive [dir] send what your archive (preserve --archive) holds of sessions your AI apps already deleted,");
|
|
902
|
+
say(" only days this computer signed, marked as from your archive (shows the plan, asks first)");
|
|
881
903
|
say(" worktrust disconnect take WorkTrust out of every AI app here (asks first)");
|
|
882
904
|
say(" worktrust status what is coupled here");
|
|
883
905
|
say(" worktrust preserve keep your AI history: the report, then the settings and a local archive, each on your yes");
|