@swarmcraftai/cli 0.1.1 → 0.1.5

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 (3) hide show
  1. package/README.md +8 -6
  2. package/dist/main.cjs +468 -112
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -57,17 +57,19 @@ Core commands:
57
57
 
58
58
  ```bash
59
59
  swarmcraft auth login --email <email>
60
- swarmcraft projects list
61
- swarmcraft init --project <project-id> --workspace <path>
62
- swarmcraft one-shot --project <project-id> --workspace <path> --agent-provider codex
60
+ swarmcraft delegation start --user <customer-email-or-id> --reason <text> --expires-in-minutes 600
61
+ swarmcraft projects list --delegation <delegation-id>
62
+ swarmcraft init --project <project-id> --workspace <path> --delegation <delegation-id>
63
+ swarmcraft one-shot --project <project-id> --workspace <path> --agent-provider codex --delegation <delegation-id>
63
64
  swarmcraft one-shot --project <project-id> --workspace <path> --agent-provider codex --run-id <known-run-id>
64
- swarmcraft resume --run <run-id> --workspace <path>
65
- swarmcraft status --run <run-id> --workspace <path>
65
+ swarmcraft resume --run <run-id> --workspace <path> --delegation <delegation-id>
66
+ swarmcraft status --run <run-id> --workspace <path> --delegation <delegation-id>
67
+ swarmcraft delegation end --delegation <delegation-id>
66
68
  ```
67
69
 
68
70
  `swarmcraft init` validates the workspace git state, seeds the shared SwarmCraft repo guidance files, materializes non-done ticket packets, links those packets back to SwarmCraft, and prints the next commands.
69
71
 
70
- `swarmcraft one-shot` bootstraps the workspace, runs doing and checking through the selected agent provider, writes `.swarmcraft/runs/<run-id>/manifest.json`, and stops at reviewing by default. Use `--done-policy auto --commit-policy done` only when automatic done moves and commits are intended.
72
+ `swarmcraft one-shot` bootstraps the workspace, runs the doing lane through the selected agent provider, syncs packet updates back to SwarmCraft, writes `.swarmcraft/runs/<run-id>/manifest.json`, streams progress to the terminal, writes agent logs under `.swarmcraft/runs/<run-id>/logs/`, and moves accepted work to `done` by default. Customer one-shots should normally pair this with `--commit-policy done` so each completed ticket creates a local commit. Use `--checking on` or `--reviewing on` only for internal validation runs that deliberately need those extra lanes.
71
73
 
72
74
  Useful safety and support commands:
73
75
 
package/dist/main.cjs CHANGED
@@ -9,6 +9,10 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
9
  var __commonJS = (cb, mod) => function __require() {
10
10
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
11
  };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
12
16
  var __copyProps = (to, from, except, desc) => {
13
17
  if (from && typeof from === "object" || typeof from === "function") {
14
18
  for (let key of __getOwnPropNames(from))
@@ -25,6 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
29
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
30
  mod
27
31
  ));
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
33
 
29
34
  // ../../packages/packet-workflow/dist/assistantActions.js
