flightdesk 0.6.0 → 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 +287 -183
  3. package/main.js.map +4 -4
  4. package/package.json +1 -1
package/main.js CHANGED
@@ -3053,6 +3053,9 @@ function readFolderCredential() {
3053
3053
  return null;
3054
3054
  }
3055
3055
  var folderCredential;
3056
+ function credentialSource() {
3057
+ return folder()?.source ?? `home (${CONFIG_FILE})`;
3058
+ }
3056
3059
  function folder() {
3057
3060
  if (folderCredential === void 0) folderCredential = readFolderCredential();
3058
3061
  return folderCredential;
@@ -3281,12 +3284,35 @@ var FlightDeskAPI = class _FlightDeskAPI {
3281
3284
  prNumber
3282
3285
  sessionViewUrl
3283
3286
  sessionTeleportId
3287
+ sourceSystem
3288
+ sourceRef
3289
+ sourceUrl
3284
3290
  }
3285
3291
  }
3286
3292
  `;
3287
3293
  const result = await this.graphql(query, { taskId, input });
3288
3294
  return result.userUpdateTask;
3289
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
+ }
3290
3316
  async registerSession(taskId, input) {
3291
3317
  const query = `
3292
3318
  mutation RegisterSession($taskId: String!, $input: RegisterSessionInput!) {
@@ -3603,7 +3629,7 @@ async function execute(query, variables) {
3603
3629
  }
3604
3630
  function registerQuestionCommands(program3) {
3605
3631
  const questions = program3.command("questions").description("Ask roles, poll questions, and consume answers");
3606
- 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(
3607
3633
  (taskId, options) => execute("mutation($input:JSON!){userAskQuestion(input:$input)}", {
3608
3634
  input: {
3609
3635
  taskId,
@@ -3613,6 +3639,7 @@ function registerQuestionCommands(program3) {
3613
3639
  body: options.body,
3614
3640
  context: options.context,
3615
3641
  proposedDefault: options.default,
3642
+ requiresExplicitAnswer: options.requiresExplicitAnswer,
3616
3643
  blocking: !options.nonblocking,
3617
3644
  options: options.options,
3618
3645
  dueAt: options.dueAt
@@ -3645,18 +3672,29 @@ async function execute2(query, variables, attempts = 3) {
3645
3672
  }
3646
3673
  function registerPlanCommands(program3) {
3647
3674
  const plan = program3.command("plan").description("Task plans held in FlightDesk");
3648
- 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) => {
3649
- const body = options.file ? (0, import_node_fs.readFileSync)(options.file, "utf8") : options.body;
3650
- if (!body?.trim()) {
3651
- console.error("Provide --file <path> or --body <markdown>");
3652
- 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
+ );
3653
3696
  }
3654
- await execute2(
3655
- "mutation($input:JSON!){userSubmitPlan(input:$input)}",
3656
- { input: { taskId, body, summary: options.summary } },
3657
- 1
3658
- );
3659
- });
3697
+ );
3660
3698
  plan.command("show <taskId>").description("Print the plan revisions and approval state").action(
3661
3699
  (taskId) => execute2("query($taskId:String!){userTaskPlan(taskId:$taskId)}", { taskId })
3662
3700
  );
@@ -3670,6 +3708,39 @@ function registerPlanCommands(program3) {
3670
3708
  );
3671
3709
  }
3672
3710
 
3711
+ // apps/cli/src/commands/whoami.ts
3712
+ function registerWhoamiCommand(program3) {
3713
+ program3.command("whoami").description("Show who the resolved API key authenticates as, and where the key came from").option("--json", "Machine-readable output").action(async (options) => {
3714
+ const config = loadConfig();
3715
+ if (!config.apiKey) {
3716
+ console.error("No API key configured. Run: flightdesk init");
3717
+ process.exit(1);
3718
+ }
3719
+ const apiUrl = getApiUrl();
3720
+ const source = credentialSource();
3721
+ try {
3722
+ const me = await fetchUserInfo(config.apiKey, apiUrl);
3723
+ const email = me.emails?.find((e) => e.primary)?.email ?? me.emails?.[0]?.email ?? null;
3724
+ const organizations = (me.organizations ?? []).map((o) => o.organization);
3725
+ if (options.json) {
3726
+ console.log(JSON.stringify({ id: me.id, email, organizations, apiUrl, source }, null, 2));
3727
+ return;
3728
+ }
3729
+ console.log(`${email ?? me.id}${email ? ` (${me.id})` : ""}`);
3730
+ console.log(`API: ${apiUrl}`);
3731
+ console.log(`Key: ${source}`);
3732
+ console.log(
3733
+ `Orgs: ${organizations.length ? organizations.map((o) => `${o.name} (${o.id})`).join(", ") : "none"}`
3734
+ );
3735
+ } catch (error) {
3736
+ console.error(
3737
+ `Not authenticated against ${apiUrl} (key from ${source}): ${error.message}`
3738
+ );
3739
+ process.exit(1);
3740
+ }
3741
+ });
3742
+ }
3743
+
3673
3744
  // apps/cli/src/commands/dispatch.ts
3674
3745
  var os2 = __toESM(require("node:os"));
3675
3746
  async function execute3(query, variables) {
@@ -3728,6 +3799,11 @@ function registerDispatchCommands(program3) {
3728
3799
  });
3729
3800
  });
3730
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
+ );
3731
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(
3732
3808
  (options) => execute3("mutation($input: JSON!) { userEndTurn(input: $input) }", {
3733
3809
  input: {
@@ -3909,10 +3985,7 @@ async function registerCommand(taskId, options) {
3909
3985
  const { config, org: org2 } = requireActiveOrg();
3910
3986
  const api = FlightDeskAPI.fromConfig(config, org2);
3911
3987
  try {
3912
- let projectId = options.project;
3913
- if (!projectId) {
3914
- projectId = await autoDetectProject(api, org2);
3915
- }
3988
+ const target = taskId ? { taskId } : { projectId: options.project || await autoDetectProject(api, org2) };
3916
3989
  let viewUrl = options.viewUrl;
3917
3990
  let teleportId = options.teleportId;
3918
3991
  if (options.prompt) {
@@ -3928,15 +4001,17 @@ async function registerCommand(taskId, options) {
3928
4001
  viewUrl = viewUrl || parsed.viewUrl;
3929
4002
  teleportId = teleportId || parsed.teleportId;
3930
4003
  }
3931
- let actualTaskId = taskId;
3932
- if (!actualTaskId) {
4004
+ let actualTaskId;
4005
+ if ("taskId" in target) {
4006
+ actualTaskId = target.taskId;
4007
+ } else {
3933
4008
  if (!options.title) {
3934
4009
  console.error('Task title required when creating a new task. Use --title "..."');
3935
4010
  process.exit(1);
3936
4011
  }
3937
4012
  console.log(`Creating task: ${options.title}`);
3938
4013
  const task2 = await api.createTask({
3939
- projectId,
4014
+ projectId: target.projectId,
3940
4015
  title: options.title,
3941
4016
  // Launch-first orchestrators do not know the FlightDesk UUID when the
3942
4017
  // cloud prompt is sent. Persisting that same prompt on the task lets
@@ -3957,8 +4032,7 @@ async function registerCommand(taskId, options) {
3957
4032
  } else {
3958
4033
  await api.updateTask(actualTaskId, {
3959
4034
  sessionViewUrl: viewUrl,
3960
- sessionTeleportId: teleportId,
3961
- status: "DISPATCHED"
4035
+ sessionTeleportId: teleportId
3962
4036
  });
3963
4037
  }
3964
4038
  console.log("\u2705 Session registered");
@@ -4020,30 +4094,25 @@ Output: ${output.slice(0, 500)}`));
4020
4094
  }
4021
4095
  function readStdin() {
4022
4096
  return new Promise((resolve) => {
4097
+ const input = process.stdin;
4023
4098
  let data = "";
4024
- let resolved = false;
4025
- process.stdin.setEncoding("utf8");
4026
- process.stdin.on("data", (chunk) => {
4099
+ const append = (chunk) => {
4027
4100
  data += chunk;
4028
- });
4029
- process.stdin.on("end", () => {
4030
- if (!resolved) {
4031
- resolved = true;
4032
- resolve(data);
4033
- }
4034
- });
4035
- process.stdin.on("close", () => {
4036
- if (!resolved) {
4037
- resolved = true;
4038
- resolve(data);
4039
- }
4040
- });
4041
- setTimeout(() => {
4042
- if (!resolved) {
4043
- resolved = true;
4044
- resolve(data);
4045
- }
4046
- }, 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();
4047
4116
  });
4048
4117
  }
4049
4118
  function parseClaudeOutput(output) {
@@ -4112,6 +4181,32 @@ async function autoDetectProject(api, activeOrg) {
4112
4181
  return project2.id;
4113
4182
  }
4114
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
+
4115
4210
  // apps/cli/src/commands/status.ts
4116
4211
  async function statusCommand(options) {
4117
4212
  const { config, org: org2 } = requireActiveOrg();
@@ -4122,33 +4217,18 @@ async function statusCommand(options) {
4122
4217
  console.log("No tasks found.");
4123
4218
  return;
4124
4219
  }
4125
- const byStatus = {};
4220
+ const byStatus = /* @__PURE__ */ new Map();
4126
4221
  for (const task2 of tasks) {
4127
- if (!byStatus[task2.status]) {
4128
- byStatus[task2.status] = [];
4129
- }
4130
- byStatus[task2.status].push(task2);
4222
+ const group = byStatus.get(task2.status) ?? [];
4223
+ group.push(task2);
4224
+ byStatus.set(task2.status, group);
4131
4225
  }
4132
- const statusOrder = [
4133
- "PENDING",
4134
- "DISPATCHED",
4135
- "IN_PROGRESS",
4136
- "BRANCH_CREATED",
4137
- "PR_OPEN",
4138
- "PREVIEW_STARTING",
4139
- "PREVIEW_READY",
4140
- "REVIEW_RUNNING",
4141
- "REVIEW_DONE",
4142
- "QA_READY",
4143
- "QA_APPROVED",
4144
- "MERGED",
4145
- "ARCHIVED"
4146
- ];
4226
+ const statusOrder = /* @__PURE__ */ new Set([...TASK_STATUS_ORDER, null, ...byStatus.keys()]);
4147
4227
  console.log("\n\u{1F4CB} FlightDesk Tasks\n");
4148
4228
  for (const status of statusOrder) {
4149
- const statusTasks = byStatus[status];
4229
+ const statusTasks = byStatus.get(status);
4150
4230
  if (!statusTasks || statusTasks.length === 0) continue;
4151
- console.log(`${getStatusEmoji(status)} ${status} (${statusTasks.length})`);
4231
+ console.log(`${taskStatusEmoji(status)} ${taskStatusLabel(status)} (${statusTasks.length})`);
4152
4232
  for (const task2 of statusTasks) {
4153
4233
  console.log(` ${task2.id.slice(0, 8)} - ${task2.title}`);
4154
4234
  if (task2.branchName) {
@@ -4165,40 +4245,21 @@ async function statusCommand(options) {
4165
4245
  process.exit(1);
4166
4246
  }
4167
4247
  }
4168
- function getStatusEmoji(status) {
4169
- switch (status) {
4170
- case "PENDING":
4171
- return "\u2B1C";
4172
- case "DISPATCHED":
4173
- return "\u{1F680}";
4174
- case "IN_PROGRESS":
4175
- return "\u{1F504}";
4176
- case "BRANCH_CREATED":
4177
- return "\u{1F33F}";
4178
- case "PR_OPEN":
4179
- return "\u{1F4DD}";
4180
- case "PREVIEW_STARTING":
4181
- return "\u23F3";
4182
- case "PREVIEW_READY":
4183
- return "\u{1F310}";
4184
- case "REVIEW_RUNNING":
4185
- return "\u{1F50D}";
4186
- case "REVIEW_DONE":
4187
- return "\u2705";
4188
- case "QA_READY":
4189
- return "\u{1F9EA}";
4190
- case "QA_APPROVED":
4191
- return "\u{1F44D}";
4192
- case "MERGED":
4193
- return "\u{1F389}";
4194
- case "ARCHIVED":
4195
- return "\u{1F4E6}";
4196
- default:
4197
- return "\u2753";
4198
- }
4199
- }
4200
4248
 
4201
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
+ }
4202
4263
  async function taskCommand(action, options) {
4203
4264
  const { config, org: org2 } = requireActiveOrg();
4204
4265
  const api = FlightDeskAPI.fromConfig(config, org2);
@@ -4234,13 +4295,15 @@ async function handleCreate(api, options) {
4234
4295
  subprojectId: options.subproject,
4235
4296
  sourceSystem: options.sourceSystem,
4236
4297
  sourceRef: options.sourceRef,
4237
- sourceUrl: options.sourceUrl
4298
+ sourceUrl: options.sourceUrl,
4299
+ followUpOf: options.followUpOf,
4300
+ followUpDispatchId: options.followUpDispatch
4238
4301
  });
4239
4302
  console.log(`
4240
4303
  \u2705 Task created`);
4241
4304
  console.log(` ID: ${task2.id}`);
4242
4305
  console.log(` Title: ${task2.title}`);
4243
- console.log(` Status: ${task2.status}`);
4306
+ console.log(` Status: ${taskStatusLabel(task2.status)}`);
4244
4307
  }
4245
4308
  async function handleList(api, options) {
4246
4309
  const tasks = await api.listTasks({
@@ -4253,10 +4316,10 @@ async function handleList(api, options) {
4253
4316
  }
4254
4317
  console.log("\nTasks:\n");
4255
4318
  for (const task2 of tasks) {
4256
- const statusEmoji = getStatusEmoji2(task2.status);
4319
+ const statusEmoji = taskStatusEmoji(task2.status);
4257
4320
  console.log(`${statusEmoji} ${task2.id.slice(0, 8)} - ${task2.title}`);
4258
4321
  if (task2.project?.name) console.log(` Project: ${task2.project.name}`);
4259
- console.log(` Status: ${task2.status}`);
4322
+ console.log(` Status: ${taskStatusLabel(task2.status)}`);
4260
4323
  if (task2.branchName) console.log(` Branch: ${task2.branchName}`);
4261
4324
  if (task2.prUrl) console.log(` PR: ${task2.prUrl}`);
4262
4325
  console.log("");
@@ -4272,11 +4335,10 @@ async function handleStatus(api, options) {
4272
4335
  \u{1F4CB} Task: ${task2.title}
4273
4336
  `);
4274
4337
  console.log(`ID: ${task2.id}`);
4275
- console.log(`Status: ${getStatusEmoji2(task2.status)} ${task2.status}`);
4338
+ console.log(`Status: ${taskStatusEmoji(task2.status)} ${taskStatusLabel(task2.status)}`);
4276
4339
  if (task2.project) {
4277
- console.log(
4278
- `Project: ${task2.project.name}${task2.project.githubRepo ? ` (${task2.project.githubRepo})` : ""}`
4279
- );
4340
+ const repo = task2.project.githubRepo ? " (" + task2.project.githubRepo + ")" : "";
4341
+ console.log(`Project: ${task2.project.name}${repo}`);
4280
4342
  }
4281
4343
  if (task2.description) {
4282
4344
  console.log(`
@@ -4302,31 +4364,56 @@ function parseSessionId(value) {
4302
4364
  }
4303
4365
  return value;
4304
4366
  }
4305
- async function handleUpdate(api, options) {
4306
- 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;
4307
4372
  if (options.status) input.status = options.status;
4308
4373
  if (options.branch) input.branchName = options.branch;
4309
4374
  if (options.prUrl) {
4310
4375
  input.prUrl = options.prUrl;
4311
- const prNumberMatch = options.prUrl.match(/\/pull\/(\d+)/);
4312
- if (prNumberMatch) {
4313
- input.prNumber = Number.parseInt(prNumberMatch[1], 10);
4314
- }
4376
+ const prNumberMatch = PR_NUMBER_IN_URL.exec(options.prUrl);
4377
+ if (prNumberMatch) input.prNumber = Number.parseInt(prNumberMatch[1], 10);
4315
4378
  }
4316
- if (options.prNumber) {
4317
- 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}`);
4318
4406
  }
4407
+ }
4408
+ async function handleUpdate(api, options) {
4409
+ const input = updateInput(options);
4319
4410
  if (Object.keys(input).length === 0 && !options.session) {
4320
- 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
+ );
4321
4414
  process.exit(1);
4322
4415
  }
4323
- if (options.branch && !options.status) {
4324
- const current = await api.getTask(options.taskId);
4325
- if (current?.status === "DISPATCHED") {
4326
- input.status = "IN_PROGRESS";
4327
- console.log(` Auto-transitioning DISPATCHED \u2192 IN_PROGRESS`);
4328
- }
4329
- }
4416
+ await autoTransition(api, options, input);
4330
4417
  console.log(`Updating task ${options.taskId}...`);
4331
4418
  let task2;
4332
4419
  if (options.session) {
@@ -4336,27 +4423,14 @@ async function handleUpdate(api, options) {
4336
4423
  teleportId: sessionId
4337
4424
  });
4338
4425
  }
4339
- if (Object.keys(input).length > 0) {
4340
- task2 = await api.updateTask(options.taskId, input);
4341
- }
4426
+ if (Object.keys(input).length > 0) task2 = await api.updateTask(options.taskId, input);
4342
4427
  if (!task2) return;
4343
- console.log(`
4344
- \u2705 Task updated`);
4345
- console.log(` Status: ${task2.status}`);
4346
- if (task2.branchName) console.log(` Branch: ${task2.branchName}`);
4347
- if (task2.prUrl) console.log(` PR: ${task2.prUrl}`);
4348
- if (task2.prNumber) console.log(` PR #: ${task2.prNumber}`);
4349
- if (task2.sessionViewUrl) console.log(` Session: ${task2.sessionViewUrl}`);
4350
- if (options.prUrl) {
4351
- console.log(`
4352
- \u{1F504} Syncing PR review state...`);
4353
- try {
4354
- const syncResult = await api.syncTaskChecks(options.taskId);
4355
- printSyncResult(syncResult);
4356
- } catch (err) {
4357
- console.log(` \u26A0\uFE0F Sync failed (checks will update when webhooks arrive): ${err}`);
4358
- }
4359
- }
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)";
4360
4434
  }
4361
4435
  async function handleSync(api, options) {
4362
4436
  console.log(`Syncing PR review state for task ${options.taskId}...`);
@@ -4389,36 +4463,54 @@ function getCheckIcon(status) {
4389
4463
  return "\u2753";
4390
4464
  }
4391
4465
  }
4392
- function getStatusEmoji2(status) {
4393
- switch (status) {
4394
- case "PENDING":
4395
- return "\u2B1C";
4396
- case "DISPATCHED":
4397
- return "\u{1F680}";
4398
- case "IN_PROGRESS":
4399
- return "\u{1F504}";
4400
- case "BRANCH_CREATED":
4401
- return "\u{1F33F}";
4402
- case "PR_OPEN":
4403
- return "\u{1F4DD}";
4404
- case "PREVIEW_STARTING":
4405
- return "\u23F3";
4406
- case "PREVIEW_READY":
4407
- return "\u{1F310}";
4408
- case "REVIEW_RUNNING":
4409
- return "\u{1F50D}";
4410
- case "REVIEW_DONE":
4411
- return "\u2705";
4412
- case "QA_READY":
4413
- return "\u{1F9EA}";
4414
- case "QA_APPROVED":
4415
- return "\u{1F44D}";
4416
- case "MERGED":
4417
- return "\u{1F389}";
4418
- case "ARCHIVED":
4419
- return "\u{1F4E6}";
4420
- default:
4421
- 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);
4422
4514
  }
4423
4515
  }
4424
4516
 
@@ -4426,7 +4518,7 @@ function getStatusEmoji2(status) {
4426
4518
  async function promptCommand(taskId, options) {
4427
4519
  const { config, org: org2 } = requireActiveOrg();
4428
4520
  const api = FlightDeskAPI.fromConfig(config, org2);
4429
- const validTypes = ["review", "test_plan", "summary", "handoff"];
4521
+ const validTypes = ["review", "test_plan"];
4430
4522
  if (!validTypes.includes(options.type)) {
4431
4523
  console.error(`Invalid prompt type: ${options.type}`);
4432
4524
  console.error(`Valid types: ${validTypes.join(", ")}`);
@@ -4782,11 +4874,12 @@ async function previewCommand(action, options) {
4782
4874
  process.exit(1);
4783
4875
  }
4784
4876
  }
4785
- function getStatusEmoji3(status) {
4877
+ function getStatusEmoji(status) {
4786
4878
  const statusEmojis = {
4787
4879
  STARTING: "\u{1F504}",
4788
4880
  READY: "\u2705",
4789
4881
  SUSPENDED: "\u{1F4A4}",
4882
+ STOPPED: "\u{1F4A4}",
4790
4883
  RESTARTING: "\u{1F504}",
4791
4884
  TEARDOWN: "\u{1F5D1}\uFE0F",
4792
4885
  ERROR: "\u274C"
@@ -4801,7 +4894,7 @@ async function handleStatus2(api, options) {
4801
4894
  console.log(" or the PR may be closed.");
4802
4895
  return;
4803
4896
  }
4804
- const emoji = getStatusEmoji3(instance.status);
4897
+ const emoji = getStatusEmoji(instance.status);
4805
4898
  console.log("\n\u{1F4E6} Preview Environment");
4806
4899
  console.log("\u2500".repeat(50));
4807
4900
  console.log(`${emoji} Status: ${instance.status}`);
@@ -4880,8 +4973,8 @@ async function handleMount(api, options) {
4880
4973
  console.error("No preview environment found for this task");
4881
4974
  process.exit(1);
4882
4975
  }
4883
- if (instance.status === "SUSPENDED") {
4884
- 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...");
4885
4978
  await api.resumeInstance(options.taskId);
4886
4979
  await new Promise((resolve) => setTimeout(resolve, 3e3));
4887
4980
  }
@@ -5012,7 +5105,8 @@ var program2 = new Command();
5012
5105
  registerDispatchCommands(program2);
5013
5106
  registerQuestionCommands(program2);
5014
5107
  registerPlanCommands(program2);
5015
- program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.6.0").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
5108
+ registerWhoamiCommand(program2);
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");
5016
5110
  program2.hook("preAction", () => {
5017
5111
  const opts = program2.opts();
5018
5112
  if (opts.api) {
@@ -5025,19 +5119,29 @@ program2.hook("preAction", () => {
5025
5119
  }
5026
5120
  });
5027
5121
  program2.command("init").description("Configure FlightDesk CLI with your API credentials").action(initCommand);
5028
- program2.command("register [task-id]").description(
5029
- "Register a Claude Code session with a FlightDesk task (auto-detects project from git repo)"
5030
- ).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);
5031
5123
  var project = program2.command("project").description("Project management commands");
5032
5124
  project.command("list").description("List projects in the active organization").action(() => projectCommand("list", {}));
5033
5125
  var task = program2.command("task").description("Task management commands");
5034
- 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));
5035
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));
5036
5131
  task.command("status <task-id>").description("Get task status").action((taskId) => taskCommand("status", { taskId }));
5037
- 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));
5038
5142
  task.command("sync <task-id>").description("Sync PR review state (Copilot feedback, Claude reviews, human reviews) from GitHub").action((taskId) => taskCommand("sync", { taskId }));
5039
5143
  program2.command("status").description("Show status of all active tasks").option("-p, --project <id>", "Filter by project").action(statusCommand);
5040
- 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);
5041
5145
  var org = program2.command("org").description("Organization management");
5042
5146
  org.command("list").description("List your organizations").action(orgListCommand);
5043
5147
  org.command("switch <name>").description("Switch active organization").action(orgSwitchCommand);