@crewx/cli 0.9.0-rc.14 → 0.9.0-rc.140

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 (65) hide show
  1. package/dist/bootstrap/codex-writable-roots.d.ts +13 -0
  2. package/dist/bootstrap/codex-writable-roots.js +25 -0
  3. package/dist/bootstrap/crewx-cli.js +27 -6
  4. package/dist/builtin.d.ts +1 -1
  5. package/dist/builtin.js +7 -2
  6. package/dist/commands/agent.js +1 -107
  7. package/dist/commands/cal.d.ts +15 -0
  8. package/dist/commands/cal.js +44 -0
  9. package/dist/commands/db.d.ts +3 -0
  10. package/dist/commands/db.js +533 -1
  11. package/dist/commands/detached.d.ts +95 -0
  12. package/dist/commands/detached.js +620 -0
  13. package/dist/commands/doctor.d.ts +77 -0
  14. package/dist/commands/doctor.js +475 -22
  15. package/dist/commands/emit-trailer.d.ts +25 -0
  16. package/dist/commands/emit-trailer.js +33 -0
  17. package/dist/commands/execute.d.ts +8 -4
  18. package/dist/commands/execute.js +200 -51
  19. package/dist/commands/hook/command-marker.d.ts +2 -0
  20. package/dist/commands/hook/command-marker.js +5 -0
  21. package/dist/commands/hook/install.d.ts +0 -1
  22. package/dist/commands/hook/install.js +75 -61
  23. package/dist/commands/hook/status.js +3 -3
  24. package/dist/commands/hook/uninstall.js +2 -2
  25. package/dist/commands/init.d.ts +34 -0
  26. package/dist/commands/init.js +132 -59
  27. package/dist/commands/kill.d.ts +2 -8
  28. package/dist/commands/kill.js +6 -47
  29. package/dist/commands/log.js +4 -3
  30. package/dist/commands/parse-common-flags.d.ts +5 -1
  31. package/dist/commands/parse-common-flags.js +35 -4
  32. package/dist/commands/ps.d.ts +2 -2
  33. package/dist/commands/ps.js +25 -15
  34. package/dist/commands/publish.d.ts +1 -0
  35. package/dist/commands/publish.js +303 -0
  36. package/dist/commands/query.d.ts +7 -2
  37. package/dist/commands/query.js +195 -48
  38. package/dist/commands/registry.js +4 -2
  39. package/dist/commands/resolve-prompt.js +23 -1
  40. package/dist/commands/resolve-recipient.d.ts +14 -0
  41. package/dist/commands/resolve-recipient.js +47 -0
  42. package/dist/commands/restart.d.ts +3 -1
  43. package/dist/commands/restart.js +14 -4
  44. package/dist/commands/result.d.ts +8 -3
  45. package/dist/commands/result.js +149 -10
  46. package/dist/commands/shortcut.d.ts +1 -0
  47. package/dist/commands/shortcut.js +267 -0
  48. package/dist/commands/slack.js +2 -1
  49. package/dist/commands/stop.d.ts +8 -0
  50. package/dist/commands/stop.js +119 -0
  51. package/dist/index.d.ts +1 -1
  52. package/dist/index.js +4 -1
  53. package/dist/logging.d.ts +1 -1
  54. package/dist/logging.js +3 -2
  55. package/dist/main.d.ts +3 -2
  56. package/dist/main.js +99 -24
  57. package/dist/utils/env-defaults.d.ts +2 -5
  58. package/dist/utils/env-defaults.js +10 -5
  59. package/dist/utils/inherited-trace.d.ts +24 -0
  60. package/dist/utils/inherited-trace.js +40 -0
  61. package/dist/utils/product-package-root.d.ts +6 -0
  62. package/dist/utils/product-package-root.js +39 -0
  63. package/dist/utils/sdk-compat.d.ts +24 -0
  64. package/dist/utils/sdk-compat.js +120 -0
  65. package/package.json +16 -12
@@ -11,12 +11,77 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
11
11
  return (mod && mod.__esModule) ? mod : { "default": mod };
12
12
  };
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.USAGE_BACKFILL_HELP = exports.REQUEST_LOG_MIGRATION_HELP = exports.TASK_LOG_MIGRATION_HELP = void 0;
14
15
  exports.handleDb = handleDb;
15
16
  const path_1 = __importDefault(require("path"));
16
17
  const os_1 = __importDefault(require("os"));
17
18
  const readline_1 = __importDefault(require("readline"));
18
19
  const repository_1 = require("@crewx/sdk/repository");
19
20
  const repository_2 = require("@crewx/sdk/repository");
