worktrust 0.9.1 → 0.9.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
@@ -176,6 +176,8 @@ nothing it does not:
176
176
  | `quality.change_blocks`, `checked_blocks`, `rework_cycles`, `stage` | how many change blocks (edits with no check between them) a check followed, how often a failed check was followed by another change before it ran again (rework), and the furthest step the stretch reached here: attempted, verified, committed, pushed, pr, deployed; counts and one word only (0.8.9) |
177
177
  | `timing` | how fast the stretch moved: seconds from your first turn to the first action and from the first change to the first check, the actions and seconds from a failure to the change that answered it, the calls from a failed check to it passing, and the calls back to work after a compaction; medians from the order and the clocks, never what was said (0.9.0) |
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
+ | `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
+ | `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) |
179
181
  | `collector_version` | the CLI version that measured it |
180
182
  | `seq`, `prev`, `hash` | the line's place, the hash of the line before it, and its own hash |
181
183
 
package/log-session.mjs CHANGED
@@ -153,6 +153,31 @@ const contextFilesOf = (paths) => { const out = {}; for (const path of paths) fo
153
153
  const AI_COAUTHOR = /\b(claude|anthropic|codex|openai|chatgpt|copilot|cursor|devin|gemini|jules|aider|windsurf|cline)\b/i;
154
154
  /** A path that holds tests, by the conventions of the common test runners (read here; the path never leaves). */
155
155
  const TEST_PATH = /(^|[\\/])(__tests__|tests?|spec|e2e)[\\/]|\.(test|spec)\.[cm]?[jt]sx?$|_test\.(go|py|rb)$|(^|[\\/])test_[^\\/]+\.py$|Tests?\.(java|kt|cs|swift)$/;
156
+ /**
157
+ * THE ROUTE FROM GIT (0.9.2), of the stretch's own commits, read here and never kept: commits over twenty files, a fix
158
+ * that followed another commit of the stretch, a commit naming an issue (#12, ABC-123); and of the changed files, how
159
+ * many are CI, scripts or docs (what lets another person run it again). Subjects and paths never leave.
160
+ */
161
+ const CI_PATH = /(^|[\\/])\.github[\\/]workflows[\\/]|(^|[\\/])\.gitlab-ci\.ya?ml$|(^|[\\/])Jenkinsfile$|(^|[\\/])\.circleci[\\/]|(^|[\\/])azure-pipelines\.ya?ml$/;
162
+ const SCRIPT_PATH = /(^|[\\/])scripts?[\\/]|(^|[\\/])Makefile$|(^|[\\/])justfile$|\.(sh|ps1)$/;
163
+ const DOC_PATH = /(^|[\\/])docs?[\\/]|(^|[\\/])README(\.[a-z]+)?$|(^|[\\/])CHANGELOG\.md$/i;
164
+ const FIX_SUBJECT = /^(fix|hotfix|revert)\b/i, ISSUE_REF = /#\d+\b|\b[A-Z][A-Z0-9]+-\d+\b/;
165
+ const gitRouteOf = (cwd, shas, paths) => {
166
+ const out = {};
167
+ if (cwd && shas.length > 0) {
168
+ const commits = git(cwd, "log", "--no-walk", "--date-order", "--reverse", "--format=%x1e%s", "--shortstat", ...shas.slice(0, 50)).split("\x1e").map((block) => block.trim()).filter(Boolean)
169
+ .map((block) => ({ subject: block.split("\n")[0], files: Number(/(\d+) files? changed/.exec(block)?.[1] ?? 0) }));
170
+ const large = commits.filter((commit) => commit.files > 20).length, fixes = commits.slice(1).filter((commit) => FIX_SUBJECT.test(commit.subject)).length, issues = commits.filter((commit) => ISSUE_REF.test(commit.subject)).length;
171
+ if (large) out.commits_large = large;
172
+ if (fixes) out.fix_followups = fixes;
173
+ if (issues) out.issue_refs = issues;
174
+ }
175
+ const ci = paths.filter((path) => CI_PATH.test(path)).length, scripts = paths.filter((path) => SCRIPT_PATH.test(path)).length, docs = paths.filter((path) => DOC_PATH.test(path)).length;
176
+ if (ci) out.ci_files = ci;
177
+ if (scripts) out.script_files = scripts;
178
+ if (docs) out.doc_files = docs;
179
+ return out;
180
+ };
156
181
  /** The derived record of a stretch, kept for the local archive only (see payloadFor). */
157
182
  const DERIVED = new WeakMap();
158
183
  const AGENT_RUNS_MAX = 10000, AGENT_SECONDS_MAX = 2592000;
@@ -622,7 +647,8 @@ function payloadFor(stretch) {
622
647
  const changed = [...new Set(touched.paths)], tests = changed.filter((path) => TEST_PATH.test(path)).length;
623
648
  // 0.8.7: of the stretch's commits, how many name an AI as co-author (the Co-authored-by trailer, read here, never kept).
624
649
  const coauthored = cwd && touched.shas.length > 0 ? git(cwd, "log", "--no-walk", "--format=%(trailers:key=Co-authored-by,valueonly,separator=%x01)%x00", ...touched.shas.slice(0, 50)).split("\0").filter((entry, index) => index < Math.min(50, touched.shas.length) && AI_COAUTHOR.test(entry)).length : 0;
625
- const derived = stretch.derived ? { ...stretch.derived, ...(changed.length > 0 ? { changes: { files: changed.length, test_files: tests } } : {}), ...(touched.shas.length > 0 ? { authorship: { commits: Math.min(50, touched.shas.length), ai_coauthored: coauthored } } : {}), ...(contextFilesOf(changed) ? { context_files: contextFilesOf(changed) } : {}), ...(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 }) } : {}) } : null;
650
+ const route = { ...(stretch.derived?.route ?? {}), ...(touched.committed ? gitRouteOf(cwd, touched.shas, changed) : {}) };
651
+ const derived = stretch.derived ? { ...stretch.derived, ...(Object.keys(route).length > 0 ? { route } : {}), ...(changed.length > 0 ? { changes: { files: changed.length, test_files: tests } } : {}), ...(touched.shas.length > 0 ? { authorship: { commits: Math.min(50, touched.shas.length), ai_coauthored: coauthored } } : {}), ...(contextFilesOf(changed) ? { context_files: contextFilesOf(changed) } : {}), ...(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 }) } : {}) } : null;
626
652
  const payload = {
627
653
  title: layer ? `AI-assisted work · ${layer}` : "AI-assisted work",
628
654
  kind,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktrust",
3
- "version": "0.9.1",
3
+ "version": "0.9.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": {
@@ -122,6 +122,18 @@ export function archiveLine(entry, behaviour = null, { ids = {}, collector, salt
122
122
  if (typeof entry.hygiene.checked_after_delivery === "boolean") hygiene.checked_after_delivery = entry.hygiene.checked_after_delivery;
123
123
  if (Object.keys(hygiene).length > 0) line.hygiene = hygiene;
124
124
  }
125
+ // 0.9.2: the route the work took; whole counts only.
126
+ if (entry.route && typeof entry.route === "object") {
127
+ const route = Object.fromEntries(ROUTE_KEYS.filter((key) => whole(entry.route[key])).map((key) => [key, entry.route[key]]));
128
+ if (Object.keys(route).length > 0) line.route = route;
129
+ }
130
+ // 0.9.3: evidence read and adaptation; whole counts, and reads by kind.
131
+ if (entry.adaptation && typeof entry.adaptation === "object") {
132
+ const adaptation = Object.fromEntries(ADAPTATION_KEYS.filter((key) => whole(entry.adaptation[key])).map((key) => [key, entry.adaptation[key]]));
133
+ const sources = entry.adaptation.sources && typeof entry.adaptation.sources === "object" ? Object.fromEntries(READ_KINDS.filter((kind) => whole(entry.adaptation.sources[kind]) && entry.adaptation.sources[kind] > 0).map((kind) => [kind, entry.adaptation.sources[kind]])) : {};
134
+ if (Object.keys(sources).length > 0) adaptation.sources = sources;
135
+ if (Object.keys(adaptation).length > 0) line.adaptation = adaptation;
136
+ }
125
137
  const risk = pick(entry.risk, ["proposed", "refused", "run"]);
126
138
  if (risk && risk.proposed > 0 && risk.refused + risk.run <= risk.proposed) line.risk = risk;
127
139
  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; };
@@ -181,10 +193,15 @@ export function archiveEntries(dir, ownKey) {
181
193
  /** The furthest step a stretch reached on the computer (0.8.9). */
182
194
  const STAGES = ["attempted", "verified", "committed", "pushed", "pr", "deployed"];
183
195
  /** How fast a stretch moved (0.9.0). */
196
+ /** Evidence read and adaptation (0.9.3). */
197
+ const ADAPTATION_KEYS = ["replans", "escalations", "reconciled", "outages", "outages_continued"];
198
+ const READ_KINDS = ["docs", "tests", "logs", "config", "source", "web"];
199
+ /** The route the work took (0.9.2). */
200
+ const ROUTE_KEYS = ["branches", "pushes_to_main", "commits_large", "fix_followups", "issue_refs", "ci_files", "script_files", "doc_files"];
184
201
  /** Risk and hygiene (0.9.1). */
185
202
  const HYGIENE_KEYS = ["rollbacks", "privileged", "bypasses", "secrets", "destructive", "destructive_checked", "guard_retried", "guard_changed", "guard_stopped"];
186
203
  const TIMING_KEYS = ["first_action_s", "first_check_s", "detect_actions", "detect_s", "recovery_calls", "resume_actions"];
187
- 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", "interrupts", "steers", "utc_offset"];
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"];
188
205
  export function supplementFor(line, earlier) {
189
206
  const missing = DERIVED_KEYS.filter((key) => line[key] !== undefined && !earlier.some((old) => old[key] !== undefined));
190
207
  if (missing.length === 0) return null;
@@ -46,6 +46,9 @@ const MARKS = [
46
46
  ["rollback", /\bgit\s+(revert|restore)\b|\bgit\s+reset\s+--hard\b|\b(vercel|fly|netlify)\s+rollback\b|\bkubectl\s+rollout\s+undo\b/],
47
47
  ["privilege", /(^|[\s;&|])sudo\s|\bgit\s+push\b[^\n]*\s(-f|--force(-with-lease)?)\b|\bchmod\s+(-R\s+)?777\b/],
48
48
  ["bypass", /\s--no-verify\b/],
49
+ // 0.9.2: the route the work took: a branch of its own, or a push straight to the default branch.
50
+ ["branch", /\bgit\s+(checkout\s+-b|switch\s+-c)\s|\bgit\s+branch\s+(?!-)[\w./-]+/],
51
+ ["push_main", /\bgit\s+push\b[^\n;&|]*\s(main|master)\b/],
49
52
  ];
50
53
  const SECRET_FILE = /(^|[\/\s'"])(\.env(\.[\w-]+)?|id_(rsa|ed25519|ecdsa)|[\w.-]+\.(pem|p12|pfx|key)|credentials(\.json)?|\.npmrc|\.netrc|\.pgpass)(['"\s]|$)/;
51
54
  const PRINTS = /\b(cat|less|more|head|tail|bat|type|Get-Content|grep|sed|awk|cp|scp|base64)\b/;
@@ -59,6 +62,23 @@ export const marksOf = (block) => {
59
62
  return marks;
60
63
  };
61
64
 
65
+ /**
66
+ * WHAT A READ WAS (0.9.3), by kind: docs, tests, logs, config, source, or the web. The path is read here and never kept.
67
+ */
68
+ const READ_KINDS = [
69
+ ["docs", /(^|[\\/])docs?[\\/]|(^|[\\/])README|\.(md|mdx|rst|txt)$/i],
70
+ ["tests", /(^|[\\/])(__tests__|tests?|spec|e2e)[\\/]|\.(test|spec)\.[cm]?[jt]sx?$|_test\.(go|py|rb)$/],
71
+ ["logs", /\.log$|(^|[\\/])logs?[\\/]/i],
72
+ ["config", /\.(json|ya?ml|toml|ini|env\.example)$|(^|[\\/])\.[\w-]+rc$|config/i],
73
+ ];
74
+ export const readKindOf = (block) => {
75
+ const name = String(block?.name ?? "");
76
+ if (/^(WebFetch|WebSearch|web_fetch|web_search)$/.test(name)) return "web";
77
+ const path = READ_TOOLS.has(name) ? block.input?.file_path ?? block.input?.path ?? null : null;
78
+ if (typeof path !== "string") return null;
79
+ return READ_KINDS.find(([, pattern]) => pattern.test(path))?.[0] ?? "source";
80
+ };
81
+
62
82
  /**
63
83
  * A TOOL RESULT AS THE STRETCH READS IT (0.8.1): its call's id, whether it failed, and, read here and never kept, WHO
64
84
  * refused it when it was refused: the PERSON (Claude Code's fixed sentence when they decline a tool use or a plan) or a
@@ -66,18 +86,21 @@ export const marksOf = (block) => {
66
86
  */
67
87
  const PERSON_REFUSED = /^\s*(The user doesn't want to (proceed|take this action)|User rejected|The user rejected|The user declined)/i;
68
88
  const GUARD_REFUSED = /^\s*Permission for this (command|action) was denied by/i;
89
+ const OUTAGE = /\b(overloaded|rate[ -]?limit(ed)?|too many requests|429|503|service unavailable|timed? ?out|ETIMEDOUT|ECONNRESET|ECONNREFUSED|ENOTFOUND)\b/i;
69
90
  export const resultOf = (block) => {
70
91
  const text = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((part) => (part && typeof part.text === "string" ? part.text : "")).join("") : "";
71
92
  const failed = block.is_error === true;
72
93
  // 0.8.5: a reader that could not see the outcome (Codex's code mode, a background terminal) says so; unknown is never a pass.
73
94
  if (!failed && block.outcome === "unknown") return { id: block.tool_use_id, failed: null, refusal: null };
74
- return { id: block.tool_use_id, failed, refusal: failed && PERSON_REFUSED.test(text) ? "person" : failed && GUARD_REFUSED.test(text) ? "guard" : null };
95
+ // 0.9.3: an OUTAGE (the model or a tool overloaded, rate-limited, timed out, unreachable), read here and never kept.
96
+ const outage = failed && OUTAGE.test(text);
97
+ return { id: block.tool_use_id, failed, refusal: failed && PERSON_REFUSED.test(text) ? "person" : failed && GUARD_REFUSED.test(text) ? "guard" : null, ...(outage ? { outage: true } : {}) };
75
98
  };
76
99
  /** A to-do list the agent wrote (TodoWrite): how many items and how many done, never what they say. */
77
100
  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);
78
101
 
79
102
  /** 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). */
80
- export const callOf = (block) => { const kinds = callKindsOf(block), plan = planOf(block), marks = marksOf(block); return { id: block.id, kinds, ...(marks.length ? { marks } : {}), family: `${String(block.name)}|${kinds.join("+")}`, digest: createHash("sha256").update(JSON.stringify(block.input ?? null)).digest("base64url").slice(0, 16), ...(plan ? { plan } : {}) }; };
103
+ export const callOf = (block) => { const kinds = callKindsOf(block), plan = planOf(block), marks = marksOf(block), read = readKindOf(block); return { id: block.id, kinds, ...(marks.length ? { marks } : {}), ...(read ? { read } : {}), family: `${String(block.name)}|${kinds.join("+")}`, digest: createHash("sha256").update(JSON.stringify(block.input ?? null)).digest("base64url").slice(0, 16), ...(plan ? { plan } : {}) }; };
81
104
 
82
105
  /**
83
106
  * VERIFICATION PER STRETCH (0.7.1; framework L7, L12): per kind of check, how many ran and how many failed; per step of
@@ -437,9 +460,58 @@ function hygieneOf(messages) {
437
460
  return Object.keys(kept).length > 0 ? kept : null;
438
461
  }
439
462
 
463
+ /** THE ROUTE (0.9.2), from the commands: branches made, pushes straight to the default branch that went through. */
464
+ function routeOf(messages) {
465
+ const results = new Map();
466
+ for (const message of messages) for (const result of message.results ?? []) results.set(result.id, result);
467
+ let branches = 0, pushesToMain = 0;
468
+ for (const message of messages) for (const call of message.calls ?? []) {
469
+ const marks = call.marks ?? [];
470
+ if (marks.includes("branch")) branches += 1;
471
+ if (marks.includes("push_main") && results.get(call.id)?.failed === false) pushesToMain += 1;
472
+ }
473
+ return branches + pushesToMain > 0 ? { ...(branches ? { branches } : {}), ...(pushesToMain ? { pushes_to_main: pushesToMain } : {}) } : null;
474
+ }
475
+
476
+ /**
477
+ * EVIDENCE AND ADAPTATION (0.9.3): what the stretch read, by kind; a plan written again after a failure; a model change
478
+ * within three calls after a failure (escalation); two or more agents whose work a check met before the next delivery
479
+ * (reconciliation, a proxy); and outages (the model or a tool overloaded, rate-limited, timed out) and how many the work
480
+ * went on past (a later call succeeded). Counts only.
481
+ */
482
+ function adaptationOf(messages) {
483
+ const results = new Map();
484
+ for (const message of messages) for (const result of message.results ?? []) results.set(result.id, result);
485
+ const sources = {}, out = { replans: 0, escalations: 0, reconciled: 0, outages: 0, outages_continued: 0 };
486
+ let failedSince = false, lastFailureCall = null, index = 0, model = null, agentsOpen = 0, outageOpen = false;
487
+ for (const message of messages) {
488
+ if (message.bridge) continue;
489
+ if (message.type === "assistant" && message.model) {
490
+ // Once per failure: the change that answered it, not every switch after it.
491
+ if (model && message.model !== model && lastFailureCall !== null && index - lastFailureCall <= 3) { out.escalations += 1; lastFailureCall = null; }
492
+ model = message.model;
493
+ }
494
+ for (const call of message.calls ?? []) {
495
+ index += 1;
496
+ const tool = call.family.split("|")[0], result = results.get(call.id);
497
+ if (call.read) sources[call.read] = (sources[call.read] ?? 0) + 1;
498
+ if (call.plan && failedSince) { out.replans += 1; failedSince = false; }
499
+ if (tool === "Agent" || tool === "Task") agentsOpen += 1;
500
+ if (call.kinds.some((kind) => CHECK_KINDS.includes(kind)) && agentsOpen >= 2) { out.reconciled += 1; agentsOpen = 0; }
501
+ if (call.kinds.some((kind) => DELIVERY_KINDS.includes(kind))) agentsOpen = 0;
502
+ if (result?.outage) { out.outages += 1; outageOpen = true; }
503
+ else if (outageOpen && result?.failed === false) { out.outages_continued += 1; outageOpen = false; }
504
+ if (result?.failed === true) { failedSince = true; lastFailureCall = index; }
505
+ }
506
+ }
507
+ const kept = Object.fromEntries(Object.entries(out).filter(([, value]) => value > 0));
508
+ if (Object.keys(sources).length > 0) kept.sources = sources;
509
+ return Object.keys(kept).length > 0 ? kept : null;
510
+ }
511
+
440
512
  /** The stretch's whole derived record; `helpers` are the hook's own readers of a human turn, so both read it the same way. */
441
513
  export function deriveStretch(messages, helpers) {
442
514
  const context = contextOf(messages, helpers), routing = routingOf(messages), steering = steeringOf(messages, helpers);
443
- const timing = timingOf(messages, helpers), hygiene = hygieneOf(messages), tools = toolsOf(messages), oversight = oversightOf(messages), planning = planningOf(messages), autonomy = autonomyOf(messages), practice = practiceOf(messages, helpers);
444
- return { ...(practice ?? {}), ...(oversight ? { oversight } : {}), ...(planning ? { planning } : {}), ...(autonomy ? { autonomy } : {}), ...verificationOf(messages, helpers), ...(context ? { context } : {}), ...(routing ? { routing } : {}), ...(steering ? { steering } : {}), ...(tools ? { tools } : {}), ...(timing ? { timing } : {}), ...(hygiene ? { hygiene } : {}) };
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 } : {}) };
445
517
  }
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.1";
76
+ const CLI_VERSION = "0.9.3";
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");