worktrust 0.9.8 → 0.9.9

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
@@ -185,6 +185,7 @@ nothing it does not:
185
185
  | `hygiene.secrets` | counts a secret touched only when a file that holds one (a .env, a private key, credentials.json, .aws/credentials) is shown or copied (cat, head, tail, base64, cp); a search for a word (grep, sed, awk) is no read (0.9.5) |
186
186
  | `hygiene.secrets` | counts a secret shown only when a reader (cat, head, tail, base64, cp) takes the file as its own argument; a command that uses a .env and reads its output, and a template (.env.example), are no secret shown (0.9.6) |
187
187
  | `outcomes` | outcomes of the steps: per kind of check whether its first run passed, delivery attempts that failed, a model change and a new plan after a failure each followed by a passing check; and a check whose output goes through a pipe is read from its output's words (an error fails, a pass passes, a silent type check or lint passes, anything else is unconfirmed), never from the pipe's exit code (0.9.8) |
188
+ | `retrieval` | retrieval and grounding: searches (grep, rg, find, Grep, Glob, a web search, an MCP search tool), the same search again, a search followed by a read or a change (found) or by another search (again), searches before the first change and right after a failure, and deliveries with docs, tests or a search since the last change (grounded); counts only, nothing searched is kept (0.9.9) |
188
189
  | `collector_version` | the CLI version that measured it |
189
190
  | `seq`, `prev`, `hash` | the line's place, the hash of the line before it, and its own hash |
190
191
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktrust",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
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": {
@@ -147,6 +147,11 @@ export function archiveLine(entry, behaviour = null, { ids = {}, collector, salt
147
147
  if (Object.keys(first).length > 0) outcomes.first_pass = first;
148
148
  if (Object.keys(outcomes).length > 0) line.outcomes = outcomes;
149
149
  }
150
+ // 0.9.9: retrieval and grounding; whole counts only.
151
+ if (entry.retrieval && typeof entry.retrieval === "object") {
152
+ const retrieval = Object.fromEntries(RETRIEVAL_KEYS.filter((key) => whole(entry.retrieval[key])).map((key) => [key, entry.retrieval[key]]));
153
+ if (Object.keys(retrieval).length > 0) line.retrieval = retrieval;
154
+ }
150
155
  const risk = pick(entry.risk, ["proposed", "refused", "run"]);
151
156
  if (risk && risk.proposed > 0 && risk.refused + risk.run <= risk.proposed) line.risk = risk;
152
157
  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; };