21
+ const repository_3 = require("@crewx/sdk/repository");
22
+ const repository_4 = require("@crewx/sdk/repository");
23
+ const repository_5 = require("@crewx/sdk/repository");
24
+ exports.TASK_LOG_MIGRATION_HELP = `Usage:
25
+ crewx db migrate-task-logs --dry-run [--db PATH]
26
+ crewx db migrate-task-logs --apply [--db PATH] [--batch-tasks N]
27
+ crewx db migrate-task-logs --verify [--db PATH]
28
+
29
+ Modes:
30
+ --dry-run Read and validate legacy blobs without writing or creating a backup.
31
+ --apply Create a consistent SQLite backup, then migrate one task per transaction.
32
+ --verify Check source invariants, event sequences, counts, and orphan rows.
33
+
34
+ Apply policy:
35
+ Only blob tasks with status success, failed, or completed are migrated.
36
+ pending, running, paused, and unknown statuses are deferred for a later run.
37
+ There is no --force or --include-active option. Apply requires the estimated
38
+ database/event growth plus a 10 GiB free-space reserve.
39
+
40
+ Exit codes:
41
+ 0 Completed with no malformed rows or invariant failures.
42
+ 1 Safe tasks completed but a row/invariant failed, or apply was blocked.
43
+ 2 Invalid command-line arguments.
44
+
45
+ The command never runs automatically at server startup and never runs VACUUM.`;
46
+ exports.REQUEST_LOG_MIGRATION_HELP = `Usage:
47
+ crewx db migrate-request-logs [--batch N] [--dry-run]
48
+
49
+ Moves the request log rows still stored in crewx.db into logs.db, the file next
50
+ to it. Each distinct request/response body is stored once in logs.db, keyed by
51
+ its SHA-256; a row points to its bodies by hash, so nothing is dropped.
52
+
53
+ Options:
54
+ --batch N Rows per batch (default 250; a batch is also capped at about 8 MB).
55
+ --dry-run Count rows and distinct bodies without writing anything (logs.db is not created).
56
+
57
+ Safe while crewx is running: each batch is committed to logs.db, verified there,
58
+ and only then deleted from crewx.db in one short transaction. Interrupt it and run
59
+ it again at any time. The database is chosen like everywhere else (CREWX_DB, else
60
+ ~/.crewx/crewx.db). No VACUUM and no DROP: crewx.db keeps its size until a later compaction.
61
+
62
+ Exit codes:
63
+ 0 Every row is in logs.db and none is left in crewx.db (or the dry run finished).
64
+ 1 Rows are still in crewx.db, or an error stopped the move (progress is kept).
65
+ 2 Invalid command-line arguments.`;
66
+ exports.USAGE_BACKFILL_HELP = `Usage:
67
+ crewx db backfill-usage [--dry-run|--apply|--verify] [--db PATH] [--backup PATH]
68
+ crewx db backfill-usage --restore SNAPSHOT [--db PATH]
69
+
70
+ Modes:
71
+ --dry-run Show provider/reason recovery plans without changing the database (default).
72
+ --apply Create a row-level undo snapshot, then account one task per transaction.
73
+ --verify Recheck remaining targets and usageAccounting markers without changing the database.
74
+ --restore Restore rows from SNAPSHOT when their current values still match the snapshot after-state.
75
+
76
+ Apply policy:
77
+ Only success, failed, and completed tasks with zero recorded usage are considered.
78
+ Workflow/MCP anchors and plugin/mock rows are excluded. Rows completed within 24 hours
79
+ are not marked unavailable when their logs do not contain usage.
80
+
81
+ Exit codes:
82
+ 0 Completed successfully.
83
+ 1 Apply was blocked or one or more task updates failed.
84
+ 2 Invalid command-line arguments.`;
20
85
  function defaultDbPath() {
21
86
  return path_1.default.join(os_1.default.homedir(), '.crewx', 'crewx.db');
22
87
  }
