@swarmcraftai/cli 0.1.0 → 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 +10 -8
  2. package/dist/main.cjs +511 -112
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # SwarmCraft CLI
2
2
 
3
- Customer-facing command line tool for preparing a repository for a SwarmCraft project board and running packet-based one-shot work.
3
+ SwarmCraft CLI prepares a local repository for a SwarmCraft project board and runs packet-based agent workflows from the terminal.
4
4
 
5
- The CLI is separate from `apps/driver`. The driver remains internal scenario tooling for local operator runs, docs evidence, reports, and weekly validation. Customers should install and run the SwarmCraft CLI, not the internal driver scripts.
5
+ Use it to authenticate with SwarmCraft, list available projects, seed workspace guidance files, materialize ticket packets, run one-shot implementation passes, resume or inspect runs, and create support bundles.
6
6
 
7
7
  ## Package Shape
8
8
 
@@ -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,15 +2977,66 @@ 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");
2977
2990
  var import_node_process = require("node:process");
2978
2991
  var import_packet_workflow3 = __toESM(require_dist(), 1);
2979
2992
 
2993
+ // package.json
2994
+ var package_default = {
2995
+ name: "@swarmcraftai/cli",
2996
+ version: "0.1.5",
2997
+ description: "Prepare repositories and run packet-based SwarmCraft agent workflows from the terminal.",
2998
+ license: "SEE LICENSE IN LICENSE.txt",
2999
+ type: "module",
3000
+ homepage: "https://swarmcraft.ai",
3001
+ bugs: {
3002
+ url: "https://swarmcraft.ai/login?next=%2Fapp%2Fsupport"
3003
+ },
3004
+ bin: {
3005
+ swarmcraft: "dist/main.cjs"
3006
+ },
3007
+ files: [
3008
+ "dist",
3009
+ "README.md",
3010
+ "LICENSE.txt"
3011
+ ],
3012
+ publishConfig: {
3013
+ access: "public"
3014
+ },
3015
+ engines: {
3016
+ node: ">=20.19.0"
3017
+ },
3018
+ scripts: {
3019
+ clean: `node -e "require('node:fs').rmSync('dist',{recursive:true,force:true})"`,
3020
+ typecheck: "tsc --noEmit -p tsconfig.json",
3021
+ build: "pnpm --filter @swarmcraft/repo-seed build && pnpm run typecheck && pnpm run clean && esbuild src/main.ts --bundle --platform=node --format=cjs --target=node20.19 --outfile=dist/main.cjs",
3022
+ check: "pnpm --filter @swarmcraft/repo-seed build && pnpm run typecheck && vitest run && pnpm run build && node ./dist/main.cjs --help",
3023
+ dev: "pnpm run build && node ./dist/main.cjs",
3024
+ "release:smoke": "pnpm run build && node ./scripts/smoke-packed-cli.mjs"
3025
+ },
3026
+ devDependencies: {
3027
+ "@swarmcraft/packet-workflow": "workspace:*",
3028
+ "@swarmcraft/repo-seed": "workspace:*",
3029
+ "@types/node": "^24.10.1",
3030
+ esbuild: "^0.25.11",
3031
+ typescript: "^5.9.3",
3032
+ vitest: "^3.2.4"
3033
+ }
3034
+ };
3035
+
2980
3036
  // src/apiClient.ts
2981
3037
  var API_REQUEST_TIMEOUT_MS = 15e3;
2982
3038
  var CLIENT_SOURCE_HEADER = "X-SwarmCraft-Client";
3039
+ var DELEGATION_HEADER = "X-SwarmCraft-Delegation";
2983
3040
  var REFRESH_TOKEN_COOKIE_NAME = "swarmcraft_refresh_token";
2984
3041
  var SwarmCraftApiError = class extends Error {
2985
3042
  status;
@@ -3031,56 +3088,88 @@ var SwarmCraftClient = class {
3031
3088
  accessToken
3032
3089
  );
3033
3090
  }
3034
- listProjects(accessToken) {
3035
- 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);
3036
3117
  }
3037
- loadProject(projectId, accessToken) {
3038
- 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);
3039
3120
  }
3040
- loadBoard(projectId, accessToken) {
3041
- 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);
3042
3123
  }
