@workscribe/cli 3.1.0 → 3.2.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 CHANGED
@@ -19,7 +19,7 @@ On feature/auth:
19
19
  - Committed: add JWT validation middleware
20
20
  - Added dependencies: bcrypt, jsonwebtoken
21
21
  - Pushed to origin/feature/auth
22
- With an AI coding assistant (126 commands) · 3 file saves in the editor
22
+ With Claude Code (2 sessions) · 3 file saves in the editor
23
23
 
24
24
  ## infrastructure — 45m
25
25
  - Note: staging was flaky after the compose upgrade
package/dist/index.js CHANGED
@@ -972,6 +972,13 @@ function maskQuotedText(text) {
972
972
  }
973
973
  return masked.join("");
974
974
  }
975
+ function maskLiteralText(text) {
976
+ const masked = Array.from({ length: text.length }, () => "_");
977
+ for (const { index, quote } of quotedCharacters(text)) {
978
+ if (quote !== "'") masked[index] = text[index];
979
+ }
980
+ return masked.join("");
981
+ }
975
982
  function startsWithWord(step2, word) {
976
983
  return step2 === word || step2.startsWith(`${word} `) || step2.startsWith(`${word} `);
977
984
  }
@@ -1175,57 +1182,6 @@ var init_exclusions = __esm({
1175
1182
  }
1176
1183
  });
1177
1184
 
1178
- // src/normalize/compound.ts
1179
- function rankOf(category) {
1180
- const index = STEP_CATEGORY_PRIORITY.indexOf(category);
1181
- if (index !== -1) return index;
1182
- if (category === UNCLASSIFIED.category) return UNCLASSIFIED_RANK;
1183
- if (DEFAULT_EXCLUDED_CATEGORIES.has(category)) return EXCLUDED_CATEGORY_RANK;
1184
- return OTHER_CATEGORY_RANK;
1185
- }
1186
- function primaryStepEvent(steps, matchCommand) {
1187
- const events = steps.map((step2) => ({ ...matchCommand(simpleCommandOf(step2), step2), matchedStep: step2 }));
1188
- return events.reduce(
1189
- (best, event) => rankOf(event.category) < rankOf(best.category) ? event : best,
1190
- UNCLASSIFIED
1191
- );
1192
- }
1193
- function classifyCompound(command, matchCommand) {
1194
- const workSteps = commandSteps(command).filter((step2) => !isGlueStep(step2));
1195
- const wholeCommandEvent = {
1196
- ...matchCommand(command, command),
1197
- matchedStep: workSteps.length === 1 ? workSteps[0] : command
1198
- };
1199
- const isRecognisedSingleCommand = workSteps.length <= 1 && wholeCommandEvent.category !== UNCLASSIFIED.category;
1200
- if (isRecognisedSingleCommand || workSteps.length === 0) return wholeCommandEvent;
1201
- const primaryEvent = primaryStepEvent(workSteps, matchCommand);
1202
- return primaryEvent.category === UNCLASSIFIED.category ? wholeCommandEvent : primaryEvent;
1203
- }
1204
- var UNCLASSIFIED, STEP_CATEGORY_PRIORITY, OTHER_CATEGORY_RANK, EXCLUDED_CATEGORY_RANK, UNCLASSIFIED_RANK;
1205
- var init_compound = __esm({
1206
- "src/normalize/compound.ts"() {
1207
- "use strict";
1208
- init_command_steps();
1209
- init_exclusions();
1210
- UNCLASSIFIED = { category: "other", metadata: {} };
1211
- STEP_CATEGORY_PRIORITY = [
1212
- "testing",
1213
- "building",
1214
- "releasing",
1215
- "shipping",
1216
- "dependencies",
1217
- "database",
1218
- "infrastructure",
1219
- "running",
1220
- "files",
1221
- "versioning"
1222
- ];
1223
- OTHER_CATEGORY_RANK = STEP_CATEGORY_PRIORITY.length;
1224
- EXCLUDED_CATEGORY_RANK = OTHER_CATEGORY_RANK + 1;
1225
- UNCLASSIFIED_RANK = EXCLUDED_CATEGORY_RANK + 1;
1226
- }
1227
- });
1228
-
1229
1185
  // src/normalize/git-invocation.ts
1230
1186
  function parseGitInvocation(command) {
1231
1187
  const words = shellWords(command.trim());
@@ -1282,6 +1238,198 @@ var init_git_invocation = __esm({
1282
1238
  }
1283
1239
  });
1284
1240
 