@@ -57,6 +122,12 @@ function formatPreview(result, dbPath) {
57
122
  function hasChanges(result) {
58
123
  return result.created.length > 0 || result.altered.length > 0;
59
124
  }
125
+ function formatStatusCounts(counts) {
126
+ const entries = Object.entries(counts ?? {});
127
+ return entries.length > 0
128
+ ? entries.map(([status, count]) => `${status}=${count}`).join(', ')
129
+ : '(none)';
130
+ }
60
131
  function prompt(question) {
61
132
  const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
62
133
  return new Promise((resolve) => {
@@ -68,13 +139,474 @@ function prompt(question) {
68
139
  }
69
140
  async function handleDb(args) {
70
141
  const subcommand = args[0];
142
+ if (subcommand === 'migrate-task-logs') {
143
+ await handleTaskLogMigration(args.slice(1));
144
+ return;
145
+ }
146
+ if (subcommand === 'migrate-request-logs') {
147
+ await handleRequestLogMigration(args.slice(1));
148
+ return;
149
+ }
150
+ if (subcommand === 'backfill-usage') {
151
+ await handleUsageBackfill(args.slice(1));
152
+ return;
153
+ }
154
+ if (subcommand === '--help' || subcommand === '-h') {
155
+ console.log('Usage: crewx db push [--force] [--dry-run]');
156
+ console.log(exports.TASK_LOG_MIGRATION_HELP);
157
+ console.log(exports.REQUEST_LOG_MIGRATION_HELP);
158
+ return;
159
+ }
71
160
  if (!subcommand || subcommand === 'push') {
72
161
  await handleDbPush(args.slice(subcommand === 'push' ? 1 : 0));
73
162
  return;
74
163
  }
75
164
  console.error(`Unknown db subcommand: ${subcommand}`);
76
165
  console.error('Usage: crewx db push [--force] [--dry-run]');
77
- process.exit(1);
166
+ console.error(exports.TASK_LOG_MIGRATION_HELP);
167
+ process.exitCode = 2;
168
+ }
169
+ class TaskLogMigrationUsageError extends Error {
170
+ }
171
+ class UsageBackfillUsageError extends Error {
172
+ }
173
+ function parseTaskLogMigrationArgs(args) {
174
+ let mode;
175
+ let dbPath;
176
+ let batchTasks;
177
+ const setMode = (next) => {
178
+ if (mode)
179
+ throw new TaskLogMigrationUsageError('Choose exactly one of --dry-run, --apply, or --verify.');
180
+ mode = next;
181
+ };
182
+ const requireValue = (args, index, flag) => {
183
+ const value = args[index + 1];
184
+ if (!value || value.startsWith('--'))
185
+ throw new TaskLogMigrationUsageError(`${flag} requires a value.`);
186
+ return value;
187
+ };
188
+ for (let index = 0; index < args.length; index += 1) {
189
+ const arg = args[index];
190
+ if (arg === '--help' || arg === '-h') {
191
+ throw new TaskLogMigrationUsageError(exports.TASK_LOG_MIGRATION_HELP);
192
+ }
193
+ if (arg === '--dry-run') {
194
+ setMode('dry-run');
195
+ continue;
196
+ }
197
+ if (arg === '--apply') {
198
+ setMode('apply');
199
+ continue;
200
+ }
201
+ if (arg === '--verify') {
202
+ setMode('verify');
203
+ continue;
204
+ }
205
+ if (arg === '--db') {
206
+ dbPath = requireValue(args, index, '--db');
207
+ index += 1;
208
+ continue;
209
+ }
210
+ if (arg.startsWith('--db=')) {
211
+ dbPath = arg.slice('--db='.length);
212
+ if (!dbPath)
213
+ throw new TaskLogMigrationUsageError('--db requires a value.');
214
+ continue;
215
+ }
216
+ if (arg === '--batch-tasks') {
217
+ const value = requireValue(args, index, '--batch-tasks');
218
+ const parsed = Number(value);
219
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
220
+ throw new TaskLogMigrationUsageError('--batch-tasks must be a positive integer.');
221
+ }
222
+ batchTasks = parsed;
223
+ index += 1;
224
+ continue;
225
+ }
226
+ if (arg.startsWith('--batch-tasks=')) {
227
+ const value = arg.slice('--batch-tasks='.length);
228
+ const parsed = Number(value);
229
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
230
+ throw new TaskLogMigrationUsageError('--batch-tasks must be a positive integer.');
231
+ }
232
+ batchTasks = parsed;
233
+ continue;
234
+ }
235
+ throw new TaskLogMigrationUsageError(`Unknown option: ${arg}`);
236
+ }
237
+ if (!mode)
238
+ throw new TaskLogMigrationUsageError('Choose one of --dry-run, --apply, or --verify.');
239
+ if (batchTasks !== undefined && mode !== 'apply') {
240
+ throw new TaskLogMigrationUsageError('--batch-tasks is only valid with --apply.');
241
+ }
242
+ return { mode, dbPath, batchTasks };
243
+ }
244
+ function formatMigrationReport(report) {
245
+ const modeLabel = report.mode === 'dry-run' ? 'Dry-run' : report.mode === 'apply' ? 'Apply' : 'Verify';
246
+ const lines = [
247
+ `[crewx] Task-log migration — ${modeLabel}`,
248
+ ` Database: ${report.dbPath}`,
249
+ ` Tasks scanned: ${report.taskCount}`,
250
+ ` Blob candidates: ${report.candidateTaskCount}`,
251
+ ` Eligible terminal blobs: ${report.eligibleTaskCount}`,
252
+ ` Status counts: ${formatStatusCounts(report.statusCounts)}`,
253
+ ` Deferred blob tasks: ${report.deferredTaskCount}`,
254
+ ` Deferred statuses: ${formatStatusCounts(report.deferredStatusCounts)}`,
255
+ ` Entries: ${report.entryCount}`,
256
+ ` Source bytes: ${report.sourceBytes}`,
257
+ ` Estimated free-space need: ${report.estimatedFreeSpaceBytes}`,
258
+ ` Required free space (with reserve): ${report.requiredFreeSpaceBytes}`,
259
+ ` Available free space: ${report.availableFreeSpaceBytes}`,
260
+ ` Free-space gate: ${report.freeSpaceGatePassed ? 'PASS' : 'FAIL'}`,
261
+ ` Migrated: ${report.migratedTasks} task(s), ${report.migratedEntries} entr${report.migratedEntries === 1 ? 'y' : 'ies'}`,
262
+ ` Skipped event-source tasks: ${report.skippedTasks}`,
263
+ ` Failed tasks: ${report.failedTasks}`,
264
+ ` Logical source bytes removed: ${report.logicalSourceBytesRemoved}`,
265
+ ];
266
+ if (report.remainingTaskCount !== undefined)
267
+ lines.push(` Remaining blob tasks: ${report.remainingTaskCount}`);
268
+ if (report.backupPath) {
269
+ lines.push(` Backup: ${report.backupPath}`);
270
+ if (report.backupMethod)
271
+ lines.push(` Backup method: ${report.backupMethod}`);
272
+ }
273
+ if (report.verification) {
274
+ lines.push(` Event rows checked: ${report.verification.eventCount}`);
275
+ lines.push(` Verification issues: ${report.verification.issues.length}`);
276
+ }
277
+ if (report.blockedReason)
278
+ lines.push(` Blocked: ${report.blockedReason}`);
279
+ for (const warning of report.warnings ?? [])
280
+ lines.push(` Warning: ${warning}`);
281
+ return lines.join('\n');
282
+ }
283
+ async function handleTaskLogMigration(args) {
284
+ let parsed;
285
+ try {
286
+ parsed = parseTaskLogMigrationArgs(args);
287
+ }
288
+ catch (error) {
289
+ const message = error instanceof Error ? error.message : String(error);
290
+ if (message === exports.TASK_LOG_MIGRATION_HELP) {
291
+ console.log(message);
292
+ }
293
+ else {
294
+ console.error(message);
295
+ console.error(exports.TASK_LOG_MIGRATION_HELP);
296
+ process.exitCode = 2;
297
+ }
298
+ return;
299
+ }
300
+ try {
301
+ const report = await (0, repository_3.runTaskLogMigration)(parsed);
302
+ console.log(formatMigrationReport(report));
303
+ if (!report.ok) {
304
+ if (report.blockedReason)
305
+ console.error(report.blockedReason);
306
+ const failures = report.failures ?? report.malformedRows ?? [];
307
+ for (const failure of failures) {
308
+ console.error(` ${failure.taskId}: ${failure.reason}`);
309
+ }
310
+ process.exitCode = 1;
311
+ }
312
+ else {
313
+ process.exitCode = 0;
314
+ }
315
+ }
316
+ catch (error) {
317
+ console.error(error instanceof Error ? error.message : String(error));
318
+ process.exitCode = 1;
319
+ }
320
+ }
321
+ class RequestLogMigrationUsageError extends Error {
322
+ }
323
+ function parseRequestLogMigrationArgs(args) {
324
+ let batchRows;
325
+ let dryRun = false;
326
+ const parseBatch = (value) => {
327
+ const parsed = Number(value);
328
+ if (!value || !Number.isSafeInteger(parsed) || parsed < 1) {
329
+ throw new RequestLogMigrationUsageError('--batch must be a positive integer.');
330
+ }
331
+ return parsed;
332
+ };
333
+ for (let index = 0; index < args.length; index += 1) {
334
+ const arg = args[index];
335
+ if (arg === '--help' || arg === '-h')
336
+ throw new RequestLogMigrationUsageError(exports.REQUEST_LOG_MIGRATION_HELP);
337
+ if (arg === '--dry-run') {
338
+ dryRun = true;
339
+ continue;
340
+ }
341
+ if (arg === '--batch') {
342
+ batchRows = parseBatch(args[index + 1]);
343
+ index += 1;
344
+ continue;
345
+ }
346
+ if (arg.startsWith('--batch=')) {
347
+ batchRows = parseBatch(arg.slice('--batch='.length));
348
+ continue;
349
+ }
350
+ throw new RequestLogMigrationUsageError(`Unknown option: ${arg}`);
351
+ }
352
+ return { batchRows, dryRun };
353
+ }
354
+ function formatBytes(bytes) {
355
+ if (bytes < 1024)
356
+ return `${bytes} B`;
357
+ const units = ['KiB', 'MiB', 'GiB', 'TiB'];
358
+ let value = bytes;
359
+ let unit = -1;
360
+ while (value >= 1024 && unit < units.length - 1) {
361
+ value /= 1024;
362
+ unit += 1;
363
+ }
364
+ return `${value.toFixed(value >= 100 ? 0 : 1)} ${units[unit]} (${bytes} bytes)`;
365
+ }
366
+ function percentile(values, p) {
367
+ const sorted = [...values].sort((a, b) => a - b);
368
+ return sorted[Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length))];
369
+ }
370
+ function formatSeconds(ms) {
371
+ return `${(ms / 1000).toFixed(1)}s`;
372
+ }
373
+ const REQUEST_LOG_PROGRESS_INTERVAL_MS = 10_000;
374
+ function printRequestLogSummary(result, status) {
375
+ if (result.dryRun) {
376
+ const scan = result.scan;
377
+ console.log('[crewx] Dry run finished — nothing was written.');
378
+ console.log(` Rows scanned: ${scan?.rows ?? 0}`);
379
+ console.log(` Bodies: ${scan?.bodies ?? 0} (${formatBytes(scan?.bodyBytes ?? 0)})`);
380
+ console.log(` Distinct bodies: ${scan?.distinctBodies ?? 0} (${formatBytes(scan?.distinctBodyBytes ?? 0)}) — what logs.db would store`);
381
+ console.log(` Elapsed: ${formatSeconds(result.elapsedMs)}`);
382
+ return;
383
+ }
384
+ console.log('[crewx] Request-log move finished.');
385
+ console.log(` Moved rows: ${result.movedRows}`);
386
+ console.log(` Remaining rows in crewx.db: ${status.crewxRows}`);
387
+ console.log(` logs.db request_logs: ${status.logsRows ?? 0}`);
388
+ console.log(` logs.db request_log_bodies: ${status.logsBodies ?? 0}`);
389
+ console.log(` logs.db size: ${formatBytes(status.logsFileBytes)} (+ wal ${formatBytes(status.logsWalBytes)})`);
390
+ console.log(` crewx.db freelist pages: ${status.crewxFreelistPages}`);
391
+ console.log(` Batches: ${result.batches}, bodies added: ${result.bodiesAdded}`);
392
+ if (result.deleteTxMs.length > 0) {
393
+ console.log(` crewx.db delete transaction per batch: p50 ${percentile(result.deleteTxMs, 50).toFixed(1)} ms, p90 ${percentile(result.deleteTxMs, 90).toFixed(1)} ms, max ${result.maxDeleteTxMs.toFixed(1)} ms`);
394
+ console.log(` Largest crewx.db -wal seen after a batch: ${formatBytes(result.maxWalBytes)}`);
395
+ }
396
+ console.log(` Elapsed: ${formatSeconds(result.elapsedMs)}`);
397
+ if (status.crewxRows > 0) {
398
+ console.log(` ${status.crewxRows} row(s) are still in crewx.db (new rows arrived or the run stopped early). Run it again.`);
399
+ }
400
+ }
401
+ async function handleRequestLogMigration(args) {
402
+ let parsed;
403
+ try {
404
+ parsed = parseRequestLogMigrationArgs(args);
405
+ }
406
+ catch (error) {
407
+ const message = error instanceof Error ? error.message : String(error);
408
+ if (message === exports.REQUEST_LOG_MIGRATION_HELP) {
409
+ console.log(message);
410
+ }
411
+ else {
412
+ console.error(message);
413
+ console.error(exports.REQUEST_LOG_MIGRATION_HELP);
414
+ process.exitCode = 2;
415
+ }
416
+ return;
417
+ }
418
+ try {
419
+ const before = (0, repository_4.inspectRequestLogsMove)();
420
+ console.log(`[crewx] Request logs → logs.db${parsed.dryRun ? ' (dry run)' : ''}`);
421
+ console.log(` crewx.db: ${before.crewxDbPath}`);
422
+ console.log(` logs.db: ${before.logsDbPath}`);
423
+ console.log(` Rows to move: ${before.crewxRows}`);
424
+ let lastPrint = Date.now();
425
+ const onProgress = (progress) => {
426
+ const now = Date.now();
427
+ if (now - lastPrint < REQUEST_LOG_PROGRESS_INTERVAL_MS)
428
+ return;
429
+ lastPrint = now;
430
+ const done = parsed.dryRun ? progress.scannedRows : progress.movedRows;
431
+ const remaining = Math.max(0, before.crewxRows - done);
432
+ console.log(` ${parsed.dryRun ? 'scanned' : 'moved'} ${done} | remaining ~${remaining} | elapsed ${formatSeconds(progress.elapsedMs)}`);
433
+ };
434
+ const result = (0, repository_4.moveRequestLogsToLogsDb)({
435
+ batchRows: parsed.batchRows,
436
+ dryRun: parsed.dryRun,
437
+ onProgress,
438
+ });
439
+ printRequestLogSummary(result, (0, repository_4.inspectRequestLogsMove)());
440
+ process.exitCode = parsed.dryRun || result.remainingRows === 0 ? 0 : 1;
441
+ }
442
+ catch (error) {
443
+ console.error(error instanceof Error ? error.message : String(error));
444
+ process.exitCode = 1;
445
+ }
446
+ }
447
+ function parseUsageBackfillArgs(args) {
448
+ let mode;
449
+ let dbPath;
450
+ let backupPath;
451
+ let restorePath;
452
+ const setMode = (next) => {
453
+ if (mode)
454
+ throw new UsageBackfillUsageError('Choose exactly one of --dry-run, --apply, --verify, or --restore.');
455
+ mode = next;
456
+ };
457
+ const requireValue = (index, flag) => {
458
+ const value = args[index + 1];
459
+ if (!value || value.startsWith('--'))
460
+ throw new UsageBackfillUsageError(`${flag} requires a value.`);
461
+ return value;
462
+ };
463
+ for (let index = 0; index < args.length; index += 1) {
464
+ const arg = args[index];
465
+ if (arg === '--help' || arg === '-h')
466
+ throw new UsageBackfillUsageError(exports.USAGE_BACKFILL_HELP);
467
+ if (arg === '--dry-run') {
468
+ setMode('dry-run');
469
+ continue;
470
+ }
471
+ if (arg === '--apply') {
472
+ setMode('apply');
473
+ continue;
474
+ }
475
+ if (arg === '--verify') {
476
+ setMode('verify');
477
+ continue;
478
+ }
479
+ if (arg === '--restore') {
480
+ setMode('restore');
481
+ restorePath = requireValue(index, '--restore');
482
+ index += 1;
483
+ continue;
484
+ }
485
+ if (arg.startsWith('--restore=')) {
486
+ setMode('restore');
487
+ restorePath = arg.slice('--restore='.length);
488
+ if (!restorePath)
489
+ throw new UsageBackfillUsageError('--restore requires a value.');
490
+ continue;
491
+ }
492
+ if (arg === '--db') {
493
+ dbPath = requireValue(index, '--db');
494
+ index += 1;
495
+ continue;
496
+ }
497
+ if (arg.startsWith('--db=')) {
498
+ dbPath = arg.slice('--db='.length);
499
+ if (!dbPath)
500
+ throw new UsageBackfillUsageError('--db requires a value.');
501
+ continue;
502
+ }
503
+ if (arg === '--backup') {
504
+ backupPath = requireValue(index, '--backup');
505
+ index += 1;
506
+ continue;
507
+ }
508
+ if (arg.startsWith('--backup=')) {
509
+ backupPath = arg.slice('--backup='.length);
510
+ if (!backupPath)
511
+ throw new UsageBackfillUsageError('--backup requires a value.');
512
+ continue;
513
+ }
514
+ throw new UsageBackfillUsageError(`Unknown option: ${arg}`);
515
+ }
516
+ const selectedMode = mode ?? 'dry-run';
517
+ if (backupPath && selectedMode !== 'apply') {
518
+ throw new UsageBackfillUsageError('--backup is only valid with --apply.');
519
+ }
520
+ if (restorePath && selectedMode !== 'restore') {
521
+ throw new UsageBackfillUsageError('--restore is only valid with restore mode.');
522
+ }
523
+ return { mode: selectedMode, dbPath, backupPath, restorePath };
524
+ }
525
+ function formatUsageBackfillReport(report) {
526
+ const modeLabel = report.mode === 'dry-run'
527
+ ? 'Dry-run'
528
+ : report.mode === 'apply'
529
+ ? 'Apply'
530
+ : report.mode === 'restore'
531
+ ? 'Restore'
532
+ : 'Verify';
533
+ const lines = [
534
+ `[crewx] Usage backfill — ${modeLabel}`,
535
+ ` Database: ${report.dbPath}`,
536
+ ` Tasks scanned: ${report.scannedTasks}`,
537
+ ` Terminal tasks: ${report.terminalTasks}`,
538
+ ` Raw target tasks: ${report.rawTargetTasks}`,
539
+ ` Candidate tasks: ${report.candidateTasks}`,
540
+ ` Recovered: ${report.recoveredTasks}`,
541
+ ` Unavailable: ${report.unavailableTasks}`,
542
+ ` Deferred (within 24h): ${report.deferredTasks}`,
543
+ ` Changed: ${report.changedTasks}`,
544
+ ` Would change: ${report.wouldChangeTasks}`,
545
+ ` Restored: ${report.restored}`,
546
+ ` Skipped conflicts: ${report.skippedConflict}`,
547
+ ` Remaining target tasks: ${report.remainingTargetTasks}`,
548
+ ` Tasks with usageAccounting: ${report.accountingTasks}`,
549
+ ' Provider distribution:',
550
+ ];
551
+ const providers = Object.entries(report.providerDistribution);
552
+ if (providers.length === 0) {
553
+ lines.push(' (none)');
554
+ }
555
+ else {
556
+ for (const [provider, stats] of providers) {
557
+ lines.push(` ${provider}: candidates=${stats.candidates}, recovered=${stats.recovered}, unavailable=${stats.unavailable}, deferred=${stats.deferred}`);
558
+ }
559
+ }
560
+ lines.push(' Reason counts:');
561
+ for (const [reason, count] of Object.entries(report.reasonCounts))
562
+ lines.push(` ${reason}: ${count}`);
563
+ lines.push(` Estimated free-space need: ${report.estimatedFreeSpaceBytes}`);
564
+ lines.push(` Required free space (with reserve): ${report.requiredFreeSpaceBytes}`);
565
+ lines.push(` Available free space: ${report.availableFreeSpaceBytes}`);
566
+ lines.push(` Free-space gate: ${report.freeSpaceGatePassed ? 'PASS' : 'FAIL'}`);
567
+ const snapshotPath = report.snapshotPath ?? report.backupPath;
568
+ if (snapshotPath) {
569
+ lines.push(` Undo snapshot: ${snapshotPath}`);
570
+ if (report.backupMethod)
571
+ lines.push(` Snapshot method: ${report.backupMethod}`);
572
+ }
573
+ if (report.blockedReason)
574
+ lines.push(` Blocked: ${report.blockedReason}`);
575
+ for (const conflict of report.conflictSamples) {
576
+ lines.push(` Conflict sample: ${conflict.taskId}: ${conflict.fields.join(', ')}`);
577
+ }
578
+ for (const failure of report.failures)
579
+ lines.push(` Failure: ${failure.taskId}: ${failure.reason}`);
580
+ for (const warning of report.warnings)
581
+ lines.push(` Warning: ${warning}`);
582
+ return lines.join('\n');
583
+ }
584
+ async function handleUsageBackfill(args) {
585
+ let parsed;
586
+ try {
587
+ parsed = parseUsageBackfillArgs(args);
588
+ }
589
+ catch (error) {
590
+ const message = error instanceof Error ? error.message : String(error);
591
+ if (message === exports.USAGE_BACKFILL_HELP) {
592
+ console.log(message);
593
+ }
594
+ else {
595
+ console.error(message);
596
+ console.error(exports.USAGE_BACKFILL_HELP);
597
+ process.exitCode = 2;
598
+ }
599
+ return;
600
+ }
601
+ try {
602
+ const report = await (0, repository_5.runUsageBackfill)(parsed);
603
+ console.log(formatUsageBackfillReport(report));
604
+ process.exitCode = report.ok ? 0 : 1;
605
+ }
606
+ catch (error) {
607
+ console.error(error instanceof Error ? error.message : String(error));
608
+ process.exitCode = 1;
609
+ }
78
610
  }
