worktrust 0.9.3 → 0.9.4

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,7 @@ 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) |
181
182
  | `collector_version` | the CLI version that measured it |
182
183
  | `seq`, `prev`, `hash` | the line's place, the hash of the line before it, and its own hash |
183
184
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktrust",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
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,7 +49,19 @@ 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
  ];
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"]);
53
65
  const SECRET_FILE = /(^|[\/\s'"])(\.env(\.[\w-]+)?|id_(rsa|ed25519|ecdsa)|[\w.-]+\.(pem|p12|pfx|key)|credentials(\.json)?|\.npmrc|\.netrc|\.pgpass)(['"\s]|$)/;
54
66
  const PRINTS = /\b(cat|less|more|head|tail|bat|type|Get-Content|grep|sed|awk|cp|scp|base64)\b/;
55
67
  const READ_TOOLS = new Set(["Read", "read_file", "view", "open_file"]);
@@ -58,6 +70,8 @@ export const marksOf = (block) => {
58
70
  const command = SHELL_TOOLS.has(name) ? block.input?.command ?? block.input?.CommandLine ?? block.input?.cmd : null;
59
71
  const path = READ_TOOLS.has(name) ? block.input?.file_path ?? block.input?.path ?? null : null;
60
72
  const marks = typeof command === "string" ? MARKS.filter(([, pattern]) => pattern.test(command)).map(([mark]) => mark) : [];
73
+ const written = WRITE_TOOLS.has(name) ? block.input?.file_path ?? block.input?.path ?? null : null;
74
+ if (typeof written === "string" && PLAN_DOC.test(written)) marks.push("plan_doc");
61
75
  if ((typeof path === "string" && SECRET_FILE.test(path)) || (typeof command === "string" && PRINTS.test(command) && SECRET_FILE.test(command))) marks.push("secret");
62
76
  return marks;
63
77
  };
@@ -509,9 +523,34 @@ function adaptationOf(messages) {
509
523
  return Object.keys(kept).length > 0 ? kept : null;
510
524
  }
511
525
 
526
+ /**
527
+ * THE WORK'S REACH AND ITS PLAN (0.9.4): commands on infrastructure, data and cloud; plan or roadmap documents written,
528
+ * and whether one came before the first code change; reviews approved, sent back or commented and issues opened or closed
529
+ * from the terminal. Counts, and one flag.
530
+ */
531
+ function reachOf(messages) {
532
+ const results = new Map();
533
+ for (const message of messages) for (const result of message.results ?? []) results.set(result.id, result);
534
+ 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 };
535
+ 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" };
536
+ let changed = false, planFirst = null;
537
+ for (const message of messages) {
538
+ if (message.bridge) continue;
539
+ for (const call of message.calls ?? []) {
540
+ const tool = call.family.split("|")[0], marks = call.marks ?? [], result = results.get(call.id);
541
+ if (marks.includes("plan_doc") && !changed && planFirst === null) planFirst = true;
542
+ if (EDIT_TOOLS.has(tool) && !marks.includes("plan_doc")) { if (!changed && planFirst === null) planFirst = false; changed = true; }
543
+ for (const mark of marks) if (KEY[mark] && result?.failed !== true) out[KEY[mark]] += 1;
544
+ }
545
+ }
546
+ const kept = Object.fromEntries(Object.entries(out).filter(([, value]) => value > 0));
547
+ if (out.plan_docs > 0) kept.plan_first = planFirst === true;
548
+ return Object.keys(kept).length > 0 ? kept : null;
549
+ }
550
+
512
551
  /** 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
552
  export function deriveStretch(messages, helpers) {
514
553
  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 } : {}) };
554
+ 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);
555
+ 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
556
  }
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.4";
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");