3043
- upsertTicketPacketLink(projectId, ticketId, request, accessToken) {
3124
+ upsertTicketPacketLink(projectId, ticketId, request, accessToken, delegationId) {
3044
3125
  return this.requestJson(
3045
3126
  `/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(ticketId)}/packet-link`,
3046
3127
  {
3047
3128
  method: "PUT",
3048
3129
  body: JSON.stringify(request)
3049
3130
  },
3050
- accessToken
3131
+ accessToken,
3132
+ void 0,
3133
+ delegationId
3051
3134
  );
3052
3135
  }
3053
- moveTicket(projectId, ticketId, request, accessToken) {
3136
+ moveTicket(projectId, ticketId, request, accessToken, delegationId) {
3054
3137
  return this.requestJson(
3055
3138
  `/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(ticketId)}/move`,
3056
3139
  {
3057
3140
  method: "POST",
3058
3141
  body: JSON.stringify(request)
3059
3142
  },
3060
- accessToken
3143
+ accessToken,
3144
+ void 0,
3145
+ delegationId
3061
3146
  );
3062
3147
  }
3063
- updateTicketNotes(projectId, ticketId, request, accessToken) {
3148
+ updateTicketNotes(projectId, ticketId, request, accessToken, delegationId) {
3064
3149
  return this.requestJson(
3065
3150
  `/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(ticketId)}/notes`,
3066
3151
  {
3067
3152
  method: "PATCH",
3068
3153
  body: JSON.stringify(request)
3069
3154
  },
3070
- accessToken
3155
+ accessToken,
3156
+ void 0,
3157
+ delegationId
3071
3158
  );
3072
3159
  }
3073
- updateChecklistItem(projectId, ticketId, itemId, request, accessToken) {
3160
+ updateChecklistItem(projectId, ticketId, itemId, request, accessToken, delegationId) {
3074
3161
  return this.requestJson(
3075
3162
  `/projects/${encodeURIComponent(projectId)}/tickets/${encodeURIComponent(ticketId)}/checklist-items/${encodeURIComponent(itemId)}`,
3076
3163
  {
3077
3164
  method: "PATCH",
3078
3165
  body: JSON.stringify(request)
3079
3166
  },
3080
- accessToken
3167
+ accessToken,
3168
+ void 0,
3169
+ delegationId
3081
3170
  );
3082
3171
  }
3083
- async requestJson(path6, init, accessToken, refreshToken) {
3172
+ async requestJson(path6, init, accessToken, refreshToken, delegationId) {
3084
3173
  const url = `${this.apiBaseUrl}${path6.startsWith("/") ? path6 : `/${path6}`}`;
3085
3174
  const method = (init.method ?? "GET").toUpperCase();
3086
3175
  const headers = new Headers(init.headers);
@@ -3097,6 +3186,9 @@ var SwarmCraftClient = class {
3097
3186
  if (refreshToken) {
3098
3187
  headers.set("Cookie", `${REFRESH_TOKEN_COOKIE_NAME}=${refreshToken}`);
3099
3188
  }
3189
+ if (delegationId) {
3190
+ headers.set(DELEGATION_HEADER, delegationId);
3191
+ }
3100
3192
  let response;
3101
3193
  try {
3102
3194
  response = await fetch(url, {
@@ -3175,6 +3267,7 @@ var import_packet_workflow = __toESM(require_dist(), 1);
3175
3267
  var import_repo_seed = __toESM(require_dist2(), 1);
3176
3268
 
3177
3269
  // src/workspace.ts
3270
+ var import_node_fs = require("node:fs");
3178
3271
  var import_promises = require("node:fs/promises");
3179
3272
  var import_node_path = __toESM(require("node:path"), 1);
3180
3273
  var import_node_child_process = require("node:child_process");
@@ -3216,7 +3309,11 @@ async function writeWorkspaceFile(workspacePath, relativePath, contents) {
3216
3309
  await (0, import_promises.mkdir)(import_node_path.default.dirname(absolutePath), { recursive: true });
3217
3310
  await (0, import_promises.writeFile)(absolutePath, contents, "utf8");
3218
3311
  }
3219
- 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;
3220
3317
  return new Promise((resolve) => {
3221
3318
  const child = (0, import_node_child_process.spawn)(command, {
3222
3319
  cwd,
@@ -3226,14 +3323,40 @@ async function runCommand(command, cwd, env = process.env) {
3226
3323
  });
3227
3324
  const stdoutChunks = [];
3228
3325
  const stderrChunks = [];
3229
- child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
3230
- 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
+ });
3231
3349
  child.on("close", (code) => {
3232
- resolve({
3350
+ const result = {
3233
3351
  exitCode: code ?? 1,
3234
3352
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
3235
3353
  stderr: Buffer.concat(stderrChunks).toString("utf8")
3236
- });
3354
+ };
3355
+ if (logStream) {
3356
+ logStream.end(() => resolve(result));
3357
+ return;
3358
+ }
3359
+ resolve(result);
3237
3360
  });
3238
3361
  });
3239
3362
  }
@@ -3258,11 +3381,11 @@ async function runGit(args, cwd) {
3258
3381
  // src/bootstrap.ts
3259
3382
  async function runBootstrap(client, accessToken, options) {
3260
3383
  const workspace = await validateWorkspace(options.workspacePath);
3261
- const project = await client.loadProject(options.projectId, accessToken);
3384
+ const project = await client.loadProject(options.projectId, accessToken, options.delegationId);
3262
3385
  if (project.boardState !== "UNLOCKED") {
3263
3386
  throw new Error(`Project ${project.id} is ${project.boardState}; unlock the board before bootstrapping a workspace.`);
3264
3387
  }
3265
- const board = await client.loadBoard(project.id, accessToken);
3388
+ const board = await client.loadBoard(project.id, accessToken, options.delegationId);
3266
3389
  const seedFilesWritten = await seedWorkspace(project, workspace.workspacePath);
3267
3390
  const selectedTickets = selectBootstrapTickets(board, options);
3268
3391
  const packetFilesCreated = [];
@@ -3312,14 +3435,17 @@ async function runBootstrap(client, accessToken, options) {
3312
3435
  await writeWorkspaceFile(workspace.workspacePath, relativePacketPath, packetContents);
3313
3436
  packetFilesCreated.push(relativePacketPath);
3314
3437
  }
3315
- await client.upsertTicketPacketLink(project.id, ticket.id, {
3316
- workspaceFolder: workspace.workspaceFolderName,
3317
- packetFilePath: relativePacketPath,
3318
- expectedRevision: ticket.revision,
3319
- lastSyncedTicketRevision: ticket.revision,
3320
- lastSyncedAt: syncedAt
3321
- }, accessToken);
3322
- 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
+ }
3323
3449
  }
3324
3450
  return {
3325
3451
  projectTitle: project.title || project.projectTitle || project.id,
@@ -3331,7 +3457,7 @@ async function runBootstrap(client, accessToken, options) {
3331
3457
  linkedPackets,
3332
3458
  skippedTickets,
3333
3459
  nextCommands: [
3334
- `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}` : ""}`,
3335
3461
  "Review generated files under .swarmcraft/, .github/, .agents/, and .codex/ before running agents."
3336
3462
  ]
3337
3463
  };
@@ -3367,21 +3493,28 @@ async function seedWorkspace(project, workspacePath) {
3367
3493
  var import_promises2 = require("node:fs/promises");
3368
3494
  var import_node_path3 = __toESM(require("node:path"), 1);
3369
3495
  var import_packet_workflow2 = __toESM(require_dist(), 1);
3370
- async function runOneShot(client, accessToken, options) {
3496
+ async function runOneShot(client, accessTokenProvider, options) {
3371
3497
  const workspace = await validateWorkspace(options.workspacePath);
3498
+ const getAccessToken = () => resolveAccessToken(accessTokenProvider);
3372
3499
  if (workspace.dirtyFileCount > 0 && !options.allowDirty) {
3373
3500
  throw new Error(`Workspace has ${workspace.dirtyFileCount} changed file(s). Commit, stash, or rerun with --allow-dirty before one-shot.`);
3374
3501
  }
3375
- await runBootstrap(client, accessToken, {
3376
- projectId: options.projectId,
3377
- workspacePath: workspace.workspacePath,
3378
- ticketIds: options.ticketIds,
3379
- ticketLimit: options.ticketLimit
3380
- });
3381
- const project = await client.loadProject(options.projectId, accessToken);
3382
- 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);
3383
3515
  const runId = options.resumeRunId ?? options.runId ?? createRunId();
3384
3516
  const manifestPath = resolveManifestPath(workspace.workspacePath, runId);
3517
+ const selectedTickets = selectTickets(board, options);
3385
3518
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3386
3519
  const manifest = await loadOrCreateManifest(manifestPath, {
3387
3520
  generator: "swarmcraft-cli",
@@ -3404,6 +3537,9 @@ async function runOneShot(client, accessToken, options) {
3404
3537
  }
3405
3538
  });
3406
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}.`);
3407
3543
  let agentRequests = manifest.tickets.flatMap((ticket) => ticket.stages).length;
3408
3544
  const completedTicketIds = new Set(
3409
3545
  manifest.tickets.filter((ticket) => ["done", "manual-review-required"].includes(ticket.outcome)).map((ticket) => ticket.ticketId)
@@ -3412,10 +3548,13 @@ async function runOneShot(client, accessToken, options) {
3412
3548
  manifest.tickets = manifest.tickets.filter((ticket) => completedTicketIds.has(ticket.ticketId));
3413
3549
  await persistManifest(manifestPath, manifest);
3414
3550
  }
3415
- for (const selectedTicket of selectTickets(board, options)) {
3551
+ let finalManifestPersisted = false;
3552
+ for (const [ticketIndex, selectedTicket] of selectedTickets.entries()) {
3416
3553
  if (completedTicketIds.has(selectedTicket.id)) {
3554
+ printProgress(`Skipping completed ticket ${ticketIndex + 1}/${selectedTickets.length}: ${selectedTicket.title}.`);
3417
3555
  continue;
3418
3556
  }
3557
+ printProgress(`Starting ticket ${ticketIndex + 1}/${selectedTickets.length}: ${selectedTicket.title}.`);
3419
3558
  const ticketOutcome = {
3420
3559
  ticketId: selectedTicket.id,
3421
3560
  title: selectedTicket.title,
@@ -3430,7 +3569,7 @@ async function runOneShot(client, accessToken, options) {
3430
3569
  await persistManifest(manifestPath, manifest);
3431
3570
  const result = await processTicket({
3432
3571
  client,
3433
- accessToken,
3572
+ getAccessToken,
3434
3573
  project,
3435
3574
  board,
3436
3575
  ticket: selectedTicket,
@@ -3448,30 +3587,59 @@ async function runOneShot(client, accessToken, options) {
3448
3587
  ticketOutcome.outcome = result.outcome;
3449
3588
  ticketOutcome.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
3450
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);
3451
3596
  if (result.outcome === "done" && options.commitPolicy === "done") {
3452
- 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;
3453
3602
  }
3454
3603
  await persistManifest(manifestPath, manifest);
3455
- 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}.`);
3456
3613
  break;
3457
3614
  }
3458
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) {
3459
3627
  manifest.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
3460
3628
  manifest.status = resolveManifestStatus(manifest);
3461
3629
  manifest.durations = (0, import_packet_workflow2.summarizeWorkflowRunDurations)(manifest);
3462
- await persistManifest(manifestPath, manifest);
3463
- return manifest;
3464
3630
  }
3465
3631
  async function processTicket(context) {
3466
3632
  let currentBoard = context.board;
3467
3633
  let currentTicket = requireTicket(currentBoard, context.ticket.id);
3468
3634
  if (currentTicket.workflowState === "todo") {
3469
- 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);
3470
3637
  currentTicket = requireTicket(currentBoard, context.ticket.id);
3471
3638
  }
3472
3639
  const packetPath = (0, import_packet_workflow2.resolvePacketFilePath)(context.project, currentTicket, currentTicket.packetLink);
3473
3640
  context.ticketOutcome.packetPath = packetPath;
3474
3641
  await refreshPacketMetadata(context.workspacePath, packetPath, currentTicket);
3642
+ printProgress(`Packet: ${packetPath}`);
3475
3643
  if (currentTicket.workflowState !== "checking" && currentTicket.workflowState !== "reviewing") {
3476
3644
  const doResult = await runStageAndSync({ ...context, board: currentBoard, ticket: currentTicket, packetPath, stage: "do" });
3477
3645
  currentBoard = doResult.board;
@@ -3481,7 +3649,8 @@ async function processTicket(context) {
3481
3649
  return moveAfterAcceptedChecking(context, currentBoard, currentTicket, packetPath);
3482
3650
  }
3483
3651
  if (currentTicket.workflowState === "doing") {
3484
- 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);
3485
3654
  currentTicket = requireTicket(currentBoard, currentTicket.id);
3486
3655
  await refreshPacketMetadata(context.workspacePath, packetPath, currentTicket);
3487
3656
  }
@@ -3491,9 +3660,11 @@ async function processTicket(context) {
3491
3660
  currentTicket = checkResult.ticket;
3492
3661
  if (currentTicket.workflowState === "doing") {
3493
3662
  if (attempt >= context.options.maxCheckingAttempts) {
3663
+ printProgress("Checking sent the ticket back to doing and retry budget is exhausted.");
3494
3664
  return { board: currentBoard, ticket: currentTicket, outcome: "blocked-checking-retries-exhausted" };
3495
3665
  }
3496
- 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);
3497
3668
  currentTicket = requireTicket(currentBoard, currentTicket.id);
3498
3669
  await refreshPacketMetadata(context.workspacePath, packetPath, currentTicket);
3499
3670
  continue;
@@ -3523,9 +3694,12 @@ async function runStageAndSync(context) {
3523
3694
  ticket: context.packetPath,
3524
3695
  workspace: context.workspacePath
3525
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`);
3526
3699
  const stageOutcome = {
3527
3700
  stage: context.stage,
3528
3701
  command,
3702
+ logPath: import_node_path3.default.relative(context.workspacePath, logPath),
3529
3703
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3530
3704
  finishedAt: null,
3531
3705
  elapsedMs: null,
@@ -3539,7 +3713,9 @@ async function runStageAndSync(context) {
3539
3713
  context.ticketOutcome.stages.push(stageOutcome);
3540
3714
  await persistManifest(context.manifestPath, context.manifest);
3541
3715
  context.incrementAgentRequests();
3542
- 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`);
3543
3719
  await (0, import_promises2.mkdir)(import_node_path3.default.dirname(costOutputPath), { recursive: true });
3544
3720
  const execution = await runCommand(command, context.workspacePath, await buildAgentEnvironment(context.options, {
3545
3721
  SWARMCRAFT_STAGE: context.stage,
@@ -3547,23 +3723,26 @@ async function runStageAndSync(context) {
3547
3723
  SWARMCRAFT_WORKSPACE: context.workspacePath,
3548
3724
  SWARMCRAFT_AGENT_PROVIDER: context.options.agentProvider,
3549
3725
  SWARMCRAFT_AGENT_COST_OUTPUT: costOutputPath
3550
- }));
3726
+ }), { logPath, teeOutput: true });
3551
3727
  stageOutcome.exitCode = execution.exitCode;
3552
3728
  if (execution.exitCode !== 0) {
3553
3729
  stageOutcome.result = "failed";
3554
3730
  stageOutcome.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
3555
3731
  stageOutcome.elapsedMs = (0, import_packet_workflow2.computeElapsedMs)(stageOutcome.startedAt, stageOutcome.finishedAt);
3556
3732
  await persistManifest(context.manifestPath, context.manifest);
3733
+ printProgress(`${context.stage} stage failed in ${formatDuration(stageOutcome.elapsedMs)}.`);
3557
3734
  throw new Error(`Agent command failed during ${context.stage}: ${execution.stderr || execution.stdout || command}`);
3558
3735
  }
3559
3736
  const syncResult = await syncPacketFile({
3560
3737
  client: context.client,
3561
- accessToken: context.accessToken,
3738
+ getAccessToken: context.getAccessToken,
3562
3739
  project: context.project,
3563
3740
  board: context.board,
3564
3741
  ticket: context.ticket,
3565
3742
  workspacePath: context.workspacePath,
3566
- relativePacketPath: context.packetPath
3743
+ relativePacketPath: context.packetPath,
3744
+ allowLaneSync: context.options.checking || context.stage !== "do",
3745
+ delegationId: context.options.delegation?.id ?? null
3567
3746
  });
3568
3747
  stageOutcome.result = "synced";
3569
3748
  stageOutcome.costUsd = await readStageCostUsd(costOutputPath);
@@ -3579,6 +3758,7 @@ async function runStageAndSync(context) {
3579
3758
  stageOutcome.laneAfter = syncResult.ticket.workflowState;
3580
3759
  stageOutcome.syncedChanges = syncResult.syncedChanges;
3581
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}.`);
3582
3762
  return { board: syncResult.board, ticket: syncResult.ticket };
3583
3763
  }
3584
3764
  async function syncPacketFile(input2) {
@@ -3609,7 +3789,7 @@ async function syncPacketFile(input2) {
3609
3789
  currentBoard = await input2.client.updateChecklistItem(input2.project.id, currentTicket.id, currentItem.id, {
3610
3790
  completed: parsedItem.completed,
3611
3791
  expectedRevision: currentItem.revision
3612
- }, input2.accessToken);
3792
+ }, await input2.getAccessToken(), input2.delegationId);
3613
3793
  currentTicket = requireTicket(currentBoard, currentTicket.id);
3614
3794
  syncedChanges += 1;
3615
3795
  }
@@ -3619,12 +3799,12 @@ async function syncPacketFile(input2) {
3619
3799
  currentBoard = await input2.client.updateTicketNotes(input2.project.id, currentTicket.id, {
3620
3800
  notes: desiredNotes,
3621
3801
  expectedRevision: currentTicket.revision
3622
- }, input2.accessToken);
3802
+ }, await input2.getAccessToken(), input2.delegationId);
3623
3803
  currentTicket = requireTicket(currentBoard, currentTicket.id);
3624
3804
  syncedChanges += 1;
3625
3805
  }
3626
- if (parsedPacket.frontmatter.lane !== currentTicket.workflowState) {
3627
- 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);
3628
3808
  currentTicket = requireTicket(currentBoard, currentTicket.id);
3629
3809
  syncedChanges += 1;
3630
3810
  }
@@ -3639,6 +3819,10 @@ async function buildAgentEnvironment(options, stageEnv) {
3639
3819
  TERM: process.env.TERM,
3640
3820
  ...stageEnv
3641
3821
  };
3822
+ if (options.commitPolicy === "done") {
3823
+ env.SWARMCRAFT_DRIVER_AUTO_COMMIT = "true";
3824
+ env.SWARMCRAFT_CLI_AUTO_COMMIT = "true";
3825
+ }
3642
3826
  if (!options.agentApiKeyEnvName) {
3643
3827
  return env;
3644
3828
  }
@@ -3675,23 +3859,28 @@ function sumStageCostUsd(manifest) {
3675
3859
  return manifest.tickets.flatMap((ticket) => ticket.stages).reduce((total, stage) => total + Number(stage.costUsd ?? 0), 0);
3676
3860
  }
3677
3861
  async function moveAfterAcceptedChecking(context, board, ticket, packetPath) {
3678
- if (context.options.donePolicy === "auto") {
3679
- 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);
3680
3865
  const doneTicket = requireTicket(doneBoard, ticket.id);
3681
3866
  await refreshPacketMetadata(context.workspacePath, packetPath, doneTicket);
3682
3867
  return { board: doneBoard, ticket: doneTicket, outcome: "done" };
3683
3868
  }
3684
- 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
+ }
3685
3874
  const reviewTicket = requireTicket(reviewBoard, ticket.id);
3686
3875
  await refreshPacketMetadata(context.workspacePath, packetPath, reviewTicket);
3687
3876
  return { board: reviewBoard, ticket: reviewTicket, outcome: "manual-review-required" };
3688
3877
  }
3689
- async function moveTicket(client, accessToken, projectId, ticket, workflowState) {
3878
+ async function moveTicket(client, accessToken, projectId, ticket, workflowState, delegationId) {
3690
3879
  return client.moveTicket(projectId, ticket.id, {
3691
3880
  workflowState,
3692
3881
  targetIndex: null,
3693
3882
  expectedRevision: ticket.revision
3694
- }, accessToken);
3883
+ }, accessToken, delegationId);
3695
3884
  }
3696
3885
  async function refreshPacketMetadata(workspacePath, relativePacketPath, ticket, syncedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3697
3886
  const contents = await readWorkspaceFile(workspacePath, relativePacketPath);
@@ -3782,6 +3971,29 @@ function describeAgentPolicy(options) {
3782
3971
  })
3783
3972
  };
3784
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
+ }
3785
3997
 
3786
3998
  // src/sessionStore.ts
3787
3999
  var import_promises3 = require("node:fs/promises");
@@ -3822,10 +4034,52 @@ function sessionExpiresSoon(session) {
3822
4034
  return expiresAt - Date.now() < 6e4;
3823
4035
  }
3824
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
+
3825
4077
  // src/main.ts
3826
- var VERSION = "0.1.0";
4078
+ var VERSION = package_default.version;
3827
4079
  var DEFAULT_API_BASE_URL = "https://swarmcraft.ai/api";
3828
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;
3829
4083
  var COMMANDS = [
3830
4084
  {
3831
4085
  name: "auth login",
@@ -3833,45 +4087,57 @@ var COMMANDS = [
3833
4087
  summary: "Sign in to SwarmCraft and store a local CLI session.",
3834
4088
  implemented: true
3835
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
+ },
3836
4102
  {
3837
4103
  name: "projects list",
3838
- usage: "swarmcraft projects list",
4104
+ usage: "swarmcraft projects list [--delegation <id>] [--on-behalf-of <email-or-id> --delegation-reason <text>]",
3839
4105
  summary: "List SwarmCraft projects available to the signed-in user.",
3840
4106
  implemented: true
3841
4107
  },
3842
4108
  {
3843
4109
  name: "init",
3844
- 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>]",
3845
4111
  summary: "Prepare a repository with SwarmCraft packet and agent guidance files.",
3846
4112
  implemented: true
3847
4113
  },
3848
4114
  {
3849
4115
  name: "one-shot",
3850
- 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]`,
3851
4117
  summary: "Run the project board packet loop with the selected agent provider.",
3852
4118
  implemented: true
3853
4119
  },
3854
4120
  {
3855
4121
  name: "resume",
3856
- usage: "swarmcraft resume --run <run-id> --workspace <path>",
4122
+ usage: "swarmcraft resume --run <run-id> --workspace <path> [--allow-dirty] [--delegation <id>]",
3857
4123
  summary: "Resume a prior CLI run from its recorded manifest.",
3858
4124
  implemented: true
3859
4125
  },
3860
4126
  {
3861
4127
  name: "status",
3862
- usage: "swarmcraft status --run <run-id> --workspace <path>",
4128
+ usage: "swarmcraft status --run <run-id> --workspace <path> [--delegation <id>]",
3863
4129
  summary: "Show the status of a recorded CLI run.",
3864
4130
  implemented: true
3865
4131
  },
3866
4132
  {
3867
4133
  name: "doctor",
3868
- usage: "swarmcraft doctor [--workspace <path>] [--agent-provider codex|copilot|custom]",
4134
+ usage: "swarmcraft doctor [--workspace <path>] [--agent-provider codex|copilot|custom] [--delegation <id>]",
3869
4135
  summary: "Check local auth, API, git, workspace, runtime, and agent prerequisites.",
3870
4136
  implemented: true
3871
4137
  },
3872
4138
  {
3873
4139
  name: "support-bundle",
3874
- usage: "swarmcraft support-bundle --run <run-id> --workspace <path>",
4140
+ usage: "swarmcraft support-bundle --run <run-id> --workspace <path> [--delegation <id>]",
3875
4141
  summary: "Collect a redacted support bundle for a CLI run.",
3876
4142
  implemented: true
3877
4143
  }
@@ -3908,6 +4174,12 @@ Run "swarmcraft --help" for available commands.`);
3908
4174
  case "auth login":
3909
4175
  await runAuthLogin(parsed);
3910
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;
3911
4183
  case "projects list":
3912
4184
  await runProjectsList(parsed);
3913
4185
  return 0;
@@ -3958,9 +4230,23 @@ async function runAuthLogin(parsed) {
3958
4230
  await writeStoredSession(toStoredSession(apiBaseUrl, tokenResponse));
3959
4231
  console.log(`Signed in as ${tokenResponse.user.email}.`);
3960
4232
  }
3961
- async function runProjectsList(parsed) {
4233
+ async function runDelegationStart(parsed) {
3962
4234
  const { client, session } = await resolveAuthenticatedClient(parsed);
3963
- 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);
3964
4250
  if (projects.length === 0) {
3965
4251
  console.log("No projects available.");
3966
4252
  return;
@@ -3975,19 +4261,25 @@ async function runInit(parsed) {
3975
4261
  const projectId = requireOption(parsed, "project");
3976
4262
  const workspacePath = requireOption(parsed, "workspace");
3977
4263
  const ticketLimit = parseTicketLimit(getOption(parsed, "ticket-limit") ?? "all");
3978
- const { client, session } = await resolveAuthenticatedClient(parsed);
4264
+ const { client, session, delegation } = await resolveProjectCommandContext(parsed);
4265
+ printDelegationContext(delegation);
3979
4266
  const summary = await runBootstrap(client, session.accessToken, {
3980
4267
  projectId,
3981
4268
  workspacePath,
3982
4269
  ticketIds: getOptions(parsed, "ticket"),
3983
- ticketLimit
4270
+ ticketLimit,
4271
+ delegationId: delegation?.id ?? null
3984
4272
  });
3985
4273
  printBootstrapSummary(summary);
3986
4274
  }
3987
4275
  async function runOneShotCommand(parsed) {
3988
- const { client, session } = await resolveAuthenticatedClient(parsed);
3989
- const options = parseOneShotOptions(parsed);
4276
+ const { client, session, delegation } = await resolveProjectCommandContext(parsed);
4277
+ const options = {
4278
+ ...parseOneShotOptions(parsed),
4279
+ delegation
4280
+ };
3990
4281
  const policy = describeAgentPolicy(options);
4282
+ printDelegationContext(delegation);
3991
4283
  console.log(`Agent provider: ${options.agentProvider}`);
3992
4284
  console.log(`Agent sandbox: ${policy.sandboxMode ?? "custom"}`);
3993
4285
  console.log(`Agent approval posture: ${policy.approvalPosture ?? "custom"}`);
@@ -3997,7 +4289,7 @@ async function runOneShotCommand(parsed) {
3997
4289
  };
3998
4290
  process.once("SIGINT", onSigint);
3999
4291
  try {
4000
- const manifest = await runOneShot(client, session.accessToken, options);
4292
+ const manifest = await runOneShot(client, createAccessTokenProvider(client, session), options);
4001
4293
  printRunSummary(manifest);
4002
4294
  } finally {
4003
4295
  process.off("SIGINT", onSigint);
@@ -4007,10 +4299,13 @@ async function runResume(parsed) {
4007
4299
  const runId = requireOption(parsed, "run");
4008
4300
  const workspacePath = requireOption(parsed, "workspace");
4009
4301
  const manifest = await readRunManifest(workspacePath, runId);
4010
- const { client, session } = await resolveAuthenticatedClient(parsed);
4011
- 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), {
4012
4305
  ...manifest.options,
4306
+ delegation,
4013
4307
  workspacePath,
4308
+ allowDirty: hasFlag(parsed, "allow-dirty") ? true : manifest.options.allowDirty,
4014
4309
  resumeRunId: runId
4015
4310
  });
4016
4311
  printRunSummary(resumed);
@@ -4018,7 +4313,8 @@ async function runResume(parsed) {
4018
4313
  async function runStatus(parsed) {
4019
4314
  const runId = requireOption(parsed, "run");
4020
4315
  const workspacePath = requireOption(parsed, "workspace");
4021
- printRunSummary(await readRunManifest(workspacePath, runId));
4316
+ const manifest = await readRunManifest(workspacePath, runId);
4317
+ printRunSummary(manifest);
4022
4318
  }
4023
4319
  async function runDoctor(parsed) {
4024
4320
  console.log(`Node: ${process.version}`);
@@ -4026,8 +4322,9 @@ async function runDoctor(parsed) {
4026
4322
  console.log(`Session: ${session ? `signed in as ${session.user.email}` : "missing"}`);
4027
4323
  if (session) {
4028
4324
  try {
4029
- const client = new SwarmCraftClient(getOption(parsed, "api-base-url") ?? session.apiBaseUrl);
4030
- 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);
4031
4328
  console.log("API: reachable");
4032
4329
  } catch (error) {
4033
4330
  console.log(`API: ${error instanceof Error ? error.message : String(error)}`);
@@ -4058,13 +4355,14 @@ async function runSupportBundle(parsed) {
4058
4355
  const manifest = JSON.parse(await (0, import_promises4.readFile)(manifestPath, "utf8"));
4059
4356
  const bundleDir = import_node_path5.default.join(workspacePath, ".swarmcraft", "runs", runId, "support-bundle");
4060
4357
  await (0, import_promises4.mkdir)(bundleDir, { recursive: true });
4061
- 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)}
4062
4359
  `, "utf8");
4063
4360
  await (0, import_promises4.writeFile)(import_node_path5.default.join(bundleDir, "summary.json"), `${JSON.stringify({
4064
4361
  runId,
4065
4362
  manifestPath,
4066
4363
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4067
- node: process.version
4364
+ node: process.version,
4365
+ delegation: summarizeDelegationForBundle(manifest)
4068
4366
  }, null, 2)}
4069
4367
  `, "utf8");
4070
4368
  console.log(`Support bundle written: ${bundleDir}`);
@@ -4076,6 +4374,10 @@ async function resolveAuthenticatedClient(parsed) {
4076
4374
  }
4077
4375
  const apiBaseUrl = getOption(parsed, "api-base-url") ?? process.env.SWARMCRAFT_API_BASE_URL ?? session.apiBaseUrl;
4078
4376
  const client = new SwarmCraftClient(apiBaseUrl);
4377
+ session = {
4378
+ ...session,
4379
+ apiBaseUrl
4380
+ };
4079
4381
  if (sessionExpiresSoon(session)) {
4080
4382
  if (!session.refreshToken) {
4081
4383
  throw new Error("Stored SwarmCraft session has expired. Run `swarmcraft auth login` again.");
@@ -4089,6 +4391,79 @@ async function resolveAuthenticatedClient(parsed) {
4089
4391
  }
4090
4392
  return { client, session };
4091
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
+ }
4092
4467
  function printBootstrapSummary(summary) {
4093
4468
  console.log(`Bootstrapped ${summary.projectTitle}`);
4094
4469
  console.log(`Workspace: ${summary.workspacePath}`);
@@ -4110,10 +4485,26 @@ function printBootstrapSummary(summary) {
4110
4485
  console.log(` ${nextCommand}`);
4111
4486
  }
4112
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
+ }
4113
4503
  function printRunSummary(manifest) {
4114
4504
  console.log(`Run: ${manifest.runId}`);
4115
4505
  console.log(`Status: ${manifest.status}`);
4116
4506
  console.log(`Workspace: ${manifest.workspacePath}`);
4507
+ printDelegationContext(manifest.options?.delegation ?? null);
4117
4508
  console.log(`Tickets: ${manifest.tickets.length}`);
4118
4509
  for (const ticket of manifest.tickets) {
4119
4510
  console.log(` - ${ticket.outcome}: ${ticket.title} (${ticket.stages.length} stage(s))`);
@@ -4124,14 +4515,16 @@ function printRunSummary(manifest) {
4124
4515
  }
4125
4516
  function parseOneShotOptions(parsed) {
4126
4517
  const agentProvider = (0, import_packet_workflow3.parseExternalAgentProvider)(requireOption(parsed, "agent-provider"));
4127
- 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");
4128
4519
  const commitPolicy = parseChoice(getOption(parsed, "commit-policy") ?? "none", ["none", "done"], "--commit-policy");
4520
+ const explicitReviewing = getOption(parsed, "reviewing");
4129
4521
  return {
4130
4522
  projectId: requireOption(parsed, "project"),
4131
4523
  workspacePath: requireOption(parsed, "workspace"),
4132
4524
  agentProvider,
4133
4525
  agentCommand: getOption(parsed, "agent-command") ?? process.env.SWARMCRAFT_AGENT_COMMAND ?? null,
4134
- checking: parseOnOff(getOption(parsed, "checking") ?? "on", "--checking"),
4526
+ checking: parseOnOff(getOption(parsed, "checking") ?? "off", "--checking"),
4527
+ reviewing: explicitReviewing ? parseOnOff(explicitReviewing, "--reviewing") : donePolicy === "stop",
4135
4528
  maxCheckingAttempts: parsePositiveInteger(getOption(parsed, "max-checking-attempts") ?? "2", "--max-checking-attempts"),
4136
4529
  donePolicy,
4137
4530
  commitPolicy,
@@ -4143,6 +4536,7 @@ function parseOneShotOptions(parsed) {
4143
4536
  agentApiKeyEnvName: getOption(parsed, "agent-api-key-env-name") ?? null,
4144
4537
  ticketIds: getOptions(parsed, "ticket"),
4145
4538
  ticketLimit: parseTicketLimit(getOption(parsed, "ticket-limit") ?? "all"),
4539
+ delegation: null,
4146
4540
  runId: parseRunId(getOption(parsed, "run-id"))
4147
4541
  };
4148
4542
  }
@@ -4178,7 +4572,7 @@ function findCommand(positionals) {
4178
4572
  if (positionals.length === 0) {
4179
4573
  return void 0;
4180
4574
  }
4181
- 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];
4182
4576
  return COMMANDS.find((command) => command.name === commandKey);
4183
4577
  }
4184
4578
  function printGeneralHelp() {
@@ -4243,6 +4637,13 @@ function parsePositiveInteger(rawValue, flagName) {
4243
4637
  }
4244
4638
  return parsed;
4245
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
+ }
4246
4647
  function parseNonNegativeNumber(rawValue, flagName) {
4247
4648
  const parsed = Number.parseFloat(rawValue);
4248
4649
  if (!Number.isFinite(parsed) || parsed < 0) {
@@ -4274,20 +4675,6 @@ function parseRunId(rawValue) {
4274
4675
  }
4275
4676
  return rawValue;
4276
4677
  }
4277
- function redact(value) {
4278
- if (Array.isArray(value)) {
4279
- return value.map(redact);
4280
- }
4281
- if (!value || typeof value !== "object") {
4282
- return value;
4283
- }
4284
- return Object.fromEntries(
4285
- Object.entries(value).map(([key, entry]) => [
4286
- key,
4287
- /token|secret|password|api[-_]?key|credential/i.test(key) ? "<redacted>" : redact(entry)
4288
- ])
4289
- );
4290
- }
4291
4678
  async function promptLine(prompt) {
4292
4679
  const readline = (0, import_promises5.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
4293
4680
  try {
@@ -4299,14 +4686,26 @@ async function promptLine(prompt) {
4299
4686
  async function promptSecret(prompt) {
4300
4687
  return promptLine(prompt);
4301
4688
  }
4302
- main(process.argv.slice(2)).then((exitCode) => {
4303
- process.exitCode = exitCode;
4304
- }).catch((error) => {
4305
- if (error instanceof SwarmCraftApiError) {
4306
- const request = error.requestId ? ` requestId=${error.requestId}` : "";
4307
- console.error(`${error.code}: ${error.message}${request}`);
4308
- } else {
4309
- console.error(error instanceof Error ? error.message : String(error));
4310
- }
4311
- 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
4312
4711
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@swarmcraftai/cli",
3
- "version": "0.1.0",
4
- "description": "Customer-facing SwarmCraft command line tool.",
3
+ "version": "0.1.5",
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",
7
7
  "homepage": "https://swarmcraft.ai",