79
611
  async function handleDbPush(args) {
80
612
  const force = args.includes('--force');
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Shared receipt-mode implementation for the query and execute commands.
3
+ *
4
+ * A receipt is persisted before a detached runner is started. The runner gets
5
+ * its own identity variables, while the trace variables remain the caller's
6
+ * trace context and are never repurposed as the runner assignment.
7
+ */
8
+ import { TaskRepository } from '@crewx/sdk/repository';
9
+ import type { DurableTaskAdmission, TaskActor } from '@crewx/sdk';
10
+ export type ReceiptActorKind = 'human' | 'agent' | 'system' | 'unknown';
11
+ export interface ReceiptActor {
12
+ kind: ReceiptActorKind;
13
+ id: string | null;
14
+ }
15
+ /** Resolve the CLI actor from metadata and the caller's current environment. */
16
+ export declare function resolveCliTaskActor(metadata: Record<string, unknown>, agentId?: string | undefined): TaskActor;
17
+ /**
18
+ * Resolve the actor at the receipt boundary while the caller's environment is
19
+ * still available. The receipt and synchronous paths share one predicate so
20
+ * cron runners cannot diverge after root context is stripped.
21
+ */
22
+ export declare function resolveReceiptActor(metadata: Record<string, unknown>, agentId?: string | undefined): ReceiptActor;
23
+ /**
24
+ * Report a blocked direct admission (WI-CTO-20260919-004 R2 finding #1) to
25
+ * the operator. Never called for an adopted (already pending) receipt — see
26
+ * admitDurableTask()'s own doc comment.
27
+ */
28
+ export declare function reportAdmissionBlocked(admission: Extract<DurableTaskAdmission, {
29
+ ok: false;
30
+ }>): void;
31
+ /**
32
+ * Remove receipt-selection flags, respecting the `--` literal-args sentinel.
33
+ */
34
+ export declare function extractReceiptFlags(args: string[]): {
35
+ detach: boolean;
36
+ wait: boolean;
37
+ rest: string[];
38
+ };
39
+ export type ReceiptModeReason = 'explicit-detach' | 'agent-context' | 'detached-runner' | 'wait' | 'default';
40
+ export interface ReceiptModeDecision {
41
+ receipt: boolean;
42
+ reason: ReceiptModeReason;
43
+ }
44
+ /** Resolve the receipt/synchronous mode contract shared by q and x. */
45
+ export declare function resolveReceiptMode(options: {
46
+ detach: boolean;
47
+ wait: boolean;
48
+ detachedRunner: boolean;
49
+ agentContext: boolean;
50
+ }): ReceiptModeDecision;
51
+ /**
52
+ * Rebuild runner argv from the parsed request. The resolved prompt is passed
53
+ * as one argv value after `--`, so stdin is never needed by the detached
54
+ * child and prompt text beginning with a dash remains positional text.
55
+ */
56
+ export declare function buildDetachedRunnerArgs(filteredArgs: string[], agentRef: string, finalMessage: string): string[];
57
+ export type AssignedTaskAdoption = {
58
+ kind: 'adopted';
59
+ taskId: string;
60
+ } | {
61
+ kind: 'stale';
62
+ } | {
63
+ kind: 'failed';
64
+ };
65
+ /**
66
+ * Claim a pre-issued receipt without overwriting an already-owned row.
67
+ * Assignment-only leakage is recoverable: discard the stale id and let the
68
+ * normal SDK path create a fresh task. A real detached runner must fail loud
69
+ * because its receipt would otherwise point at no execution row.
70
+ */
71
+ export declare function adoptAssignedTask(taskRepo: Pick<TaskRepository, 'adoptPendingTask'>, taskId: string, detachedRunner: boolean): AssignedTaskAdoption;
72
+ /** Read the actor persisted on the receipt after the runner adopts it. */
73
+ export declare function readAssignedTaskActor(taskRepo: Pick<TaskRepository, 'getTask'>, taskId: string): TaskActor;
74
+ export interface DetachedMiddleHop {
75
+ taskId: string;
76
+ logPath: string;
77
+ runnerArgs: string[];
78
+ }
79
+ /**
80
+ * Read the private Windows middle-hop contract. Invalid direct invocations
81
+ * are left untouched and therefore fail through the normal unknown-option
82
+ * path instead of becoming an undocumented public command.
83
+ */
84
+ export declare function readDetachedMiddleHop(args: string[]): DetachedMiddleHop | undefined;
85
+ /**
86
+ * Windows middle hop: transfer a transient log handle to the final runner,
87
+ * close the middle process's copy, and exit immediately. The parent treats a
88
+ * successful middle-hop exit as normal and makes adoption a database decision.
89
+ */
90
+ export declare function runDetachedMiddle(command: string, hop: DetachedMiddleHop): Promise<void>;
91
+ /**
92
+ * Persist a receipt and launch its detached runner. Unix uses one hop. Windows
93
+ * uses a short-lived middle hop so the final runner has no live parent link.
94
+ */
95
+ export declare function runDetached(command: string, filteredArgs: string[], reason: ReceiptModeReason, invocationArgs?: string[]): Promise<void>;