@ethlete/agent-rules 0.1.0-next.16 → 0.1.0-next.18

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +14 -6
  3. package/content/hooks/subagent-model-policy.py +5 -3
  4. package/content/rules/app-styling.md +47 -0
  5. package/content/rules/comments.md +4 -1
  6. package/content/rules/nx-layout.md +47 -0
  7. package/content/rules/styling.md +1 -1
  8. package/content/rules/subagent-models.md +7 -7
  9. package/content/skills/angular-patterns/SKILL.md +23 -0
  10. package/content/skills/app-testing/SKILL.md +141 -0
  11. package/content/skills/design-exploration/SKILL.md +3 -1
  12. package/content/skills/git-flow/SKILL.md +4 -4
  13. package/content/skills/query/SKILL.md +150 -82
  14. package/content/skills/rxjs-signals/SKILL.md +20 -0
  15. package/content/skills/sdk-docs/SKILL.md +35 -7
  16. package/content/skills/sdk-update/SKILL.md +12 -7
  17. package/content/skills/story-styling/SKILL.md +7 -6
  18. package/content/skills/styleguide/lint-rule-lookup.md +1 -1
  19. package/content/skills/theming/SKILL.md +3 -4
  20. package/content/skills/timetrack/SKILL.md +154 -49
  21. package/content/skills/verify-in-app/SKILL.md +107 -0
  22. package/migrations/app-styling-utilities.md +114 -0
  23. package/migrations/list-state-query-form.md +103 -0
  24. package/migrations/nx-layout.md +23 -0
  25. package/migrations/sdk-components-over-hand-built-ui.md +69 -0
  26. package/migrations/search-query-field.md +45 -0
  27. package/migrations.json +43 -0
  28. package/package.json +4 -1
  29. package/src/index.js +5 -4
  30. package/src/index.js.map +1 -1
  31. package/src/lib/config.d.ts +2 -0
  32. package/src/lib/config.js +16 -3
  33. package/src/lib/config.js.map +1 -1
  34. package/src/lib/filter.d.ts +2 -0
  35. package/src/lib/filter.js +4 -4
  36. package/src/lib/filter.js.map +1 -1
  37. package/src/lib/frontmatter.js +32 -5
  38. package/src/lib/frontmatter.js.map +1 -1
  39. package/src/lib/git-flow/parse.js +38 -12
  40. package/src/lib/git-flow/parse.js.map +1 -1
  41. package/src/lib/git-flow-repair.js +12 -1
  42. package/src/lib/git-flow-repair.js.map +1 -1
  43. package/src/lib/git.js +8 -2
  44. package/src/lib/git.js.map +1 -1
  45. package/src/lib/gitlab.js +11 -1
  46. package/src/lib/gitlab.js.map +1 -1
  47. package/src/lib/migrate.js +1 -1
  48. package/src/lib/migrate.js.map +1 -1
  49. package/src/lib/output-style.js +11 -3
  50. package/src/lib/output-style.js.map +1 -1
  51. package/src/lib/owned-paths.js +8 -2
  52. package/src/lib/owned-paths.js.map +1 -1
  53. package/src/lib/package-runner.d.ts +1 -0
  54. package/src/lib/package-runner.js +34 -0
  55. package/src/lib/package-runner.js.map +1 -0
  56. package/src/lib/plan.js +29 -3
  57. package/src/lib/plan.js.map +1 -1
  58. package/src/lib/render.d.ts +4 -0
  59. package/src/lib/render.js +8 -2
  60. package/src/lib/render.js.map +1 -1
  61. package/src/lib/sync.js +22 -12
  62. package/src/lib/sync.js.map +1 -1
  63. package/src/lib/targets/codex.js +1 -1
  64. package/src/lib/targets/codex.js.map +1 -1
  65. package/src/lib/targets/copilot.js +1 -1
  66. package/src/lib/targets/copilot.js.map +1 -1
  67. package/src/lib/targets/git-hooks.js +2 -1
  68. package/src/lib/targets/git-hooks.js.map +1 -1
  69. package/src/lib/targets/hooks-shared.js +1 -1
  70. package/src/lib/targets/hooks-shared.js.map +1 -1
  71. package/src/lib/timetrack-command.js +344 -60
  72. package/src/lib/timetrack-command.js.map +1 -1
  73. package/src/lib/timetrack.d.ts +185 -16
  74. package/src/lib/timetrack.js +66 -15
  75. package/src/lib/timetrack.js.map +1 -1
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.timetrackCommand = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const fs_1 = require("fs");
6
+ const path_1 = require("path");
6
7
  const timetrack_1 = require("./timetrack");
