worktrust 0.9.3 → 0.9.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 CHANGED
@@ -178,6 +178,8 @@ nothing it does not:
178
178
  | `hygiene` | risk and hygiene: rollbacks, privileged commands (sudo, a forced push), hooks skipped (--no-verify), secrets touched (a .env printed, a private key read), destructive commands and how many a look or a check came before, the answer to a guard (retried, changed, stopped), and whether a check followed delivery; counts only, never the command or the path (0.9.1) |
179
179
  | `route` | the route the work took: branches made, pushes straight to the default branch, and of the stretch's own commits those over twenty files, a fix that followed another, those naming an issue, and the changed CI, script and docs files; counts only, never a subject, a branch name or a path (0.9.2) |
180
180
  | `adaptation` | evidence and adaptation: what the stretch read by kind (docs, tests, logs, config, source, the web), a plan written again after a failure, a model changed right after a failure, two or more agents whose work a check met, and outages (overloaded, rate-limited, timed out) and how many the work went on past; counts only, never a path or a message (0.9.3) |
181
+ | `reach` | the work's reach and its plan: commands on infrastructure (terraform, kubectl, helm, docker), data (psql, sqlite, bq, dbt) and cloud (aws, gcloud, az, supabase, vercel); plan or roadmap documents written and whether one came before the first code change; reviews approved, sent back or commented and issues opened or closed from the terminal; counts and one flag, never a command, a file name or a path (0.9.4) |
182
+ | `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) |
181
183
  | `collector_version` | the CLI version that measured it |
182
184
  | `seq`, `prev`, `hash` | the line's place, the hash of the line before it, and its own hash |
183
185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktrust",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
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": {
@@ -134,6 +134,12 @@ export function archiveLine(entry, behaviour = null, { ids = {}, collector, salt
134
134
  if (Object.keys(sources).length > 0) adaptation.sources = sources;
135
135
  if (Object.keys(adaptation).length > 0) line.adaptation = adaptation;
136
136
  }
137
+ // 0.9.4: the work's reach and its plan; whole counts, and whether a plan came first.
138
+ if (entry.reach && typeof entry.reach === "object") {
139
+ const reach = Object.fromEntries(REACH_KEYS.filter((key) => whole(entry.reach[key])).map((key) => [key, entry.reach[key]]));
140
+ if (typeof entry.reach.plan_first === "boolean" && reach.plan_docs) reach.plan_first = entry.reach.plan_first;
141
+ if (Object.keys(reach).length > 0) line.reach = reach;
142
+ }
137
143
  const risk = pick(entry.risk, ["proposed", "refused", "run"]);
138
144
  if (risk && risk.proposed > 0 && risk.refused + risk.run <= risk.proposed) line.risk = risk;
139
145
  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; };
@@ -193,6 +199,8 @@ export function archiveEntries(dir, ownKey) {
193
199
  /** The furthest step a stretch reached on the computer (0.8.9). */
194
200
  const STAGES = ["attempted", "verified", "committed", "pushed", "pr", "deployed"];
195
201
  /** How fast a stretch moved (0.9.0). */
202
+ /** The work's reach and its plan (0.9.4). */
203
+ const REACH_KEYS = ["infra", "data", "cloud", "plan_docs", "reviews_approved", "reviews_changes", "reviews_commented", "issues_opened", "issues_closed"];
196
204
  /** Evidence read and adaptation (0.9.3). */
197
205
  const ADAPTATION_KEYS = ["replans", "escalations", "reconciled", "outages", "outages_continued"];
198
206
  const READ_KINDS = ["docs", "tests", "logs", "config", "source", "web"];
@@ -201,7 +209,7 @@ const ROUTE_KEYS = ["branches", "pushes_to_main", "commits_large", "fix_followup
201
209
  /** Risk and hygiene (0.9.1). */
202
210
  const HYGIENE_KEYS = ["rollbacks", "privileged", "bypasses", "secrets", "destructive", "destructive_checked", "guard_retried", "guard_changed", "guard_stopped"];
203
211
  const TIMING_KEYS = ["first_action_s", "first_check_s", "detect_actions", "detect_s", "recovery_calls", "resume_actions"];
204
- 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", "interrupts", "steers", "utc_offset"];
212
+ 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", "interrupts", "steers", "utc_offset"];
205
213
  export function supplementFor(line, earlier) {
206
214
  const missing = DERIVED_KEYS.filter((key) => line[key] !== undefined && !earlier.some((old) => old[key] !== undefined));
207
215
  if (missing.length === 0) return null;
@@ -49,15 +49,31 @@ const MARKS = [
49
49
  // 0.9.2: the route the work took: a branch of its own, or a push straight to the default branch.
50
50
  ["branch", /\bgit\s+(checkout\s+-b|switch\s+-c)\s|\bgit\s+branch\s+(?!-)[\w./-]+/],
51
51
  ["push_main", /\bgit\s+push\b[^\n;&|]*\s(main|master)\b/],
52
+ // 0.9.4: the kind of system a command worked on, and reviews and issues handled from the terminal.
53
+ ["infra", /(^|[\s;&|(])(terraform|tofu|pulumi|kubectl|helm|kustomize|ansible(-playbook)?|docker(\s+compose)?|docker-compose|podman)\s/],
54
+ ["data", /(^|[\s;&|(])(psql|pg_dump|pg_restore|mysql|sqlite3|duckdb|mongosh|redis-cli|bq|snowsql|dbt|clickhouse(-client)?)\b/],
55
+ ["cloud", /(^|[\s;&|(])(aws|gcloud|gsutil|az|vercel|netlify|fly|flyctl|wrangler|supabase|firebase|heroku|doctl)\s/],
56
+ ["review_approve", /\bgh\s+pr\s+review\b[^\n;&|]*\s(-a|--approve)\b/],
57
+ ["review_changes", /\bgh\s+pr\s+review\b[^\n;&|]*\s(-r|--request-changes)\b/],
58
+ ["review_comment", /\bgh\s+pr\s+review\b[^\n;&|]*\s(-c|--comment)\b/],
59
+ ["issue_open", /\bgh\s+issue\s+create\b/],
60
+ ["issue_close", /\bgh\s+issue\s+close\b/],
52
61
  ];
53
- const SECRET_FILE = /(^|[\/\s'"])(\.env(\.[\w-]+)?|id_(rsa|ed25519|ecdsa)|[\w.-]+\.(pem|p12|pfx|key)|credentials(\.json)?|\.npmrc|\.netrc|\.pgpass)(['"\s]|$)/;
54
- const PRINTS = /\b(cat|less|more|head|tail|bat|type|Get-Content|grep|sed|awk|cp|scp|base64)\b/;
62
+ /** A plan or roadmap the work wrote down (0.9.4), by the file's name; the name never leaves. */
63
+ const PLAN_DOC = /(^|[\\/])(PLAN|ROADMAP|DESIGN|SPEC|RFC|ADR)[\w.-]*\.(md|mdx|txt)$|(^|[\\/])(plans?|roadmaps?|rfcs?|adrs?|specs?)[\\/][^\\/]+\.(md|mdx)$|\.plan\.md$/i;
64
+ const WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "write_file", "edit_file", "create_file"]);
65
+ // 0.9.5: a file that holds a secret by its name, as a path token; "credentials" only as a file (a search for the word is no read).
66
+ const SECRET_FILE = /(^|[\/\s'"])(\.env(\.[\w-]+)?|id_(rsa|ed25519|ecdsa)|[\w.-]+\.(pem|p12|pfx|key)|credentials\.json|\.aws[\/]credentials|\.npmrc|\.netrc|\.pgpass)(['"\s]|$)/;
67
+ // 0.9.5: a command that shows or copies a file's content; a search (grep, sed, awk) names a word and is no read.
68
+ const PRINTS = /(^|[\s;&|(])(cat|less|more|head|tail|bat|type|Get-Content|base64|xxd|strings|cp|scp)\s/;
55
69
  const READ_TOOLS = new Set(["Read", "read_file", "view", "open_file"]);
56
70
  export const marksOf = (block) => {
57
71
  const name = String(block?.name ?? "");
58
72
  const command = SHELL_TOOLS.has(name) ? block.input?.command ?? block.input?.CommandLine ?? block.input?.cmd : null;
59
73
  const path = READ_TOOLS.has(name) ? block.input?.file_path ?? block.input?.path ?? null : null;
60
74
  const marks = typeof command === "string" ? MARKS.filter(([, pattern]) => pattern.test(command)).map(([mark]) => mark) : [];
75
+ const written = WRITE_TOOLS.has(name) ? block.input?.file_path ?? block.input?.path ?? null : null;
76
+ if (typeof written === "string" && PLAN_DOC.test(written)) marks.push("plan_doc");
61
77
  if ((typeof path === "string" && SECRET_FILE.test(path)) || (typeof command === "string" && PRINTS.test(command) && SECRET_FILE.test(command))) marks.push("secret");
62
78
  return marks;
63
79
  };
@@ -509,9 +525,34 @@ function adaptationOf(messages) {
509
525
  return Object.keys(kept).length > 0 ? kept : null;
510
526
  }
511
527
 
528
+ /**
529
+ * THE WORK'S REACH AND ITS PLAN (0.9.4): commands on infrastructure, data and cloud; plan or roadmap documents written,
530
+ * and whether one came before the first code change; reviews approved, sent back or commented and issues opened or closed
531
+ * from the terminal. Counts, and one flag.
532
+ */
533
+ function reachOf(messages) {
534
+ const results = new Map();
535
+ for (const message of messages) for (const result of message.results ?? []) results.set(result.id, result);
536
+ const out = { infra: 0, data: 0, cloud: 0, plan_docs: 0, reviews_approved: 0, reviews_changes: 0, reviews_commented: 0, issues_opened: 0, issues_closed: 0 };
537
+ const KEY = { infra: "infra", data: "data", cloud: "cloud", plan_doc: "plan_docs", review_approve: "reviews_approved", review_changes: "reviews_changes", review_comment: "reviews_commented", issue_open: "issues_opened", issue_close: "issues_closed" };
538
+ let changed = false, planFirst = null;
539
+ for (const message of messages) {
540
+ if (message.bridge) continue;
541
+ for (const call of message.calls ?? []) {
542
+ const tool = call.family.split("|")[0], marks = call.marks ?? [], result = results.get(call.id);
543
+ if (marks.includes("plan_doc") && !changed && planFirst === null) planFirst = true;
544
+ if (EDIT_TOOLS.has(tool) && !marks.includes("plan_doc")) { if (!changed && planFirst === null) planFirst = false; changed = true; }
545
+ for (const mark of marks) if (KEY[mark] && result?.failed !== true) out[KEY[mark]] += 1;
546
+ }
547
+ }
548
+ const kept = Object.fromEntries(Object.entries(out).filter(([, value]) => value > 0));
549
+ if (out.plan_docs > 0) kept.plan_first = planFirst === true;
550
+ return Object.keys(kept).length > 0 ? kept : null;
551
+ }
552
+
512
553
  /** The stretch's whole derived record; `helpers` are the hook's own readers of a human turn, so both read it the same way. */
513
554
  export function deriveStretch(messages, helpers) {
514
555
  const context = contextOf(messages, helpers), routing = routingOf(messages), steering = steeringOf(messages, helpers);
515
- const timing = timingOf(messages, helpers), hygiene = hygieneOf(messages), route = routeOf(messages), adaptation = adaptationOf(messages), tools = toolsOf(messages), oversight = oversightOf(messages), planning = planningOf(messages), autonomy = autonomyOf(messages), practice = practiceOf(messages, helpers);
516
- 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 } : {}) };
556
+ const timing = timingOf(messages, helpers), hygiene = hygieneOf(messages), route = routeOf(messages), adaptation = adaptationOf(messages), reach = reachOf(messages), tools = toolsOf(messages), oversight = oversightOf(messages), planning = planningOf(messages), autonomy = autonomyOf(messages), practice = practiceOf(messages, helpers);
557
+ 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 } : {}) };
517
558
  }
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.3";
76
+ const CLI_VERSION = "0.9.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");