flightdesk 0.6.1 → 0.6.2

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 (4) hide show
  1. package/README.md +7 -4
  2. package/main.js +250 -183
  3. package/main.js.map +4 -4
  4. package/package.json +1 -1
package/main.js CHANGED
@@ -3284,12 +3284,35 @@ var FlightDeskAPI = class _FlightDeskAPI {
3284
3284
  prNumber
3285
3285
  sessionViewUrl
3286
3286
  sessionTeleportId
3287
+ sourceSystem
3288
+ sourceRef
3289
+ sourceUrl
3287
3290
  }
3288
3291
  }
3289
3292
  `;
3290
3293
  const result = await this.graphql(query, { taskId, input });
3291
3294
  return result.userUpdateTask;
3292
3295
  }
3296
+ /** "Continue in FlightDesk": attach a locally-built PR and hand the work to the pipeline. */
3297
+ async handoffPullRequest(input) {
3298
+ const query = `
3299
+ mutation UserHandoffPullRequest($input: UserHandoffPullRequestInput!) {
3300
+ userHandoffPullRequest(input: $input) {
3301
+ taskId
3302
+ created
3303
+ orchestrated
3304
+ phase
3305
+ waitingOn
3306
+ agentUserId
3307
+ prNumber
3308
+ prUrl
3309
+ message
3310
+ }
3311
+ }
3312
+ `;
3313
+ const result = await this.graphql(query, { input });
3314
+ return result.userHandoffPullRequest;
3315
+ }
3293
3316
  async registerSession(taskId, input) {
3294
3317
  const query = `