7
8
  const plain_text_1 = require("./plain-text");
8
9
  const git_1 = require("./git");
@@ -29,33 +30,65 @@ const FLAGS_WITH_VALUE = [
29
30
  '--paths',
30
31
  '--claim',
31
32
  '--author',
33
+ '--plan',
34
+ '--delete',
35
+ '--repo',
36
+ '--name',
37
+ '--rename',
38
+ '--merge',
39
+ '--into',
32
40
  ];
33
41
  /** Every human-readable line, with anything a terminal would act on printed rather than obeyed. */
34
42
  const say = (line) => console.log((0, plain_text_1.plain)(line));
35
43
  const positionalArgs = (args) => args.filter((entry, index) => { var _a; return !entry.startsWith('--') && !FLAGS_WITH_VALUE.includes((_a = args[index - 1]) !== null && _a !== void 0 ? _a : ''); });
36
44
  const flagValue = (args, flag) => {
37
45
  const index = args.indexOf(flag);
38
- return index === -1 ? undefined : args[index + 1];
46
+ if (index === -1)
47
+ return undefined;
48
+ const value = args[index + 1];
49
+ if (value === undefined || value.startsWith('--'))
50
+ throw new Error(`${flag} needs a value.`);
51
+ return value;
39
52
  };
40
53
  const numberFlag = (args, flag) => {
41
54
  const raw = flagValue(args, flag);
42
55
  if (raw === undefined)
43
56
  return undefined;
44
- const value = Number(raw);
57
+ const value = raw.trim() ? Number(raw) : Number.NaN;
45
58
  if (!Number.isFinite(value))
46
59
  throw new Error(`${flag} takes a number, not ${raw}.`);
47
60
  return value;
48
61
  };
49
- /** `--at` takes anything `Date` reads, so an agent may pass an ISO instant or leave it out for now. */
50
- const instantFlag = (args) => {
51
- const raw = flagValue(args, '--at');
52
- if (!raw)
53
- return Date.now();
54
- const at = new Date(raw);
62
+ const countFlag = (args, flag) => {
63
+ const value = numberFlag(args, flag);
64
+ if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
65
+ throw new Error(`${flag} takes a whole number above zero, not ${value}.`);
66
+ }
67
+ return value;
68
+ };
69
+ const LOCAL_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
70
+ const LOCAL_CLOCK = /^(\d{1,2}):(\d{2})$/;
71
+ /**
72
+ * Reads an instant flag. A date alone is local midnight and a clock alone is that time today, where
73
+ * `Date` would read the first as UTC and refuse the second; anything else goes to `Date` as it is.
74
+ */
75
+ const instantOf = (flag, raw) => {
76
+ const date = LOCAL_DATE.exec(raw);
77
+ const clock = LOCAL_CLOCK.exec(raw);
78
+ const today = new Date();
79
+ const at = date
80
+ ? new Date(Number(date[1]), Number(date[2]) - 1, Number(date[3]))
81
+ : clock
82
+ ? new Date(today.getFullYear(), today.getMonth(), today.getDate(), Number(clock[1]), Number(clock[2]))
83
+ : new Date(raw);
55
84
  if (Number.isNaN(at.getTime()))
56
- throw new Error(`--at takes a date, not ${raw}.`);
85
+ throw new Error(`${flag} takes a date, not ${raw}.`);
57
86
  return at.getTime();
58
87
  };
88
+ const instantFlag = (args) => {
89
+ const raw = flagValue(args, '--at');
90
+ return raw ? instantOf('--at', raw) : Date.now();
91
+ };
59
92
  const issueLine = (issue) => [
60
93
  issue.key,
61
94
  issue.issueType,
@@ -182,12 +215,110 @@ const rowLine = (row) => {
182
215
  (_c = row.laneKey) !== null && _c !== void 0 ? _c : 'no lane',
183
216
  ].join(' ');
184
217
  };
