worktrust 0.9.0 → 0.9.2
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 +2 -0
- package/log-session.mjs +27 -1
- package/package.json +1 -1
- package/preserve-lines.mjs +16 -1
- package/stretch-evidence.mjs +75 -3
- package/worktrust.mjs +1 -1
package/README.md
CHANGED
|
@@ -175,6 +175,8 @@ nothing it does not:
|
|
|
175
175
|
| `framing`, `quality`, `risk`, `tool_mix`, `reads`, `context_files` | how the work was framed (your turns before the agent's first action, a plan first or not), checked (a check passing after the last change, the failures before a check passed, the change looked at before delivery; security scans count as checks), risked (destructive commands proposed, refused, run), which kinds of tools were used, files read again unchanged, and which of the AI app's own context files (instructions, skills, agents, commands, settings, MCP) the commits changed; kinds and counts only (0.8.8) |
|
|
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
|
+
| `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) |
|
|
178
180
|
| `collector_version` | the CLI version that measured it |
|
|
179
181
|
| `seq`, `prev`, `hash` | the line's place, the hash of the line before it, and its own hash |
|
|
180
182
|
|
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
|
|
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
package/preserve-lines.mjs
CHANGED
|
@@ -116,6 +116,17 @@ export function archiveLine(entry, behaviour = null, { ids = {}, collector, salt
|
|
|
116
116
|
const timing = Object.fromEntries(TIMING_KEYS.filter((key) => whole(entry.timing[key])).map((key) => [key, entry.timing[key]]));
|
|
117
117
|
if (Object.keys(timing).length > 0) line.timing = timing;
|
|
118
118
|
}
|
|
119
|
+
// 0.9.1: risk and hygiene; whole counts, and whether a check followed delivery.
|
|
120
|
+
if (entry.hygiene && typeof entry.hygiene === "object") {
|
|
121
|
+
const hygiene = Object.fromEntries(HYGIENE_KEYS.filter((key) => whole(entry.hygiene[key])).map((key) => [key, entry.hygiene[key]]));
|
|
122
|
+
if (typeof entry.hygiene.checked_after_delivery === "boolean") hygiene.checked_after_delivery = entry.hygiene.checked_after_delivery;
|
|
123
|
+
if (Object.keys(hygiene).length > 0) line.hygiene = hygiene;
|
|
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
|
+
}
|
|
119
130
|
const risk = pick(entry.risk, ["proposed", "refused", "run"]);
|
|
120
131
|
if (risk && risk.proposed > 0 && risk.refused + risk.run <= risk.proposed) line.risk = risk;
|
|
121
132
|
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; };
|
|
@@ -175,8 +186,12 @@ export function archiveEntries(dir, ownKey) {
|
|
|
175
186
|
/** The furthest step a stretch reached on the computer (0.8.9). */
|
|
176
187
|
const STAGES = ["attempted", "verified", "committed", "pushed", "pr", "deployed"];
|
|
177
188
|
/** How fast a stretch moved (0.9.0). */
|
|
189
|
+
/** The route the work took (0.9.2). */
|
|
190
|
+
const ROUTE_KEYS = ["branches", "pushes_to_main", "commits_large", "fix_followups", "issue_refs", "ci_files", "script_files", "doc_files"];
|
|
191
|
+
/** Risk and hygiene (0.9.1). */
|
|
192
|
+
const HYGIENE_KEYS = ["rollbacks", "privileged", "bypasses", "secrets", "destructive", "destructive_checked", "guard_retried", "guard_changed", "guard_stopped"];
|
|
178
193
|
const TIMING_KEYS = ["first_action_s", "first_check_s", "detect_actions", "detect_s", "recovery_calls", "resume_actions"];
|
|
179
|
-
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", "interrupts", "steers", "utc_offset"];
|
|
194
|
+
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", "interrupts", "steers", "utc_offset"];
|
|
180
195
|
export function supplementFor(line, earlier) {
|
|
181
196
|
const missing = DERIVED_KEYS.filter((key) => line[key] !== undefined && !earlier.some((old) => old[key] !== undefined));
|
|
182
197
|
if (missing.length === 0) return null;
|
package/stretch-evidence.mjs
CHANGED
|
@@ -36,6 +36,32 @@ export const callKindsOf = (block) => {
|
|
|
36
36
|
const command = block && SHELL_TOOLS.has(String(block.name)) ? block.input?.command ?? block.input?.CommandLine ?? block.input?.cmd : null;
|
|
37
37
|
return typeof command === "string" ? CALL_KINDS.filter(([, pattern]) => pattern.test(command)).map(([kind]) => kind) : [];
|
|
38
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* RISK AND HYGIENE MARKS (0.9.1), beside the kinds and outside a call's family so no earlier measure moves: a ROLLBACK
|
|
41
|
+
* (git revert or restore, a hard reset, a deploy rolled back), a PRIVILEGED command (sudo, a forced push, a world-
|
|
42
|
+
* writable chmod), a BYPASS (hooks skipped with --no-verify), and a SECRET touched (a .env, a private key, a credentials
|
|
43
|
+
* or token file read or printed). The path and the command are read here and never kept; only the mark travels.
|
|
44
|
+
*/
|
|
45
|
+
const MARKS = [
|
|
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
|
+
["privilege", /(^|[\s;&|])sudo\s|\bgit\s+push\b[^\n]*\s(-f|--force(-with-lease)?)\b|\bchmod\s+(-R\s+)?777\b/],
|
|
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/],
|
|
52
|
+
];
|
|
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/;
|
|
55
|
+
const READ_TOOLS = new Set(["Read", "read_file", "view", "open_file"]);
|
|
56
|
+
export const marksOf = (block) => {
|
|
57
|
+
const name = String(block?.name ?? "");
|
|
58
|
+
const command = SHELL_TOOLS.has(name) ? block.input?.command ?? block.input?.CommandLine ?? block.input?.cmd : null;
|
|
59
|
+
const path = READ_TOOLS.has(name) ? block.input?.file_path ?? block.input?.path ?? null : null;
|
|
60
|
+
const marks = typeof command === "string" ? MARKS.filter(([, pattern]) => pattern.test(command)).map(([mark]) => mark) : [];
|
|
61
|
+
if ((typeof path === "string" && SECRET_FILE.test(path)) || (typeof command === "string" && PRINTS.test(command) && SECRET_FILE.test(command))) marks.push("secret");
|
|
62
|
+
return marks;
|
|
63
|
+
};
|
|
64
|
+
|
|
39
65
|
/**
|
|
40
66
|
* 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
|
|
41
67
|
* refused it when it was refused: the PERSON (Claude Code's fixed sentence when they decline a tool use or a plan) or a
|
|
@@ -54,7 +80,7 @@ export const resultOf = (block) => {
|
|
|
54
80
|
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);
|
|
55
81
|
|
|
56
82
|
/** 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). */
|
|
57
|
-
export const callOf = (block) => { const kinds = callKindsOf(block), plan = planOf(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), ...(plan ? { plan } : {}) }; };
|
|
83
|
+
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 } : {}) }; };
|
|
58
84
|
|
|
59
85
|
/**
|
|
60
86
|
* VERIFICATION PER STRETCH (0.7.1; framework L7, L12): per kind of check, how many ran and how many failed; per step of
|
|
@@ -381,9 +407,55 @@ function timingOf(messages, { isHumanTurn, textOf }) {
|
|
|
381
407
|
return Object.keys(kept).length > 0 ? kept : null;
|
|
382
408
|
}
|
|
383
409
|
|
|
410
|
+
/**
|
|
411
|
+
* RISK AND HYGIENE PER STRETCH (0.9.1): rollbacks, privileged commands, hooks skipped, secrets touched; destructive
|
|
412
|
+
* commands and how many a look (git diff, show, status) or a check came before since the last change; after a guard
|
|
413
|
+
* refused a call, whether the next call was the same one again (retried), another (changed), or none (stopped); and
|
|
414
|
+
* whether a check ran after the stretch's first delivery step (a push, a deploy). Counts only.
|
|
415
|
+
*/
|
|
416
|
+
function hygieneOf(messages) {
|
|
417
|
+
const results = new Map();
|
|
418
|
+
for (const message of messages) for (const result of message.results ?? []) results.set(result.id, result);
|
|
419
|
+
const out = { rollbacks: 0, privileged: 0, bypasses: 0, secrets: 0, destructive: 0, destructive_checked: 0, guard_retried: 0, guard_changed: 0, guard_stopped: 0 };
|
|
420
|
+
let lookedSinceChange = false, delivered = false, checkedAfterDelivery = false, refusedDigest = null;
|
|
421
|
+
for (const message of messages) {
|
|
422
|
+
if (message.bridge) continue;
|
|
423
|
+
for (const call of message.calls ?? []) {
|
|
424
|
+
const tool = call.family.split("|")[0], result = results.get(call.id), marks = call.marks ?? [];
|
|
425
|
+
if (refusedDigest !== null) { if (call.digest === refusedDigest) out.guard_retried += 1; else out.guard_changed += 1; refusedDigest = null; }
|
|
426
|
+
if (marks.includes("rollback")) out.rollbacks += 1;
|
|
427
|
+
if (marks.includes("privilege")) out.privileged += 1;
|
|
428
|
+
if (marks.includes("bypass")) out.bypasses += 1;
|
|
429
|
+
if (marks.includes("secret")) out.secrets += 1;
|
|
430
|
+
if (call.kinds.includes("destructive")) { out.destructive += 1; if (lookedSinceChange) out.destructive_checked += 1; }
|
|
431
|
+
if (EDIT_TOOLS.has(tool)) lookedSinceChange = false;
|
|
432
|
+
if (call.kinds.includes("inspect") || call.kinds.some((kind) => CHECK_KINDS.includes(kind))) { lookedSinceChange = true; if (delivered) checkedAfterDelivery = true; }
|
|
433
|
+
if ((call.kinds.includes("push") || call.kinds.includes("deploy")) && result?.failed === false) delivered = true;
|
|
434
|
+
if (result?.refusal === "guard") refusedDigest = call.digest;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (refusedDigest !== null) out.guard_stopped += 1;
|
|
438
|
+
const kept = Object.fromEntries(Object.entries(out).filter(([, value]) => value > 0));
|
|
439
|
+
if (delivered) kept.checked_after_delivery = checkedAfterDelivery;
|
|
440
|
+
return Object.keys(kept).length > 0 ? kept : null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** THE ROUTE (0.9.2), from the commands: branches made, pushes straight to the default branch that went through. */
|
|
444
|
+
function routeOf(messages) {
|
|
445
|
+
const results = new Map();
|
|
446
|
+
for (const message of messages) for (const result of message.results ?? []) results.set(result.id, result);
|
|
447
|
+
let branches = 0, pushesToMain = 0;
|
|
448
|
+
for (const message of messages) for (const call of message.calls ?? []) {
|
|
449
|
+
const marks = call.marks ?? [];
|
|
450
|
+
if (marks.includes("branch")) branches += 1;
|
|
451
|
+
if (marks.includes("push_main") && results.get(call.id)?.failed === false) pushesToMain += 1;
|
|
452
|
+
}
|
|
453
|
+
return branches + pushesToMain > 0 ? { ...(branches ? { branches } : {}), ...(pushesToMain ? { pushes_to_main: pushesToMain } : {}) } : null;
|
|
454
|
+
}
|
|
455
|
+
|
|
384
456
|
/** The stretch's whole derived record; `helpers` are the hook's own readers of a human turn, so both read it the same way. */
|
|
385
457
|
export function deriveStretch(messages, helpers) {
|
|
386
458
|
const context = contextOf(messages, helpers), routing = routingOf(messages), steering = steeringOf(messages, helpers);
|
|
387
|
-
const timing = timingOf(messages, helpers), tools = toolsOf(messages), oversight = oversightOf(messages), planning = planningOf(messages), autonomy = autonomyOf(messages), practice = practiceOf(messages, helpers);
|
|
388
|
-
return { ...(practice ?? {}), ...(oversight ? { oversight } : {}), ...(planning ? { planning } : {}), ...(autonomy ? { autonomy } : {}), ...verificationOf(messages, helpers), ...(context ? { context } : {}), ...(routing ? { routing } : {}), ...(steering ? { steering } : {}), ...(tools ? { tools } : {}), ...(timing ? { timing } : {}) };
|
|
459
|
+
const timing = timingOf(messages, helpers), hygiene = hygieneOf(messages), route = routeOf(messages), tools = toolsOf(messages), oversight = oversightOf(messages), planning = planningOf(messages), autonomy = autonomyOf(messages), practice = practiceOf(messages, helpers);
|
|
460
|
+
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 } : {}) };
|
|
389
461
|
}
|
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.
|
|
76
|
+
const CLI_VERSION = "0.9.2";
|
|
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");
|