3295
3318
  mutation RegisterSession($taskId: String!, $input: RegisterSessionInput!) {
@@ -3606,7 +3629,7 @@ async function execute(query, variables) {
3606
3629
  }
3607
3630
  function registerQuestionCommands(program3) {
3608
3631
  const questions = program3.command("questions").description("Ask roles, poll questions, and consume answers");
3609
- questions.command("ask <taskId>").option("--to-role <role>", "requester, product_owner, tech_owner, client_contact, triager").option("--to-user <id>", "Address a specific human member directly").requiredOption("--key <key>", "Stable retry key").requiredOption("--body <text>", "Question").requiredOption("--context <text>", "Short answer context").option("--default <text>", "Proposed default; required for agents").option("--options <values...>", "Suggested answer choices").option("--due-at <iso>", "Optional due timestamp").option("--nonblocking", "Allow work to continue").action(
3632
+ questions.command("ask <taskId>").option("--to-role <role>", "requester, product_owner, tech_owner, client_contact, triager").option("--to-user <id>", "Address a specific human member directly").requiredOption("--key <key>", "Stable retry key").requiredOption("--body <text>", "Question").requiredOption("--context <text>", "Short answer context").option("--default <text>", "Proposed default; required for ordinary agent questions").option("--requires-explicit-answer", "Permission decision: blocking, with no default").option("--options <values...>", "Suggested answer choices").option("--due-at <iso>", "Optional due timestamp").option("--nonblocking", "Allow work to continue").action(
3610
3633
  (taskId, options) => execute("mutation($input:JSON!){userAskQuestion(input:$input)}", {
3611
3634
  input: {
3612
3635
  taskId,
@@ -3616,6 +3639,7 @@ function registerQuestionCommands(program3) {
3616
3639
  body: options.body,
3617
3640
  context: options.context,
3618
3641
  proposedDefault: options.default,
3642
+ requiresExplicitAnswer: options.requiresExplicitAnswer,
3619
3643
  blocking: !options.nonblocking,
3620
3644
  options: options.options,
3621
3645
  dueAt: options.dueAt
@@ -3648,18 +3672,29 @@ async function execute2(query, variables, attempts = 3) {
3648
3672
  }
3649
3673
  function registerPlanCommands(program3) {
3650
3674
  const plan = program3.command("plan").description("Task plans held in FlightDesk");
3651
- plan.command("submit <taskId>").description("Submit a plan revision from a markdown file (or --body)").option("-f, --file <path>", "Markdown file holding the plan").option("--body <markdown>", "Plan body inline (prefer --file)").option("--summary <text>", "One-paragraph summary").action(async (taskId, options) => {
3652
- const body = options.file ? (0, import_node_fs.readFileSync)(options.file, "utf8") : options.body;
3653
- if (!body?.trim()) {
3654
- console.error("Provide --file <path> or --body <markdown>");
3655
- process.exit(1);
3675
+ plan.command("submit <taskId>").description("Submit a plan revision from a markdown file (or --body)").option("-f, --file <path>", "Markdown file holding the plan").option("--body <markdown>", "Plan body inline (prefer --file)").option("--summary <text>", "One-paragraph summary").option("--test-plan <path>", "Markdown file: the steps a human runs on the preview").option("--release-note <text>", "One or two sentences for staff, confirmed at closeout").action(
3676
+ async (taskId, options) => {
3677
+ const body = options.file ? (0, import_node_fs.readFileSync)(options.file, "utf8") : options.body;
3678
+ if (!body?.trim()) {
3679
+ console.error("Provide --file <path> or --body <markdown>");
3680
+ process.exit(1);
3681
+ }
3682
+ const testPlan = options.testPlan ? (0, import_node_fs.readFileSync)(options.testPlan, "utf8") : void 0;
3683
+ await execute2(
3684
+ "mutation($input:JSON!){userSubmitPlan(input:$input)}",
3685
+ {
3686
+ input: {
3687
+ taskId,
3688
+ body,
3689
+ summary: options.summary,
3690
+ testPlan,
3691
+ releaseNote: options.releaseNote
3692
+ }
3693
+ },
3694
+ 1
3695
+ );
3656
3696
  }
3657
- await execute2(
3658
- "mutation($input:JSON!){userSubmitPlan(input:$input)}",
3659
- { input: { taskId, body, summary: options.summary } },
3660
- 1
3661
- );
3662
- });
3697
+ );
3663
3698
  plan.command("show <taskId>").description("Print the plan revisions and approval state").action(
3664
3699
  (taskId) => execute2("query($taskId:String!){userTaskPlan(taskId:$taskId)}", { taskId })
3665
3700
  );
@@ -3764,6 +3799,11 @@ function registerDispatchCommands(program3) {
3764
3799
  });
3765
3800
  });
3766
3801
  const turn = program3.command("turn").description("Report the outcome of an agent turn");
3802
+ turn.command("progress <line>").description("Say what this turn is doing now \u2014 one line, no outcome, safe to repeat").requiredOption("--dispatch <id>", "Dispatch id ($FLIGHTDESK_DISPATCH_ID)").action(
3803
+ (line, options) => execute3("mutation($input: JSON!) { userReportProgress(input: $input) }", {
3804
+ input: { dispatchId: options.dispatch, message: line }
3805
+ })
3806
+ );
3767
3807
  turn.command("end").requiredOption("--dispatch <id>", "Dispatch id ($FLIGHTDESK_DISPATCH_ID)").requiredOption("--outcome <outcome>", "done | waiting | failed | blocked").option("--waiting-on <what>", "session | ci | review | qa | merge | deploy | dependency").option("--ref <url>", "PR URL or other reference").option("--summary <text>", "One-paragraph summary").action(
3768
3808
  (options) => execute3("mutation($input: JSON!) { userEndTurn(input: $input) }", {
3769
3809
  input: {
@@ -3945,10 +3985,7 @@ async function registerCommand(taskId, options) {
3945
3985
  const { config, org: org2 } = requireActiveOrg();
3946
3986
  const api = FlightDeskAPI.fromConfig(config, org2);
3947
3987
  try {
3948
- let projectId = options.project;
3949
- if (!projectId) {
3950
- projectId = await autoDetectProject(api, org2);
3951
- }
3988
+ const target = taskId ? { taskId } : { projectId: options.project || await autoDetectProject(api, org2) };
3952
3989
  let viewUrl = options.viewUrl;
3953
3990
  let teleportId = options.teleportId;
3954
3991
  if (options.prompt) {
@@ -3964,15 +4001,17 @@ async function registerCommand(taskId, options) {
3964
4001
  viewUrl = viewUrl || parsed.viewUrl;
3965
4002
  teleportId = teleportId || parsed.teleportId;
3966
4003
  }
3967
- let actualTaskId = taskId;
3968
- if (!actualTaskId) {
4004
+ let actualTaskId;
4005
+ if ("taskId" in target) {
4006
+ actualTaskId = target.taskId;
4007
+ } else {
3969
4008
  if (!options.title) {
3970
4009
  console.error('Task title required when creating a new task. Use --title "..."');
3971
4010
  process.exit(1);
3972
4011
  }
3973
4012
  console.log(`Creating task: ${options.title}`);
3974
4013
  const task2 = await api.createTask({
3975
- projectId,
4014
+ projectId: target.projectId,
3976
4015
  title: options.title,
3977
4016
  // Launch-first orchestrators do not know the FlightDesk UUID when the
3978
4017
  // cloud prompt is sent. Persisting that same prompt on the task lets
@@ -3993,8 +4032,7 @@ async function registerCommand(taskId, options) {
3993
4032
  } else {
3994
4033
  await api.updateTask(actualTaskId, {
3995
4034
  sessionViewUrl: viewUrl,
3996
- sessionTeleportId: teleportId,
3997
- status: "DISPATCHED"
4035
+ sessionTeleportId: teleportId
3998
4036
  });
3999
4037
  }
4000
4038
  console.log("\u2705 Session registered");
@@ -4056,30 +4094,25 @@ Output: ${output.slice(0, 500)}`));
4056
4094
  }
4057
4095
  function readStdin() {
4058
4096
  return new Promise((resolve) => {
4097
+ const input = process.stdin;
4059
4098
  let data = "";
4060
- let resolved = false;
4061
- process.stdin.setEncoding("utf8");
4062
- process.stdin.on("data", (chunk) => {
4099
+ const append = (chunk) => {
4063
4100
  data += chunk;
4064
- });
4065
- process.stdin.on("end", () => {
4066
- if (!resolved) {
4067
- resolved = true;
4068
- resolve(data);
4069
- }
4070
- });
4071
- process.stdin.on("close", () => {
4072
- if (!resolved) {
4073
- resolved = true;
4074
- resolve(data);
4075
- }
4076
- });
4077
- setTimeout(() => {
4078
- if (!resolved) {
4079
- resolved = true;
4080
- resolve(data);
4081
- }
4082
- }, 3e4);
4101
+ };
4102
+ const finish = () => {
4103
+ clearTimeout(timeout);
4104
+ input.off("data", append);
4105
+ input.off("end", finish);
4106
+ input.off("close", finish);
4107
+ input.pause();
4108
+ resolve(data);
4109
+ };
4110
+ const timeout = setTimeout(finish, 3e4);
4111
+ input.setEncoding("utf8");
4112
+ input.on("data", append);
4113
+ input.once("end", finish);
4114
+ input.once("close", finish);
4115
+ if (input.readableEnded || input.destroyed) finish();
4083
4116
  });
4084
4117
  }
4085
4118
  function parseClaudeOutput(output) {
@@ -4148,6 +4181,32 @@ async function autoDetectProject(api, activeOrg) {
4148
4181
  return project2.id;
4149
4182
  }
4150
4183
 
4184
+ // apps/cli/src/lib/task-status.ts
4185
+ var TASK_STATUSES = [
4186
+ ["PENDING", "\u2B1C"],
4187
+ ["DISPATCHED", "\u{1F680}"],
4188
+ ["IN_PROGRESS", "\u{1F504}"],
4189
+ ["BRANCH_CREATED", "\u{1F33F}"],
4190
+ ["PR_OPEN", "\u{1F4DD}"],
4191
+ ["PREVIEW_STARTING", "\u23F3"],
4192
+ ["PREVIEW_READY", "\u{1F310}"],
4193
+ ["REVIEW_RUNNING", "\u{1F50D}"],
4194
+ ["REVIEW_DONE", "\u2705"],
4195
+ ["QA_READY", "\u{1F9EA}"],
4196
+ ["QA_CHANGES_REQUESTED", "\u{1F527}"],
4197
+ ["QA_APPROVED", "\u{1F44D}"],
4198
+ ["MERGED", "\u{1F389}"],
4199
+ ["ARCHIVED", "\u{1F4E6}"]
4200
+ ];
4201
+ var TASK_STATUS_ORDER = TASK_STATUSES.map(([status]) => status);
4202
+ var EMOJI_BY_STATUS = new Map(TASK_STATUSES);
4203
+ function taskStatusLabel(status) {
4204
+ return status ?? "No coding run";
4205
+ }
4206
+ function taskStatusEmoji(status) {
4207
+ return status === null ? "\u2B1C" : EMOJI_BY_STATUS.get(status) ?? "\u2753";
4208
+ }
4209
+
4151
4210
  // apps/cli/src/commands/status.ts
4152
4211
  async function statusCommand(options) {
4153
4212
  const { config, org: org2 } = requireActiveOrg();
@@ -4158,33 +4217,18 @@ async function statusCommand(options) {
4158
4217
  console.log("No tasks found.");
4159
4218
  return;
4160
4219
  }
4161
- const byStatus = {};
4220
+ const byStatus = /* @__PURE__ */ new Map();
4162
4221
  for (const task2 of tasks) {
4163
- if (!byStatus[task2.status]) {
4164
- byStatus[task2.status] = [];
4165
- }
4166
- byStatus[task2.status].push(task2);
4222
+ const group = byStatus.get(task2.status) ?? [];
4223
+ group.push(task2);
4224
+ byStatus.set(task2.status, group);
4167
4225
  }
4168
- const statusOrder = [
4169
- "PENDING",
4170
- "DISPATCHED",
4171
- "IN_PROGRESS",
4172
- "BRANCH_CREATED",
4173
- "PR_OPEN",
4174
- "PREVIEW_STARTING",
4175
- "PREVIEW_READY",
4176
- "REVIEW_RUNNING",
4177
- "REVIEW_DONE",
4178
- "QA_READY",
4179
- "QA_APPROVED",
4180
- "MERGED",
4181
- "ARCHIVED"
4182
- ];
4226
+ const statusOrder = /* @__PURE__ */ new Set([...TASK_STATUS_ORDER, null, ...byStatus.keys()]);
4183
4227
  console.log("\n\u{1F4CB} FlightDesk Tasks\n");
4184
4228
  for (const status of statusOrder) {
4185
- const statusTasks = byStatus[status];
4229
+ const statusTasks = byStatus.get(status);
4186
4230
  if (!statusTasks || statusTasks.length === 0) continue;
4187
- console.log(`${getStatusEmoji(status)} ${status} (${statusTasks.length})`);
4231
+ console.log(`${taskStatusEmoji(status)} ${taskStatusLabel(status)} (${statusTasks.length})`);
4188
4232
  for (const task2 of statusTasks) {
4189
4233
  console.log(` ${task2.id.slice(0, 8)} - ${task2.title}`);
4190
4234
  if (task2.branchName) {
@@ -4201,40 +4245,21 @@ async function statusCommand(options) {
4201
4245
  process.exit(1);
4202
4246
  }
4203
4247
  }
4204
- function getStatusEmoji(status) {
4205
- switch (status) {
4206
- case "PENDING":
4207
- return "\u2B1C";
4208
- case "DISPATCHED":
4209
- return "\u{1F680}";
4210
- case "IN_PROGRESS":
4211
- return "\u{1F504}";
4212
- case "BRANCH_CREATED":
4213
- return "\u{1F33F}";
4214
- case "PR_OPEN":
4215
- return "\u{1F4DD}";
4216
- case "PREVIEW_STARTING":
4217
- return "\u23F3";
4218
- case "PREVIEW_READY":
4219
- return "\u{1F310}";
4220
- case "REVIEW_RUNNING":
4221
- return "\u{1F50D}";
4222
- case "REVIEW_DONE":
4223
- return "\u2705";
4224
- case "QA_READY":
4225
- return "\u{1F9EA}";
4226
- case "QA_APPROVED":
4227
- return "\u{1F44D}";
4228
- case "MERGED":
4229
- return "\u{1F389}";
4230
- case "ARCHIVED":
4231
- return "\u{1F4E6}";
4232
- default:
4233
- return "\u2753";
4234
- }
4235
- }
4236
4248
 
4237
4249
  // apps/cli/src/commands/task.ts
4250
+ function sourceIdentityEdit(options) {
4251
+ const set = {
4252
+ ...options.sourceSystem === void 0 ? {} : { sourceSystem: options.sourceSystem },
4253
+ ...options.sourceRef === void 0 ? {} : { sourceRef: options.sourceRef },
4254
+ ...options.sourceUrl === void 0 ? {} : { sourceUrl: options.sourceUrl }
4255
+ };
4256
+ if (!options.clearSource) return set;
4257
+ if (Object.keys(set).length > 0)
4258
+ throw new Error(
4259
+ "--clear-source cannot be combined with --source-system/--source-ref/--source-url"
4260
+ );
4261
+ return { sourceSystem: null, sourceRef: null, sourceUrl: null };
4262
+ }
4238
4263
  async function taskCommand(action, options) {
4239
4264
  const { config, org: org2 } = requireActiveOrg();
4240
4265
  const api = FlightDeskAPI.fromConfig(config, org2);
@@ -4270,13 +4295,15 @@ async function handleCreate(api, options) {
4270
4295
  subprojectId: options.subproject,
4271
4296
  sourceSystem: options.sourceSystem,
4272
4297
  sourceRef: options.sourceRef,
4273
- sourceUrl: options.sourceUrl
4298
+ sourceUrl: options.sourceUrl,
4299
+ followUpOf: options.followUpOf,
4300
+ followUpDispatchId: options.followUpDispatch
4274
4301
  });
4275
4302
  console.log(`
4276
4303
  \u2705 Task created`);
4277
4304
  console.log(` ID: ${task2.id}`);
4278
4305
  console.log(` Title: ${task2.title}`);
4279
- console.log(` Status: ${task2.status}`);
4306
+ console.log(` Status: ${taskStatusLabel(task2.status)}`);
4280
4307
  }
4281
4308
  async function handleList(api, options) {
4282
4309
  const tasks = await api.listTasks({
@@ -4289,10 +4316,10 @@ async function handleList(api, options) {
4289
4316
  }
4290
4317
  console.log("\nTasks:\n");
4291
4318
  for (const task2 of tasks) {
4292
- const statusEmoji = getStatusEmoji2(task2.status);
4319
+ const statusEmoji = taskStatusEmoji(task2.status);
4293
4320
  console.log(`${statusEmoji} ${task2.id.slice(0, 8)} - ${task2.title}`);
4294
4321
  if (task2.project?.name) console.log(` Project: ${task2.project.name}`);
4295
- console.log(` Status: ${task2.status}`);
4322
+ console.log(` Status: ${taskStatusLabel(task2.status)}`);
4296
4323
  if (task2.branchName) console.log(` Branch: ${task2.branchName}`);
4297
4324
  if (task2.prUrl) console.log(` PR: ${task2.prUrl}`);
4298
4325
  console.log("");
@@ -4308,11 +4335,10 @@ async function handleStatus(api, options) {
4308
4335
  \u{1F4CB} Task: ${task2.title}
4309
4336
  `);
4310
4337
  console.log(`ID: ${task2.id}`);
4311
- console.log(`Status: ${getStatusEmoji2(task2.status)} ${task2.status}`);
4338
+ console.log(`Status: ${taskStatusEmoji(task2.status)} ${taskStatusLabel(task2.status)}`);
4312
4339
  if (task2.project) {
4313
- console.log(
4314
- `Project: ${task2.project.name}${task2.project.githubRepo ? ` (${task2.project.githubRepo})` : ""}`
4315
- );
4340
+ const repo = task2.project.githubRepo ? " (" + task2.project.githubRepo + ")" : "";
4341
+ console.log(`Project: ${task2.project.name}${repo}`);
4316
4342
  }
4317
4343
  if (task2.description) {
4318
4344
  console.log(`
@@ -4338,31 +4364,56 @@ function parseSessionId(value) {
4338
4364
  }
4339
4365
  return value;
4340
4366
  }
4341
- async function handleUpdate(api, options) {
4342
- const input = {};
4367
+ var PR_NUMBER_IN_URL = /\/pull\/(\d+)/;
4368
+ var SOURCE_FIELDS = ["sourceSystem", "sourceRef", "sourceUrl"];
4369
+ function updateInput(options) {
4370
+ const input = { ...sourceIdentityEdit(options) };
4371
+ if (options.releaseNote !== void 0) input.releaseNote = options.releaseNote;
4343
4372
  if (options.status) input.status = options.status;
4344
4373
  if (options.branch) input.branchName = options.branch;
4345
4374
  if (options.prUrl) {
4346
4375
  input.prUrl = options.prUrl;
4347
- const prNumberMatch = options.prUrl.match(/\/pull\/(\d+)/);
4348
- if (prNumberMatch) {
4349
- input.prNumber = Number.parseInt(prNumberMatch[1], 10);
4350
- }
4376
+ const prNumberMatch = PR_NUMBER_IN_URL.exec(options.prUrl);
4377
+ if (prNumberMatch) input.prNumber = Number.parseInt(prNumberMatch[1], 10);
4351
4378
  }
4352
- if (options.prNumber) {
4353
- input.prNumber = Number.parseInt(options.prNumber, 10);
4379
+ if (options.prNumber) input.prNumber = Number.parseInt(options.prNumber, 10);
4380
+ return input;
4381
+ }
4382
+ async function autoTransition(api, options, input) {
4383
+ if (!options.branch || options.status) return;
4384
+ const current = await api.getTask(options.taskId);
4385
+ if (current?.status !== "DISPATCHED") return;
4386
+ input.status = "IN_PROGRESS";
4387
+ console.log(` Auto-transitioning DISPATCHED \u2192 IN_PROGRESS`);
4388
+ }
4389
+ function printUpdated(task2, input) {
4390
+ console.log(`
4391
+ \u2705 Task updated`);
4392
+ console.log(` Status: ${taskStatusLabel(task2.status)}`);
4393
+ if (task2.branchName) console.log(` Branch: ${task2.branchName}`);
4394
+ if (task2.prUrl) console.log(` PR: ${task2.prUrl}`);
4395
+ if (task2.prNumber) console.log(` PR #: ${task2.prNumber}`);
4396
+ if (task2.sessionViewUrl) console.log(` Session: ${task2.sessionViewUrl}`);
4397
+ if (SOURCE_FIELDS.some((field) => field in input)) console.log(` Source: ${describeSource(task2)}`);
4398
+ }
4399
+ async function syncAfterPrUrl(api, taskId) {
4400
+ console.log(`
4401
+ \u{1F504} Syncing PR review state...`);
4402
+ try {
4403
+ printSyncResult(await api.syncTaskChecks(taskId));
4404
+ } catch (err) {
4405
+ console.log(` \u26A0\uFE0F Sync failed (checks will update when webhooks arrive): ${err}`);
4354
4406
  }
4407
+ }
4408
+ async function handleUpdate(api, options) {
4409
+ const input = updateInput(options);
4355
4410
  if (Object.keys(input).length === 0 && !options.session) {
4356
- console.error("No updates specified. Use --status, --branch, --pr-url, or --session");
4411
+ console.error(
4412
+ "No updates specified. Use --status, --branch, --pr-url, --session, --release-note, --source-system/--source-ref/--source-url or --clear-source"
4413
+ );
4357
4414
  process.exit(1);
4358
4415
  }
4359
- if (options.branch && !options.status) {
4360
- const current = await api.getTask(options.taskId);
4361
- if (current?.status === "DISPATCHED") {
4362
- input.status = "IN_PROGRESS";
4363
- console.log(` Auto-transitioning DISPATCHED \u2192 IN_PROGRESS`);
4364
- }
4365
- }
4416
+ await autoTransition(api, options, input);
4366
4417
  console.log(`Updating task ${options.taskId}...`);
4367
4418
  let task2;
4368
4419
  if (options.session) {
@@ -4372,27 +4423,14 @@ async function handleUpdate(api, options) {
4372
4423
  teleportId: sessionId
4373
4424
  });
4374
4425
  }
4375
- if (Object.keys(input).length > 0) {
4376
- task2 = await api.updateTask(options.taskId, input);
4377
- }
4426
+ if (Object.keys(input).length > 0) task2 = await api.updateTask(options.taskId, input);
4378
4427
  if (!task2) return;
4379
- console.log(`
4380
- \u2705 Task updated`);
4381
- console.log(` Status: ${task2.status}`);
4382
- if (task2.branchName) console.log(` Branch: ${task2.branchName}`);
4383
- if (task2.prUrl) console.log(` PR: ${task2.prUrl}`);
4384
- if (task2.prNumber) console.log(` PR #: ${task2.prNumber}`);
4385
- if (task2.sessionViewUrl) console.log(` Session: ${task2.sessionViewUrl}`);
4386
- if (options.prUrl) {
4387
- console.log(`
4388
- \u{1F504} Syncing PR review state...`);
4389
- try {
4390
- const syncResult = await api.syncTaskChecks(options.taskId);
4391
- printSyncResult(syncResult);
4392
- } catch (err) {
4393
- console.log(` \u26A0\uFE0F Sync failed (checks will update when webhooks arrive): ${err}`);
4394
- }
4395
- }
4428
+ printUpdated(task2, input);
4429
+ if (options.prUrl) await syncAfterPrUrl(api, options.taskId);
4430
+ }
4431
+ function describeSource(task2) {
4432
+ const parts = [task2.sourceSystem, task2.sourceRef, task2.sourceUrl].filter(Boolean);
4433
+ return parts.length > 0 ? parts.join(" ") : "(cleared)";
4396
4434
  }
4397
4435
  async function handleSync(api, options) {
4398
4436
  console.log(`Syncing PR review state for task ${options.taskId}...`);
@@ -4425,36 +4463,54 @@ function getCheckIcon(status) {
4425
4463
  return "\u2753";
4426
4464
  }
4427
4465
  }
4428
- function getStatusEmoji2(status) {
4429
- switch (status) {
4430
- case "PENDING":
4431
- return "\u2B1C";
4432
- case "DISPATCHED":
4433
- return "\u{1F680}";
4434
- case "IN_PROGRESS":
4435
- return "\u{1F504}";
4436
- case "BRANCH_CREATED":
4437
- return "\u{1F33F}";
4438
- case "PR_OPEN":
4439
- return "\u{1F4DD}";
4440
- case "PREVIEW_STARTING":
4441
- return "\u23F3";
4442
- case "PREVIEW_READY":
4443
- return "\u{1F310}";
4444
- case "REVIEW_RUNNING":
4445
- return "\u{1F50D}";
4446
- case "REVIEW_DONE":
4447
- return "\u2705";
4448
- case "QA_READY":
4449
- return "\u{1F9EA}";
4450
- case "QA_APPROVED":
4451
- return "\u{1F44D}";
4452
- case "MERGED":
4453
- return "\u{1F389}";
4454
- case "ARCHIVED":
4455
- return "\u{1F4E6}";
4456
- default:
4457
- return "\u2753";
4466
+
4467
+ // apps/cli/src/commands/handoff.ts
4468
+ var PR_URL = /^https?:\/\/(?:www\.)?github\.com\/([^/\s]+\/[^/\s]+)\/pull\/([1-9]\d*)(?:[/?#]|$)/i;
4469
+ var PR_NUMBER = /^#?([1-9]\d*)$/;
4470
+ function parsePrArgument(value) {
4471
+ const trimmed = value.trim();
4472
+ const url = PR_URL.exec(trimmed);
4473
+ if (url) return { prUrl: trimmed, prNumber: Number(url[2]), repo: url[1] };
4474
+ const number = PR_NUMBER.exec(trimmed);
4475
+ if (number) return { prNumber: Number(number[1]) };
4476
+ throw new Error(`"${value}" is not a pull request: give its GitHub URL or its number`);
4477
+ }
4478
+ async function projectForRepo(api, repo) {
4479
+ const slug = repo ?? detectGitRepo()?.remote;
4480
+ if (!slug)
4481
+ throw new Error(
4482
+ "Name the task (--task) or project (--project), or run this inside the repository"
4483
+ );
4484
+ const matches = (await api.listProjects()).filter(
4485
+ (project2) => project2.githubRepo?.toLowerCase() === slug.toLowerCase()
4486
+ );
4487
+ if (matches.length === 1) return matches[0].id;
4488
+ if (!matches.length) throw new Error(`No FlightDesk project tracks ${slug}; pass --project`);
4489
+ throw new Error(`Several projects track ${slug}; pass --project`);
4490
+ }
4491
+ async function handoffCommand(options) {
4492
+ const { config, org: org2 } = requireActiveOrg();
4493
+ const api = FlightDeskAPI.fromConfig(config, org2);
4494
+ try {
4495
+ const pr = parsePrArgument(options.pr);
4496
+ const projectId = options.task ? void 0 : options.project ?? await projectForRepo(api, pr.repo);
4497
+ const result = await api.handoffPullRequest({
4498
+ taskId: options.task,
4499
+ projectId,
4500
+ prUrl: pr.prUrl,
4501
+ prNumber: pr.prNumber,
4502
+ title: options.title
4503
+ });
4504
+ const created = result.created ? " (new task)" : "";
4505
+ const waiting = result.waitingOn ? ` (waiting on ${result.waitingOn})` : "";
4506
+ console.log(`
4507
+ \u2705 PR #${result.prNumber} handed off${created}`);
4508
+ console.log(` Task: ${result.taskId}`);
4509
+ console.log(` Phase: ${result.phase}${waiting}`);
4510
+ console.log(` ${result.message}`);
4511
+ } catch (error) {
4512
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
4513
+ process.exit(1);
4458
4514
  }
4459
4515
  }
4460
4516
 
@@ -4462,7 +4518,7 @@ function getStatusEmoji2(status) {
4462
4518
  async function promptCommand(taskId, options) {
4463
4519
  const { config, org: org2 } = requireActiveOrg();
4464
4520
  const api = FlightDeskAPI.fromConfig(config, org2);
4465
- const validTypes = ["review", "test_plan", "summary", "handoff"];
4521
+ const validTypes = ["review", "test_plan"];
4466
4522
  if (!validTypes.includes(options.type)) {
4467
4523
  console.error(`Invalid prompt type: ${options.type}`);
4468
4524
  console.error(`Valid types: ${validTypes.join(", ")}`);
@@ -4818,11 +4874,12 @@ async function previewCommand(action, options) {
4818
4874
  process.exit(1);
4819
4875
  }
4820
4876
  }
4821
- function getStatusEmoji3(status) {
4877
+ function getStatusEmoji(status) {
4822
4878
  const statusEmojis = {
4823
4879
  STARTING: "\u{1F504}",
4824
4880
  READY: "\u2705",
4825
4881
  SUSPENDED: "\u{1F4A4}",
4882
+ STOPPED: "\u{1F4A4}",
4826
4883
  RESTARTING: "\u{1F504}",
4827
4884
  TEARDOWN: "\u{1F5D1}\uFE0F",
4828
4885
  ERROR: "\u274C"
@@ -4837,7 +4894,7 @@ async function handleStatus2(api, options) {
4837
4894
  console.log(" or the PR may be closed.");
4838
4895
  return;
4839
4896
  }
4840
- const emoji = getStatusEmoji3(instance.status);
4897
+ const emoji = getStatusEmoji(instance.status);
4841
4898
  console.log("\n\u{1F4E6} Preview Environment");
4842
4899
  console.log("\u2500".repeat(50));
4843
4900
  console.log(`${emoji} Status: ${instance.status}`);
@@ -4916,8 +4973,8 @@ async function handleMount(api, options) {
4916
4973
  console.error("No preview environment found for this task");
4917
4974
  process.exit(1);
4918
4975
  }
4919
- if (instance.status === "SUSPENDED") {
4920
- console.log("\u23F8\uFE0F Instance is suspended. Resuming...");
4976
+ if (instance.status === "SUSPENDED" || instance.status === "STOPPED") {
4977
+ console.log("\u23F8\uFE0F Instance is stopped. Starting it...");
4921
4978
  await api.resumeInstance(options.taskId);
4922
4979
  await new Promise((resolve) => setTimeout(resolve, 3e3));
4923
4980
  }
@@ -5049,7 +5106,7 @@ registerDispatchCommands(program2);
5049
5106
  registerQuestionCommands(program2);
5050
5107
  registerPlanCommands(program2);
5051
5108
  registerWhoamiCommand(program2);
5052
- program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.6.1").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
5109
+ program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.6.2").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
5053
5110
  program2.hook("preAction", () => {
5054
5111
  const opts = program2.opts();
5055
5112
  if (opts.api) {
@@ -5062,19 +5119,29 @@ program2.hook("preAction", () => {
5062
5119
  }
5063
5120
  });
5064
5121
  program2.command("init").description("Configure FlightDesk CLI with your API credentials").action(initCommand);
5065
- program2.command("register [task-id]").description(
5066
- "Register a Claude Code session with a FlightDesk task (auto-detects project from git repo)"
5067
- ).option("-p, --project <id>", "Project ID (auto-detected from git repo if not provided)").option("--subproject <id>", "Subproject ID (required if project has subprojects configured)").option("--view-url <url>", "Claude Code session view URL").option("--teleport-id <id>", "Claude Code teleport ID").option("--title <title>", "Task title (creates new task if task-id not provided)").option("--description <description>", "Task description").option("--prompt <prompt>", "Prompt to pass to claude --remote (avoids pipe detection issues)").option("--source-system <system>", "External source system (for example notion or linear)").option("--source-ref <ref>", "External source reference or page ID").option("--source-url <url>", "External source URL used for deterministic branch binding").action(registerCommand);
5122
+ program2.command("register [task-id]").description("Register a Claude Code session with an existing task, or create a new task").option("-p, --project <id>", "Project for a new task (auto-detected from git repo if omitted)").option("--subproject <id>", "Subproject ID (required if project has subprojects configured)").option("--view-url <url>", "Claude Code session view URL").option("--teleport-id <id>", "Claude Code teleport ID").option("--title <title>", "Task title (creates new task if task-id not provided)").option("--description <description>", "Task description").option("--prompt <prompt>", "Prompt to pass to claude --remote (avoids pipe detection issues)").option("--source-system <system>", "External source system (for example notion or linear)").option("--source-ref <ref>", "External source reference or page ID").option("--source-url <url>", "External source URL used for deterministic branch binding").action(registerCommand);
5068
5123
  var project = program2.command("project").description("Project management commands");
5069
5124
  project.command("list").description("List projects in the active organization").action(() => projectCommand("list", {}));
5070
5125
  var task = program2.command("task").description("Task management commands");
5071
- task.command("create").description("Create a new task").requiredOption("-p, --project <id>", "Project ID").requiredOption("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").option("--subproject <id>", "Subproject ID (required if project has subprojects configured)").option("--source-system <system>", "External source system (for example notion or linear)").option("--source-ref <ref>", "External source reference or page ID").option("--source-url <url>", "External source URL used for deterministic branch binding").action((options) => taskCommand("create", options));
5126
+ task.command("create").description("Create a new task").requiredOption("-p, --project <id>", "Project ID").requiredOption("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").option("--subproject <id>", "Subproject ID (required if project has subprojects configured)").option("--source-system <system>", "External source system (for example notion or linear)").option("--source-ref <ref>", "External source reference or page ID").option("--source-url <url>", "External source URL used for deterministic branch binding").option(
5127
+ "--follow-up-of <taskId>",
5128
+ "File this as a finding from another task: something real and pre-existing that is out of that task's scope. Lands in Backlog, unassigned, and never dispatches itself."
5129
+ ).option("--follow-up-dispatch <id>", "Your $FLIGHTDESK_DISPATCH_ID, when filing a follow-up").action((options) => taskCommand("create", options));
5072
5130
  task.command("list").description("List tasks").option("-p, --project <id>", "Filter by project ID").option("--status <status>", "Filter by status").action((options) => taskCommand("list", options));
5073
5131
  task.command("status <task-id>").description("Get task status").action((taskId) => taskCommand("status", { taskId }));
5074
- task.command("update <task-id>").description("Update task").option("-s, --status <status>", "New status").option("--branch <branch>", "Branch name").option("--pr-url <url>", "Pull request URL").option("--pr-number <number>", "Pull request number (use if --pr-url is not enough)").option("--session <session>", "Claude Code session (URL or session ID)").action((taskId, options) => taskCommand("update", { taskId, ...options }));
5132
+ task.command("update <task-id>").description("Update task").option("-s, --status <status>", "New status").option("--branch <branch>", "Branch name").option("--pr-url <url>", "Pull request URL").option("--release-note <text>", "Confirm or revise the staff-facing release note").option("--pr-number <number>", "Pull request number (use if --pr-url is not enough)").option("--session <session>", "Claude Code session (URL or session ID)").option("--source-system <system>", "Source system: linear, notion or qalatra (humans only)").option(
5133
+ "--source-ref <ref>",
5134
+ "Source issue key, e.g. PIR-256 (must use a team key the project allows)"
5135
+ ).option("--source-url <url>", "Source issue URL").option("--clear-source", "Clear a wrong source identity so PRs naming it stop binding here").action((taskId, options) => taskCommand("update", { taskId, ...options }));
5136
+ task.command("handoff").description(
5137
+ "Continue in FlightDesk: attach a PR you built locally and hand the work to the pipeline (CI, then QA; failures go to the agent as a fix turn)"
5138
+ ).requiredOption("--pr <url|number>", "The pull request: its GitHub URL or its number").option("--task <id>", "The task to hand off (omit to use or create the task for the PR)").option(
5139
+ "-p, --project <id>",
5140
+ "Project for the task (detected from the PR URL or repo if omitted)"
5141
+ ).option("--title <title>", "Title for a task the hand-off creates (defaults to the PR title)").action((options) => handoffCommand(options));
5075
5142
  task.command("sync <task-id>").description("Sync PR review state (Copilot feedback, Claude reviews, human reviews) from GitHub").action((taskId) => taskCommand("sync", { taskId }));
5076
5143
  program2.command("status").description("Show status of all active tasks").option("-p, --project <id>", "Filter by project").action(statusCommand);
5077
- program2.command("prompt <task-id>").description("Get a prompt for a task (ready to paste into Claude)").option("--type <type>", "Prompt type: review, test_plan, summary, handoff", "review").action(promptCommand);
5144
+ program2.command("prompt <task-id>").description("Get a prompt for a task (ready to paste into Claude)").option("--type <type>", "Prompt type: review, test_plan", "review").action(promptCommand);
5078
5145
  var org = program2.command("org").description("Organization management");
5079
5146
  org.command("list").description("List your organizations").action(orgListCommand);
5080
5147
  org.command("switch <name>").description("Switch active organization").action(orgSwitchCommand);