185
- /** `--from` and `--to` take anything `Date` reads, so a caller may pass an ISO instant or a clock. */
186
- const instantOf = (flag, raw) => {
187
- const at = new Date(raw);
188
- if (Number.isNaN(at.getTime()))
189
- throw new Error(`${flag} takes a date, not ${raw}.`);
190
- return at.getTime();
218
+ const shiftDay = (day, days) => {
219
+ const [year, month, date] = day.split('-').map(Number);
220
+ const shifted = new Date(year, month - 1, date + days);
221
+ return `${shifted.getFullYear()}-${pad(shifted.getMonth() + 1)}-${pad(shifted.getDate())}`;
222
+ };
223
+ const minutesOf = (ms) => Math.round(ms / MINUTE_MS);
224
+ const tempoDayLines = (worklogs) => {
225
+ var _a, _b, _c;
226
+ const days = new Map();
227
+ for (const worklog of worklogs) {
228
+ const issues = (_a = days.get(worklog.day)) !== null && _a !== void 0 ? _a : new Map();
229
+ const key = (_b = worklog.issueKey) !== null && _b !== void 0 ? _b : `#${worklog.issueId}`;
230
+ issues.set(key, ((_c = issues.get(key)) !== null && _c !== void 0 ? _c : 0) + worklog.durationMs);
231
+ days.set(worklog.day, issues);
232
+ }
233
+ return [...days]
234
+ .sort(([left], [right]) => left.localeCompare(right))
235
+ .map(([day, issues]) => {
236
+ const total = [...issues.values()].reduce((sum, ms) => sum + ms, 0);
237
+ const parts = [...issues]
238
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
239
+ .map(([key, ms]) => `${key} ${minutesOf(ms)}m`);
240
+ return `${day} ${hours(total)} ${parts.join(' ')}`;
241
+ });
242
+ };
243
+ const syncWriteLine = (write) => {
244
+ var _a;
245
+ return [
246
+ write.kind.padEnd(6),
247
+ (_a = write.issueKey) !== null && _a !== void 0 ? _a : write.proposalId,
248
+ `${clock(write.fromMs)}-${clock(write.fromMs + write.durationMs)}`,
249
+ `${minutesOf(write.durationMs)}m`,
250
+ write.reason,
251
+ ...(write.tempoWorklogId ? [`worklog ${write.tempoWorklogId}`] : []),
252
+ write.description || '(no description)',
253
+ ...(write.blocked ? [`HELD BACK: ${write.blocked}`] : []),
254
+ ].join(' ');
255
+ };
256
+ /** What a sync of a day would write, row by row, and what it leaves alone. */
257
+ const syncPlanLines = (plan) => {
258
+ const count = (kind) => plan.writes.filter((write) => write.kind === kind).length;
259
+ const blocked = plan.writes.filter((write) => write.blocked).length;
260
+ return [
261
+ plan.writes.length
262
+ ? `${plan.day} plan ${plan.planHash} ${count('create')} create, ${count('update')} update, ${count('delete')} delete`
263
+ : `${plan.day} nothing to write`,
264
+ ...plan.writes.map((write) => ` ${syncWriteLine(write)}`),
265
+ `${plan.unchanged} unchanged, ${plan.skipped} skipped (still awaiting review)`,
266
+ ...(blocked ? [`${blocked} row(s) held back: Tempo would refuse them until they are fixed on the day.`] : []),
267
+ ...(plan.unresolvedKeys.length
268
+ ? [`Jira does not know ${plan.unresolvedKeys.join(', ')}, so nothing is written for those rows.`]
269
+ : []),
270
+ ...(plan.coveredMs
271
+ ? [`${minutesOf(plan.coveredMs)}m of the day is already logged by foreign worklogs, so it is left out.`]
272
+ : []),
273
+ `Foreign worklogs (${plan.foreign.length}), never touched`,
274
+ ...plan.foreign.map((worklog) => { var _a; return ` ${(_a = worklog.issueKey) !== null && _a !== void 0 ? _a : `#${worklog.issueId}`} ${clock(worklog.fromMs)} ${minutesOf(worklog.durationMs)}m ${worklog.description || '(no description)'}`; }),
275
+ ];
276
+ };
277
+ /** Every row a write attempted, and what the user has to do about the ones that did not land. */
278
+ const syncRunLines = (run) => [
279
+ ...run.rows.map((row) => {
280
+ var _a;
281
+ return [
282
+ row.status.padEnd(7),
283
+ row.kind.padEnd(6),
284
+ (_a = row.issueKey) !== null && _a !== void 0 ? _a : row.proposalId,
285
+ ...(row.tempoWorklogId ? [`worklog ${row.tempoWorklogId}`] : []),
286
+ ...(row.detail ? [row.detail] : []),
287
+ ].join(' ');
288
+ }),
289
+ ...(run.retryCount
290
+ ? [
291
+ `${run.retryCount} row(s) did not land. Retry them on the Sync page, which retries this run's own plan: a new plan read straight after a write can miss what Tempo just took.`,
292
+ ]
293
+ : []),
294
+ ...(run.unrecorded
295
+ ? [
296
+ `Written, but not recorded: ${run.unrecorded}. Tempo holds these worklogs and the app no longer owns them. Delete them in Tempo before writing this day again, or the time is logged twice.`,
297
+ ]
298
+ : []),
299
+ ];
300
+ const meetingMs = (events) => events
301
+ .filter((event) => !event.allDay && event.response !== 'declined')
302
+ .reduce((sum, event) => sum + event.endMs - event.startMs, 0);
303
+ const calendarEventLine = (event) => {
304
+ const start = new Date(event.startMs);
305
+ const when = event.allDay
306
+ ? 'all-day'
307
+ : `${pad(start.getHours())}:${pad(start.getMinutes())} ${minutesOf(event.endMs - event.startMs)}m`;
308
+ const answer = event.response === 'accepted' || event.response === 'organizer' ? [] : [event.response];
309
+ return [when, event.title, ...answer].join(' ');
310
+ };
311
+ const calendarDayLines = (events) => {
312
+ var _a;
313
+ const days = new Map();
314
+ for (const event of events)
315
+ days.set(event.day, [...((_a = days.get(event.day)) !== null && _a !== void 0 ? _a : []), event]);
316
+ return [...days]
317
+ .sort(([left], [right]) => left.localeCompare(right))
318
+ .flatMap(([day, dayEvents]) => [
319
+ `${day} ${dayEvents.length} event(s), ${hours(meetingMs(dayEvents))}`,
320
+ ...dayEvents.map((event) => ` ${calendarEventLine(event)}`),
321
+ ]);
191
322
  };
192
323
  /** Which edits the flags name. `--from` with `--to` is the one pair that names a single change. */
193
324
  const namedEdits = (argv) => {
@@ -256,7 +387,7 @@ const writeExport = (options) => {
256
387
  (0, fs_1.unlinkSync)(path);
257
388
  const handle = (0, fs_1.openSync)(path, 'wx', 0o600);
258
389
  try {
259
- (0, fs_1.writeSync)(handle, data);
390
+ (0, fs_1.writeFileSync)(handle, data);
260
391
  }
261
392
  finally {
262
393
  (0, fs_1.closeSync)(handle);
@@ -268,6 +399,49 @@ const printed = (value, json) => {
268
399
  console.log(JSON.stringify(value, null, 2));
269
400
  return 0;
270
401
  };
402
+ /** Every write waits in the app for the user's press, so what a write prints is where to look next. */
403
+ const queuedLines = (queued, what) => [
404
+ `Queued in Timetrack for the user's approval: ${what}.`,
405
+ `Nothing is written until they approve it. Read the outcome with: timetrack approval ${queued.approvalId}`,
406
+ ];
407
+ const printedQueued = (options) => {
408
+ if (!options.json)
409
+ queuedLines(options.queued, options.what).forEach(say);
410
+ return printed(options.queued, options.json);
411
+ };
412
+ const isSyncWithRun = (value) => typeof value === 'object' && value !== null && 'run' in value && typeof value.run === 'object';
413
+ const UNDECIDED = {
414
+ queued: "still waits for the user's approval in Timetrack.",
415
+ rejected: 'the user rejected it. Nothing was written.',
416
+ expired: 'expired at the end of the day it was asked on. Nothing was written. Ask again.',
417
+ };
418
+ const approvalLines = (found) => {
419
+ if (found.status !== 'approved')
420
+ return [`${found.approvalId} ${UNDECIDED[found.status]}`];
421
+ if (found.error !== undefined)
422
+ return [`${found.approvalId} approved, but it failed: ${found.error}`];
423
+ const unrecorded = unrecordedOf(found.result);
424
+ return [
425
+ `${found.approvalId} approved and carried out.`,
426
+ ...(isSyncWithRun(found.result) ? syncRunLines(found.result.run) : ['Pass --json for what it answered.']),
427
+ ...(unrecorded && !isSyncWithRun(found.result)
428
+ ? [`Carried out, but the app's ledger did not follow: ${unrecorded}. A later sync may undo it.`]
429
+ : []),
430
+ ];
431
+ };
432
+ const unrecordedOf = (value) => typeof value === 'object' && value !== null && 'unrecorded' in value && typeof value.unrecorded === 'string'
433
+ ? value.unrecorded
434
+ : undefined;
435
+ /** Non-zero for anything that did not land in full, so a caller polling in a script can tell. */
436
+ const approvalExitCode = (found) => {
437
+ if (found.status === 'queued')
438
+ return 0;
439
+ if (found.status !== 'approved' || found.error !== undefined)
440
+ return 1;
441
+ if (unrecordedOf(found.result))
442
+ return 1;
443
+ return isSyncWithRun(found.result) && (found.result.run.retryCount || found.result.run.unrecorded) ? 1 : 0;
444
+ };
271
445
  /**
272
446
  * Cuts a placeholder that covered a whole checkout into one per directory its commits worked in.
273
447
  *
@@ -303,16 +477,8 @@ const splitStandIn = (options) => tslib_1.__awaiter(void 0, void 0, void 0, func
303
477
  const claim = flagValue(argv, '--claim');
304
478
  const apply = argv.includes('--force');
305
479
  const projectRoots = (0, git_1.projectRootsOf)(checkout);
306
- const answer = yield (0, timetrack_1.timetrackSplitStandIn)({
307
- id,
308
- branch,
309
- repoPath,
310
- commits,
311
- projectRoots,
312
- paths,
313
- claim,
314
- apply,
315
- });
480
+ const request = { id, branch, repoPath, commits, projectRoots, paths, claim };
481
+ const answer = yield (0, timetrack_1.timetrackSplitStandIn)(request);
316
482
  if (!json) {
317
483
  say(`${standIn.name} ${standIn.days.length} day(s), branch ${branch}`);
318
484
  say(`Directories the commits name (${answer.candidates.length})`);
@@ -320,10 +486,7 @@ const splitStandIn = (options) => tslib_1.__awaiter(void 0, void 0, void 0, func
320
486
  if (!answer.pieces.length) {
321
487
  say('None of them is an automatic grain. Pick the pieces with --paths <dir>,<dir>.');
322
488
  }
323
- else if (apply) {
324
- say(`Split into ${answer.pieces.length}, and the rule that named it rewritten one per directory.`);
325
- }
326
- else {
489
+ else if (!apply) {
327
490
  say(`Would write ${answer.pieces.map((piece) => piece.workPath).join(', ')}. Pass --force to carry it out.`);
328
491
  }
329
492
  if (claim) {
@@ -338,11 +501,16 @@ const splitStandIn = (options) => tslib_1.__awaiter(void 0, void 0, void 0, func
338
501
  say('Name them in the day review, or re-run with --claim <dir> to give them a directory.');
339
502
  }
340
503
  }
504
+ if (apply && answer.pieces.length) {
505
+ const queued = yield (0, timetrack_1.timetrackApplyStandInSplit)(request);
506
+ return printedQueued({ queued, what: `split ${standIn.name} into ${answer.pieces.length}`, json });
507
+ }
341
508
  return printed(answer, json);
342
509
  });
343
510
  const USAGE = `ethlete-agents timetrack — ask the running Timetrack app about Jira
344
511
 
345
512
  The app holds this machine's Jira credentials, so no repository needs a token of its own.
513
+ Every write waits in the app until the user approves it there, and prints an approval id.
346
514
 
347
515
  timetrack status Whether the app is reachable, and which projects it holds
348
516
  timetrack instance The instance's own levels and its branch-subject candidates
@@ -361,10 +529,23 @@ The app holds this machine's Jira credentials, so no repository needs a token of
361
529
  Delete one placeholder, and the rule that named it
362
530
  timetrack standins --rename <id> --name <text>
363
531
  Give one placeholder another name, keeping its days and its rules
532
+ timetrack standins --merge <id> --into <id>
533
+ Fold one open placeholder into another, with its days and its rules
364
534
  timetrack standins --split <id> [--repo <dir>] [--paths <dir>,<dir>] [--claim <dir>] [--author <email>]
365
535
  [--force]
366
536
  Cut one that covered a whole checkout into one per directory
537
+ timetrack worklogs [from] [to]
538
+ The account's own Tempo worklogs per day (default: the last 7 days)
539
+ timetrack calendar [from] [to]
540
+ The watched calendars' events per day (default: the last 7 days)
541
+ timetrack sync <YYYY-MM-DD> What a Tempo sync of the day would write, as the Sync page plans it
542
+ timetrack sync <YYYY-MM-DD> --write --plan <hash>
543
+ Write that plan to Tempo, once the user confirmed its rows
544
+ timetrack worklog --delete <id> --day <YYYY-MM-DD>
545
+ Delete one of the account's own Tempo worklogs, once the user asked for it
367
546
  timetrack naming [YYYY-MM-DD] Which checkouts the day offers a name for, and why the rest do not
547
+ timetrack resync [path…] Read the agent session logs of checkouts again, after they got a link
548
+ timetrack approval <id> Where a queued write stands, and what it answered once approved
368
549
 
369
550
  Options for search
370
551
  --project <KEY> Search this project instead of the picked ones
@@ -389,6 +570,14 @@ Options for day
389
570
  --out <path> Write the raw answer to a new file, readable by you alone
390
571
  --overwrite Replace the file --out names, which is refused otherwise
391
572
 
573
+ Options for resync
574
+ --replace Overwrite what the store holds for those sessions, after a parser fix
575
+
576
+ Options for sync
577
+ --write Queue the write of the plan. Refused without --plan; the app refuses it on
578
+ approval when the plan changed since
579
+ --plan <hash> The plan hash the read printed, for the rows the user confirmed
580
+
392
581
  Options for edit — pass exactly one change
393
582
  --day <YYYY-MM-DD> The day the row is on (default: today)
394
583
  --from <date> --to <date>
@@ -411,7 +600,7 @@ Options everywhere
411
600
  * one per checkout.
412
601
  */
413
602
  const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
414
- var _a;
603
+ var _a, _b, _c;
415
604
  const { root, argv } = options;
416
605
  const [subcommand, value] = positionalArgs(argv);
417
606
  const json = argv.includes('--json');
@@ -450,7 +639,7 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
450
639
  text: value,
451
640
  projectKey: flagValue(argv, '--project'),
452
641
  assignedToMe: argv.includes('--mine'),
453
- limit: numberFlag(argv, '--limit'),
642
+ limit: countFlag(argv, '--limit'),
454
643
  });
455
644
  if (!json) {
456
645
  if (issues.length === 0)
@@ -460,7 +649,7 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
460
649
  return printed(issues, json);
461
650
  }
462
651
  if (subcommand === 'project') {
463
- const found = yield (0, timetrack_1.timetrackRepoProject)(value !== null && value !== void 0 ? value : root);
652
+ const found = yield (0, timetrack_1.timetrackRepoProject)((0, path_1.resolve)(root, value !== null && value !== void 0 ? value : '.'));
464
653
  if (!json) {
465
654
  const where = found.inherited ? ' (from a directory above it)' : '';
466
655
  if (found.private)
@@ -479,7 +668,7 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
479
668
  const summary = flagValue(argv, '--summary');
480
669
  if (!summary)
481
670
  throw new Error('Pass --summary "…".');
482
- const issue = yield (0, timetrack_1.timetrackCreateIssue)({
671
+ const queued = yield (0, timetrack_1.timetrackCreateIssue)({
483
672
  summary,
484
673
  description: flagValue(argv, '--description'),
485
674
  projectKey: flagValue(argv, '--project'),
@@ -487,9 +676,7 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
487
676
  parentKey: flagValue(argv, '--parent'),
488
677
  subject: flagValue(argv, '--subject'),
489
678
  });
490
- if (!json)
491
- say(`${issue.key} ${summary}`);
492
- return printed(issue, json);
679
+ return printedQueued({ queued, what: `file the issue ${summary}`, json });
493
680
  }
494
681
  if (subcommand === 'log') {
495
682
  const issueKey = flagValue(argv, '--issue');
@@ -498,26 +685,27 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
498
685
  throw new Error('Pass --issue <KEY>.');
499
686
  if (!minutes || minutes <= 0)
500
687
  throw new Error('Pass --minutes <n> above zero.');
501
- const worklog = yield (0, timetrack_1.timetrackAddWorklog)({
688
+ const queued = yield (0, timetrack_1.timetrackAddWorklog)({
502
689
  issueKey,
503
690
  description: flagValue(argv, '--description'),
504
691
  fromMs: instantFlag(argv),
505
692
  durationMs: Math.round(minutes * 60000),
506
693
  });
507
- if (!json) {
508
- say(`${worklog.issueKey} ${minutes}m on ${worklog.day}`);
509
- say('It is a row on the day, not a Tempo entry — review the day in Timetrack, then sync it.');
510
- }
511
- return printed(worklog, json);
694
+ if (!json)
695
+ say('Once approved it is a row on the day, not a Tempo entry — the day still needs a sync.');
696
+ return printedQueued({ queued, what: `a ${minutes}m row for ${issueKey}`, json });
512
697
  }
513
698
  if (subcommand === 'day') {
514
699
  const day = value !== null && value !== void 0 ? value : today();
515
700
  if (!DAY.test(day))
516
701
  throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
517
702
  const found = yield (0, timetrack_1.timetrackDayEvents)(day);
518
- const out = flagValue(argv, '--out');
519
- if (out) {
703
+ const outFlag = flagValue(argv, '--out');
704
+ if (outFlag) {
705
+ const out = (0, path_1.resolve)(root, outFlag);
520
706
  writeExport({ path: out, data: JSON.stringify(found), overwrite: argv.includes('--overwrite') });
707
+ if (json)
708
+ return printed({ day: found.day, events: found.events.length, out }, json);
521
709
  say(`${found.day} ${found.events.length} events written to ${out}, readable by you alone`);
522
710
  say('It holds the day as it was observed: window titles, paths and messages. Delete it when you are done.');
523
711
  return 0;
@@ -548,13 +736,8 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
548
736
  throw new Error('Pass the row id to edit, as `timetrack rows` prints it.');
549
737
  if (!DAY.test(day))
550
738
  throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
551
- const edited = yield (0, timetrack_1.timetrackEditDay)({ day, edits: [editOf({ argv, rowId: value })] });
552
- if (!json) {
553
- say(`${edited.day} ${edited.applied} of 1 edit landed`);
554
- edited.rows.forEach((row) => say(` ${rowLine(row)}`));
555
- edited.warnings.forEach((warning) => say(` ! ${warning.kind}: ${warning.detail}`));
556
- }
557
- return printed(edited, json);
739
+ const queued = yield (0, timetrack_1.timetrackEditDay)({ day, edits: [editOf({ argv, rowId: value })] });
740
+ return printedQueued({ queued, what: `an edit of row ${value} on ${day}`, json });
558
741
  }
559
742
  if (subcommand === 'rules') {
560
743
  const rules = yield (0, timetrack_1.timetrackRules)();
@@ -567,8 +750,90 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
567
750
  }
568
751
  return printed(rules, json);
569
752
  }
753
+ if (subcommand === 'resync') {
754
+ const named = positionalArgs(argv).slice(1);
755
+ const paths = (named.length ? named : ['.']).map((path) => (0, path_1.resolve)(root, path));
756
+ const replace = argv.includes('--replace');
757
+ const queued = yield (0, timetrack_1.timetrackResyncAgentSessions)(paths, { replace });
758
+ const how = replace ? ', replacing what it stored for them' : '';
759
+ return printedQueued({ queued, what: `read the agent sessions of ${paths.join(', ')} again${how}`, json });
760
+ }
761
+ if (subcommand === 'worklogs') {
762
+ const to = (_b = positionalArgs(argv)[2]) !== null && _b !== void 0 ? _b : today();
763
+ const from = value !== null && value !== void 0 ? value : shiftDay(to, -6);
764
+ if (!DAY.test(from))
765
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${from}.`);
766
+ if (!DAY.test(to))
767
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${to}.`);
768
+ const found = yield (0, timetrack_1.timetrackTempoWorklogs)({ from, to });
769
+ if (!json) {
770
+ const total = found.worklogs.reduce((sum, worklog) => sum + worklog.durationMs, 0);
771
+ say(`${found.from} … ${found.to} ${found.worklogs.length} worklog(s), ${hours(total)}`);
772
+ tempoDayLines(found.worklogs).forEach((line) => say(` ${line}`));
773
+ }
774
+ return printed(found, json);
775
+ }
776
+ if (subcommand === 'worklog') {
777
+ if (argv.includes('--help')) {
778
+ say(USAGE);
779
+ return 0;
780
+ }
781
+ const worklogId = flagValue(argv, '--delete');
782
+ const day = flagValue(argv, '--day');
783
+ if (!worklogId || !/^\d+$/.test(worklogId))
784
+ throw new Error('Pass the Tempo worklog id to delete with --delete <id>.');
785
+ if (!day || !DAY.test(day))
786
+ throw new Error('Pass the day the worklog is on with --day <YYYY-MM-DD>.');
787
+ const queued = yield (0, timetrack_1.timetrackTempoDelete)({ day, worklogId });
788
+ return printedQueued({ queued, what: `delete Tempo worklog ${worklogId} on ${day}`, json });
789
+ }
790
+ if (subcommand === 'sync') {
791
+ if (!value || !DAY.test(value))
792
+ throw new Error('Pass the day to sync as YYYY-MM-DD.');
793
+ const planHash = flagValue(argv, '--plan');
794
+ const write = argv.includes('--write');
795
+ if (write && !planHash) {
796
+ say(`A write names the plan the user confirmed. Read it with \`timetrack sync ${value}\`, show the rows,`);
797
+ say('and pass the hash it prints with --plan once they agree.');
798
+ return 1;
799
+ }
800
+ if (planHash && !write) {
801
+ say('--plan only means something with --write. Nothing was written.');
802
+ return 1;
803
+ }
804
+ if (write && planHash) {
805
+ const queued = yield (0, timetrack_1.timetrackTempoSyncWrite)({ day: value, planHash });
806
+ if (!json)
807
+ say('The app checks the plan again on approval, and refuses it if the day changed since.');
808
+ return printedQueued({ queued, what: `write plan ${planHash} of ${value} to Tempo`, json });
809
+ }
810
+ const found = yield (0, timetrack_1.timetrackTempoSync)(value);
811
+ if (!json) {
812
+ syncPlanLines(found).forEach(say);
813
+ if (found.writes.length) {
814
+ say(`Once the user has confirmed these rows: timetrack sync ${found.day} --write --plan ${found.planHash}`);
815
+ }
816
+ }
817
+ return printed(found, json);
818
+ }
819
+ if (subcommand === 'calendar') {
820
+ const to = (_c = positionalArgs(argv)[2]) !== null && _c !== void 0 ? _c : today();
821
+ const from = value !== null && value !== void 0 ? value : shiftDay(to, -6);
822
+ if (!DAY.test(from))
823
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${from}.`);
824
+ if (!DAY.test(to))
825
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${to}.`);
826
+ const found = yield (0, timetrack_1.timetrackCalendarEvents)({ from, to });
827
+ if (!json) {
828
+ say(`${found.from} … ${found.to} ${found.events.length} event(s) from ${found.calendarIds.length} calendar(s), ${hours(meetingMs(found.events))} in meetings`);
829
+ calendarDayLines(found.events).forEach((line) => say(` ${line}`));
830
+ }
831
+ return printed(found, json);
832
+ }
570
833
  if (subcommand === 'naming') {
571
- const day = value && DAY.test(value) ? value : today();
834
+ const day = value !== null && value !== void 0 ? value : today();
835
+ if (!DAY.test(day))
836
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
572
837
  const naming = yield (0, timetrack_1.timetrackNaming)(day);
573
838
  if (!json) {
574
839
  say(`${day} tempo ${naming.tempoReady ? 'configured' : 'not configured'}`);
@@ -582,6 +847,15 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
582
847
  }
583
848
  return printed(naming, json);
584
849
  }
850
+ if (subcommand === 'approval') {
851
+ if (!value)
852
+ throw new Error('Pass the approval id a write printed, e.g. `timetrack approval <id>`.');
853
+ const found = yield (0, timetrack_1.timetrackApprovalStatus)(value);
854
+ if (!json)
855
+ approvalLines(found).forEach(say);
856
+ printed(found, json);
857
+ return approvalExitCode(found);
858
+ }
585
859
  if (subcommand === 'standins') {
586
860
  const split = flagValue(argv, '--split');
587
861
  if (split)
@@ -593,10 +867,18 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
593
867
  return 1;
594
868
  }
595
869
  if (rename && name) {
596
- const renamed = yield (0, timetrack_1.timetrackRenameStandIn)({ id: rename, name });
597
- if (!json)
598
- say(`Renamed ${rename} to ${name}.`);
599
- return printed(renamed, json);
870
+ const queued = yield (0, timetrack_1.timetrackRenameStandIn)({ id: rename, name });
871
+ return printedQueued({ queued, what: `rename ${rename} to ${name}`, json });
872
+ }
873
+ const merge = flagValue(argv, '--merge');
874
+ const into = flagValue(argv, '--into');
875
+ if (merge && !into) {
876
+ say(`A merge needs the placeholder it goes into. Pass --into <id> with --merge ${merge}.`);
877
+ return 1;
878
+ }
879
+ if (merge && into) {
880
+ const queued = yield (0, timetrack_1.timetrackMergeStandIn)({ id: merge, into });
881
+ return printedQueued({ queued, what: `merge ${merge} into ${into}`, json });
600
882
  }
601
883
  const remove = flagValue(argv, '--remove');
602
884
  const stranded = remove ? yield strandedDays(remove) : [];
@@ -606,14 +888,16 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
606
888
  say('The app reopens a placeholder for today only. Pass --force if that is what you want.');
607
889
  return 1;
608
890
  }
609
- const standIns = remove ? yield (0, timetrack_1.timetrackRemoveStandIn)(remove) : yield (0, timetrack_1.timetrackStandIns)();
891
+ if (remove) {
892
+ const queued = yield (0, timetrack_1.timetrackRemoveStandIn)(remove);
893
+ return printedQueued({ queued, what: `delete ${remove} and the rule that named it`, json });
894
+ }
895
+ const standIns = yield (0, timetrack_1.timetrackStandIns)();
610
896
  const rules = json ? [] : (yield (0, timetrack_1.timetrackRules)()).attributionRules;
611
897
  const open = standIns
612
898
  .filter((standIn) => standIn.state === 'open')
613
899
  .sort((left, right) => left.createdAtMs - right.createdAtMs);
614
900
  if (!json) {
615
- if (remove)
616
- say(`Deleted ${remove}, and the rule that named it.`);
617
901
  say(`${open.length} open, ${standIns.length - open.length} resolved`);
618
902
  open.forEach((standIn) => say(` ${describeStandIn({ standIn, rules })}`));
619
903
  if (open.length)