1241
+ // src/normalize/read-only.ts
1242
+ function hasNoOption(args, options) {
1243
+ return !args.some((arg) => options.some((option) => arg === option || arg.startsWith(`${option}=`)));
1244
+ }
1245
+ function runsInlineCode(args, inlineFlags) {
1246
+ const firstMeaningful = args.find(
1247
+ (arg) => inlineFlags.includes(arg) || arg.startsWith("--eval=") || !arg.startsWith("-")
1248
+ );
1249
+ return firstMeaningful !== void 0 && (inlineFlags.includes(firstMeaningful) || firstMeaningful.startsWith("--eval="));
1250
+ }
1251
+ function readSedShortOptions(option, nextArg, parsed) {
1252
+ const letters = option.slice(1);
1253
+ const expressionAt = letters.indexOf("e");
1254
+ if (SED_UNSAFE_SHORT_OPTION.test(letters) || expressionAt !== -1 && expressionAt !== letters.length - 1) {
1255
+ parsed.isSafe = false;
1256
+ }
1257
+ if (letters.includes("n")) parsed.isQuiet = true;
1258
+ if (expressionAt === -1) return 0;
1259
+ parsed.scripts.push(nextArg ?? "");
1260
+ return 1;
1261
+ }
1262
+ function readSedArguments(args) {
1263
+ const parsed = { isQuiet: false, isSafe: true, scripts: [] };
1264
+ const positional = [];
1265
+ for (let index = 0; index < args.length; index++) {
1266
+ const arg = args[index];
1267
+ if (arg === "--quiet" || arg === "--silent") parsed.isQuiet = true;
1268
+ else if (arg.startsWith("--in-place") || arg.startsWith("--file")) parsed.isSafe = false;
1269
+ else if (arg === "--expression") parsed.scripts.push(args[++index] ?? "");
1270
+ else if (arg.startsWith("--expression=")) parsed.scripts.push(arg.slice("--expression=".length));
1271
+ else if (SED_SHORT_OPTIONS.test(arg)) index += readSedShortOptions(arg, args[index + 1], parsed);
1272
+ else if (!arg.startsWith("-")) positional.push(arg);
1273
+ }
1274
+ return { ...parsed, positional };
1275
+ }
1276
+ function sedOnlyPrints(args) {
1277
+ const { isQuiet, isSafe, scripts, positional } = readSedArguments(args);
1278
+ const allScripts = scripts.length > 0 ? scripts : positional.slice(0, 1);
1279
+ if (!isQuiet || !isSafe || allScripts.length === 0) return false;
1280
+ return allScripts.every(
1281
+ (script) => script.split(";").filter((part) => part.trim() !== "").every((part) => SED_PRINT_COMMAND.test(part.trim()))
1282
+ );
1283
+ }
1284
+ function isGitRead(command) {
1285
+ const invocation = parseGitInvocation(command);
1286
+ if (!invocation?.subcommand) return false;
1287
+ const args = invocation.args.map(({ word }) => word);
1288
+ if (invocation.subcommand === "branch") return args.length === 0;
1289
+ return GIT_READS.has(invocation.subcommand) && hasNoOption(args, GIT_UNSAFE_OPTIONS);
1290
+ }
1291
+ function isReadingStage(stage, checks) {
1292
+ const command = simpleCommandOf(stage);
1293
+ const [name, ...args] = shellWords(command).map(({ word }) => word);
1294
+ if (name === "git") return isGitRead(command);
1295
+ return Object.hasOwn(checks, name ?? "") && checks[name](args);
1296
+ }
1297
+ function substitutionsOnlyRead(step2) {
1298
+ if (!containsCommandSubstitution(step2)) return true;
1299
+ if (PROCESS_SUBSTITUTION.test(maskQuotedText(step2))) return false;
1300
+ const literalMasked = maskLiteralText(step2);
1301
+ const commands = substitutionCommands(literalMasked);
1302
+ const openers = literalMasked.split("$(").length - 1 + Math.ceil((literalMasked.split("`").length - 1) / 2);
1303
+ return commands.length === openers && commands.every(isReadOnlyCommand);
1304
+ }
1305
+ function isReadOnlyStep(step2) {
1306
+ if (!substitutionsOnlyRead(step2)) return false;
1307
+ const [firstStage = "", ...laterStages] = splitOutsideQuotes(step2, PIPE_SEPARATORS2);
1308
+ return isReadingStage(firstStage, READERS) && laterStages.every((stage) => isReadingStage(stage, { ...READERS, ...PIPE_FILTERS }));
1309
+ }
1310
+ function writesToFile(step2) {
1311
+ return [...maskQuotedText(step2).matchAll(OUTPUT_REDIRECTION)].some(([, target]) => !HARMLESS_TARGET.test(target));
1312
+ }
1313
+ function isReadOnlyCommand(command) {
1314
+ const steps = commandSteps(command);
1315
+ if (steps.some(writesToFile)) return false;
1316
+ const workSteps = steps.filter((step2) => !isGlueStep(step2));
1317
+ return workSteps.length > 0 && workSteps.every(isReadOnlyStep);
1318
+ }
1319
+ function readOnlyMetadata(command) {
1320
+ const firstWorkStep = commandSteps(command).find((step2) => !isGlueStep(step2)) ?? "";
1321
+ const [firstStage = ""] = splitOutsideQuotes(firstWorkStep, PIPE_SEPARATORS2);
1322
+ const simple = simpleCommandOf(firstStage);
1323
+ const invocation = parseGitInvocation(simple);
1324
+ return invocation ? { subcommand: invocation.subcommand } : { tool: shellWords(simple)[0]?.word ?? null };
1325
+ }
1326
+ var READ_ONLY_CATEGORY, PIPE_SEPARATORS2, ALWAYS_READS, GIT_READS, GIT_UNSAFE_OPTIONS, FIND_UNSAFE_ACTION, SED_ADDRESS, SED_PRINT_COMMAND, SED_SHORT_OPTIONS, SED_UNSAFE_SHORT_OPTION, OUTPUT_REDIRECTION, HARMLESS_TARGET, PROCESS_SUBSTITUTION, READERS, PIPE_FILTERS;
1327
+ var init_read_only = __esm({
1328
+ "src/normalize/read-only.ts"() {
1329
+ "use strict";
1330
+ init_command_steps();
1331
+ init_shell_syntax();
1332
+ init_git_invocation();
1333
+ READ_ONLY_CATEGORY = "exploring";
1334
+ PIPE_SEPARATORS2 = ["|"];
1335
+ ALWAYS_READS = () => true;
1336
+ GIT_READS = /* @__PURE__ */ new Set(["diff", "log", "show", "status"]);
1337
+ GIT_UNSAFE_OPTIONS = ["--output", "--ext-diff", "--textconv"];
1338
+ FIND_UNSAFE_ACTION = /^-(?:delete|exec|execdir|ok|okdir|fls|fprint\w*)$/;
1339
+ SED_ADDRESS = String.raw`(?:\d+|\$|/(?:[^/\\]|\\.)*/)`;
1340
+ SED_PRINT_COMMAND = new RegExp(String.raw`^(?:${SED_ADDRESS}(?:,${SED_ADDRESS})?)?\s*!?\s*[p=]$`);
1341
+ SED_SHORT_OPTIONS = /^-[a-zA-Z]+$/;
1342
+ SED_UNSAFE_SHORT_OPTION = /[iIf]/;
1343
+ OUTPUT_REDIRECTION = /(?:&>>?|\d*>>?\|?)\s*(&?[^\s;|&()]*)/g;
1344
+ HARMLESS_TARGET = /^(?:&\d+|\/dev\/null)$/;
1345
+ PROCESS_SUBSTITUTION = /[<>]\(/;
1346
+ READERS = {
1347
+ grep: ALWAYS_READS,
1348
+ egrep: ALWAYS_READS,
1349
+ fgrep: ALWAYS_READS,
1350
+ ag: ALWAYS_READS,
1351
+ ack: ALWAYS_READS,
1352
+ cat: ALWAYS_READS,
1353
+ bat: ALWAYS_READS,
1354
+ less: ALWAYS_READS,
1355
+ more: ALWAYS_READS,
1356
+ head: ALWAYS_READS,
1357
+ tail: ALWAYS_READS,
1358
+ ls: ALWAYS_READS,
1359
+ wc: ALWAYS_READS,
1360
+ // `--pre` runs a command on every file
1361
+ rg: (args) => hasNoOption(args, ["--pre"]),
1362
+ ripgrep: (args) => hasNoOption(args, ["--pre"]),
1363
+ find: (args) => !args.some((arg) => FIND_UNSAFE_ACTION.test(arg)),
1364
+ sed: sedOnlyPrints,
1365
+ python: (args) => runsInlineCode(args, ["-c", "-"]),
1366
+ python3: (args) => runsInlineCode(args, ["-c", "-"]),
1367
+ node: (args) => runsInlineCode(args, ["-e", "--eval"])
1368
+ };
1369
+ PIPE_FILTERS = {
1370
+ sort: (args) => hasNoOption(args, ["-o", "--output"]) && !args.some((arg) => /^-[a-zA-Z]*o/.test(arg)),
1371
+ // `uniq in out` writes to `out`
1372
+ uniq: (args) => args.filter((arg) => !arg.startsWith("-")).length <= 1,
1373
+ cut: ALWAYS_READS,
1374
+ tr: ALWAYS_READS,
1375
+ jq: ALWAYS_READS
1376
+ };
1377
+ }
1378
+ });
1379
+
1380
+ // src/normalize/compound.ts
1381
+ function rankOf(category) {
1382
+ const index = STEP_CATEGORY_PRIORITY.indexOf(category);
1383
+ if (index !== -1) return index;
1384
+ if (category === UNCLASSIFIED.category) return UNCLASSIFIED_RANK;
1385
+ if (category === READ_ONLY_CATEGORY) return READ_ONLY_RANK;
1386
+ if (DEFAULT_EXCLUDED_CATEGORIES.has(category)) return EXCLUDED_CATEGORY_RANK;
1387
+ return OTHER_CATEGORY_RANK;
1388
+ }
1389
+ function primaryStepEvent(stepEvents) {
1390
+ return stepEvents.reduce((best, event) => rankOf(event.category) < rankOf(best.category) ? event : best);
1391
+ }
1392
+ function classifyCompound(command, matchCommand) {
1393
+ const workSteps = commandSteps(command).filter((step2) => !isGlueStep(step2));
1394
+ const wholeCommandEvent = {
1395
+ ...matchCommand(command, command),
1396
+ matchedStep: workSteps.length === 1 ? workSteps[0] : command
1397
+ };
1398
+ const isRecognisedSingleCommand = workSteps.length <= 1 && wholeCommandEvent.category !== UNCLASSIFIED.category;
1399
+ if (isRecognisedSingleCommand || workSteps.length === 0) return wholeCommandEvent;
1400
+ const stepEvents = workSteps.map((step2) => ({ ...matchCommand(simpleCommandOf(step2), step2), matchedStep: step2 }));
1401
+ const primaryEvent = primaryStepEvent(stepEvents);
1402
+ if (primaryEvent.category !== UNCLASSIFIED.category) return primaryEvent;
1403
+ const isAnyStepRecognised = stepEvents.some((event) => event.category !== UNCLASSIFIED.category);
1404
+ return isAnyStepRecognised ? { ...UNCLASSIFIED, matchedStep: command } : wholeCommandEvent;
1405
+ }
1406
+ var UNCLASSIFIED, STEP_CATEGORY_PRIORITY, OTHER_CATEGORY_RANK, EXCLUDED_CATEGORY_RANK, UNCLASSIFIED_RANK, READ_ONLY_RANK;
1407
+ var init_compound = __esm({
1408
+ "src/normalize/compound.ts"() {
1409
+ "use strict";
1410
+ init_command_steps();
1411
+ init_exclusions();
1412
+ init_read_only();
1413
+ UNCLASSIFIED = { category: "other", metadata: {} };
1414
+ STEP_CATEGORY_PRIORITY = [
1415
+ "testing",
1416
+ "building",
1417
+ "releasing",
1418
+ "shipping",
1419
+ "dependencies",
1420
+ "database",
1421
+ "infrastructure",
1422
+ "running",
1423
+ "files",
1424
+ "versioning"
1425
+ ];
1426
+ OTHER_CATEGORY_RANK = STEP_CATEGORY_PRIORITY.length;
1427
+ EXCLUDED_CATEGORY_RANK = OTHER_CATEGORY_RANK + 1;
1428
+ UNCLASSIFIED_RANK = EXCLUDED_CATEGORY_RANK + 1;
1429
+ READ_ONLY_RANK = UNCLASSIFIED_RANK + 1;
1430
+ }
1431
+ });
1432
+
1285
1433
  // src/normalize/commit-message.ts
1286
1434
  function firstLineOf(text) {
1287
1435
  const line = text?.split("\n").map((part) => part.trim()).find((part) => part !== "");
@@ -1435,7 +1583,7 @@ function matchRules(command, context) {
1435
1583
  function normalizeCommand(command) {
1436
1584
  return classifyCompound(command, (simple, raw) => matchRules(simple, { raw, original: command }));
1437
1585
  }
1438
- var NX_TARGET_CATEGORIES, NX_COMMAND, nxRule, BASE_RULES, activeRules;
1586
+ var NX_TARGET_CATEGORIES, NX_COMMAND, nxRule, readOnlyRule, BASE_RULES, activeRules;
1439
1587
  var init_patterns = __esm({
1440
1588
  "src/normalize/patterns.ts"() {
1441
1589
  "use strict";
@@ -1446,6 +1594,7 @@ var init_patterns = __esm({
1446
1594
  init_commit_message();
1447
1595
  init_dependency_metadata();
1448
1596
  init_git_invocation();
1597
+ init_read_only();
1449
1598
  NX_TARGET_CATEGORIES = {
1450
1599
  build: "building",
1451
1600
  test: "testing",
@@ -1463,7 +1612,13 @@ var init_patterns = __esm({
1463
1612
  return category ? { category, metadata: { tool: "nx", target } } : null;
1464
1613
  }
1465
1614
  };
1615
+ readOnlyRule = {
1616
+ match(_cmd, { raw }) {
1617
+ return isReadOnlyCommand(raw) ? { category: READ_ONLY_CATEGORY, metadata: readOnlyMetadata(raw) } : null;
1618
+ }
1619
+ };
1466
1620
  BASE_RULES = [
1621
+ readOnlyRule,
1467
1622
  // ─── Git operations ───────────────────────────────────────────────────────
1468
1623
  makeRule(/^git\s+commit\b/, "shipping", (_m, _cmd, { raw, original }) => {
1469
1624
  const commitMessage = commitMessageOf(raw, original);
@@ -1648,13 +1803,6 @@ var init_patterns = __esm({
1648
1803
  tool: "bun",
1649
1804
  script: cmd.split(/\s+/)[2]
1650
1805
  })),
1651
- // ─── Code search ─────────────────────────────────────────────────────────
1652
- makeRule(/^(grep|egrep|fgrep)\b/, "exploring", (match) => ({ tool: match[1] })),
1653
- makeRule(/^(rg|ag|ack|ripgrep)\b/, "exploring", (match) => ({ tool: match[1] })),
1654
- makeRule(/^find\s/, "exploring", () => ({ tool: "find" })),
1655
- // ─── Code read ────────────────────────────────────────────────────────────
1656
- makeRule(/^(cat|bat|less|more)\s+\S+/, "exploring", (match) => ({ tool: match[1] })),
1657
- makeRule(/^(head|tail)\b/, "exploring", (match) => ({ tool: match[1] })),
1658
1806
  // ─── File operations ──────────────────────────────────────────────────────
1659
1807
  makeRule(/^(cp|rsync)\b/, "files", (match) => ({ tool: match[1] })),
1660
1808
  makeRule(/^(mv)\b/, "files", () => ({ tool: "mv" })),
@@ -3021,7 +3169,7 @@ var init_aider = __esm({
3021
3169
  // src/capture/workscribe-command.ts
3022
3170
  function isWorkscribeStep(step2) {
3023
3171
  if (containsCommandSubstitution(step2)) return false;
3024
- const stages = splitOutsideQuotes(step2, PIPE_SEPARATORS2);
3172
+ const stages = splitOutsideQuotes(step2, PIPE_SEPARATORS3);
3025
3173
  if (startsWithWord(stages[0] ?? "", "workscribe")) return true;
3026
3174
  const isTextOrWorkscribe = (stage) => startsWithWord(stage, "workscribe") || TEXT_SOURCES.some((source) => startsWithWord(stage, source));
3027
3175
  return stages.every(isTextOrWorkscribe) && stages.some((stage) => startsWithWord(stage, "workscribe"));
@@ -3031,13 +3179,13 @@ function isWorkscribeOnlyCommand(rawCommand) {
3031
3179
  const workSteps = steps.filter((step2) => !isGlueStep(step2));
3032
3180
  return workSteps.length > 0 && workSteps.every(isWorkscribeStep);
3033
3181
  }
3034
- var PIPE_SEPARATORS2;
3182
+ var PIPE_SEPARATORS3;
3035
3183
  var init_workscribe_command = __esm({
3036
3184
  "src/capture/workscribe-command.ts"() {
3037
3185
  "use strict";
3038
3186
  init_shell_syntax();
3039
3187
  init_command_steps();
3040
- PIPE_SEPARATORS2 = ["|"];
3188
+ PIPE_SEPARATORS3 = ["|"];
3041
3189
  }
3042
3190
  });
3043
3191
 
@@ -4517,10 +4665,10 @@ function installIntoFolder(options) {
4517
4665
  const outcome = installSkill({
4518
4666
  sourceDir: options.sourceDir,
4519
4667
  skillsFolder: target.skillsFolder,
4520
- version: "3.1.0",
4668
+ version: "3.2.0",
4521
4669
  force: options.force
4522
4670
  });
4523
- return describeInstallOutcome({ ...target, outcome, version: "3.1.0" });
4671
+ return describeInstallOutcome({ ...target, outcome, version: "3.2.0" });
4524
4672
  } catch (error) {
4525
4673
  return describeInstallFailure({ ...target, error });
4526
4674
  }
@@ -4539,8 +4687,8 @@ function removeSkills(agents, options) {
4539
4687
  }
4540
4688
  function formatAgentSkillLine(agent, homeDir = import_os6.default.homedir()) {
4541
4689
  const location = displayPath(import_path19.default.join(homeDir, agent.skillsFolder, SKILL_NAME), homeDir);
4542
- const installState = readInstallState(import_path19.default.join(homeDir, agent.skillsFolder), "3.1.0");
4543
- const status = describeSkillStatus(installState, "3.1.0");
4690
+ const installState = readInstallState(import_path19.default.join(homeDir, agent.skillsFolder), "3.2.0");
4691
+ const status = describeSkillStatus(installState, "3.2.0");
4544
4692
  return `${agent.name.padEnd(AGENT_NAME_COLUMN_WIDTH)} ${STATUS_COLOURS[status.level](status.text)} ${import_chalk2.default.dim(location)}`;
4545
4693
  }
4546
4694
  function formatUndetectedAgentLine(agent) {
@@ -4832,10 +4980,10 @@ async function runAIToolsStep() {
4832
4980
  }
4833
4981
  async function offerSkillInstall(group, sourceDir) {
4834
4982
  const target = describeTarget(group);
4835
- const state = readInstallState(target.skillsFolder, "3.1.0");
4983
+ const state = readInstallState(target.skillsFolder, "3.2.0");
4836
4984
  const knownOutcome = outcomeWithoutPrompt(state);
4837
4985
  if (knownOutcome) {
4838
- printMessage(describeInstallOutcome({ ...target, outcome: knownOutcome, version: "3.1.0" }));
4986
+ printMessage(describeInstallOutcome({ ...target, outcome: knownOutcome, version: "3.2.0" }));
4839
4987
  return;
4840
4988
  }
4841
4989
  const { installSkill: shouldInstall } = await (0, import_prompts2.default)({
@@ -5923,6 +6071,73 @@ var init_scrub_secrets = __esm({
5923
6071
  }
5924
6072
  });
5925
6073
 
6074
+ // src/db/jobs/read-only-commands.ts
6075
+ function parsedObject(metadata) {
6076
+ try {
6077
+ const parsed = JSON.parse(metadata);
6078
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
6079
+ } catch {
6080
+ return null;
6081
+ }
6082
+ }
6083
+ function movedMetadata({ command, metadata }) {
6084
+ if (metadata === null) return JSON.stringify(readOnlyMetadata(command));
6085
+ const parsed = parsedObject(metadata);
6086
+ if (parsed === null) return metadata;
6087
+ const { hint: _hint, tool: _tool, ...kept } = parsed;
6088
+ return JSON.stringify({ ...kept, ...readOnlyMetadata(command) });
6089
+ }
6090
+ function readCandidates(db, afterId, limit) {
6091
+ return db.prepare(
6092
+ `SELECT id, command, metadata FROM events
6093
+ WHERE id > ? AND category IN (${CATEGORY_PLACEHOLDERS})
6094
+ ORDER BY id LIMIT ?`
6095
+ ).all(afterId, ...MOVABLE_CATEGORIES, limit);
6096
+ }
6097
+ function runBatch6(db, afterId, limit) {
6098
+ const events = readCandidates(db, afterId, limit);
6099
+ if (events.length === 0) return { lastId: null, issues: [] };
6100
+ const update = db.prepare("UPDATE events SET category = ?, metadata = ? WHERE id = ?");
6101
+ for (const event of events) {
6102
+ if (isReadOnlyCommand(event.command)) update.run(READ_ONLY_CATEGORY, movedMetadata(event), event.id);
6103
+ }
6104
+ return { lastId: events[events.length - 1].id, issues: [] };
6105
+ }
6106
+ function verify5(db) {
6107
+ let checked = 0;
6108
+ let unresolved = 0;
6109
+ let afterId = 0;
6110
+ for (; ; ) {
6111
+ const events = readCandidates(db, afterId, VERIFY_PAGE_SIZE2);
6112
+ if (events.length === 0) break;
6113
+ checked += events.length;
6114
+ unresolved += events.filter((event) => isReadOnlyCommand(event.command)).length;
6115
+ afterId = events[events.length - 1].id;
6116
+ }
6117
+ const notes = unresolved > 0 ? [`read-only events not moved: ${unresolved}`] : [];
6118
+ return { checked, resolved: checked - unresolved, unresolved, hasPassed: true, notes };
6119
+ }
6120
+ var JOB_NAME2, VERIFY_PAGE_SIZE2, MOVABLE_CATEGORIES, CATEGORY_PLACEHOLDERS, readOnlyCommandsJob;
6121
+ var init_read_only_commands = __esm({
6122
+ "src/db/jobs/read-only-commands.ts"() {
6123
+ "use strict";
6124
+ init_read_only();
6125
+ JOB_NAME2 = "read-only-commands";
6126
+ VERIFY_PAGE_SIZE2 = 500;
6127
+ MOVABLE_CATEGORIES = ["versioning", "other", "files", "running"];
6128
+ CATEGORY_PLACEHOLDERS = MOVABLE_CATEGORIES.map(() => "?").join(", ");
6129
+ readOnlyCommandsJob = {
6130
+ name: JOB_NAME2,
6131
+ version: 1,
6132
+ description: "Move stored read-only commands out of summaries",
6133
+ // Moved rows are simply not candidates any more, so a rerun has nothing to clear first
6134
+ prepare: () => void 0,
6135
+ runBatch: runBatch6,
6136
+ verify: verify5
6137
+ };
6138
+ }
6139
+ });
6140
+
5926
6141
  // src/db/jobs/index.ts
5927
6142
  var JOBS;
5928
6143
  var init_jobs = __esm({
@@ -5934,11 +6149,13 @@ var init_jobs = __esm({
5934
6149
  init_drop_old_sessions();
5935
6150
  init_ticket_refs();
5936
6151
  init_scrub_secrets();
6152
+ init_read_only_commands();
5937
6153
  JOBS = [
5938
6154
  createProjectDimensionsJob({ probeDirectory }),
5939
6155
  summaryAnchorsJob,
5940
6156
  ticketRefsJob,
5941
6157
  scrubSecretsJob,
6158
+ readOnlyCommandsJob,
5942
6159
  dropOldSessionsJob
5943
6160
  ];
5944
6161
  }
@@ -6836,6 +7053,7 @@ function eventRowToPayload(row) {
6836
7053
  branch: row.branch,
6837
7054
  capturedAt: row.captured_at,
6838
7055
  ...row.source ? { source: row.source } : {},
7056
+ ...row.agent_session_id == null ? {} : { agentSessionId: row.agent_session_id },
6839
7057
  ...gitMovesField(row)
6840
7058
  };
6841
7059
  if (row.category !== "other" || typeof metadata.hint !== "string") return payload;
@@ -7649,8 +7867,52 @@ var init_sessions = __esm({
7649
7867
  }
7650
7868
  });
7651
7869
 
7870
+ // src/ai/engines/template/utils.ts
7871
+ function pluralize(count, noun) {
7872
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
7873
+ }
7874
+ function stringsIn(value) {
7875
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item !== "") : [];
7876
+ }
7877
+ function unique(values) {
7878
+ return [...new Set(values)];
7879
+ }
7880
+ function eventsOf(events, category) {
7881
+ return events.filter((event) => event.category === category);
7882
+ }
7883
+ function toBulletList(bullets) {
7884
+ return bullets.map((bullet) => `- ${bullet}`).join("\n");
7885
+ }
7886
+ var init_utils3 = __esm({
7887
+ "src/ai/engines/template/utils.ts"() {
7888
+ "use strict";
7889
+ }
7890
+ });
7891
+
7892
+ // src/ai/engines/template/agents.ts
7893
+ function joinNames(names) {
7894
+ if (names.length <= 1) return names.join("");
7895
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
7896
+ }
7897
+ function describeAgents(events) {
7898
+ const agentEvents = events.filter((event) => event.source !== void 0 && AGENT_NAMES.has(event.source));
7899
+ if (agentEvents.length === 0) return null;
7900
+ const names = [...new Set(agentEvents.map((event) => AGENT_NAMES.get(event.source)))];
7901
+ const sessionIds = new Set(agentEvents.flatMap((event) => event.agentSessionId ?? []));
7902
+ return sessionIds.size > 0 ? `${joinNames(names)} (${pluralize(sessionIds.size, "session")})` : joinNames(names);
7903
+ }
7904
+ var AGENT_NAMES;
7905
+ var init_agents2 = __esm({
7906
+ "src/ai/engines/template/agents.ts"() {
7907
+ "use strict";
7908
+ init_ai_tools();
7909
+ init_utils3();
7910
+ AGENT_NAMES = new Map(ai_tools_default.flatMap((tool) => tool.hook ? [[tool.hook, tool.displayName]] : []));
7911
+ }
7912
+ });
7913
+
7652
7914
  // src/ai/engines/template/constants.ts
7653
- var MAX_SESSION_BULLETS, MAX_COMMIT_BULLETS, MAX_DAY_BULLETS, MAX_LISTED_PACKAGES, AGENT_SOURCES, TEST_WORDING, BUILD_WORDING, STORY_TEST_WORDING, STORY_BUILD_WORDING;
7915
+ var MAX_SESSION_BULLETS, MAX_COMMIT_BULLETS, MAX_DAY_BULLETS, MAX_LISTED_PACKAGES, TEST_WORDING, BUILD_WORDING, STORY_TEST_WORDING, STORY_BUILD_WORDING;
7654
7916
  var init_constants2 = __esm({
7655
7917
  "src/ai/engines/template/constants.ts"() {
7656
7918
  "use strict";
@@ -7658,7 +7920,6 @@ var init_constants2 = __esm({
7658
7920
  MAX_COMMIT_BULLETS = 3;
7659
7921
  MAX_DAY_BULLETS = 5;
7660
7922
  MAX_LISTED_PACKAGES = 6;
7661
- AGENT_SOURCES = ["claude-code"];
7662
7923
  TEST_WORDING = {
7663
7924
  category: "testing",
7664
7925
  resolved: "Resolved failing tests",
@@ -7687,28 +7948,6 @@ var init_constants2 = __esm({
7687
7948
  }
7688
7949
  });
7689
7950
 
7690
- // src/ai/engines/template/utils.ts
7691
- function pluralize(count, noun) {
7692
- return `${count} ${noun}${count === 1 ? "" : "s"}`;
7693
- }
7694
- function stringsIn(value) {
7695
- return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item !== "") : [];
7696
- }
7697
- function unique(values) {
7698
- return [...new Set(values)];
7699
- }
7700
- function eventsOf(events, category) {
7701
- return events.filter((event) => event.category === category);
7702
- }
7703
- function toBulletList(bullets) {
7704
- return bullets.map((bullet) => `- ${bullet}`).join("\n");
7705
- }
7706
- var init_utils3 = __esm({
7707
- "src/ai/engines/template/utils.ts"() {
7708
- "use strict";
7709
- }
7710
- });
7711
-
7712
7951
  // src/ai/engines/template/describe.ts
7713
7952
  function describeNotes(events) {
7714
7953
  return eventsOf(events, "note").flatMap((event) => stringsIn([event.metadata.text]).map((text) => `Note: ${text}`));
@@ -7780,8 +8019,8 @@ function describePushes(events) {
7780
8019
  return targets.length > 0 ? `Pushed to ${targets.join(", ")}` : null;
7781
8020
  }
7782
8021
  function describeAgentWork(events) {
7783
- const agentCommands = events.filter((event) => event.source !== void 0 && AGENT_SOURCES.includes(event.source));
7784
- return agentCommands.length > 0 ? `Worked with an AI coding assistant (${pluralize(agentCommands.length, "command")})` : null;
8022
+ const agents = describeAgents(events);
8023
+ return agents === null ? null : `Worked with ${agents}`;
7785
8024
  }
7786
8025
  function describeEdits(events) {
7787
8026
  const saves = eventsOf(events, "code_edit").length;
@@ -7813,6 +8052,7 @@ var init_describe = __esm({
7813
8052
  "use strict";
7814
8053
  init_commits();
7815
8054
  init_exclusions();
8055
+ init_agents2();
7816
8056
  init_constants2();
7817
8057
  init_utils3();
7818
8058
  }
@@ -7952,12 +8192,10 @@ function branchBullets(group) {
7952
8192
  ].filter((bullet) => bullet !== null);
7953
8193
  }
7954
8194
  function describeActivityLine(allEvents, events) {
7955
- const agentCommands = allEvents.filter(
7956
- (event) => event.source !== void 0 && AGENT_SOURCES.includes(event.source)
7957
- ).length;
8195
+ const agents = describeAgents(allEvents);
7958
8196
  const saves = eventsOf(events, "code_edit").length;
7959
8197
  const parts = [
7960
- agentCommands > 0 ? `With an AI coding assistant (${pluralize(agentCommands, "command")})` : null,
8198
+ agents === null ? null : `With ${agents}`,
7961
8199
  saves > 0 ? `${pluralize(saves, "file save")} in the editor` : null
7962
8200
  ].filter((part) => part !== null);
7963
8201
  return parts.length > 0 ? parts.join(" \xB7 ") : null;
@@ -8002,6 +8240,7 @@ var init_story = __esm({
8002
8240
  "src/ai/engines/template/story.ts"() {
8003
8241
  "use strict";
8004
8242
  init_heading();
8243
+ init_agents2();
8005
8244
  init_constants2();
8006
8245
  init_describe();
8007
8246
  init_branch_moves();
@@ -9349,7 +9588,7 @@ function registerBugReport(program) {
9349
9588
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9350
9589
  const reportPath = import_path30.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
9351
9590
  const systemInfo = [
9352
- `Workscribe version : ${"3.1.0"}`,
9591
+ `Workscribe version : ${"3.2.0"}`,
9353
9592
  `Node.js version : ${process.version}`,
9354
9593
  `Platform : ${process.platform}`,
9355
9594
  `Architecture : ${process.arch}`,
@@ -9380,7 +9619,7 @@ function registerBugReport(program) {
9380
9619
  process.exit(1);
9381
9620
  }
9382
9621
  const params = new URLSearchParams({
9383
- title: `Bug report \u2014 workscribe ${"3.1.0"}`,
9622
+ title: `Bug report \u2014 workscribe ${"3.2.0"}`,
9384
9623
  body: `<!-- Attach ${import_path30.default.basename(reportPath)} or paste the relevant sections below -->
9385
9624
 
9386
9625
  **Describe the issue:**
@@ -9471,7 +9710,7 @@ function reportRun(result) {
9471
9710
  function registerUpgrade(program) {
9472
9711
  program.command("upgrade").description("Bring your stored data up to date after updating Workscribe (backs it up first)").action(() => {
9473
9712
  const announced = /* @__PURE__ */ new Set();
9474
- const result = runDataJobs("3.1.0", {
9713
+ const result = runDataJobs("3.2.0", {
9475
9714
  onProgress: ({ job }) => {
9476
9715
  if (announced.has(job.name)) return;
9477
9716
  announced.add(job.name);
@@ -9961,7 +10200,7 @@ function isNewer(latest, current) {
9961
10200
  return latestPatch > currentPatch;
9962
10201
  }
9963
10202
  function checkForUpdate() {
9964
- const current = "3.1.0";
10203
+ const current = "3.2.0";
9965
10204
  const cache = readCache();
9966
10205
  if (cache && isNewer(cache.latestVersion, current)) {
9967
10206
  process.stderr.write(
@@ -9984,7 +10223,7 @@ function checkForUpdate() {
9984
10223
  function buildTelemetryPayload(config) {
9985
10224
  return {
9986
10225
  installId: config.telemetry.installId,
9987
- cliVersion: "3.1.0",
10226
+ cliVersion: "3.2.0",
9988
10227
  platform: import_os12.default.platform(),
9989
10228
  nodeVersion: process.version
9990
10229
  };
@@ -10062,10 +10301,10 @@ var require_main = __commonJS({
10062
10301
  pingTelemetry();
10063
10302
  }
10064
10303
  if (shouldRunDataJobs(process.argv.slice(2))) {
10065
- runDataJobsBeforeCommand(invokedCommand, "3.1.0");
10304
+ runDataJobsBeforeCommand(invokedCommand, "3.2.0");
10066
10305
  }
10067
10306
  var program = new import_commander.Command();
10068
- program.name("workscribe").description("Automatic developer work journal").version("3.1.0");
10307
+ program.name("workscribe").description("Automatic developer work journal").version("3.2.0");
10069
10308
  registerCapture(program);
10070
10309
  registerHook(program);
10071
10310
  registerInit(program);