30
35
  var require_assistantActions = __commonJS({
@@ -2922,7 +2927,8 @@ var require_seedProjectFiles = __commonJS({
2922
2927
  const existingEndIndex = existingContents.indexOf(renderProjectGuide_1.MANAGED_SECTION_END);
2923
2928
  if (existingStartIndex >= 0 && existingEndIndex >= 0 && existingEndIndex > existingStartIndex) {
2924
2929
  const replacementEndIndex = existingEndIndex + renderProjectGuide_1.MANAGED_SECTION_END.length;
2925
- const updatedContents = `${existingContents.slice(0, existingStartIndex)}${managedSection}${existingContents.slice(replacementEndIndex)}`;
2930
+ const afterManagedSection = existingContents.slice(replacementEndIndex).replace(/^\n+/, "\n");
2931
+ const updatedContents = `${existingContents.slice(0, existingStartIndex)}${managedSection}${afterManagedSection}`;
2926
2932
  return normalizeTrailingNewline(updatedContents);
2927
2933
  }
2928
2934
  const separator = existingContents.endsWith("\n\n") ? "" : existingContents.endsWith("\n") ? "\n" : "\n\n";
@@ -2971,6 +2977,13 @@ var require_dist2 = __commonJS({
2971
2977
  });
2972
2978
 
2973
2979
  // src/main.ts
2980
+ var main_exports = {};
2981
+ __export(main_exports, {
2982
+ findCommand: () => findCommand,
2983
+ main: () => main,
2984
+ parseArgs: () => parseArgs
2985
+ });
2986
+ module.exports = __toCommonJS(main_exports);
2974
2987
  var import_promises4 = require("node:fs/promises");
2975
2988
  var import_node_path5 = __toESM(require("node:path"), 1);
2976
2989
  var import_promises5 = require("node:readline/promises");
@@ -2980,7 +2993,7 @@ var import_packet_workflow3 = __toESM(require_dist(), 1);
2980
2993
  // package.json
2981
2994
  var package_default = {
2982
2995
  name: "@swarmcraftai/cli",
2983
- version: "0.1.1",
2996
+ version: "0.1.5",
2984
2997
  description: "Prepare repositories and run packet-based SwarmCraft agent workflows from the terminal.",
2985
2998
  license: "SEE LICENSE IN LICENSE.txt",
2986
2999
  type: "module",
@@ -3023,6 +3036,7 @@ var package_default = {
3023
3036
  // src/apiClient.ts
3024
3037
  var API_REQUEST_TIMEOUT_MS = 15e3;
3025
3038
  var CLIENT_SOURCE_HEADER = "X-SwarmCraft-Client";
3039
+ var DELEGATION_HEADER = "X-SwarmCraft-Delegation";
3026
3040
  var REFRESH_TOKEN_COOKIE_NAME = "swarmcraft_refresh_token";
3027
3041
  var SwarmCraftApiError = class extends Error {
3028
3042
  status;
@@ -3074,56 +3088,88 @@ var SwarmCraftClient = class {
3074
3088
  accessToken
3075
3089
  );
3076
3090
  }
3077
- listProjects(accessToken) {
3078
- return this.requestJson("/projects", { method: "GET" }, accessToken);
3091
+ startDelegation(accessToken, request) {
3092
+ return this.requestJson(
3093
+ "/support/delegations",
3094
+ {
3095
+ method: "POST",
3096
+ body: JSON.stringify(request)
3097
+ },
3098
+ accessToken
3099
+ );
3100
+ }
3101
+ loadDelegation(accessToken, delegationId) {
3102
+ return this.requestJson(
3103
+ `/support/delegations/${encodeURIComponent(delegationId)}`,
3104
+ { method: "GET" },
3105
+ accessToken
3106
+ );
3107
+ }
3108
+ endDelegation(accessToken, delegationId) {
3109
+ return this.requestJson(
3110
+ `/support/delegations/${encodeURIComponent(delegationId)}`,
3111
+ { method: "DELETE" },
3112
+ accessToken
3113
+ );
3114
+ }
3115
+ listProjects(accessToken, delegationId) {
3116
+ return this.requestJson("/projects", { method: "GET" }, accessToken, void 0, delegationId);
3079
3117
  }
3080
- loadProject(projectId, accessToken) {
3081
- return this.requestJson(`/projects/${encodeURIComponent(projectId)}`, { method: "GET" }, accessToken);
3118
+ loadProject(projectId, accessToken, delegationId) {
3119
+ return this.requestJson(`/projects/${encodeURIComponent(projectId)}`, { method: "GET" }, accessToken, void 0, delegationId);
3082
3120
  }
3083
- loadBoard(projectId, accessToken) {
3084
- return this.requestJson(`/projects/${encodeURIComponent(projectId)}/board`, { method: "GET" }, accessToken);
3121
+ loadBoard(projectId, accessToken, delegationId) {
3122
+ return this.requestJson(`/projects/${encodeURIComponent(projectId)}/board`, { method: "GET" }, accessToken, void 0, delegationId);
3085
3123
  }
3086
- upsertTicketPacketLink(projectId, ticketId, request, accessToken) {
3124
+ upsertTicketPacketLink(projectId, ticketId, request, accessToken, delegationId) {
3087
3125
  return this.requestJson(
3088
3126
  `/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(ticketId)}/packet-link`,
3089
3127
  {
3090
3128
  method: "PUT",
3091
3129
  body: JSON.stringify(request)
3092
3130
  },
3093
- accessToken
3131
+ accessToken,
3132
+ void 0,
3133
+ delegationId
3094
3134
  );
3095
3135
  }
3096
- moveTicket(projectId, ticketId, request, accessToken) {
3136
+ moveTicket(projectId, ticketId, request, accessToken, delegationId) {
3097
3137
  return this.requestJson(
3098
3138
  `/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(ticketId)}/move`,
3099
3139
  {
3100
3140
  method: "POST",
3101
3141
  body: JSON.stringify(request)
3102
3142
  },
3103
- accessToken
3143
+ accessToken,
3144
+ void 0,
3145
+ delegationId
3104
3146
  );
3105
3147
  }
3106
- updateTicketNotes(projectId, ticketId, request, accessToken) {
3148
+ updateTicketNotes(projectId, ticketId, request, accessToken, delegationId) {
3107
3149
  return this.requestJson(
3108
3150
  `/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(ticketId)}/notes`,
3109
3151
  {
3110
3152
  method: "PATCH",
3111
3153
  body: JSON.stringify(request)
3112
3154
  },
3113
- accessToken
3155
+ accessToken,
3156
+ void 0,
3157
+ delegationId
3114
3158
  );
3115
3159
  }
3116
- updateChecklistItem(projectId, ticketId, itemId, request, accessToken) {
3160
+ updateChecklistItem(projectId, ticketId, itemId, request, accessToken, delegationId) {
3117
3161
  return this.requestJson(
3118
3162
  `/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(ticketId)}/checklist-items/${encodeURIComponent(itemId)}`,
3119
3163
  {
3120
3164
  method: "PATCH",
3121
3165
  body: JSON.stringify(request)
3122
3166
  },
3123
- accessToken
3167
+ accessToken,
3168
+ void 0,
3169
+ delegationId
3124
3170
  );
3125
3171
  }
3126
- async requestJson(path6, init, accessToken, refreshToken) {
3172
+ async requestJson(path6, init, accessToken, refreshToken, delegationId) {
3127
3173
  const url = `${this.apiBaseUrl}${path6.startsWith("/") ? path6 : `/${path6}`}`;
3128
3174
  const method = (init.method ?? "GET").toUpperCase();
3129
3175
  const headers = new Headers(init.headers);
@@ -3140,6 +3186,9 @@ var SwarmCraftClient = class {
3140
3186
  if (refreshToken) {
3141
3187
  headers.set("Cookie", `${REFRESH_TOKEN_COOKIE_NAME}=${refreshToken}`);
3142
3188
  }
3189
+ if (delegationId) {
3190
+ headers.set(DELEGATION_HEADER, delegationId);
3191
+ }
3143
3192
  let response;
3144
3193
  try {
3145
3194
  response = await fetch(url, {
@@ -3218,6 +3267,7 @@ var import_packet_workflow = __toESM(require_dist(), 1);
3218
3267
  var import_repo_seed = __toESM(require_dist2(), 1);
3219
3268
 
3220
3269
  // src/workspace.ts
3270
+ var import_node_fs = require("node:fs");
3221
3271
  var import_promises = require("node:fs/promises");
3222
3272
  var import_node_path = __toESM(require("node:path"), 1);
3223
3273
  var import_node_child_process = require("node:child_process");
@@ -3259,7 +3309,11 @@ async function writeWorkspaceFile(workspacePath, relativePath, contents) {
3259
3309
  await (0, import_promises.mkdir)(import_node_path.default.dirname(absolutePath), { recursive: true });
3260
3310
  await (0, import_promises.writeFile)(absolutePath, contents, "utf8");
3261
3311
  }
3262
- async function runCommand(command, cwd, env = process.env) {
3312
+ async function runCommand(command, cwd, env = process.env, options = {}) {
3313
+ if (options.logPath) {
3314
+ await (0, import_promises.mkdir)(import_node_path.default.dirname(options.logPath), { recursive: true });
3315
+ }
3316
+ const logStream = options.logPath ? (0, import_node_fs.createWriteStream)(options.logPath, { flags: "a" }) : null;
3263
3317
  return new Promise((resolve) => {
3264
3318
  const child = (0, import_node_child_process.spawn)(command, {
3265
3319
  cwd,
@@ -3269,14 +3323,40 @@ async function runCommand(command, cwd, env = process.env) {
3269
3323
  });
3270
3324
  const stdoutChunks = [];
3271
3325
  const stderrChunks = [];
3272
- child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
3273
- child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
3326
+ child.stdout.on("data", (chunk) => {
3327
+ stdoutChunks.push(chunk);
3328
+ logStream?.write(chunk);
3329
+ if (options.teeOutput) {
3330
+ process.stdout.write(chunk);
3331
+ }
3332
+ });
3333
+ child.stderr.on("data", (chunk) => {
3334
+ stderrChunks.push(chunk);
3335
+ logStream?.write(chunk);
3336
+ if (options.teeOutput) {
3337
+ process.stderr.write(chunk);
3338
+ }
3339
+ });
3340
+ child.on("error", (error) => {
3341
+ const chunk = Buffer.from(`${error.message}
3342
+ `, "utf8");
3343
+ stderrChunks.push(chunk);
3344
+ logStream?.write(chunk);
3345
+ if (options.teeOutput) {
3346
+ process.stderr.write(chunk);
3347
+ }
3348
+ });
3274
3349
  child.on("close", (code) => {
3275
- resolve({
3350
+ const result = {
3276
3351
  exitCode: code ?? 1,
3277
3352
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
3278
3353
  stderr: Buffer.concat(stderrChunks).toString("utf8")
3279
- });
3354
+ };
3355
+ if (logStream) {
3356
+ logStream.end(() => resolve(result));
3357
+ return;
3358
+ }
3359
+ resolve(result);
3280
3360
  });
3281
3361
  });
3282
3362
  }
@@ -3301,11 +3381,11 @@ async function runGit(args, cwd) {
3301
3381
  // src/bootstrap.ts
3302
3382
  async function runBootstrap(client, accessToken, options) {
3303
3383
  const workspace = await validateWorkspace(options.workspacePath);
3304
- const project = await client.loadProject(options.projectId, accessToken);
3384
+ const project = await client.loadProject(options.projectId, accessToken, options.delegationId);
3305
3385
  if (project.boardState !== "UNLOCKED") {
3306
3386
  throw new Error(`Project ${project.id} is ${project.boardState}; unlock the board before bootstrapping a workspace.`);
3307
3387
  }
3308
- const board = await client.loadBoard(project.id, accessToken);
3388
+ const board = await client.loadBoard(project.id, accessToken, options.delegationId);
3309
3389
  const seedFilesWritten = await seedWorkspace(project, workspace.workspacePath);
3310
3390
  const selectedTickets = selectBootstrapTickets(board, options);
3311
3391
  const packetFilesCreated = [];
@@ -3355,14 +3435,17 @@ async function runBootstrap(client, accessToken, options) {
3355
3435
  await writeWorkspaceFile(workspace.workspacePath, relativePacketPath, packetContents);
3356
3436
  packetFilesCreated.push(relativePacketPath);
3357
3437
  }
3358
- await client.upsertTicketPacketLink(project.id, ticket.id, {
3359
- workspaceFolder: workspace.workspaceFolderName,
3360
- packetFilePath: relativePacketPath,
3361
- expectedRevision: ticket.revision,
3362
- lastSyncedTicketRevision: ticket.revision,
3363
- lastSyncedAt: syncedAt
3364
- }, accessToken);
3365
- linkedPackets.push(relativePacketPath);
3438
+ const packetLinkMatches = ticket.packetLink && ticket.packetLink.workspaceFolder === workspace.workspaceFolderName && ticket.packetLink.packetFilePath === relativePacketPath;
3439
+ if (!packetLinkMatches) {
3440
+ await client.upsertTicketPacketLink(project.id, ticket.id, {
3441
+ workspaceFolder: workspace.workspaceFolderName,
3442
+ packetFilePath: relativePacketPath,
3443
+ expectedRevision: ticket.revision,
3444
+ lastSyncedTicketRevision: ticket.revision,
3445
+ lastSyncedAt: syncedAt
3446
+ }, accessToken, options.delegationId);
3447
+ linkedPackets.push(relativePacketPath);
3448
+ }
3366
3449
  }
3367
3450
  return {
3368
3451
  projectTitle: project.title || project.projectTitle || project.id,
@@ -3374,7 +3457,7 @@ async function runBootstrap(client, accessToken, options) {
3374
3457
  linkedPackets,
3375
3458
  skippedTickets,
3376
3459
  nextCommands: [
3377
- `swarmcraft one-shot --project ${project.id} --workspace ${workspace.workspacePath} --agent-provider codex`,
3460
+ `swarmcraft one-shot --project ${project.id} --workspace ${workspace.workspacePath} --agent-provider codex${options.delegationId ? ` --delegation ${options.delegationId}` : ""}`,
3378
3461
  "Review generated files under .swarmcraft/, .github/, .agents/, and .codex/ before running agents."
3379
3462
  ]
3380
3463
  };
@@ -3410,21 +3493,28 @@ async function seedWorkspace(project, workspacePath) {
3410
3493
  var import_promises2 = require("node:fs/promises");
3411
3494
  var import_node_path3 = __toESM(require("node:path"), 1);
3412
3495
  var import_packet_workflow2 = __toESM(require_dist(), 1);
3413
- async function runOneShot(client, accessToken, options) {
3496
+ async function runOneShot(client, accessTokenProvider, options) {
3414
3497
  const workspace = await validateWorkspace(options.workspacePath);
3498
+ const getAccessToken = () => resolveAccessToken(accessTokenProvider);
3415
3499
  if (workspace.dirtyFileCount > 0 && !options.allowDirty) {
3416
3500
  throw new Error(`Workspace has ${workspace.dirtyFileCount} changed file(s). Commit, stash, or rerun with --allow-dirty before one-shot.`);
3417
3501
  }
3418
- await runBootstrap(client, accessToken, {
3419
- projectId: options.projectId,
3420
- workspacePath: workspace.workspacePath,
3421
- ticketIds: options.ticketIds,
3422
- ticketLimit: options.ticketLimit
3423
- });
3424
- const project = await client.loadProject(options.projectId, accessToken);
3425
- let board = await client.loadBoard(project.id, accessToken);
3502
+ if (options.resumeRunId) {
3503
+ printProgress("Resume detected; using existing packet files without re-running bootstrap.");
3504
+ } else {
3505
+ await runBootstrap(client, await getAccessToken(), {
3506
+ projectId: options.projectId,
3507
+ workspacePath: workspace.workspacePath,
3508
+ ticketIds: options.ticketIds,
3509
+ ticketLimit: options.ticketLimit,
3510
+ delegationId: options.delegation?.id ?? null
3511
+ });
3512
+ }
3513
+ const project = await client.loadProject(options.projectId, await getAccessToken(), options.delegation?.id ?? null);
3514
+ let board = await client.loadBoard(project.id, await getAccessToken(), options.delegation?.id ?? null);
3426
3515
  const runId = options.resumeRunId ?? options.runId ?? createRunId();
3427
3516
  const manifestPath = resolveManifestPath(workspace.workspacePath, runId);
3517
+ const selectedTickets = selectTickets(board, options);
3428
3518
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3429
3519
  const manifest = await loadOrCreateManifest(manifestPath, {
3430
3520
  generator: "swarmcraft-cli",
@@ -3447,6 +3537,9 @@ async function runOneShot(client, accessToken, options) {
3447
3537
  }
3448
3538
  });
3449
3539
  await persistManifest(manifestPath, manifest);
3540
+ printProgress(`Run ${runId} started for ${project.title || project.projectTitle || project.id}.`);
3541
+ printProgress(`Manifest: ${manifestPath}`);
3542
+ printProgress(`Selected tickets: ${selectedTickets.length}.`);
3450
3543
  let agentRequests = manifest.tickets.flatMap((ticket) => ticket.stages).length;
3451
3544
  const completedTicketIds = new Set(
3452
3545
  manifest.tickets.filter((ticket) => ["done", "manual-review-required"].includes(ticket.outcome)).map((ticket) => ticket.ticketId)
@@ -3455,10 +3548,13 @@ async function runOneShot(client, accessToken, options) {
3455
3548
  manifest.tickets = manifest.tickets.filter((ticket) => completedTicketIds.has(ticket.ticketId));
3456
3549
  await persistManifest(manifestPath, manifest);
3457
3550
  }
3458
- for (const selectedTicket of selectTickets(board, options)) {
3551
+ let finalManifestPersisted = false;
3552
+ for (const [ticketIndex, selectedTicket] of selectedTickets.entries()) {
3459
3553
  if (completedTicketIds.has(selectedTicket.id)) {
3554
+ printProgress(`Skipping completed ticket ${ticketIndex + 1}/${selectedTickets.length}: ${selectedTicket.title}.`);
3460
3555
  continue;
3461
3556
  }
3557
+ printProgress(`Starting ticket ${ticketIndex + 1}/${selectedTickets.length}: ${selectedTicket.title}.`);
3462
3558
  const ticketOutcome = {
3463
3559
  ticketId: selectedTicket.id,
3464
3560
  title: selectedTicket.title,
@@ -3473,7 +3569,7 @@ async function runOneShot(client, accessToken, options) {
3473
3569
  await persistManifest(manifestPath, manifest);
3474
3570
  const result = await processTicket({
3475
3571
  client,
3476
- accessToken,
3572
+ getAccessToken,
3477
3573
  project,
3478
3574
  board,
3479
3575
  ticket: selectedTicket,
@@ -3491,30 +3587,59 @@ async function runOneShot(client, accessToken, options) {
3491
3587
  ticketOutcome.outcome = result.outcome;
3492
3588
  ticketOutcome.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
3493
3589
  ticketOutcome.elapsedMs = (0, import_packet_workflow2.computeElapsedMs)(ticketOutcome.startedAt, ticketOutcome.finishedAt);
3590
+ printProgress(`Ticket ${ticketIndex + 1}/${selectedTickets.length} finished as ${result.outcome} in ${formatDuration(ticketOutcome.elapsedMs)}.`);
3591
+ if (result.outcome === "done" || result.outcome === "manual-review-required") {
3592
+ completedTicketIds.add(selectedTicket.id);
3593
+ }
3594
+ const willStop = result.outcome.startsWith("blocked");
3595
+ const isFinalSelectedTicket = !hasRemainingUncompletedTickets(selectedTickets, ticketIndex + 1, completedTicketIds);
3494
3596
  if (result.outcome === "done" && options.commitPolicy === "done") {
3495
- ticketOutcome.commitCreated = await gitAddAndCommit(workspace.workspacePath, `Complete ${selectedTicket.id}: ${selectedTicket.title}`);
3597
+ ticketOutcome.commitCreated = true;
3598
+ }
3599
+ if (willStop || isFinalSelectedTicket) {
3600
+ finalizeManifest(manifest);
3601
+ finalManifestPersisted = true;
3496
3602
  }
3497
3603
  await persistManifest(manifestPath, manifest);
3498
- if (result.outcome.startsWith("blocked")) {
3604
+ if (result.outcome === "done" && options.commitPolicy === "done") {
3605
+ ticketOutcome.commitCreated = await gitAddAndCommit(workspace.workspacePath, `Complete ${selectedTicket.id}: ${selectedTicket.title}`);
3606
+ if (!ticketOutcome.commitCreated) {
3607
+ await persistManifest(manifestPath, manifest);
3608
+ }
3609
+ printProgress(ticketOutcome.commitCreated ? "Committed completed ticket changes." : "No completed ticket changes to commit.");
3610
+ }
3611
+ if (willStop) {
3612
+ printProgress(`Stopping because ticket is ${result.outcome}.`);
3499
3613
  break;
3500
3614
  }
3501
3615
  }
3616
+ if (!finalManifestPersisted) {
3617
+ finalizeManifest(manifest);
3618
+ await persistManifest(manifestPath, manifest);
3619
+ }
3620
+ printProgress(`Run ${runId} finished with status ${manifest.status} in ${formatDuration(manifest.durations.elapsedMs)}.`);
3621
+ return manifest;
3622
+ }
3623
+ function hasRemainingUncompletedTickets(selectedTickets, startIndex, completedTicketIds) {
3624
+ return selectedTickets.slice(startIndex).some((ticket) => !completedTicketIds.has(ticket.id));
3625
+ }
3626
+ function finalizeManifest(manifest) {
3502
3627
  manifest.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
3503
3628
  manifest.status = resolveManifestStatus(manifest);
3504
3629
  manifest.durations = (0, import_packet_workflow2.summarizeWorkflowRunDurations)(manifest);
3505
- await persistManifest(manifestPath, manifest);
3506
- return manifest;
3507
3630
  }
3508
3631
  async function processTicket(context) {
3509
3632
  let currentBoard = context.board;
3510
3633
  let currentTicket = requireTicket(currentBoard, context.ticket.id);
3511
3634
  if (currentTicket.workflowState === "todo") {
3512
- currentBoard = await moveTicket(context.client, context.accessToken, context.project.id, currentTicket, "doing");
3635
+ printProgress("Moving ticket to doing.");
3636
+ currentBoard = await moveTicket(context.client, await context.getAccessToken(), context.project.id, currentTicket, "doing", context.options.delegation?.id ?? null);
3513
3637
  currentTicket = requireTicket(currentBoard, context.ticket.id);
3514
3638
  }
3515
3639
  const packetPath = (0, import_packet_workflow2.resolvePacketFilePath)(context.project, currentTicket, currentTicket.packetLink);
3516
3640
  context.ticketOutcome.packetPath = packetPath;
3517
3641
  await refreshPacketMetadata(context.workspacePath, packetPath, currentTicket);
3642
+ printProgress(`Packet: ${packetPath}`);
3518
3643
  if (currentTicket.workflowState !== "checking" && currentTicket.workflowState !== "reviewing") {
3519
3644
  const doResult = await runStageAndSync({ ...context, board: currentBoard, ticket: currentTicket, packetPath, stage: "do" });
3520
3645
  currentBoard = doResult.board;
@@ -3524,7 +3649,8 @@ async function processTicket(context) {
3524
3649
  return moveAfterAcceptedChecking(context, currentBoard, currentTicket, packetPath);
3525
3650
  }
3526
3651
  if (currentTicket.workflowState === "doing") {
3527
- currentBoard = await moveTicket(context.client, context.accessToken, context.project.id, currentTicket, "checking");
3652
+ printProgress("Moving ticket to checking.");
3653
+ currentBoard = await moveTicket(context.client, await context.getAccessToken(), context.project.id, currentTicket, "checking", context.options.delegation?.id ?? null);
3528
3654
  currentTicket = requireTicket(currentBoard, currentTicket.id);
3529
3655
  await refreshPacketMetadata(context.workspacePath, packetPath, currentTicket);
3530
3656
  }
@@ -3534,9 +3660,11 @@ async function processTicket(context) {
3534
3660
  currentTicket = checkResult.ticket;
3535
3661
  if (currentTicket.workflowState === "doing") {
3536
3662
  if (attempt >= context.options.maxCheckingAttempts) {
3663
+ printProgress("Checking sent the ticket back to doing and retry budget is exhausted.");
3537
3664
  return { board: currentBoard, ticket: currentTicket, outcome: "blocked-checking-retries-exhausted" };
3538
3665
  }
3539
- currentBoard = await moveTicket(context.client, context.accessToken, context.project.id, currentTicket, "checking");
3666
+ printProgress(`Checking requested more work; retrying checking attempt ${attempt + 1}/${context.options.maxCheckingAttempts}.`);
3667
+ currentBoard = await moveTicket(context.client, await context.getAccessToken(), context.project.id, currentTicket, "checking", context.options.delegation?.id ?? null);
3540
3668
  currentTicket = requireTicket(currentBoard, currentTicket.id);
3541
3669
  await refreshPacketMetadata(context.workspacePath, packetPath, currentTicket);
3542
3670
  continue;
@@ -3566,9 +3694,12 @@ async function runStageAndSync(context) {
3566
3694
  ticket: context.packetPath,
3567
3695
  workspace: context.workspacePath
3568
3696
  });
3697
+ const agentRequestNumber = context.getAgentRequests() + 1;
3698
+ const logPath = import_node_path3.default.join(import_node_path3.default.dirname(context.manifestPath), "logs", `${String(agentRequestNumber).padStart(4, "0")}-${context.stage}.log`);
3569
3699
  const stageOutcome = {
3570
3700
  stage: context.stage,
3571
3701
  command,
3702
+ logPath: import_node_path3.default.relative(context.workspacePath, logPath),
3572
3703
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3573
3704
  finishedAt: null,
3574
3705
  elapsedMs: null,
@@ -3582,7 +3713,9 @@ async function runStageAndSync(context) {
3582
3713
  context.ticketOutcome.stages.push(stageOutcome);
3583
3714
  await persistManifest(context.manifestPath, context.manifest);
3584
3715
  context.incrementAgentRequests();
3585
- const costOutputPath = import_node_path3.default.join(import_node_path3.default.dirname(context.manifestPath), "agent-cost", `${String(context.getAgentRequests() + 1).padStart(4, "0")}-${context.stage}.json`);
3716
+ printProgress(`Running ${context.stage} stage for ${context.ticket.title}.`);
3717
+ printProgress(`Agent log: ${stageOutcome.logPath}`);
3718
+ const costOutputPath = import_node_path3.default.join(import_node_path3.default.dirname(context.manifestPath), "agent-cost", `${String(agentRequestNumber).padStart(4, "0")}-${context.stage}.json`);
3586
3719
  await (0, import_promises2.mkdir)(import_node_path3.default.dirname(costOutputPath), { recursive: true });
3587
3720
  const execution = await runCommand(command, context.workspacePath, await buildAgentEnvironment(context.options, {
3588
3721
  SWARMCRAFT_STAGE: context.stage,
@@ -3590,23 +3723,26 @@ async function runStageAndSync(context) {
3590
3723
  SWARMCRAFT_WORKSPACE: context.workspacePath,
3591
3724
  SWARMCRAFT_AGENT_PROVIDER: context.options.agentProvider,
3592
3725
  SWARMCRAFT_AGENT_COST_OUTPUT: costOutputPath
3593
- }));
3726
+ }), { logPath, teeOutput: true });
3594
3727
  stageOutcome.exitCode = execution.exitCode;
3595
3728
  if (execution.exitCode !== 0) {
3596
3729
  stageOutcome.result = "failed";
3597
3730
  stageOutcome.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
3598
3731
  stageOutcome.elapsedMs = (0, import_packet_workflow2.computeElapsedMs)(stageOutcome.startedAt, stageOutcome.finishedAt);
3599
3732
  await persistManifest(context.manifestPath, context.manifest);
3733
+ printProgress(`${context.stage} stage failed in ${formatDuration(stageOutcome.elapsedMs)}.`);
3600
3734
  throw new Error(`Agent command failed during ${context.stage}: ${execution.stderr || execution.stdout || command}`);
3601
3735
  }
3602
3736
  const syncResult = await syncPacketFile({
3603
3737
  client: context.client,
3604
- accessToken: context.accessToken,
3738
+ getAccessToken: context.getAccessToken,
3605
3739
  project: context.project,
3606
3740
  board: context.board,
3607
3741
  ticket: context.ticket,
3608
3742
  workspacePath: context.workspacePath,
3609
- relativePacketPath: context.packetPath
3743
+ relativePacketPath: context.packetPath,
3744
+ allowLaneSync: context.options.checking || context.stage !== "do",
3745
+ delegationId: context.options.delegation?.id ?? null
3610
3746
  });
3611
3747
  stageOutcome.result = "synced";
3612
3748
  stageOutcome.costUsd = await readStageCostUsd(costOutputPath);
@@ -3622,6 +3758,7 @@ async function runStageAndSync(context) {
3622
3758
  stageOutcome.laneAfter = syncResult.ticket.workflowState;
3623
3759
  stageOutcome.syncedChanges = syncResult.syncedChanges;
3624
3760
  await persistManifest(context.manifestPath, context.manifest);
3761
+ printProgress(`${context.stage} stage completed in ${formatDuration(stageOutcome.elapsedMs)}; synced ${syncResult.syncedChanges} change(s); lane is ${syncResult.ticket.workflowState}.`);
3625
3762
  return { board: syncResult.board, ticket: syncResult.ticket };
3626
3763
  }
3627
3764
  async function syncPacketFile(input2) {
@@ -3652,7 +3789,7 @@ async function syncPacketFile(input2) {
3652
3789
  currentBoard = await input2.client.updateChecklistItem(input2.project.id, currentTicket.id, currentItem.id, {
3653
3790
  completed: parsedItem.completed,
3654
3791
  expectedRevision: currentItem.revision
3655
- }, input2.accessToken);
3792
+ }, await input2.getAccessToken(), input2.delegationId);
3656
3793
  currentTicket = requireTicket(currentBoard, currentTicket.id);
3657
3794
  syncedChanges += 1;
3658
3795
  }
@@ -3662,12 +3799,12 @@ async function syncPacketFile(input2) {
3662
3799
  currentBoard = await input2.client.updateTicketNotes(input2.project.id, currentTicket.id, {
3663
3800
  notes: desiredNotes,
3664
3801
  expectedRevision: currentTicket.revision
3665
- }, input2.accessToken);
3802
+ }, await input2.getAccessToken(), input2.delegationId);
3666
3803
  currentTicket = requireTicket(currentBoard, currentTicket.id);
3667
3804
  syncedChanges += 1;
3668
3805
  }
3669
- if (parsedPacket.frontmatter.lane !== currentTicket.workflowState) {
3670
- currentBoard = await moveTicket(input2.client, input2.accessToken, input2.project.id, currentTicket, parsedPacket.frontmatter.lane);
3806
+ if (input2.allowLaneSync && parsedPacket.frontmatter.lane !== currentTicket.workflowState) {
3807
+ currentBoard = await moveTicket(input2.client, await input2.getAccessToken(), input2.project.id, currentTicket, parsedPacket.frontmatter.lane, input2.delegationId);
3671
3808
  currentTicket = requireTicket(currentBoard, currentTicket.id);
3672
3809
  syncedChanges += 1;
3673
3810
  }
@@ -3682,6 +3819,10 @@ async function buildAgentEnvironment(options, stageEnv) {
3682
3819
  TERM: process.env.TERM,
3683
3820
  ...stageEnv
3684
3821
  };
3822
+ if (options.commitPolicy === "done") {
3823
+ env.SWARMCRAFT_DRIVER_AUTO_COMMIT = "true";
3824
+ env.SWARMCRAFT_CLI_AUTO_COMMIT = "true";
3825
+ }
3685
3826
  if (!options.agentApiKeyEnvName) {
3686
3827
  return env;
3687
3828
  }
@@ -3718,23 +3859,28 @@ function sumStageCostUsd(manifest) {
3718
3859
  return manifest.tickets.flatMap((ticket) => ticket.stages).reduce((total, stage) => total + Number(stage.costUsd ?? 0), 0);
3719
3860
  }
3720
3861
  async function moveAfterAcceptedChecking(context, board, ticket, packetPath) {
3721
- if (context.options.donePolicy === "auto") {
3722
- const doneBoard = ticket.workflowState === "done" ? board : await moveTicket(context.client, context.accessToken, context.project.id, ticket, "done");
3862
+ if (!context.options.reviewing) {
3863
+ printProgress("Moving accepted ticket to done.");
3864
+ const doneBoard = ticket.workflowState === "done" ? board : await moveTicket(context.client, await context.getAccessToken(), context.project.id, ticket, "done", context.options.delegation?.id ?? null);
3723
3865
  const doneTicket = requireTicket(doneBoard, ticket.id);
3724
3866
  await refreshPacketMetadata(context.workspacePath, packetPath, doneTicket);
3725
3867
  return { board: doneBoard, ticket: doneTicket, outcome: "done" };
3726
3868
  }
3727
- const reviewBoard = ticket.workflowState === "reviewing" ? board : await moveTicket(context.client, context.accessToken, context.project.id, ticket, "reviewing");
3869
+ let reviewBoard = board;
3870
+ if (ticket.workflowState !== "reviewing") {
3871
+ printProgress("Moving accepted ticket to reviewing.");
3872
+ reviewBoard = await moveTicket(context.client, await context.getAccessToken(), context.project.id, ticket, "reviewing", context.options.delegation?.id ?? null);
3873
+ }
3728
3874
  const reviewTicket = requireTicket(reviewBoard, ticket.id);
3729
3875
  await refreshPacketMetadata(context.workspacePath, packetPath, reviewTicket);
3730
3876
  return { board: reviewBoard, ticket: reviewTicket, outcome: "manual-review-required" };
3731
3877
  }
3732
- async function moveTicket(client, accessToken, projectId, ticket, workflowState) {
3878
+ async function moveTicket(client, accessToken, projectId, ticket, workflowState, delegationId) {
3733
3879
  return client.moveTicket(projectId, ticket.id, {
3734
3880
  workflowState,
3735
3881
  targetIndex: null,
3736
3882
  expectedRevision: ticket.revision
3737
- }, accessToken);
3883
+ }, accessToken, delegationId);
3738
3884
  }
3739
3885
  async function refreshPacketMetadata(workspacePath, relativePacketPath, ticket, syncedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3740
3886
  const contents = await readWorkspaceFile(workspacePath, relativePacketPath);
@@ -3825,6 +3971,29 @@ function describeAgentPolicy(options) {
3825
3971
  })
3826
3972
  };
3827
3973
  }
3974
+ function printProgress(message) {
3975
+ console.log(`[one-shot] ${message}`);
3976
+ }
3977
+ async function resolveAccessToken(accessTokenProvider) {
3978
+ return typeof accessTokenProvider === "string" ? accessTokenProvider : accessTokenProvider();
3979
+ }
3980
+ function formatDuration(elapsedMs) {
3981
+ if (elapsedMs === null || elapsedMs === void 0) {
3982
+ return "unknown";
3983
+ }
3984
+ const seconds = Math.round(elapsedMs / 1e3);
3985
+ if (seconds < 60) {
3986
+ return `${seconds}s`;
3987
+ }
3988
+ const minutes = Math.floor(seconds / 60);
3989
+ const remainingSeconds = seconds % 60;
3990
+ if (minutes < 60) {
3991
+ return remainingSeconds === 0 ? `${minutes}m` : `${minutes}m ${remainingSeconds}s`;
3992
+ }
3993
+ const hours = Math.floor(minutes / 60);
3994
+ const remainingMinutes = minutes % 60;
3995
+ return remainingMinutes === 0 ? `${hours}h` : `${hours}h ${remainingMinutes}m`;
3996
+ }
3828
3997
 
3829
3998
  // src/sessionStore.ts
3830
3999
  var import_promises3 = require("node:fs/promises");
@@ -3865,10 +4034,52 @@ function sessionExpiresSoon(session) {
3865
4034
  return expiresAt - Date.now() < 6e4;
3866
4035
  }
3867
4036
 
4037
+ // src/supportBundle.ts
4038
+ function summarizeDelegationForBundle(value) {
4039
+ if (!value || typeof value !== "object" || Array.isArray(value) || !("options" in value)) {
4040
+ return null;
4041
+ }
4042
+ const options = value.options;
4043
+ if (!options || typeof options !== "object" || Array.isArray(options) || !("delegation" in options)) {
4044
+ return null;
4045
+ }
4046
+ const delegation = options.delegation;
4047
+ if (!delegation || typeof delegation !== "object" || Array.isArray(delegation)) {
4048
+ return null;
4049
+ }
4050
+ const typedDelegation = delegation;
4051
+ return {
4052
+ id: typedDelegation.id ?? null,
4053
+ status: typedDelegation.status ?? null,
4054
+ expiresAt: typedDelegation.expiresAt ?? null,
4055
+ supportTicketId: typedDelegation.supportTicketId ?? null,
4056
+ hasActorEmail: Boolean(typedDelegation.actorEmail),
4057
+ hasTargetEmail: Boolean(typedDelegation.targetEmail),
4058
+ hasTargetUserId: Boolean(typedDelegation.targetUserId),
4059
+ hasReason: Boolean(typedDelegation.reason)
4060
+ };
4061
+ }
4062
+ function redactSupportBundleValue(value) {
4063
+ if (Array.isArray(value)) {
4064
+ return value.map(redactSupportBundleValue);
4065
+ }
4066
+ if (!value || typeof value !== "object") {
4067
+ return value;
4068
+ }
4069
+ return Object.fromEntries(
4070
+ Object.entries(value).map(([key, entry]) => [
4071
+ key,
4072
+ /token|secret|password|api[-_]?key|credential|actorEmail|targetEmail|targetUserId|actorUserId|reason/i.test(key) ? "<redacted>" : redactSupportBundleValue(entry)
4073
+ ])
4074
+ );
4075
+ }
4076
+
3868
4077
  // src/main.ts
3869
4078
  var VERSION = package_default.version;
3870
4079
  var DEFAULT_API_BASE_URL = "https://swarmcraft.ai/api";
3871
4080
  var AGENT_PROVIDER_USAGE = import_packet_workflow3.EXTERNAL_AGENT_PROVIDERS.join("|");
4081
+ var MIN_DELEGATION_EXPIRY_MINUTES = 5;
4082
+ var MAX_DELEGATION_EXPIRY_MINUTES = 600;
3872
4083
  var COMMANDS = [
3873
4084
  {
3874
4085
  name: "auth login",
@@ -3876,45 +4087,57 @@ var COMMANDS = [
3876
4087
  summary: "Sign in to SwarmCraft and store a local CLI session.",
3877
4088
  implemented: true
3878
4089
  },
4090
+ {
4091
+ name: "delegation start",
4092
+ usage: "swarmcraft delegation start --user <email-or-id> --reason <text> [--support-ticket <id>] [--expires-in-minutes <n>]",
4093
+ summary: "Start short-lived operator access to a customer project workspace.",
4094
+ implemented: true
4095
+ },
4096
+ {
4097
+ name: "delegation end",
4098
+ usage: "swarmcraft delegation end --delegation <id>",
4099
+ summary: "End an active operator delegation.",
4100
+ implemented: true
4101
+ },
3879
4102
  {
3880
4103
  name: "projects list",
3881
- usage: "swarmcraft projects list",
4104
+ usage: "swarmcraft projects list [--delegation <id>] [--on-behalf-of <email-or-id> --delegation-reason <text>]",
3882
4105
  summary: "List SwarmCraft projects available to the signed-in user.",
3883
4106
  implemented: true
3884
4107
  },
3885
4108
  {
3886
4109
  name: "init",
3887
- usage: "swarmcraft init --project <project-id> --workspace <path> [--ticket <ticket-id>] [--ticket-limit <n|all>]",
4110
+ usage: "swarmcraft init --project <project-id> --workspace <path> [--ticket <ticket-id>] [--ticket-limit <n|all>] [--delegation <id>]",
3888
4111
  summary: "Prepare a repository with SwarmCraft packet and agent guidance files.",
3889
4112
  implemented: true
3890
4113
  },
3891
4114
  {
3892
4115
  name: "one-shot",
3893
- usage: `swarmcraft one-shot --project <project-id> --workspace <path> --agent-provider ${AGENT_PROVIDER_USAGE} [--run-id <id>] [--done-policy stop|auto] [--commit-policy none|done] [--allow-dirty]`,
4116
+ usage: `swarmcraft one-shot --project <project-id> --workspace <path> --agent-provider ${AGENT_PROVIDER_USAGE} [--delegation <id>] [--run-id <id>] [--checking on|off] [--reviewing on|off] [--done-policy stop|auto] [--commit-policy none|done] [--allow-dirty]`,
3894
4117
  summary: "Run the project board packet loop with the selected agent provider.",
3895
4118
  implemented: true
3896
4119
  },
3897
4120
  {
3898
4121
  name: "resume",
3899
- usage: "swarmcraft resume --run <run-id> --workspace <path>",
4122
+ usage: "swarmcraft resume --run <run-id> --workspace <path> [--allow-dirty] [--delegation <id>]",
3900
4123
  summary: "Resume a prior CLI run from its recorded manifest.",
3901
4124
  implemented: true
3902
4125
  },
3903
4126
  {
3904
4127
  name: "status",
3905
- usage: "swarmcraft status --run <run-id> --workspace <path>",
4128
+ usage: "swarmcraft status --run <run-id> --workspace <path> [--delegation <id>]",
3906
4129
  summary: "Show the status of a recorded CLI run.",
3907
4130
  implemented: true
3908
4131
  },
3909
4132
  {
3910
4133
  name: "doctor",
3911
- usage: "swarmcraft doctor [--workspace <path>] [--agent-provider codex|copilot|custom]",
4134
+ usage: "swarmcraft doctor [--workspace <path>] [--agent-provider codex|copilot|custom] [--delegation <id>]",
3912
4135
  summary: "Check local auth, API, git, workspace, runtime, and agent prerequisites.",
3913
4136
  implemented: true
3914
4137
  },
3915
4138
  {
3916
4139
  name: "support-bundle",
3917
- usage: "swarmcraft support-bundle --run <run-id> --workspace <path>",
4140
+ usage: "swarmcraft support-bundle --run <run-id> --workspace <path> [--delegation <id>]",
3918
4141
  summary: "Collect a redacted support bundle for a CLI run.",
3919
4142
  implemented: true
3920
4143
  }
@@ -3951,6 +4174,12 @@ Run "swarmcraft --help" for available commands.`);
3951
4174
  case "auth login":
3952
4175
  await runAuthLogin(parsed);
3953
4176
  return 0;
4177
+ case "delegation start":
4178
+ await runDelegationStart(parsed);
4179
+ return 0;
4180
+ case "delegation end":
4181
+ await runDelegationEnd(parsed);
4182
+ return 0;
3954
4183
  case "projects list":
3955
4184
  await runProjectsList(parsed);
3956
4185
  return 0;
@@ -4001,9 +4230,23 @@ async function runAuthLogin(parsed) {
4001
4230
  await writeStoredSession(toStoredSession(apiBaseUrl, tokenResponse));
4002
4231
  console.log(`Signed in as ${tokenResponse.user.email}.`);
4003
4232
  }
4004
- async function runProjectsList(parsed) {
4233
+ async function runDelegationStart(parsed) {
4005
4234
  const { client, session } = await resolveAuthenticatedClient(parsed);
4006
- const projects = await client.listProjects(session.accessToken);
4235
+ const delegation = await client.startDelegation(session.accessToken, buildStartDelegationRequest(parsed, {
4236
+ userOption: "user",
4237
+ reasonOption: "reason"
4238
+ }));
4239
+ printDelegationLifecycle("Delegation started", toDelegationContext(delegation));
4240
+ }
4241
+ async function runDelegationEnd(parsed) {
4242
+ const { client, session } = await resolveAuthenticatedClient(parsed);
4243
+ const delegation = await client.endDelegation(session.accessToken, requireOption(parsed, "delegation"));
4244
+ printDelegationLifecycle("Delegation ended", toDelegationContext(delegation));
4245
+ }
4246
+ async function runProjectsList(parsed) {
4247
+ const { client, session, delegation } = await resolveProjectCommandContext(parsed);
4248
+ printDelegationContext(delegation);
4249
+ const projects = await client.listProjects(session.accessToken, delegation?.id ?? null);
4007
4250
  if (projects.length === 0) {
4008
4251
  console.log("No projects available.");
4009
4252
  return;
@@ -4018,19 +4261,25 @@ async function runInit(parsed) {
4018
4261
  const projectId = requireOption(parsed, "project");
4019
4262
  const workspacePath = requireOption(parsed, "workspace");
4020
4263
  const ticketLimit = parseTicketLimit(getOption(parsed, "ticket-limit") ?? "all");
4021
- const { client, session } = await resolveAuthenticatedClient(parsed);
4264
+ const { client, session, delegation } = await resolveProjectCommandContext(parsed);
4265
+ printDelegationContext(delegation);
4022
4266
  const summary = await runBootstrap(client, session.accessToken, {
4023
4267
  projectId,
4024
4268
  workspacePath,
4025
4269
  ticketIds: getOptions(parsed, "ticket"),
4026
- ticketLimit
4270
+ ticketLimit,
4271
+ delegationId: delegation?.id ?? null
4027
4272
  });
4028
4273
  printBootstrapSummary(summary);
4029
4274
  }
4030
4275
  async function runOneShotCommand(parsed) {
4031
- const { client, session } = await resolveAuthenticatedClient(parsed);
4032
- const options = parseOneShotOptions(parsed);
4276
+ const { client, session, delegation } = await resolveProjectCommandContext(parsed);
4277
+ const options = {
4278
+ ...parseOneShotOptions(parsed),
4279
+ delegation
4280
+ };
4033
4281
  const policy = describeAgentPolicy(options);
4282
+ printDelegationContext(delegation);
4034
4283
  console.log(`Agent provider: ${options.agentProvider}`);
4035
4284
  console.log(`Agent sandbox: ${policy.sandboxMode ?? "custom"}`);
4036
4285
  console.log(`Agent approval posture: ${policy.approvalPosture ?? "custom"}`);
@@ -4040,7 +4289,7 @@ async function runOneShotCommand(parsed) {
4040
4289
  };
4041
4290
  process.once("SIGINT", onSigint);
4042
4291
  try {
4043
- const manifest = await runOneShot(client, session.accessToken, options);
4292
+ const manifest = await runOneShot(client, createAccessTokenProvider(client, session), options);
4044
4293
  printRunSummary(manifest);
4045
4294
  } finally {
4046
4295
  process.off("SIGINT", onSigint);
@@ -4050,10 +4299,13 @@ async function runResume(parsed) {
4050
4299
  const runId = requireOption(parsed, "run");
4051
4300
  const workspacePath = requireOption(parsed, "workspace");
4052
4301
  const manifest = await readRunManifest(workspacePath, runId);
4053
- const { client, session } = await resolveAuthenticatedClient(parsed);
4054
- const resumed = await runOneShot(client, session.accessToken, {
4302
+ const { client, session, delegation } = await resolveProjectCommandContext(parsed, manifest.options.delegation ?? null);
4303
+ printDelegationContext(delegation);
4304
+ const resumed = await runOneShot(client, createAccessTokenProvider(client, session), {
4055
4305
  ...manifest.options,
4306
+ delegation,
4056
4307
  workspacePath,
4308
+ allowDirty: hasFlag(parsed, "allow-dirty") ? true : manifest.options.allowDirty,
4057
4309
  resumeRunId: runId
4058
4310
  });
4059
4311
  printRunSummary(resumed);
@@ -4061,7 +4313,8 @@ async function runResume(parsed) {
4061
4313
  async function runStatus(parsed) {
4062
4314
  const runId = requireOption(parsed, "run");
4063
4315
  const workspacePath = requireOption(parsed, "workspace");
4064
- printRunSummary(await readRunManifest(workspacePath, runId));
4316
+ const manifest = await readRunManifest(workspacePath, runId);
4317
+ printRunSummary(manifest);
4065
4318
  }
4066
4319
  async function runDoctor(parsed) {
4067
4320
  console.log(`Node: ${process.version}`);
@@ -4069,8 +4322,9 @@ async function runDoctor(parsed) {
4069
4322
  console.log(`Session: ${session ? `signed in as ${session.user.email}` : "missing"}`);
4070
4323
  if (session) {
4071
4324
  try {
4072
- const client = new SwarmCraftClient(getOption(parsed, "api-base-url") ?? session.apiBaseUrl);
4073
- await client.listProjects(session.accessToken);
4325
+ const { client, session: resolvedSession, delegation } = await resolveProjectCommandContext(parsed);
4326
+ await client.listProjects(resolvedSession.accessToken, delegation?.id ?? null);
4327
+ printDelegationContext(delegation);
4074
4328
  console.log("API: reachable");
4075
4329
  } catch (error) {
4076
4330
  console.log(`API: ${error instanceof Error ? error.message : String(error)}`);
@@ -4101,13 +4355,14 @@ async function runSupportBundle(parsed) {
4101
4355
  const manifest = JSON.parse(await (0, import_promises4.readFile)(manifestPath, "utf8"));
4102
4356
  const bundleDir = import_node_path5.default.join(workspacePath, ".swarmcraft", "runs", runId, "support-bundle");
4103
4357
  await (0, import_promises4.mkdir)(bundleDir, { recursive: true });
4104
- await (0, import_promises4.writeFile)(import_node_path5.default.join(bundleDir, "manifest.redacted.json"), `${JSON.stringify(redact(manifest), null, 2)}
4358
+ await (0, import_promises4.writeFile)(import_node_path5.default.join(bundleDir, "manifest.redacted.json"), `${JSON.stringify(redactSupportBundleValue(manifest), null, 2)}
4105
4359
  `, "utf8");
4106
4360
  await (0, import_promises4.writeFile)(import_node_path5.default.join(bundleDir, "summary.json"), `${JSON.stringify({
4107
4361
  runId,
4108
4362
  manifestPath,
4109
4363
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4110
- node: process.version
4364
+ node: process.version,
4365
+ delegation: summarizeDelegationForBundle(manifest)
4111
4366
  }, null, 2)}
4112
4367
  `, "utf8");
4113
4368
  console.log(`Support bundle written: ${bundleDir}`);
@@ -4119,6 +4374,10 @@ async function resolveAuthenticatedClient(parsed) {
4119
4374
  }
4120
4375
  const apiBaseUrl = getOption(parsed, "api-base-url") ?? process.env.SWARMCRAFT_API_BASE_URL ?? session.apiBaseUrl;
4121
4376
  const client = new SwarmCraftClient(apiBaseUrl);
4377
+ session = {
4378
+ ...session,
4379
+ apiBaseUrl
4380
+ };
4122
4381
  if (sessionExpiresSoon(session)) {
4123
4382
  if (!session.refreshToken) {
4124
4383
  throw new Error("Stored SwarmCraft session has expired. Run `swarmcraft auth login` again.");
@@ -4132,6 +4391,79 @@ async function resolveAuthenticatedClient(parsed) {
4132
4391
  }
4133
4392
  return { client, session };
4134
4393
  }
4394
+ function createAccessTokenProvider(client, initialSession) {
4395
+ let session = initialSession;
4396
+ let refreshPromise = null;
4397
+ return async () => {
4398
+ if (!sessionExpiresSoon(session)) {
4399
+ return session.accessToken;
4400
+ }
4401
+ if (!session.refreshToken) {
4402
+ throw new Error("Stored SwarmCraft session has expired. Run `swarmcraft auth login` again.");
4403
+ }
4404
+ refreshPromise ??= (async () => {
4405
+ const refreshed = await client.refresh(session.refreshToken);
4406
+ session = toStoredSession(session.apiBaseUrl, {
4407
+ ...refreshed,
4408
+ refreshToken: refreshed.refreshToken ?? session.refreshToken
4409
+ });
4410
+ await writeStoredSession(session);
4411
+ })().finally(() => {
4412
+ refreshPromise = null;
4413
+ });
4414
+ await refreshPromise;
4415
+ return session.accessToken;
4416
+ };
4417
+ }
4418
+ async function resolveProjectCommandContext(parsed, fallbackDelegation = null) {
4419
+ const { client, session } = await resolveAuthenticatedClient(parsed);
4420
+ const delegation = await resolveDelegationContext(client, session, parsed, fallbackDelegation);
4421
+ return { client, session, delegation };
4422
+ }
4423
+ async function resolveDelegationContext(client, session, parsed, fallbackDelegation) {
4424
+ const delegationId = getOption(parsed, "delegation");
4425
+ const onBehalfOf = getOption(parsed, "on-behalf-of");
4426
+ if (delegationId && onBehalfOf) {
4427
+ throw new Error("Use either --delegation or --on-behalf-of, not both.");
4428
+ }
4429
+ if (delegationId) {
4430
+ return toDelegationContext(await client.loadDelegation(session.accessToken, delegationId));
4431
+ }
4432
+ if (onBehalfOf) {
4433
+ const delegation = await client.startDelegation(session.accessToken, buildStartDelegationRequest(parsed, {
4434
+ userOption: "on-behalf-of",
4435
+ reasonOption: "delegation-reason"
4436
+ }));
4437
+ const context = toDelegationContext(delegation);
4438
+ printDelegationLifecycle("Delegation started", context);
4439
+ return context;
4440
+ }
4441
+ if (hasFlag(parsed, "delegation-reason")) {
4442
+ throw new Error("--delegation-reason requires --on-behalf-of.");
4443
+ }
4444
+ return fallbackDelegation;
4445
+ }
4446
+ function buildStartDelegationRequest(parsed, options) {
4447
+ const expiresInMinutes = getOption(parsed, "expires-in-minutes") ? parseDelegationExpiry(getOption(parsed, "expires-in-minutes")) : null;
4448
+ return {
4449
+ user: requireOption(parsed, options.userOption),
4450
+ reason: requireOption(parsed, options.reasonOption),
4451
+ supportTicketId: getOption(parsed, "support-ticket") ?? null,
4452
+ expiresInMinutes
4453
+ };
4454
+ }
4455
+ function toDelegationContext(delegation) {
4456
+ return {
4457
+ id: delegation.id,
4458
+ actorEmail: delegation.actorEmail,
4459
+ targetUserId: delegation.targetUserId,
4460
+ targetEmail: delegation.targetEmail,
4461
+ reason: delegation.reason,
4462
+ supportTicketId: delegation.supportTicketId,
4463
+ status: delegation.status,
4464
+ expiresAt: delegation.expiresAt
4465
+ };
4466
+ }
4135
4467
  function printBootstrapSummary(summary) {
4136
4468
  console.log(`Bootstrapped ${summary.projectTitle}`);
4137
4469
  console.log(`Workspace: ${summary.workspacePath}`);
@@ -4153,10 +4485,26 @@ function printBootstrapSummary(summary) {
4153
4485
  console.log(` ${nextCommand}`);
4154
4486
  }
4155
4487
  }
4488
+ function printDelegationLifecycle(prefix, delegation) {
4489
+ console.log(`${prefix}: ${delegation.id}`);
4490
+ printDelegationContext(delegation);
4491
+ if (delegation.expiresAt) {
4492
+ console.log(`Delegation expires: ${delegation.expiresAt}`);
4493
+ }
4494
+ }
4495
+ function printDelegationContext(delegation) {
4496
+ if (!delegation) {
4497
+ return;
4498
+ }
4499
+ const actor = delegation.actorEmail ?? "operator";
4500
+ const target = delegation.targetEmail ?? delegation.targetUserId ?? "target user";
4501
+ console.log(`Delegated access: ${actor} -> ${target} (${delegation.id})`);
4502
+ }
4156
4503
  function printRunSummary(manifest) {
4157
4504
  console.log(`Run: ${manifest.runId}`);
4158
4505
  console.log(`Status: ${manifest.status}`);
4159
4506
  console.log(`Workspace: ${manifest.workspacePath}`);
4507
+ printDelegationContext(manifest.options?.delegation ?? null);
4160
4508
  console.log(`Tickets: ${manifest.tickets.length}`);
4161
4509
  for (const ticket of manifest.tickets) {
4162
4510
  console.log(` - ${ticket.outcome}: ${ticket.title} (${ticket.stages.length} stage(s))`);
@@ -4167,14 +4515,16 @@ function printRunSummary(manifest) {
4167
4515
  }
4168
4516
  function parseOneShotOptions(parsed) {
4169
4517
  const agentProvider = (0, import_packet_workflow3.parseExternalAgentProvider)(requireOption(parsed, "agent-provider"));
4170
- const donePolicy = parseChoice(getOption(parsed, "done-policy") ?? "stop", ["stop", "auto"], "--done-policy");
4518
+ const donePolicy = parseChoice(getOption(parsed, "done-policy") ?? "auto", ["stop", "auto"], "--done-policy");
4171
4519
  const commitPolicy = parseChoice(getOption(parsed, "commit-policy") ?? "none", ["none", "done"], "--commit-policy");
4520
+ const explicitReviewing = getOption(parsed, "reviewing");
4172
4521
  return {
4173
4522
  projectId: requireOption(parsed, "project"),
4174
4523
  workspacePath: requireOption(parsed, "workspace"),
4175
4524
  agentProvider,
4176
4525
  agentCommand: getOption(parsed, "agent-command") ?? process.env.SWARMCRAFT_AGENT_COMMAND ?? null,
4177
- checking: parseOnOff(getOption(parsed, "checking") ?? "on", "--checking"),
4526
+ checking: parseOnOff(getOption(parsed, "checking") ?? "off", "--checking"),
4527
+ reviewing: explicitReviewing ? parseOnOff(explicitReviewing, "--reviewing") : donePolicy === "stop",
4178
4528
  maxCheckingAttempts: parsePositiveInteger(getOption(parsed, "max-checking-attempts") ?? "2", "--max-checking-attempts"),
4179
4529
  donePolicy,
4180
4530
  commitPolicy,
@@ -4186,6 +4536,7 @@ function parseOneShotOptions(parsed) {
4186
4536
  agentApiKeyEnvName: getOption(parsed, "agent-api-key-env-name") ?? null,
4187
4537
  ticketIds: getOptions(parsed, "ticket"),
4188
4538
  ticketLimit: parseTicketLimit(getOption(parsed, "ticket-limit") ?? "all"),
4539
+ delegation: null,
4189
4540
  runId: parseRunId(getOption(parsed, "run-id"))
4190
4541
  };
4191
4542
  }
@@ -4221,7 +4572,7 @@ function findCommand(positionals) {
4221
4572
  if (positionals.length === 0) {
4222
4573
  return void 0;
4223
4574
  }
4224
- const commandKey = positionals[0] === "auth" || positionals[0] === "projects" ? positionals.slice(0, 2).join(" ") : positionals[0];
4575
+ const commandKey = positionals[0] === "auth" || positionals[0] === "projects" || positionals[0] === "delegation" ? positionals.slice(0, 2).join(" ") : positionals[0];
4225
4576
  return COMMANDS.find((command) => command.name === commandKey);
4226
4577
  }
4227
4578
  function printGeneralHelp() {
@@ -4286,6 +4637,13 @@ function parsePositiveInteger(rawValue, flagName) {
4286
4637
  }
4287
4638
  return parsed;
4288
4639
  }
4640
+ function parseDelegationExpiry(rawValue) {
4641
+ const parsed = parsePositiveInteger(rawValue, "--expires-in-minutes");
4642
+ if (parsed < MIN_DELEGATION_EXPIRY_MINUTES || parsed > MAX_DELEGATION_EXPIRY_MINUTES) {
4643
+ throw new Error(`--expires-in-minutes must be between ${MIN_DELEGATION_EXPIRY_MINUTES} and ${MAX_DELEGATION_EXPIRY_MINUTES}.`);
4644
+ }
4645
+ return parsed;
4646
+ }
4289
4647
  function parseNonNegativeNumber(rawValue, flagName) {
4290
4648
  const parsed = Number.parseFloat(rawValue);
4291
4649
  if (!Number.isFinite(parsed) || parsed < 0) {
@@ -4317,20 +4675,6 @@ function parseRunId(rawValue) {
4317
4675
  }
4318
4676
  return rawValue;
4319
4677
  }
4320
- function redact(value) {
4321
- if (Array.isArray(value)) {
4322
- return value.map(redact);
4323
- }
4324
- if (!value || typeof value !== "object") {
4325
- return value;
4326
- }
4327
- return Object.fromEntries(
4328
- Object.entries(value).map(([key, entry]) => [
4329
- key,
4330
- /token|secret|password|api[-_]?key|credential/i.test(key) ? "<redacted>" : redact(entry)
4331
- ])
4332
- );
4333
- }
4334
4678
  async function promptLine(prompt) {
4335
4679
  const readline = (0, import_promises5.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
4336
4680
  try {
@@ -4342,14 +4686,26 @@ async function promptLine(prompt) {
4342
4686
  async function promptSecret(prompt) {
4343
4687
  return promptLine(prompt);
4344
4688
  }
4345
- main(process.argv.slice(2)).then((exitCode) => {
4346
- process.exitCode = exitCode;
4347
- }).catch((error) => {
4348
- if (error instanceof SwarmCraftApiError) {
4349
- const request = error.requestId ? ` requestId=${error.requestId}` : "";
4350
- console.error(`${error.code}: ${error.message}${request}`);
4351
- } else {
4352
- console.error(error instanceof Error ? error.message : String(error));
4353
- }
4354
- process.exitCode = 1;
4689
+ if (isCliEntrypoint()) {
4690
+ main(process.argv.slice(2)).then((exitCode) => {
4691
+ process.exitCode = exitCode;
4692
+ }).catch((error) => {
4693
+ if (error instanceof SwarmCraftApiError) {
4694
+ const request = error.requestId ? ` requestId=${error.requestId}` : "";
4695
+ console.error(`${error.code}: ${error.message}${request}`);
4696
+ } else {
4697
+ console.error(error instanceof Error ? error.message : String(error));
4698
+ }
4699
+ process.exitCode = 1;
4700
+ });
4701
+ }
4702
+ function isCliEntrypoint() {
4703
+ const invokedScript = process.argv[1] ? import_node_path5.default.basename(process.argv[1]) : "";
4704
+ return invokedScript === "main.cjs" || invokedScript === "main.js" || invokedScript === "main.ts";
4705
+ }
4706
+ // Annotate the CommonJS export names for ESM import in node:
4707
+ 0 && (module.exports = {
4708
+ findCommand,
4709
+ main,
4710
+ parseArgs
4355
4711
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmcraftai/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.5",
4
4
  "description": "Prepare repositories and run packet-based SwarmCraft agent workflows from the terminal.",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "type": "module",