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.
- package/README.md +7 -4
- package/main.js +250 -183
- package/main.js.map +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
|
|
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").
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3968
|
-
if (
|
|
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
|
-
|
|
4061
|
-
process.stdin.setEncoding("utf8");
|
|
4062
|
-
process.stdin.on("data", (chunk) => {
|
|
4099
|
+
const append = (chunk) => {
|
|
4063
4100
|
data += chunk;
|
|
4064
|
-
}
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
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
|
-
|
|
4164
|
-
|
|
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
|
|
4229
|
+
const statusTasks = byStatus.get(status);
|
|
4186
4230
|
if (!statusTasks || statusTasks.length === 0) continue;
|
|
4187
|
-
console.log(`${
|
|
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 =
|
|
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: ${
|
|
4338
|
+
console.log(`Status: ${taskStatusEmoji(task2.status)} ${taskStatusLabel(task2.status)}`);
|
|
4312
4339
|
if (task2.project) {
|
|
4313
|
-
|
|
4314
|
-
|
|
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
|
-
|
|
4342
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
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
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
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"
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
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").
|
|
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)").
|
|
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
|
|
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);
|