worktrust 0.7.1 → 0.7.3

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 CHANGED
@@ -160,6 +160,8 @@ nothing it does not:
160
160
  | `device_id`, `profile_id` | which computer and which user profile on it, as one-way hashes (the machine's own id never appears) |
161
161
  | `signals`, `analyzer_version` | the behaviour signals of that stretch as keys and counts from the local counter's rubric (framing, steering, verification, recovery), and the rubric's version; derived on this computer from your own turns, never a word of them (0.7.0) |
162
162
  | `verification`, `delivery` | per kind of check run in the stretch (test, typecheck, lint, build, the project's gate, CI) how many ran and how many failed; per delivery step (commit, PR, push, deploy) how many succeeded, and whether a check had passed before the first; named from each command on this computer, never the command itself (0.7.1) |
163
+ | `recovery` | of the tool calls that failed in the stretch, how many a later call of the same kind recovered, the middle time that took, how many were retried unchanged and how many with a different approach; inputs compared on this computer by digest, never kept (0.7.2) |
164
+ | `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) |
163
165
  | `collector_version` | the CLI version that measured it |
164
166
  | `seq`, `prev`, `hash` | the line's place, the hash of the line before it, and its own hash |
165
167
 
package/log-session.mjs CHANGED
@@ -415,7 +415,8 @@ const messageOf = (line, at) => {
415
415
  waitsForPerson: line.type === "assistant" && blocks.some((block) => block && block.type === "tool_use" && WAITS_FOR_PERSON.has(String(block.name))),
416
416
  agentic: line.agentic === true, cumulative: line.cumulative === true, // the session's running totals (Hermes, Goose without a ledger)
417
417
  // 0.7.1: each tool call's kinds (never its command) and each tool result's outcome, joined on the call's id in `measure`.
418
- calls: line.type === "assistant" ? blocks.filter((block) => block && block.type === "tool_use" && typeof block.id === "string").map((block) => ({ id: block.id, kinds: callKindsOf(block) })).filter((call) => call.kinds.length > 0) : [],
418
+ // 0.7.2: and the call's FAMILY (the tool, and its check kinds) and a digest of its input, compared here and never kept.
419
+ calls: line.type === "assistant" ? blocks.filter((block) => block && block.type === "tool_use" && typeof block.id === "string").map((block) => { const kinds = callKindsOf(block); return { id: block.id, kinds, family: `${String(block.name)}|${kinds.join("+")}`, digest: createHash("sha256").update(JSON.stringify(block.input ?? null)).digest("base64url").slice(0, 16) }; }) : [],
419
420
  results: blocks.filter((block) => block && block.type === "tool_result" && typeof block.tool_use_id === "string").map((block) => ({ id: block.tool_use_id, failed: block.is_error === true })),
420
421
  };
421
422
  };
@@ -511,7 +512,63 @@ function verificationOf(messages) {
511
512
  }
512
513
  }
513
514
  const any = (record) => Object.keys(record).length > 0;
514
- return { ...(any(checks) ? { verification: checks } : {}), ...(any(delivered) ? { delivery: { ...delivered, verified_first: passedFirst } } : {}) };
515
+ const recovery = recoveryOf(messages), delegation = delegationOf(messages);
516
+ return { ...(any(checks) ? { verification: checks } : {}), ...(any(delivered) ? { delivery: { ...delivered, verified_first: passedFirst } } : {}), ...(recovery ? { recovery } : {}), ...(delegation ? { delegation } : {}) };
517
+ }
518
+
519
+ /**
520
+ * DELEGATION AND CHECKPOINTS PER STRETCH (0.7.3; framework L3, L5): how many tool actions the agent took on its own
521
+ * between two moments the person stepped in (their own turn, or stopping it), as the longest and the middle chain; and
522
+ * how often the agent stopped for the person on purpose: a question it asked them, a plan it asked them to approve.
523
+ * Null when the stretch made no tool call.
524
+ */
525
+ function delegationOf(messages) {
526
+ const chains = [];
527
+ let chain = 0, calls = 0, questions = 0, plans = 0;
528
+ for (const message of messages) {
529
+ if (message.bridge) continue;
530
+ const stepIn = message.type === "user" && !message.meta && message.kind !== "tool_result" && (isHumanTurn(message.content) || INTERRUPTED.test(textOf(message.content).trim()));
531
+ if (stepIn) { if (chain > 0) chains.push(chain); chain = 0; continue; }
532
+ for (const call of message.calls ?? []) {
533
+ const tool = call.family.split("|")[0];
534
+ calls += 1; chain += 1;
535
+ if (tool === "AskUserQuestion") questions += 1;
536
+ if (tool === "ExitPlanMode") plans += 1;
537
+ }
538
+ }
539
+ if (chain > 0) chains.push(chain);
540
+ if (calls === 0) return null;
541
+ chains.sort((a, b) => a - b);
542
+ return { chain_max: chains.at(-1), chain_median: chains[Math.floor((chains.length - 1) / 2)], questions, plans };
543
+ }
544
+
545
+ /**
546
+ * RECOVERY PER STRETCH (0.7.2; framework L9): of the tool calls that failed, how many were followed by a success of the
547
+ * same family (the same tool, the same kind of check) later in the stretch, the middle time that took, and what came
548
+ * next: the same input again (a blind retry) or a different one (a changed strategy). Inputs are compared by digest on
549
+ * this computer and never kept. Null when nothing failed.
550
+ */
551
+ function recoveryOf(messages) {
552
+ const calls = [];
553
+ const byId = new Map();
554
+ for (const message of messages) {
555
+ for (const call of message.calls ?? []) { const entry = { ...call, at: message.at, failed: null }; calls.push(entry); byId.set(call.id, entry); }
556
+ for (const result of message.results ?? []) { const call = byId.get(result.id); if (call) call.failed = result.failed; }
557
+ }
558
+ const done = calls.filter((call) => call.failed !== null);
559
+ let failures = 0, recovered = 0, blind = 0, changed = 0;
560
+ const latencies = [];
561
+ done.forEach((call, index) => {
562
+ if (!call.failed) return;
563
+ failures += 1;
564
+ const later = done.slice(index + 1).filter((next) => next.family === call.family);
565
+ if (later.length > 0) { if (later[0].digest === call.digest) blind += 1; else changed += 1; }
566
+ const success = later.find((next) => !next.failed);
567
+ if (success) { recovered += 1; latencies.push(Math.max(0, Math.round((success.at - call.at) / 1000))); }
568
+ });
569
+ if (failures === 0) return null;
570
+ latencies.sort((a, b) => a - b);
571
+ return { failures, recovered, blind_retries: blind, strategy_changed: changed, ...(latencies.length ? { median_seconds: latencies[Math.floor((latencies.length - 1) / 2)] } : {}) };
515
572
  }
516
573
 
517
574
  /** One day's messages → the stretch, measured. Null when there is nothing honest to send. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktrust",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Couple this computer to WorkTrust: approve a code in your browser, and every AI app here reports what you built. Metadata only, no dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -58,6 +58,16 @@ export function archiveLine(entry, behaviour = null, { ids = {}, collector } = {
58
58
  const kept = Object.entries(entry.delivery).filter(([kind, count]) => DELIVERY_KINDS.includes(kind) && whole(count) && count > 0).sort();
59
59
  if (kept.length > 0) line.delivery = { ...Object.fromEntries(kept), verified_first: entry.delivery.verified_first === true };
60
60
  }
61
+ // RECOVERY (0.7.2): failures, recoveries, blind retries, changed strategies and the middle time to recover, whole numbers.
62
+ if (entry.recovery && typeof entry.recovery === "object") {
63
+ const { failures, recovered, blind_retries: blind, strategy_changed: changed, median_seconds: median } = entry.recovery;
64
+ if ([failures, recovered, blind, changed].every(whole) && failures > 0 && recovered <= failures && blind + changed <= failures && (median === undefined || whole(median))) line.recovery = { failures, recovered, blind_retries: blind, strategy_changed: changed, ...(median !== undefined ? { median_seconds: median } : {}) };
65
+ }
66
+ // DELEGATION (0.7.3): the longest and middle chain of the agent's own actions between the person's steps, questions, plans.
67
+ if (entry.delegation && typeof entry.delegation === "object") {
68
+ const { chain_max: most, chain_median: middle, questions, plans } = entry.delegation;
69
+ if ([most, middle, questions, plans].every(whole) && most > 0 && middle <= most) line.delegation = { chain_max: most, chain_median: middle, questions, plans };
70
+ }
61
71
  // The behaviour signals of this stretch (0.7.0): keys of the counter's rubric and whole counts, never a word of a turn.
62
72
  if (behaviour && behaviour.signals && typeof behaviour.analyzer_version === "string" && /^counter@\d+\.\d+\.\d+$/.test(behaviour.analyzer_version)) {
63
73
  const kept = Object.entries(behaviour.signals).filter(([key, count]) => SIGNAL.test(key) && Number.isInteger(count) && count > 0).sort();
package/worktrust.mjs CHANGED
@@ -71,7 +71,7 @@ const command = args.find((arg, at) => !arg.startsWith("--") && !(at > 0 && VALU
71
71
  const flag = (name) => { const at = args.indexOf(`--${name}`); return at >= 0 ? args[at + 1] : undefined; };
72
72
  const has = (name) => args.includes(`--${name}`);
73
73
  /** 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.7.1";
74
+ const CLI_VERSION = "0.7.3";
75
75
  const ORIGIN = (flag("origin") ?? process.env.WORKTRUST_ORIGIN ?? "https://app.worktrust.io").replace(/\/$/, "");
76
76
  const MCP = flag("url") ?? process.env.WORKTRUST_MCP_URL ?? `${ORIGIN}/api/mcp`;
77
77
  const HOME_DIR = join(homedir(), ".worktrust");