@@ -206,6 +211,8 @@ export function archiveEntries(dir, ownKey) {
206
211
  /** The furthest step a stretch reached on the computer (0.8.9). */
207
212
  const STAGES = ["attempted", "verified", "committed", "pushed", "pr", "deployed"];
208
213
  /** How fast a stretch moved (0.9.0). */
214
+ /** Retrieval and grounding (0.9.9). */
215
+ const RETRIEVAL_KEYS = ["searches", "repeated", "found", "again", "before_change", "after_failure", "deliveries", "grounded"];
209
216
  /** Outcomes of the steps (0.9.8). */
210
217
  const OUTCOME_KEYS = ["deliveries_failed", "escalations_helped", "replans_helped"];
211
218
  const CHECK_KINDS_KEPT = ["test", "typecheck", "lint", "build", "gate", "ci", "security"];
@@ -219,7 +226,7 @@ const ROUTE_KEYS = ["branches", "pushes_to_main", "commits_large", "fix_followup
219
226
  /** Risk and hygiene (0.9.1). */
220
227
  const HYGIENE_KEYS = ["rollbacks", "privileged", "bypasses", "secrets", "destructive", "destructive_checked", "guard_retried", "guard_changed", "guard_stopped"];
221
228
  const TIMING_KEYS = ["first_action_s", "first_check_s", "detect_actions", "detect_s", "recovery_calls", "resume_actions"];
222
- 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", "stage", "timing", "hygiene", "route", "adaptation", "reach", "outcomes", "interrupts", "steers", "utc_offset"];
229
+ 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", "stage", "timing", "hygiene", "route", "adaptation", "reach", "outcomes", "retrieval", "interrupts", "steers", "utc_offset"];
223
230
  export function supplementFor(line, earlier) {
224
231
  const missing = DERIVED_KEYS.filter((key) => line[key] !== undefined && !earlier.some((old) => old[key] !== undefined));
225
232
  if (missing.length === 0) return null;
@@ -104,6 +104,18 @@ export const readKindOf = (block) => {
104
104
  * about the check (68% of the owner's checks were so). Outside quotes, the piece holding the check pipes into another
105
105
  * command and no pipefail is set. Its outcome is then read from its output's words, here and never kept.
106
106
  */
107
+ /** A shell command that is a search (0.9.9): its first word, outside quotes; the pattern is never kept. */
108
+ export const shellSearch = (block) => {
109
+ if (!SHELL_TOOLS.has(String(block?.name ?? ""))) return false;
110
+ const raw = block.input?.command ?? block.input?.CommandLine ?? block.input?.cmd;
111
+ return typeof raw === "string" && raw.split(/;|&&|\n/).some((piece) => /^\s*(rg|grep|egrep|fgrep|ag|ack|fd|find|git\s+grep|locate)\b/.test(piece.split("|")[0]));
112
+ };
113
+ /** A shell command that reads a file (cat, sed -n, head, tail, less, bat): after a search, it found something (0.9.9). */
114
+ export const shellReads = (block) => {
115
+ if (!SHELL_TOOLS.has(String(block?.name ?? ""))) return false;
116
+ const raw = block.input?.command ?? block.input?.CommandLine ?? block.input?.cmd;
117
+ return typeof raw === "string" && /^\s*(cat|sed\s+-n|head|tail|less|bat|nl|awk)\b/.test(raw.split("|")[0]);
118
+ };
107
119
  export const pipedCheck = (block) => {
108
120
  if (!SHELL_TOOLS.has(String(block?.name ?? ""))) return false;
109
121
  const raw = block.input?.command ?? block.input?.CommandLine ?? block.input?.cmd;
@@ -137,7 +149,7 @@ export const resultOf = (block) => {
137
149
  const planOf = (block) => (block.name === "TodoWrite" && Array.isArray(block.input?.todos) ? { items: block.input.todos.length, done: block.input.todos.filter((todo) => todo && todo.status === "completed").length } : null);
138
150
 
139
151
  /** One tool call as the stretch reads it: its id, its kinds, its FAMILY (the tool and its check kinds) and a digest of its input (compared, never kept). */
140
- export const callOf = (block) => { const kinds = callKindsOf(block), plan = planOf(block), marks = marksOf(block), read = readKindOf(block), piped = pipedCheck(block); return { id: block.id, kinds, ...(marks.length ? { marks } : {}), ...(read ? { read } : {}), ...(piped ? { piped } : {}), family: `${String(block.name)}|${kinds.join("+")}`, digest: createHash("sha256").update(JSON.stringify(block.input ?? null)).digest("base64url").slice(0, 16), ...(plan ? { plan } : {}) }; };
152
+ export const callOf = (block) => { const kinds = callKindsOf(block), plan = planOf(block), marks = marksOf(block), read = readKindOf(block), piped = pipedCheck(block), search = shellSearch(block), shellRead = shellReads(block); return { id: block.id, kinds, ...(marks.length ? { marks } : {}), ...(read ? { read } : {}), ...(piped ? { piped } : {}), ...(search ? { search } : {}), ...(shellRead ? { shellRead } : {}), family: `${String(block.name)}|${kinds.join("+")}`, digest: createHash("sha256").update(JSON.stringify(block.input ?? null)).digest("base64url").slice(0, 16), ...(plan ? { plan } : {}) }; };
141
153
 
142
154
  /**
143
155
  * VERIFICATION PER STRETCH (0.7.1; framework L7, L12): per kind of check, how many ran and how many failed; per step of
@@ -600,6 +612,46 @@ function outcomesOf(messages) {
600
612
  return Object.keys(kept).length > 0 ? kept : null;
601
613
  }
602
614
 
615
+ /**
616
+ * RETRIEVAL AND GROUNDING (0.9.9): searches (grep, rg, find, git grep, Grep, Glob, a web search, an MCP search or
617
+ * retrieval tool), the same search again, a search followed by a read or a change (it found something) against another
618
+ * search straight after (again), searches before the first change and right after a failure, and deliveries
619
+ * with docs or tests read, or a search, since the last change (grounded). What was searched is never kept.
620
+ */
621
+ const SEARCH_TOOLS = new Set(["Grep", "Glob", "WebSearch", "web_search", "grep_search", "file_search", "codebase_search", "search_files", "find_files"]);
622
+ const SEARCH_COMMAND = /^\s*(rg|grep|egrep|fgrep|ag|ack|fd|find|git\s+grep|locate)\b/;
623
+ const isSearch = (call) => {
624
+ const tool = call.family.split("|")[0];
625
+ return SEARCH_TOOLS.has(tool) || (/^mcp__/.test(tool) && /search|query|retriev|vector|semantic|lookup/i.test(tool)) || call.search === true;
626
+ };
627
+ function retrievalOf(messages) {
628
+ const results = new Map();
629
+ for (const message of messages) for (const result of message.results ?? []) results.set(result.id, result);
630
+ const out = { searches: 0, repeated: 0, found: 0, again: 0, before_change: 0, after_failure: 0, deliveries: 0, grounded: 0 };
631
+ const seen = new Set();
632
+ let previousSearch = false, changed = false, sinceFailure = null, groundedSinceChange = false;
633
+ for (const message of messages) {
634
+ if (message.bridge) continue;
635
+ for (const call of message.calls ?? []) {
636
+ const tool = call.family.split("|")[0], result = results.get(call.id), search = isSearch(call);
637
+ if (previousSearch) { if (search) out.again += 1; else if (tool === "Read" || EDIT_TOOLS.has(tool) || call.shellRead) out.found += 1; }
638
+ if (search) {
639
+ out.searches += 1;
640
+ if (seen.has(call.digest)) out.repeated += 1; else seen.add(call.digest);
641
+ if (!changed) out.before_change += 1;
642
+ if (sinceFailure !== null && sinceFailure <= 3) out.after_failure += 1;
643
+ groundedSinceChange = true;
644
+ }
645
+ if (call.read === "docs" || call.read === "tests") groundedSinceChange = true;
646
+ if (EDIT_TOOLS.has(tool)) { changed = true; groundedSinceChange = false; }
647
+ if (call.kinds.some((kind) => DELIVERY_KINDS.includes(kind)) && result?.failed === false) { out.deliveries += 1; if (groundedSinceChange) out.grounded += 1; }
648
+ previousSearch = search;
649
+ sinceFailure = result?.failed === true ? 0 : sinceFailure === null ? null : sinceFailure + 1;
650
+ }
651
+ }
652
+ return out.searches > 0 || out.deliveries > 0 ? Object.fromEntries(Object.entries(out).filter(([, value]) => value > 0)) : null;
653
+ }
654
+
603
655
  /** The stretch's whole derived record; `helpers` are the hook's own readers of a human turn, so both read it the same way. */
604
656
  /** A piped check's outcome from its output's words: failed or passed when they say so, unknown (null) when they do not. */
605
657
  function settlePiped(messages) {
@@ -615,6 +667,6 @@ function settlePiped(messages) {
615
667
  export function deriveStretch(raw, helpers) {
616
668
  const messages = settlePiped(raw);
617
669
  const context = contextOf(messages, helpers), routing = routingOf(messages), steering = steeringOf(messages, helpers);
618
- const timing = timingOf(messages, helpers), hygiene = hygieneOf(messages), route = routeOf(messages), adaptation = adaptationOf(messages), reach = reachOf(messages), outcomes = outcomesOf(messages), tools = toolsOf(messages), oversight = oversightOf(messages), planning = planningOf(messages), autonomy = autonomyOf(messages), practice = practiceOf(messages, helpers);
619
- return { ...(practice ?? {}), ...(oversight ? { oversight } : {}), ...(planning ? { planning } : {}), ...(autonomy ? { autonomy } : {}), ...verificationOf(messages, helpers), ...(context ? { context } : {}), ...(routing ? { routing } : {}), ...(steering ? { steering } : {}), ...(tools ? { tools } : {}), ...(timing ? { timing } : {}), ...(hygiene ? { hygiene } : {}), ...(route ? { route } : {}), ...(adaptation ? { adaptation } : {}), ...(reach ? { reach } : {}), ...(outcomes ? { outcomes } : {}) };
670
+ const timing = timingOf(messages, helpers), hygiene = hygieneOf(messages), route = routeOf(messages), adaptation = adaptationOf(messages), reach = reachOf(messages), outcomes = outcomesOf(messages), retrieval = retrievalOf(messages), tools = toolsOf(messages), oversight = oversightOf(messages), planning = planningOf(messages), autonomy = autonomyOf(messages), practice = practiceOf(messages, helpers);
671
+ return { ...(practice ?? {}), ...(oversight ? { oversight } : {}), ...(planning ? { planning } : {}), ...(autonomy ? { autonomy } : {}), ...verificationOf(messages, helpers), ...(context ? { context } : {}), ...(routing ? { routing } : {}), ...(steering ? { steering } : {}), ...(tools ? { tools } : {}), ...(timing ? { timing } : {}), ...(hygiene ? { hygiene } : {}), ...(route ? { route } : {}), ...(adaptation ? { adaptation } : {}), ...(reach ? { reach } : {}), ...(outcomes ? { outcomes } : {}), ...(retrieval ? { retrieval } : {}) };
620
672
  }
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.9.8";
76
+ const CLI_VERSION = "0.9.9";
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");