worktrust 0.6.18 → 0.7.0
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 +1 -0
- package/count-behaviour.mjs +29 -3
- package/package.json +1 -1
- package/preserve.mjs +23 -2
- package/worktrust.mjs +1 -1
package/README.md
CHANGED
|
@@ -158,6 +158,7 @@ nothing it does not:
|
|
|
158
158
|
| `project` | a one-way hash of the project's name: which days belong together, never which project |
|
|
159
159
|
| `stretch_ref` | a hash of the session's id and the day, so a stretch is archived once |
|
|
160
160
|
| `device_id`, `profile_id` | which computer and which user profile on it, as one-way hashes (the machine's own id never appears) |
|
|
161
|
+
| `signals`, `analyzer_version` | the behaviour signals of that stretch as keys and counts from the local counter's rubric (framing, steering, verification, recovery), and the rubric's version; derived on this computer from your own turns, never a word of them (0.7.0) |
|
|
161
162
|
| `collector_version` | the CLI version that measured it |
|
|
162
163
|
| `seq`, `prev`, `hash` | the line's place, the hash of the line before it, and its own hash |
|
|
163
164
|
|
package/count-behaviour.mjs
CHANGED
|
@@ -788,6 +788,18 @@ export const isoWeek = (day) => {
|
|
|
788
788
|
return `${thursday.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
|
|
789
789
|
};
|
|
790
790
|
const byWeek = new Map(); // "month|week" → Map(signal → count)
|
|
791
|
+
/**
|
|
792
|
+
* THE WORK STRETCH (0.7.0, the owner's capability framework v4 §13: "work_stretch_id, the stable unit joining AI
|
|
793
|
+
* activity, verification, failures and delivery"). The same observation is tallied a third time, per stretch: one
|
|
794
|
+
* session's UTC day, named by the hash the session hook files the stretch under (`stretch_ref` in log-session.mjs), so a
|
|
795
|
+
* signal and the stretch's clocks, tokens and commits meet on one key. Only sessions read from a file carry it (Claude
|
|
796
|
+
* Code, Codex, Antigravity); the session's id is hashed with the day and never leaves the computer. Read by `preserve`
|
|
797
|
+
* through --stretch-signals; nothing of it is sent yet.
|
|
798
|
+
*/
|
|
799
|
+
const byStretch = new Map(); // stretch_ref → { day, signals: { signal: count } }
|
|
800
|
+
let currentStretchBase = null; // the session's own id, as the session hook hashes it — local only
|
|
801
|
+
const stretchBaseOf = (session) => !session.path ? null : /(^|[\\/])brain[\\/][^\\/]+[\\/]\.system_generated[\\/]logs[\\/]transcript\.jsonl$/.test(session.path) ? String(session.path).split(/[\\/]/).at(-4) : String(session.path).split(/[\\/]/).pop().replace(/\.jsonl?$/, "");
|
|
802
|
+
const stretchRefOf = (base, day) => createHash("sha256").update(`worktrust-stretch|${base}|${day}`).digest("base64url");
|
|
791
803
|
const bump = (month, signal, n = 1) => {
|
|
792
804
|
{
|
|
793
805
|
const key = `${month}|${currentDay && currentDay.startsWith(month) ? isoWeek(currentDay) : "U"}`;
|
|
@@ -795,6 +807,11 @@ const bump = (month, signal, n = 1) => {
|
|
|
795
807
|
week.set(signal, (week.get(signal) ?? 0) + n);
|
|
796
808
|
byWeek.set(key, week);
|
|
797
809
|
}
|
|
810
|
+
if (currentStretchBase && currentDay) {
|
|
811
|
+
const ref = stretchRefOf(currentStretchBase, currentDay), entry = byStretch.get(ref) ?? { day: currentDay, signals: {} };
|
|
812
|
+
entry.signals[signal] = (entry.signals[signal] ?? 0) + n;
|
|
813
|
+
byStretch.set(ref, entry);
|
|
814
|
+
}
|
|
798
815
|
if (currentUnit) {
|
|
799
816
|
const bag = unitsByMonth.get(month) ?? new Map();
|
|
800
817
|
bag.set(`${currentUnit}|${signal}`, { unit: currentUnit, signal, induced: currentInduced, day: currentDay, context: currentContext });
|
|
@@ -827,10 +844,10 @@ const seenLines = new Set(); // v0.10: line identities already counted (a uuid o
|
|
|
827
844
|
const manifest = [];
|
|
828
845
|
for(const session of sessions())manifest.push(sessionDigest(session));
|
|
829
846
|
exportConversations = 0; copilotSessionCount = 0; codexRolloutCount = 0; antigravityCount = 0; skippedDirs.length = 0; countedDirs.length = 0; skippedConversations.length = 0;
|
|
830
|
-
const checkpoint = flag("no-checkpoint") || LABEL_OUT ? null : checkpointStore(
|
|
847
|
+
const checkpoint = flag("no-checkpoint") || flag("stretch-signals") || LABEL_OUT ? null : checkpointStore(
|
|
831
848
|
value("checkpoint") ?? join(homedir(), ".worktrust", "counter-checkpoint.json"),
|
|
832
849
|
fingerprint({code:readFileSync(new URL(import.meta.url)),version:ANALYZER_VERSION,months:MONTHS_BACK,thisMonth,exclude:EXCLUDE,leaveOut:LEAVE_OUT,exports:EXPORTS}),manifest);
|
|
833
|
-
const checkpointMaps = {byMonth,byWeek,unitsByMonth,contextNames,contextBasenames,byMonthContext,monthToolNames,modelFirstSeen,monthMcpServers,rolePatterns,contextCwds,projectFirstSeen,sessionsByMonth};
|
|
850
|
+
const checkpointMaps = {byMonth,byWeek,byStretch,unitsByMonth,contextNames,contextBasenames,byMonthContext,monthToolNames,modelFirstSeen,monthMcpServers,rolePatterns,contextCwds,projectFirstSeen,sessionsByMonth};
|
|
834
851
|
const checkpointSets = {openingHashes,toolFirstSeen,seenLines};
|
|
835
852
|
if (checkpoint?.saved) {
|
|
836
853
|
for(const [key,map] of Object.entries(checkpointMaps)) for(const [k,v] of checkpoint.saved.state.maps[key] ?? []) map.set(k,v);
|
|
@@ -849,6 +866,7 @@ for (const session of sessions()) {
|
|
|
849
866
|
if(sessionIndex++<resumeAt)continue;
|
|
850
867
|
const streamHash = session.path ? createHash("sha256").update(`file:${session.path}\n`) : null;
|
|
851
868
|
const file = session.key;
|
|
869
|
+
currentStretchBase = stretchBaseOf(session);
|
|
852
870
|
files += 1;
|
|
853
871
|
// One file is one session: order preserved, so the STRUCTURAL half reads the shape.
|
|
854
872
|
let sessionMonth = null;
|
|
@@ -1152,7 +1170,7 @@ for (const session of sessions()) {
|
|
|
1152
1170
|
checkpoint?.close();process.exit(0);
|
|
1153
1171
|
}
|
|
1154
1172
|
}
|
|
1155
|
-
currentUnit = null; currentDay = null; // month-level aggregates carry no unit and no
|
|
1173
|
+
currentUnit = null; currentDay = null; currentStretchBase = null; // month-level aggregates carry no unit, no day and no stretch
|
|
1156
1174
|
for (const [month, set] of monthToolNames) bump(month, "toolBreadth", set.size);
|
|
1157
1175
|
for (const [month, set] of monthMcpServers) if (set.size > 0) bump(month, "mcpServersDistinct", set.size);
|
|
1158
1176
|
for (const [month, byPattern] of rolePatterns) for (const sessions of byPattern.values()) if (sessions.size >= RECURRENCE_FLOOR) bump(month, "recurringPatternCandidate");
|
|
@@ -1190,6 +1208,14 @@ for (let i = 0; i < sessionSpans.length; i += 1) {
|
|
|
1190
1208
|
if (overlapping.has(i)) { currentUnit = a.unit; currentDay = a.day ?? null; currentContext = a.context ?? null; bump(a.month, "parallelSessions"); currentUnit = null; currentDay = null; currentContext = null; }
|
|
1191
1209
|
}
|
|
1192
1210
|
|
|
1211
|
+
// THE STRETCHES, FOR PRESERVE (0.7.0): one JSON line per stretch that carries a signal, the analyzer's version on each,
|
|
1212
|
+
// and a closing count so the reader can tell a whole answer from a cut one. Counts and keys only; nothing is sent.
|
|
1213
|
+
if (flag("stretch-signals")) {
|
|
1214
|
+
const rows = [...byStretch.entries()].sort(([, a], [, b]) => a.day.localeCompare(b.day)).map(([stretch_ref, entry]) => JSON.stringify({ stretch_ref, day: entry.day, analyzer_version: ANALYZER_VERSION, signals: Object.fromEntries(Object.entries(entry.signals).sort().map(([signal, count]) => [signal, Math.min(COUNT_CEILING, count)])) }));
|
|
1215
|
+
await new Promise((resolve) => process.stdout.write(`${[...rows, JSON.stringify({ stretch_signals_end: rows.length })].join("\n")}\n`, resolve));
|
|
1216
|
+
process.exit(0);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1193
1219
|
const months = [...byMonth.keys()].sort();
|
|
1194
1220
|
console.log(`${ANALYZER_VERSION} · ${files} transcript file(s), ${messages} own messages, ${months.length} month(s) with counts`);
|
|
1195
1221
|
console.log(` projects counted: ${countedDirs.length}${countedDirs.length ? ` (${countedDirs.join(", ")})` : ""}`);
|
package/package.json
CHANGED
package/preserve.mjs
CHANGED
|
@@ -187,7 +187,22 @@ function computerIds() {
|
|
|
187
187
|
const hash = (text) => createHash("sha256").update(text).digest("base64url");
|
|
188
188
|
return { device_id: hash(`worktrust-machine|${machine.toLowerCase()}`), profile_id: hash(`worktrust-profile|${machine.toLowerCase()}|${homedir()}`) };
|
|
189
189
|
}
|
|
190
|
-
|
|
190
|
+
/**
|
|
191
|
+
* THE STRETCH'S BEHAVIOUR SIGNALS (0.7.0): the local counter's keys and counts per stretch, joined on `stretch_ref`,
|
|
192
|
+
* with the rubric's version. Read on this computer by count-behaviour.mjs --stretch-signals, which sends nothing; a
|
|
193
|
+
* counter that is missing, fails or answers cut leaves the archive without signals and says so, never half of them.
|
|
194
|
+
*/
|
|
195
|
+
function stretchSignals() {
|
|
196
|
+
const counter = join(HERE, "count-behaviour.mjs");
|
|
197
|
+
if (!existsSync(counter)) return null;
|
|
198
|
+
const run = spawnSync(process.execPath, [counter, "--stretch-signals", "--months", "120"], { encoding: "utf8", maxBuffer: 1 << 29, env: { ...process.env, WORKTRUST_MCP_URL: "", WORKTRUST_MCP_TOKEN: "" } });
|
|
199
|
+
const rows = (run.stdout ?? "").split("\n").filter((text) => text.startsWith("{")).map((text) => { try { return JSON.parse(text); } catch { return null; } });
|
|
200
|
+
const end = rows.at(-1)?.stretch_signals_end;
|
|
201
|
+
if (run.status !== 0 || !Number.isInteger(end) || end !== rows.length - 1) { say(" The behaviour counter did not answer whole, so these lines carry no signals; the next run adds them to new lines."); return null; }
|
|
202
|
+
return new Map(rows.slice(0, -1).filter(Boolean).map((row) => [row.stretch_ref, row]));
|
|
203
|
+
}
|
|
204
|
+
const SIGNAL = /^[a-zA-Z][A-Za-z0-9._-]{1,63}$/;
|
|
205
|
+
function archiveLine(entry, behaviour = null) {
|
|
191
206
|
if (typeof entry?.client !== "string" || !/^[a-z][a-z0-9-]{0,31}$/.test(entry.client) || !ISO.test(entry.started_at ?? "") || !ISO.test(entry.at ?? "") || !/^[A-Za-z0-9_-]{16,128}$/.test(entry.stretch_ref ?? "")) return null;
|
|
192
207
|
const line = { client: entry.client, day: entry.started_at.slice(0, 10), started_at: entry.started_at, ended_at: entry.at };
|
|
193
208
|
for (const key of COUNTS) if (typeof entry[key] === "number" && Number.isFinite(entry[key]) && entry[key] >= 0) line[key] = entry[key];
|
|
@@ -202,6 +217,11 @@ function archiveLine(entry) {
|
|
|
202
217
|
// The project as a one-way hash of its name (owner/name, else the folder's own name): which days belong together, never which project.
|
|
203
218
|
const name = entry.repo ?? entry.folder;
|
|
204
219
|
if (typeof name === "string" && name) line.project = sha256(`worktrust-project|${name}`);
|
|
220
|
+
// The behaviour signals of this stretch (0.7.0): keys of the counter's rubric and whole counts, never a word of a turn.
|
|
221
|
+
if (behaviour && behaviour.signals && typeof behaviour.analyzer_version === "string" && /^counter@\d+\.\d+\.\d+$/.test(behaviour.analyzer_version)) {
|
|
222
|
+
const kept = Object.entries(behaviour.signals).filter(([key, count]) => SIGNAL.test(key) && Number.isInteger(count) && count > 0).sort();
|
|
223
|
+
if (kept.length > 0) { line.signals = Object.fromEntries(kept); line.analyzer_version = behaviour.analyzer_version; }
|
|
224
|
+
}
|
|
205
225
|
return { ...line, stretch_ref: entry.stretch_ref, ...IDS, collector_version: COLLECTOR };
|
|
206
226
|
}
|
|
207
227
|
|
|
@@ -282,11 +302,12 @@ function archive(dir) {
|
|
|
282
302
|
// A WHOLE ANSWER OR NOTHING: the hook ends with its own count; one line missing or cut is a read to refuse, never part of an archive.
|
|
283
303
|
const rows = hook.stdout.split("\n").filter(Boolean), end = (() => { try { return JSON.parse(rows.at(-1) ?? "").archive_end; } catch { return undefined; } })();
|
|
284
304
|
if (!Number.isInteger(end) || end !== rows.length - 1) { say(` The session hook's answer came back incomplete (${rows.length - 1} of ${end ?? "an unknown number of"} lines), so nothing was added. Run it again.`); process.exit(1); }
|
|
305
|
+
const signals = stretchSignals();
|
|
285
306
|
const known = new Set(existing.lines.map((line) => line.stretch_ref)), fresh = [], measured = new Set();
|
|
286
307
|
let waiting = 0;
|
|
287
308
|
for (const text of rows.slice(0, -1)) {
|
|
288
309
|
let entry; try { entry = JSON.parse(text); } catch { continue; }
|
|
289
|
-
const line = archiveLine(entry);
|
|
310
|
+
const line = archiveLine(entry, signals?.get(entry.stretch_ref) ?? null);
|
|
290
311
|
if (line && line.day < today()) measured.add(line.stretch_ref); if (!line || known.has(line.stretch_ref)) continue; // a file present twice is one stretch
|
|
291
312
|
if (line.day >= today()) { waiting += 1; continue; }
|
|
292
313
|
known.add(line.stretch_ref);
|
package/worktrust.mjs
CHANGED
|
@@ -71,7 +71,7 @@ const command = args.find((arg, at) => !arg.startsWith("--") && !(at > 0 && VALU
|
|
|
71
71
|
const flag = (name) => { const at = args.indexOf(`--${name}`); return at >= 0 ? args[at + 1] : undefined; };
|
|
72
72
|
const has = (name) => args.includes(`--${name}`);
|
|
73
73
|
/** 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). */
|
|
74
|
-
const CLI_VERSION = "0.
|
|
74
|
+
const CLI_VERSION = "0.7.0";
|
|
75
75
|
const ORIGIN = (flag("origin") ?? process.env.WORKTRUST_ORIGIN ?? "https://app.worktrust.io").replace(/\/$/, "");
|
|
76
76
|
const MCP = flag("url") ?? process.env.WORKTRUST_MCP_URL ?? `${ORIGIN}/api/mcp`;
|
|
77
77
|
const HOME_DIR = join(homedir(), ".worktrust");
|