pi-subagents 0.83.0 → 0.83.1

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 (63) hide show
  1. package/CHANGELOG.md +22 -2
  2. package/bundle/agents.js +4 -4
  3. package/bundle/capability-ceiling.js +1 -1
  4. package/bundle/child-tool-plan.js +3 -3
  5. package/bundle/{chunk-P2NCCTZP.js → chunk-2D52JFLF.js} +1 -1
  6. package/bundle/{chunk-WG2OTTAK.js → chunk-2DHZTCOL.js} +2 -2
  7. package/bundle/{chunk-MBDOYZ2V.js → chunk-2S4K53MV.js} +1 -1
  8. package/bundle/{chunk-HJJAVHFB.js → chunk-5YIITCPD.js} +23 -23
  9. package/bundle/{chunk-CNLDYEH4.js → chunk-AHIFYGR3.js} +11 -11
  10. package/bundle/{chunk-MHX22USZ.js → chunk-C5P5CHYK.js} +1 -1
  11. package/bundle/{chunk-XXKG3P2W.js → chunk-C7JMPU5F.js} +4 -4
  12. package/bundle/{chunk-VTMNBPCQ.js → chunk-D3JAFGMO.js} +1 -1
  13. package/bundle/{chunk-BKFWQ642.js → chunk-F4ASL6OV.js} +1 -1
  14. package/bundle/{chunk-CDI3GMNV.js → chunk-F5DBXJ2G.js} +15 -15
  15. package/bundle/{chunk-JHZ6DZ5M.js → chunk-G2Z6ZH26.js} +1 -1
  16. package/bundle/{chunk-TJQJ3MEA.js → chunk-IPGZMQXV.js} +10 -10
  17. package/bundle/{chunk-Y3I6H6DV.js → chunk-LFDBD4PW.js} +2 -2
  18. package/bundle/{chunk-7F5SK2U3.js → chunk-LNRJCP2Y.js} +17 -14
  19. package/bundle/{chunk-6B4ITO2W.js → chunk-MABL3FUM.js} +1 -1
  20. package/bundle/{chunk-CGXWSNPA.js → chunk-NPIYWVOD.js} +4 -4
  21. package/bundle/{chunk-RZJN63GU.js → chunk-QP5ZWNIS.js} +2 -2
  22. package/bundle/{chunk-Z7KVHQHL.js → chunk-QXASAHIA.js} +2 -2
  23. package/bundle/{chunk-5ESXK62X.js → chunk-RNIZLSEA.js} +416 -389
  24. package/bundle/{chunk-HLE22FSH.js → chunk-SSG7IEP5.js} +2 -2
  25. package/bundle/{chunk-CZTMUBUI.js → chunk-UFL7IYCW.js} +4 -4
  26. package/bundle/{chunk-LDMXJIA6.js → chunk-UUP372DR.js} +44 -67
  27. package/bundle/{chunk-OOFW75PV.js → chunk-UYK5MZWE.js} +15 -11
  28. package/bundle/{chunk-CSSZRTKY.js → chunk-V2BZYLEP.js} +2 -2
  29. package/bundle/{chunk-4BW55BTL.js → chunk-VLFZIN6D.js} +4 -4
  30. package/bundle/{chunk-527E3D2J.js → chunk-YXFJVCOL.js} +5 -5
  31. package/bundle/{chunk-HXVKGS33.js → chunk-ZVOAMTUQ.js} +4 -4
  32. package/bundle/control-channel.js +3 -3
  33. package/bundle/index.js +1 -1
  34. package/bundle/intercom-bridge.js +3 -3
  35. package/bundle/preflight.js +9 -9
  36. package/bundle/project-panes.js +4 -4
  37. package/docs/configuration.md +12 -1
  38. package/docs/extension-api.md +2 -0
  39. package/docs/missions.md +2 -0
  40. package/docs/tool-reference.md +1 -1
  41. package/package.json +1 -1
  42. package/src/agents/agent-management.d.ts +1 -1
  43. package/src/agents/agent-management.js +2 -2
  44. package/src/agents/agents.js +1 -2
  45. package/src/agents/frontmatter.js +22 -18
  46. package/src/extension/index.js +2 -2
  47. package/src/missions/actions.js +7 -2
  48. package/src/missions/lifecycle.d.ts +11 -0
  49. package/src/missions/lifecycle.js +37 -6
  50. package/src/missions/store.js +1 -0
  51. package/src/missions/types.d.ts +2 -0
  52. package/src/runs/background/async-execution.d.ts +1 -1
  53. package/src/runs/background/async-execution.js +3 -3
  54. package/src/runs/background/async-resume.js +3 -2
  55. package/src/runs/background/retained-children.js +1 -1
  56. package/src/runs/foreground/subagent-executor.js +24 -51
  57. package/src/runs/shared/capability-ceiling.d.ts +2 -1
  58. package/src/runs/shared/capability-ceiling.js +3 -2
  59. package/src/runs/shared/parallel-handoff.d.ts +1 -1
  60. package/src/runs/shared/parallel-handoff.js +10 -6
  61. package/src/shared/types.d.ts +1 -1
  62. package/src/workflows/scripted-workflow.js +4 -1
  63. package/workflow-script-worker.cjs +2 -1
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PROJECT_SUBAGENTS_RELATIVE_DIR
3
- } from "./chunk-VTMNBPCQ.js";
3
+ } from "./chunk-D3JAFGMO.js";
4
4
  import {
5
5
  DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS,
6
6
  DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
@@ -22,7 +22,7 @@ import {
22
22
  withFileLease,
23
23
  writeAtomicJson,
24
24
  writePrivateAtomicJson
25
- } from "./chunk-7F5SK2U3.js";
25
+ } from "./chunk-LNRJCP2Y.js";
26
26
  import {
27
27
  DIRS,
28
28
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
@@ -2511,7 +2511,8 @@ ${script}
2511
2511
  }
2512
2512
  const wrapper = astNode(root.body) ? void 0 : Array.isArray(root.body) && astNode(root.body[0]) && astNode(root.body[0].expression) && astNode(root.body[0].expression.callee) ? root.body[0].expression.callee : void 0;
2513
2513
  const workflowBody = wrapper && astNode(wrapper.body) ? wrapper.body : root;
2514
- const agentNameError = options.agentNameError && !invalidatesRunsBinding(workflowBody) ? options.agentNameError : void 0;
2514
+ const runsRebound = invalidatesRunsBinding(workflowBody);
2515
+ const agentNameError = options.agentNameError && !runsRebound ? options.agentNameError : void 0;
2515
2516
  walkAst(workflowBody, (node) => {
2516
2517
  if (node !== wrapper && node.async === true && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression")) {
2517
2518
  errors.push({ message: "workflowScript does not support nested async functions. Use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains.", ...nodeLocation(node) });
@@ -2521,6 +2522,7 @@ ${script}
2521
2522
  const keyNode = astNode(args[0]) ? args[0] : void 0;
2522
2523
  const key = literalString(keyNode);
2523
2524
  if (keyNode && key !== void 0 && !KEY_PATTERN.test(key)) errors.push({ message: "runs.run key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.", ...nodeLocation(keyNode) });
2525
+ if (keyNode?.type === "ObjectExpression" && !runsRebound) errors.push({ message: `runs.run expects (key, params); got an object as the first argument. Use runs.run("plan", { agent, task }) or runs.all([{ key, agent, task }]).`, ...nodeLocation(keyNode) });
2524
2526
  if (astNode(args[1])) {
2525
2527
  errors.push(...validateStaticBaseRef(args[1], "runs.run"));
2526
2528
  const message = definitelyNonJson(args[1]);
@@ -3365,6 +3367,7 @@ function parseWorkflowChild(value, label) {
3365
3367
  ...optionalString(input.phase, `${label}.phase`) ? { phase: input.phase } : {},
3366
3368
  ...input.completedAt !== void 0 ? { completedAt: timestamp(input.completedAt, `${label}.completedAt`) } : {},
3367
3369
  ...optionalString(input.sessionPath, `${label}.sessionPath`) ? { sessionPath: input.sessionPath } : {},
3370
+ ...optionalString(input.acceptanceStatus, `${label}.acceptanceStatus`) ? { acceptanceStatus: input.acceptanceStatus } : {},
3368
3371
  ...heartbeat ? { heartbeat: {
3369
3372
  updatedAt: timestamp(heartbeat.updatedAt, `${label}.heartbeat.updatedAt`),
3370
3373
  ...optionalString(heartbeat.status, `${label}.heartbeat.status`) ? { status: heartbeat.status } : {},
@@ -4005,103 +4008,425 @@ function createMissionWorkflowState(location, missionId, options = {}) {
4005
4008
  }
4006
4009
 
4007
4010
  // src/missions/actions.ts
4011
+ import * as fs11 from "node:fs";
4012
+ import * as path12 from "node:path";
4013
+
4014
+ // src/missions/lifecycle.ts
4008
4015
  import * as fs10 from "node:fs";
4009
4016
  import * as path11 from "node:path";
4010
- var MISSION_ACTIONS = [
4011
- "mission.create",
4012
- "mission.list",
4013
- "mission.show",
4014
- "mission.update",
4015
- "mission.resolve-decision",
4016
- "mission.attach-run",
4017
- "mission.close"
4018
- ];
4019
- function textResult(text, details) {
4020
- return { content: [{ type: "text", text }], details };
4021
- }
4022
- function requireMissionId(params) {
4023
- return validateMissionId(params.missionId, "missionId");
4024
- }
4025
- function validateStatus(value, label) {
4026
- if (typeof value !== "string" || !MISSION_STATUSES.includes(value)) {
4027
- throw new Error(`${label} must be one of ${MISSION_STATUSES.join(", ")}`);
4017
+ import { createHash as createHash4 } from "node:crypto";
4018
+ var MISSION_BINDING_FILE = "mission.json";
4019
+ function workflowObjective(params) {
4020
+ const objective = params.task?.trim() || params.tasks?.find((task) => task.task?.trim())?.task?.trim() || params.chain?.find((step) => step.task?.trim())?.task?.trim();
4021
+ if (objective) return objective;
4022
+ for (const step of params.chain ?? []) {
4023
+ const parallel = Array.isArray(step.parallel) ? step.parallel : step.parallel ? [step.parallel] : [];
4024
+ const task = parallel.find((child) => child.task?.trim())?.task?.trim();
4025
+ if (task) return task;
4028
4026
  }
4029
- return value;
4027
+ return void 0;
4030
4028
  }
4031
- function validateMissionLaunch(value) {
4032
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("mission must be an object");
4033
- const input = value;
4034
- for (const key of Object.keys(input)) {
4035
- if (key !== "title" && key !== "summary" && key !== "objective" && key !== "goal" && key !== "budget" && key !== "labels") throw new Error(`mission.${key} is unknown`);
4029
+ function prepareMissionLaunch(input) {
4030
+ const hasMissionId = input.params.missionId !== void 0;
4031
+ if (hasMissionId && input.params.mission !== void 0) throw new Error("Use missionId or mission, not both");
4032
+ if (input.params.mission === false) return void 0;
4033
+ const objective = workflowObjective(input.params);
4034
+ const missionsEnabled = input.config?.enabled !== false;
4035
+ const shouldCreate = input.params.mission !== void 0 || missionsEnabled && objective !== void 0;
4036
+ if (!hasMissionId && !shouldCreate) return void 0;
4037
+ const location = resolveMissionStoreLocation({ projectRoot: input.projectRoot, ...input.config ? { config: input.config } : {} });
4038
+ if (hasMissionId) {
4039
+ const missionId = validateMissionId(input.params.missionId);
4040
+ readMission(location, missionId);
4041
+ updateMission(location, missionId, { status: "active" });
4042
+ return { missionId, location, autoCreated: false, announceInContent: true };
4036
4043
  }
4037
- if (input.title !== void 0 && input.summary !== void 0) throw new Error("mission.title and mission.summary cannot both be set");
4038
- const title = input.title ?? input.summary;
4039
- if (typeof title !== "string" || !title.trim()) throw new Error("mission.title or mission.summary must be a non-empty string");
4040
- if (input.objective !== void 0 && (typeof input.objective !== "string" || !input.objective.trim())) throw new Error("mission.objective must be a non-empty string");
4041
- if (input.goal !== void 0 && input.goal !== true) throw new Error("mission.goal must be true when supplied");
4042
- const budget = input.budget;
4043
- if (budget !== void 0 && (!budget || typeof budget !== "object" || Array.isArray(budget) || !Number.isSafeInteger(budget.tokens) || budget.tokens < 1)) {
4044
- throw new Error("mission.budget.tokens must be a positive integer");
4044
+ const mission = input.params.mission !== void 0 ? validateMissionLaunch(input.params.mission) : void 0;
4045
+ const promptDerivedObjective = mission?.objective ?? (mission ? mission.title : objective ? PROMPT_REDACTED : void 0);
4046
+ const title = mission?.title || PROMPT_REDACTED;
4047
+ const record = createMission(location, {
4048
+ title,
4049
+ objective: promptDerivedObjective || title,
4050
+ ...mission?.goal === true ? { goal: true } : {},
4051
+ ...mission?.budget ? { budget: mission.budget } : {},
4052
+ status: "active",
4053
+ ...mission?.labels ? { labels: mission.labels } : {},
4054
+ ...input.ownerSessionId ? { ownerSessionId: input.ownerSessionId } : {}
4055
+ }, /* @__PURE__ */ new Date(), input.config?.retainTerminal);
4056
+ return { missionId: record.id, location, autoCreated: input.params.mission === void 0, announceInContent: true };
4057
+ }
4058
+ function toolResultIsError(result) {
4059
+ return "isError" in result && result.isError === true;
4060
+ }
4061
+ function missionRunModeForResult(mode) {
4062
+ return mode === "management" ? "external" : mode;
4063
+ }
4064
+ function runStatusForResult(result) {
4065
+ if (result.details.asyncDir) return "active";
4066
+ if (result.details.results.some((child) => child.interrupted || child.detached)) return "paused";
4067
+ if (toolResultIsError(result) || result.details.results.some((child) => child.exitCode !== 0)) return "failed";
4068
+ return "completed";
4069
+ }
4070
+ var UNACCEPTED_ACCEPTANCE_STATUSES = /* @__PURE__ */ new Set(["pending", "claimed", "review-required", "rejected"]);
4071
+ function missionAcceptanceStatus(results) {
4072
+ const statuses = (Array.isArray(results) ? results : []).map((result) => result?.acceptance?.status).filter((status) => typeof status === "string" && status.trim() !== "");
4073
+ return statuses.find((status) => UNACCEPTED_ACCEPTANCE_STATUSES.has(status)) ?? statuses[0];
4074
+ }
4075
+ function finishedMissionStatus(record, autoCreated, runAcceptance) {
4076
+ if (autoCreated) return "completed";
4077
+ if (runAcceptance.some((run) => UNACCEPTED_ACCEPTANCE_STATUSES.has(run.status))) return "waiting";
4078
+ const reported = new Set(runAcceptance.map((run) => run.runId));
4079
+ return record.workflowChildren.some((child) => !reported.has(child.runId) && UNACCEPTED_ACCEPTANCE_STATUSES.has(child.acceptanceStatus)) ? "waiting" : "completed";
4080
+ }
4081
+ function missionStatusForRun(record, runId, runStatus, acceptanceStatus, autoCreated) {
4082
+ if (record.status === "completed" || record.status === "failed" || record.status === "cancelled") return record.status;
4083
+ if (runStatus === "active" || runStatus === "queued" || runStatus === "running") return "active";
4084
+ if (record.goal) return "active";
4085
+ if (runStatus === "paused") return "waiting";
4086
+ const otherActive = record.runs.some((run) => run.runId !== runId && (run.status === "active" || run.status === "queued" || run.status === "running"));
4087
+ if (otherActive) return "active";
4088
+ if (runStatus === "completed" || runStatus === "complete") return finishedMissionStatus(record, autoCreated, [{ runId, status: acceptanceStatus }]);
4089
+ if (runStatus === "stopped" || runStatus === "rejected" || runStatus === "cancelled") return "cancelled";
4090
+ return "failed";
4091
+ }
4092
+ function usageForResult(result) {
4093
+ const tokens = result.details.results.reduce((total, child) => total + child.usage.input + child.usage.output, 0);
4094
+ return tokens > 0 ? { tokens } : void 0;
4095
+ }
4096
+ function usageFromUnknown(value) {
4097
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
4098
+ const total = value.total;
4099
+ return Number.isSafeInteger(total) && total >= 0 ? { tokens: total } : void 0;
4100
+ }
4101
+ function artifactsForResult(result) {
4102
+ const artifacts = [];
4103
+ if (result.details.asyncDir) {
4104
+ artifacts.push(
4105
+ { kind: "status", path: path11.join(result.details.asyncDir, "status.json") },
4106
+ { kind: "other", path: path11.join(result.details.asyncDir, "events.jsonl"), description: "Lifecycle events" }
4107
+ );
4045
4108
  }
4046
- if (input.goal === true && budget === void 0) throw new Error("mission.budget is required when mission.goal is true");
4047
- if (input.labels !== void 0 && (!Array.isArray(input.labels) || input.labels.some((label) => typeof label !== "string" || !label.trim()))) {
4048
- throw new Error("mission.labels must contain only non-empty strings");
4109
+ for (const child of result.details.results) {
4110
+ if (child.artifactPaths?.outputPath) artifacts.push({ kind: "output", path: child.artifactPaths.outputPath });
4111
+ if (child.savedOutputPath) artifacts.push({ kind: "output", path: child.savedOutputPath });
4112
+ if (child.transcriptPath) artifacts.push({ kind: "other", path: child.transcriptPath, description: "Child transcript" });
4113
+ if (child.structuredOutputPath) artifacts.push({ kind: "output", path: child.structuredOutputPath, description: "Structured output" });
4049
4114
  }
4050
- return {
4051
- title: title.trim(),
4052
- ...typeof input.objective === "string" ? { objective: input.objective.trim() } : {},
4053
- ...input.goal === true ? { goal: true } : {},
4054
- ...budget !== void 0 ? { budget: { tokens: budget.tokens } } : {},
4055
- ...input.labels !== void 0 ? { labels: input.labels.map((label) => label.trim()) } : {}
4056
- };
4115
+ if (result.details.parallelHandoff?.path) artifacts.push({ kind: "manifest", path: result.details.parallelHandoff.path });
4116
+ return artifacts;
4057
4117
  }
4058
- function validateArtifact(value, index) {
4059
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`missionUpdate.artifacts[${index}] must be an object`);
4060
- const input = value;
4061
- const kinds = ["status", "output", "patch", "manifest", "review", "note", "other"];
4062
- if (typeof input.kind !== "string" || !kinds.includes(input.kind)) throw new Error(`missionUpdate.artifacts[${index}].kind is invalid`);
4063
- if (typeof input.path !== "string" || !input.path.trim()) throw new Error(`missionUpdate.artifacts[${index}].path must be a non-empty string`);
4064
- if (input.description !== void 0 && (typeof input.description !== "string" || !input.description.trim())) throw new Error(`missionUpdate.artifacts[${index}].description must be a non-empty string`);
4118
+ function firstText(result) {
4119
+ const text = result.content.find((item) => item.type === "text")?.text.trim();
4120
+ if (!text) return void 0;
4121
+ return text.length > 2e3 ? `${text.slice(0, 1997)}...` : text;
4122
+ }
4123
+ function persistedBinding(binding) {
4065
4124
  return {
4066
- kind: input.kind,
4067
- path: input.path,
4068
- ...typeof input.description === "string" ? { description: input.description } : {}
4125
+ schemaVersion: 1,
4126
+ missionId: binding.missionId,
4127
+ projectRoot: binding.location.projectRoot,
4128
+ missionDir: binding.location.missionDir,
4129
+ globalIndexDir: binding.location.globalIndexDir,
4130
+ writeGlobalIndex: binding.location.writeGlobalIndex,
4131
+ ...binding.location.retainTerminal !== void 0 ? { retainTerminal: binding.location.retainTerminal } : {},
4132
+ autoCreated: binding.autoCreated
4069
4133
  };
4070
4134
  }
4071
- function validateReceipt(value, index) {
4072
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`missionUpdate.receipts[${index}] must be an object`);
4073
- const input = value;
4074
- for (const key of Object.keys(input)) {
4075
- if (!["kind", "status", "title", "url", "description"].includes(key)) throw new Error(`missionUpdate.receipts[${index}].${key} is unknown`);
4076
- }
4077
- const kinds = ["pull_request", "ci", "deployment", "release"];
4078
- const statuses = ["pending", "ready", "succeeded", "failed"];
4079
- if (typeof input.kind !== "string" || !kinds.includes(input.kind)) throw new Error(`missionUpdate.receipts[${index}].kind is invalid`);
4080
- if (typeof input.status !== "string" || !statuses.includes(input.status)) throw new Error(`missionUpdate.receipts[${index}].status is invalid`);
4081
- for (const field of ["title", "url"]) {
4082
- if (typeof input[field] !== "string" || !input[field].trim()) throw new Error(`missionUpdate.receipts[${index}].${field} must be a non-empty string`);
4135
+ function writeMissionAsyncBinding(asyncDir, binding) {
4136
+ writePrivateAtomicJson(path11.join(asyncDir, MISSION_BINDING_FILE), persistedBinding(binding));
4137
+ }
4138
+ function attachMissionToLaunchResult(input) {
4139
+ const runId = input.result.details.runId ?? input.result.details.asyncId;
4140
+ if (!runId) {
4141
+ const current2 = readMission(input.binding.location, input.binding.missionId);
4142
+ const activeRunExists = current2.runs.some((run2) => run2.status === "active" || run2.status === "queued" || run2.status === "running");
4143
+ const mission2 = toolResultIsError(input.result) ? updateMission(input.binding.location, input.binding.missionId, {
4144
+ status: activeRunExists ? "active" : "failed",
4145
+ ...firstText(input.result) ? { summary: firstText(input.result) } : {}
4146
+ }) : current2;
4147
+ return {
4148
+ ...input.result,
4149
+ details: {
4150
+ ...input.result.details,
4151
+ missionId: input.binding.missionId,
4152
+ missionPath: missionRecordPath(input.binding.location, input.binding.missionId),
4153
+ mission: mission2
4154
+ }
4155
+ };
4083
4156
  }
4084
- try {
4085
- new URL(input.url);
4086
- } catch {
4087
- throw new Error(`missionUpdate.receipts[${index}].url must be an absolute URL`);
4157
+ const runStatus = runStatusForResult(input.result);
4158
+ const current = readMission(input.binding.location, input.binding.missionId);
4159
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
4160
+ const usage = usageForResult(input.result);
4161
+ const run = {
4162
+ runId,
4163
+ mode: missionRunModeForResult(input.result.details.mode),
4164
+ status: runStatus,
4165
+ startedAt,
4166
+ ...input.result.details.asyncDir ? { asyncDir: input.result.details.asyncDir } : {},
4167
+ ...input.result.details.results.length === 1 && input.result.details.results[0]?.agent ? { agent: input.result.details.results[0].agent } : {},
4168
+ ...runStatus !== "active" ? { completedAt: startedAt } : {},
4169
+ ...usage ? { usage } : {}
4170
+ };
4171
+ let mission = updateMission(input.binding.location, input.binding.missionId, {
4172
+ status: missionStatusForRun(current, runId, runStatus, missionAcceptanceStatus(input.result.details.results), input.binding.autoCreated),
4173
+ addRuns: [run],
4174
+ addArtifacts: artifactsForResult(input.result),
4175
+ ...firstText(input.result) && runStatus !== "active" ? { summary: firstText(input.result) } : {},
4176
+ ...input.result.details.results.length === 1 && input.result.details.results[0]?.acceptance ? { acceptance: input.result.details.results[0].acceptance } : {}
4177
+ });
4178
+ if (input.result.details.asyncDir) {
4179
+ writeMissionAsyncBinding(input.result.details.asyncDir, input.binding);
4180
+ const statusPath = path11.join(input.result.details.asyncDir, "status.json");
4181
+ if (fs10.existsSync(statusPath)) {
4182
+ try {
4183
+ const status = JSON.parse(fs10.readFileSync(statusPath, "utf-8"));
4184
+ if (typeof status.state === "string" && !["queued", "running"].includes(status.state)) {
4185
+ mission = syncMissionFromAsyncCompletion({
4186
+ runId,
4187
+ asyncDir: input.result.details.asyncDir,
4188
+ mode: input.result.details.mode,
4189
+ state: status.state,
4190
+ // A run that finished before this launch result was attached keeps its acceptance in the status steps.
4191
+ ...Array.isArray(status.steps) ? { results: status.steps.map((step) => ({ acceptance: step?.acceptance })) } : {},
4192
+ ...firstText(input.result) ? { summary: firstText(input.result) } : {}
4193
+ }) ?? mission;
4194
+ }
4195
+ } catch (error) {
4196
+ throw new Error(`Failed to reconcile mission from terminal async status '${statusPath}': ${error instanceof Error ? error.message : String(error)}`);
4197
+ }
4198
+ }
4088
4199
  }
4089
- if (input.description !== void 0 && (typeof input.description !== "string" || !input.description.trim())) throw new Error(`missionUpdate.receipts[${index}].description must be a non-empty string`);
4200
+ const lastTextIndex = input.result.content.findLastIndex((item) => item.type === "text");
4201
+ const hasStructuredOutput = input.result.details.results.some((child) => child.structuredOutputPath !== void 0);
4202
+ const textIsJson = lastTextIndex >= 0 && (() => {
4203
+ try {
4204
+ JSON.parse(input.result.content[lastTextIndex].text);
4205
+ return true;
4206
+ } catch {
4207
+ return false;
4208
+ }
4209
+ })();
4090
4210
  return {
4091
- kind: input.kind,
4092
- status: input.status,
4093
- title: input.title.trim(),
4094
- url: input.url.trim(),
4095
- ...typeof input.description === "string" ? { description: input.description.trim() } : {}
4211
+ ...input.result,
4212
+ content: input.binding.announceInContent === true && lastTextIndex >= 0 && !hasStructuredOutput && !textIsJson ? input.result.content.map((item, index) => index === lastTextIndex && item.type === "text" ? { ...item, text: `${item.text}
4213
+ Mission: ${mission.id} (${mission.status})` } : item) : input.result.content,
4214
+ details: {
4215
+ ...input.result.details,
4216
+ missionId: mission.id,
4217
+ missionPath: missionRecordPath(input.binding.location, mission.id),
4218
+ mission
4219
+ }
4096
4220
  };
4097
4221
  }
4098
- function validateMissionUpdate(value) {
4099
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("missionUpdate must be an object");
4222
+ function parsePersistedBinding(value, source) {
4223
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${source} must be an object`);
4100
4224
  const input = value;
4101
- for (const key of Object.keys(input)) {
4102
- if (!["title", "objective", "goal", "budget", "status", "summary", "labels", "artifacts", "receipts", "decisions"].includes(key)) throw new Error(`missionUpdate.${key} is unknown`);
4225
+ if (input.schemaVersion !== 1) throw new Error(`${source}.schemaVersion must be 1`);
4226
+ for (const field of ["projectRoot", "missionDir", "globalIndexDir"]) {
4227
+ if (typeof input[field] !== "string" || !input[field].trim()) throw new Error(`${source}.${field} must be a non-empty string`);
4103
4228
  }
4104
- const update = {};
4229
+ if (typeof input.writeGlobalIndex !== "boolean") throw new Error(`${source}.writeGlobalIndex must be boolean`);
4230
+ if (input.retainTerminal !== void 0 && (!Number.isInteger(input.retainTerminal) || input.retainTerminal < 1)) throw new Error(`${source}.retainTerminal must be a positive integer`);
4231
+ if (input.autoCreated !== void 0 && typeof input.autoCreated !== "boolean") throw new Error(`${source}.autoCreated must be boolean`);
4232
+ return {
4233
+ missionId: validateMissionId(input.missionId, `${source}.missionId`),
4234
+ // Bindings saved before this field existed belong to runs that completed their mission without an acceptance check.
4235
+ autoCreated: input.autoCreated !== false,
4236
+ location: {
4237
+ projectRoot: input.projectRoot,
4238
+ missionDir: input.missionDir,
4239
+ globalIndexDir: input.globalIndexDir,
4240
+ writeGlobalIndex: input.writeGlobalIndex,
4241
+ ...input.retainTerminal !== void 0 ? { retainTerminal: input.retainTerminal } : {}
4242
+ }
4243
+ };
4244
+ }
4245
+ function readMissionBinding(asyncDir) {
4246
+ const bindingPath = path11.join(asyncDir, MISSION_BINDING_FILE);
4247
+ if (!fs10.existsSync(bindingPath)) return void 0;
4248
+ const raw = fs10.readFileSync(bindingPath, "utf-8");
4249
+ try {
4250
+ return parsePersistedBinding(JSON.parse(raw), bindingPath);
4251
+ } catch (error) {
4252
+ throw new Error(`Invalid mission binding file '${bindingPath}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
4253
+ }
4254
+ }
4255
+ function syncMissionFromAsyncCompletion(value) {
4256
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
4257
+ const event = value;
4258
+ if (typeof event.asyncDir !== "string" || !event.asyncDir.trim()) return void 0;
4259
+ const binding = readMissionBinding(event.asyncDir);
4260
+ if (!binding) return void 0;
4261
+ const runId = typeof event.runId === "string" ? event.runId : typeof event.id === "string" ? event.id : void 0;
4262
+ if (!runId) throw new Error("Async mission completion is missing runId");
4263
+ const runStatus = typeof event.state === "string" ? event.state : event.success === true ? "completed" : "failed";
4264
+ let current;
4265
+ try {
4266
+ current = readMission(binding.location, binding.missionId);
4267
+ } catch (error) {
4268
+ if (!(error instanceof MissionNotFoundError)) throw error;
4269
+ const reason = "mission-record-missing";
4270
+ const markerId = createHash4("sha256").update(JSON.stringify([runId, binding.missionId, reason])).digest("hex");
4271
+ const markerPath3 = path11.join(event.asyncDir, `.mission-sync-skipped-${markerId}.json`);
4272
+ try {
4273
+ fs10.writeFileSync(markerPath3, `${JSON.stringify({ runId, missionId: binding.missionId, reason })}
4274
+ `, { encoding: "utf-8", mode: 384, flag: "wx" });
4275
+ fs10.appendFileSync(path11.join(event.asyncDir, "events.jsonl"), `${JSON.stringify({
4276
+ type: "subagent.mission.sync.skipped",
4277
+ ts: Date.now(),
4278
+ runId,
4279
+ missionId: binding.missionId,
4280
+ reason,
4281
+ missionPath: missionRecordPath(binding.location, binding.missionId)
4282
+ })}
4283
+ `, "utf-8");
4284
+ } catch {
4285
+ }
4286
+ return void 0;
4287
+ }
4288
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
4289
+ const artifacts = [
4290
+ { kind: "status", path: path11.join(event.asyncDir, "status.json") },
4291
+ { kind: "other", path: path11.join(event.asyncDir, "events.jsonl"), description: "Lifecycle events" }
4292
+ ];
4293
+ if (event.parallelHandoff && typeof event.parallelHandoff === "object" && typeof event.parallelHandoff.path === "string") {
4294
+ artifacts.push({ kind: "manifest", path: event.parallelHandoff.path });
4295
+ }
4296
+ if (Array.isArray(event.results)) {
4297
+ for (const result of event.results) {
4298
+ if (!result || typeof result !== "object") continue;
4299
+ const child = result;
4300
+ if (typeof child.artifactPath === "string") artifacts.push({ kind: "output", path: child.artifactPath });
4301
+ if (child.artifactPaths && typeof child.artifactPaths === "object" && typeof child.artifactPaths.outputPath === "string") {
4302
+ artifacts.push({ kind: "output", path: child.artifactPaths.outputPath });
4303
+ }
4304
+ }
4305
+ }
4306
+ const summary = typeof event.summary === "string" && event.summary.trim() ? event.summary.slice(0, 2e3) : void 0;
4307
+ const usage = usageFromUnknown(event.totalTokens) ?? (Array.isArray(event.results) ? { tokens: event.results.reduce((total, result) => {
4308
+ if (!result || typeof result !== "object") return total;
4309
+ return total + (usageFromUnknown(result.tokens)?.tokens ?? 0);
4310
+ }, 0) } : void 0);
4311
+ const workflowRunId = typeof event.parentWorkflowRunId === "string" && event.parentWorkflowRunId.trim() ? event.parentWorkflowRunId.trim() : void 0;
4312
+ const workflowKey = typeof event.workflowKey === "string" && event.workflowKey.trim() ? event.workflowKey.trim() : void 0;
4313
+ const workflowChildStatus = runStatus === "complete" || runStatus === "completed" || event.success === true ? "completed" : runStatus === "paused" ? "paused" : runStatus === "stopped" ? "stopped" : "failed";
4314
+ const workflowChildTerminal = !["running", "queued", "active", "paused"].includes(workflowChildStatus);
4315
+ const acceptanceStatus = missionAcceptanceStatus(event.results);
4316
+ return updateMission(binding.location, binding.missionId, {
4317
+ status: missionStatusForRun(current, runId, runStatus, acceptanceStatus, binding.autoCreated),
4318
+ addRuns: [{ runId, mode: typeof event.mode === "string" && ["single", "parallel", "chain", "workflow"].includes(event.mode) ? event.mode : "external", asyncDir: event.asyncDir, status: runStatus, completedAt, ...usage && usage.tokens > 0 ? { usage } : {} }],
4319
+ addArtifacts: artifacts,
4320
+ ...workflowRunId && workflowKey ? { upsertWorkflowChildren: [{
4321
+ workflowRunId,
4322
+ key: workflowKey,
4323
+ runId,
4324
+ status: workflowChildStatus,
4325
+ artifactPaths: artifacts.map((artifact) => artifact.path),
4326
+ ...workflowChildTerminal ? { completedAt } : {},
4327
+ ...acceptanceStatus ? { acceptanceStatus } : {},
4328
+ heartbeat: { status: workflowChildStatus, ...summary ? { message: summary } : {} }
4329
+ }] } : {},
4330
+ ...summary ? { summary } : {}
4331
+ });
4332
+ }
4333
+
4334
+ // src/missions/actions.ts
4335
+ var MISSION_ACTIONS = [
4336
+ "mission.create",
4337
+ "mission.list",
4338
+ "mission.show",
4339
+ "mission.update",
4340
+ "mission.resolve-decision",
4341
+ "mission.attach-run",
4342
+ "mission.close"
4343
+ ];
4344
+ function textResult(text, details) {
4345
+ return { content: [{ type: "text", text }], details };
4346
+ }
4347
+ function requireMissionId(params) {
4348
+ return validateMissionId(params.missionId, "missionId");
4349
+ }
4350
+ function validateStatus(value, label) {
4351
+ if (typeof value !== "string" || !MISSION_STATUSES.includes(value)) {
4352
+ throw new Error(`${label} must be one of ${MISSION_STATUSES.join(", ")}`);
4353
+ }
4354
+ return value;
4355
+ }
4356
+ function validateMissionLaunch(value) {
4357
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("mission must be an object");
4358
+ const input = value;
4359
+ for (const key of Object.keys(input)) {
4360
+ if (key !== "title" && key !== "summary" && key !== "objective" && key !== "goal" && key !== "budget" && key !== "labels") throw new Error(`mission.${key} is unknown`);
4361
+ }
4362
+ if (input.title !== void 0 && input.summary !== void 0) throw new Error("mission.title and mission.summary cannot both be set");
4363
+ const title = input.title ?? input.summary;
4364
+ if (typeof title !== "string" || !title.trim()) throw new Error("mission.title or mission.summary must be a non-empty string");
4365
+ if (input.objective !== void 0 && (typeof input.objective !== "string" || !input.objective.trim())) throw new Error("mission.objective must be a non-empty string");
4366
+ if (input.goal !== void 0 && input.goal !== true) throw new Error("mission.goal must be true when supplied");
4367
+ const budget = input.budget;
4368
+ if (budget !== void 0 && (!budget || typeof budget !== "object" || Array.isArray(budget) || !Number.isSafeInteger(budget.tokens) || budget.tokens < 1)) {
4369
+ throw new Error("mission.budget.tokens must be a positive integer");
4370
+ }
4371
+ if (input.goal === true && budget === void 0) throw new Error("mission.budget is required when mission.goal is true");
4372
+ if (input.labels !== void 0 && (!Array.isArray(input.labels) || input.labels.some((label) => typeof label !== "string" || !label.trim()))) {
4373
+ throw new Error("mission.labels must contain only non-empty strings");
4374
+ }
4375
+ return {
4376
+ title: title.trim(),
4377
+ ...typeof input.objective === "string" ? { objective: input.objective.trim() } : {},
4378
+ ...input.goal === true ? { goal: true } : {},
4379
+ ...budget !== void 0 ? { budget: { tokens: budget.tokens } } : {},
4380
+ ...input.labels !== void 0 ? { labels: input.labels.map((label) => label.trim()) } : {}
4381
+ };
4382
+ }
4383
+ function validateArtifact(value, index) {
4384
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`missionUpdate.artifacts[${index}] must be an object`);
4385
+ const input = value;
4386
+ const kinds = ["status", "output", "patch", "manifest", "review", "note", "other"];
4387
+ if (typeof input.kind !== "string" || !kinds.includes(input.kind)) throw new Error(`missionUpdate.artifacts[${index}].kind is invalid`);
4388
+ if (typeof input.path !== "string" || !input.path.trim()) throw new Error(`missionUpdate.artifacts[${index}].path must be a non-empty string`);
4389
+ if (input.description !== void 0 && (typeof input.description !== "string" || !input.description.trim())) throw new Error(`missionUpdate.artifacts[${index}].description must be a non-empty string`);
4390
+ return {
4391
+ kind: input.kind,
4392
+ path: input.path,
4393
+ ...typeof input.description === "string" ? { description: input.description } : {}
4394
+ };
4395
+ }
4396
+ function validateReceipt(value, index) {
4397
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`missionUpdate.receipts[${index}] must be an object`);
4398
+ const input = value;
4399
+ for (const key of Object.keys(input)) {
4400
+ if (!["kind", "status", "title", "url", "description"].includes(key)) throw new Error(`missionUpdate.receipts[${index}].${key} is unknown`);
4401
+ }
4402
+ const kinds = ["pull_request", "ci", "deployment", "release"];
4403
+ const statuses = ["pending", "ready", "succeeded", "failed"];
4404
+ if (typeof input.kind !== "string" || !kinds.includes(input.kind)) throw new Error(`missionUpdate.receipts[${index}].kind is invalid`);
4405
+ if (typeof input.status !== "string" || !statuses.includes(input.status)) throw new Error(`missionUpdate.receipts[${index}].status is invalid`);
4406
+ for (const field of ["title", "url"]) {
4407
+ if (typeof input[field] !== "string" || !input[field].trim()) throw new Error(`missionUpdate.receipts[${index}].${field} must be a non-empty string`);
4408
+ }
4409
+ try {
4410
+ new URL(input.url);
4411
+ } catch {
4412
+ throw new Error(`missionUpdate.receipts[${index}].url must be an absolute URL`);
4413
+ }
4414
+ if (input.description !== void 0 && (typeof input.description !== "string" || !input.description.trim())) throw new Error(`missionUpdate.receipts[${index}].description must be a non-empty string`);
4415
+ return {
4416
+ kind: input.kind,
4417
+ status: input.status,
4418
+ title: input.title.trim(),
4419
+ url: input.url.trim(),
4420
+ ...typeof input.description === "string" ? { description: input.description.trim() } : {}
4421
+ };
4422
+ }
4423
+ function validateMissionUpdate(value) {
4424
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("missionUpdate must be an object");
4425
+ const input = value;
4426
+ for (const key of Object.keys(input)) {
4427
+ if (!["title", "objective", "goal", "budget", "status", "summary", "labels", "artifacts", "receipts", "decisions"].includes(key)) throw new Error(`missionUpdate.${key} is unknown`);
4428
+ }
4429
+ const update = {};
4105
4430
  for (const field of ["title", "objective", "summary"]) {
4106
4431
  const candidate = input[field];
4107
4432
  if (candidate === void 0) continue;
@@ -4156,13 +4481,14 @@ function validateMissionUpdate(value) {
4156
4481
  }
4157
4482
  function refreshLinkedRunStatus(location, record) {
4158
4483
  const warnings = [];
4484
+ const runAcceptance = [];
4159
4485
  const updates = record.runs.flatMap((run) => {
4160
4486
  if (!run.asyncDir) return [];
4161
- const statusPath = path11.join(run.asyncDir, "status.json");
4162
- if (!fs10.existsSync(statusPath)) return [];
4487
+ const statusPath = path12.join(run.asyncDir, "status.json");
4488
+ if (!fs11.existsSync(statusPath)) return [];
4163
4489
  let status2;
4164
4490
  try {
4165
- status2 = JSON.parse(fs10.readFileSync(statusPath, "utf-8"));
4491
+ status2 = JSON.parse(fs11.readFileSync(statusPath, "utf-8"));
4166
4492
  } catch (error) {
4167
4493
  warnings.push(`Failed to read linked run status '${statusPath}': ${error instanceof Error ? error.message : String(error)}`);
4168
4494
  return [];
@@ -4176,6 +4502,7 @@ function refreshLinkedRunStatus(location, record) {
4176
4502
  warnings.push(`Linked run status '${statusPath}' is missing state`);
4177
4503
  return [];
4178
4504
  }
4505
+ runAcceptance.push({ runId: run.runId, status: missionAcceptanceStatus(status2.steps) });
4179
4506
  if (state === run.status) return [];
4180
4507
  return [{
4181
4508
  ...run,
@@ -4186,7 +4513,7 @@ function refreshLinkedRunStatus(location, record) {
4186
4513
  if (updates.length === 0) return { record, warnings };
4187
4514
  const merged = record.runs.map((run) => updates.find((update) => update.runId === run.runId && update.childIndex === run.childIndex) ?? run);
4188
4515
  const states = merged.map((run) => run.status);
4189
- const status = record.status === "completed" || record.status === "failed" || record.status === "cancelled" ? record.status : record.goal && !states.some((state) => state === "queued" || state === "running" || state === "active") ? "active" : states.some((state) => state === "queued" || state === "running" || state === "active") ? "active" : states.some((state) => state === "paused") ? "waiting" : states.some((state) => state === "failed") ? "failed" : states.some((state) => state === "stopped" || state === "rejected" || state === "cancelled") ? "cancelled" : states.length > 0 && states.every((state) => state === "complete" || state === "completed") ? "completed" : record.status;
4516
+ const status = record.status === "completed" || record.status === "failed" || record.status === "cancelled" ? record.status : record.goal && !states.some((state) => state === "queued" || state === "running" || state === "active") ? "active" : states.some((state) => state === "queued" || state === "running" || state === "active") ? "active" : states.some((state) => state === "paused") ? "waiting" : states.some((state) => state === "failed") ? "failed" : states.some((state) => state === "stopped" || state === "rejected" || state === "cancelled") ? "cancelled" : states.length > 0 && states.every((state) => state === "complete" || state === "completed") ? finishedMissionStatus(record, !merged.some((run) => run.asyncDir && readMissionBinding(run.asyncDir)?.autoCreated === false), runAcceptance) : record.status;
4190
4517
  return { record: updateMission(location, record.id, { status, addRuns: updates }), warnings };
4191
4518
  }
4192
4519
  function formatMission(record) {
@@ -4210,7 +4537,8 @@ function formatMission(record) {
4210
4537
  const identity = child.runId ? `${child.key} (${child.runId})` : child.key;
4211
4538
  const heartbeat = child.heartbeat ? `; heartbeat ${child.heartbeat.status ?? child.status}${child.heartbeat.phase ? `/${child.heartbeat.phase}` : ""} at ${child.heartbeat.updatedAt}` : "";
4212
4539
  const recovery = child.sessionPath ? `; session ${child.sessionPath}` : child.artifactPaths.length ? `; artifacts ${child.artifactPaths.length}` : "";
4213
- lines.push(` ${identity}: ${child.status}${child.agent ? ` \u2014 ${child.agent}` : ""}${child.phase ? ` [${child.phase}]` : ""}; updated ${child.updatedAt}${heartbeat}${recovery}`);
4540
+ const acceptance = child.acceptanceStatus ? `; acceptance ${child.acceptanceStatus}` : "";
4541
+ lines.push(` ${identity}: ${child.status}${child.agent ? ` \u2014 ${child.agent}` : ""}${child.phase ? ` [${child.phase}]` : ""}${acceptance}; updated ${child.updatedAt}${heartbeat}${recovery}`);
4214
4542
  }
4215
4543
  }
4216
4544
  if (record.decisions.length) {
@@ -4331,308 +4659,6 @@ ${formatMission(record2)}`, { mode: "management", results: [], missionId: record
4331
4659
  }
4332
4660
  }
4333
4661
 
4334
- // src/missions/lifecycle.ts
4335
- import * as fs11 from "node:fs";
4336
- import * as path12 from "node:path";
4337
- import { createHash as createHash4 } from "node:crypto";
4338
- var MISSION_BINDING_FILE = "mission.json";
4339
- function workflowObjective(params) {
4340
- const objective = params.task?.trim() || params.tasks?.find((task) => task.task?.trim())?.task?.trim() || params.chain?.find((step) => step.task?.trim())?.task?.trim();
4341
- if (objective) return objective;
4342
- for (const step of params.chain ?? []) {
4343
- const parallel = Array.isArray(step.parallel) ? step.parallel : step.parallel ? [step.parallel] : [];
4344
- const task = parallel.find((child) => child.task?.trim())?.task?.trim();
4345
- if (task) return task;
4346
- }
4347
- return void 0;
4348
- }
4349
- function prepareMissionLaunch(input) {
4350
- const hasMissionId = input.params.missionId !== void 0;
4351
- if (hasMissionId && input.params.mission !== void 0) throw new Error("Use missionId or mission, not both");
4352
- if (input.params.mission === false) return void 0;
4353
- const objective = workflowObjective(input.params);
4354
- const missionsEnabled = input.config?.enabled !== false;
4355
- const shouldCreate = input.params.mission !== void 0 || missionsEnabled && objective !== void 0;
4356
- if (!hasMissionId && !shouldCreate) return void 0;
4357
- const location = resolveMissionStoreLocation({ projectRoot: input.projectRoot, ...input.config ? { config: input.config } : {} });
4358
- if (hasMissionId) {
4359
- const missionId = validateMissionId(input.params.missionId);
4360
- readMission(location, missionId);
4361
- updateMission(location, missionId, { status: "active" });
4362
- return { missionId, location, autoCreated: false, announceInContent: true };
4363
- }
4364
- const mission = input.params.mission !== void 0 ? validateMissionLaunch(input.params.mission) : void 0;
4365
- const promptDerivedObjective = mission?.objective ?? (mission ? mission.title : objective ? PROMPT_REDACTED : void 0);
4366
- const title = mission?.title || PROMPT_REDACTED;
4367
- const record = createMission(location, {
4368
- title,
4369
- objective: promptDerivedObjective || title,
4370
- ...mission?.goal === true ? { goal: true } : {},
4371
- ...mission?.budget ? { budget: mission.budget } : {},
4372
- status: "active",
4373
- ...mission?.labels ? { labels: mission.labels } : {},
4374
- ...input.ownerSessionId ? { ownerSessionId: input.ownerSessionId } : {}
4375
- }, /* @__PURE__ */ new Date(), input.config?.retainTerminal);
4376
- return { missionId: record.id, location, autoCreated: true, announceInContent: true };
4377
- }
4378
- function toolResultIsError(result) {
4379
- return "isError" in result && result.isError === true;
4380
- }
4381
- function missionRunModeForResult(mode) {
4382
- return mode === "management" ? "external" : mode;
4383
- }
4384
- function runStatusForResult(result) {
4385
- if (result.details.asyncDir) return "active";
4386
- if (result.details.results.some((child) => child.interrupted || child.detached)) return "paused";
4387
- if (toolResultIsError(result) || result.details.results.some((child) => child.exitCode !== 0)) return "failed";
4388
- return "completed";
4389
- }
4390
- function missionStatusForRun(record, runId, runStatus) {
4391
- if (record.status === "completed" || record.status === "failed" || record.status === "cancelled") return record.status;
4392
- if (runStatus === "active" || runStatus === "queued" || runStatus === "running") return "active";
4393
- if (record.goal) return "active";
4394
- if (runStatus === "paused") return "waiting";
4395
- const otherActive = record.runs.some((run) => run.runId !== runId && (run.status === "active" || run.status === "queued" || run.status === "running"));
4396
- if (otherActive) return "active";
4397
- if (runStatus === "completed" || runStatus === "complete") return "completed";
4398
- if (runStatus === "stopped" || runStatus === "rejected" || runStatus === "cancelled") return "cancelled";
4399
- return "failed";
4400
- }
4401
- function usageForResult(result) {
4402
- const tokens = result.details.results.reduce((total, child) => total + child.usage.input + child.usage.output, 0);
4403
- return tokens > 0 ? { tokens } : void 0;
4404
- }
4405
- function usageFromUnknown(value) {
4406
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
4407
- const total = value.total;
4408
- return Number.isSafeInteger(total) && total >= 0 ? { tokens: total } : void 0;
4409
- }
4410
- function artifactsForResult(result) {
4411
- const artifacts = [];
4412
- if (result.details.asyncDir) {
4413
- artifacts.push(
4414
- { kind: "status", path: path12.join(result.details.asyncDir, "status.json") },
4415
- { kind: "other", path: path12.join(result.details.asyncDir, "events.jsonl"), description: "Lifecycle events" }
4416
- );
4417
- }
4418
- for (const child of result.details.results) {
4419
- if (child.artifactPaths?.outputPath) artifacts.push({ kind: "output", path: child.artifactPaths.outputPath });
4420
- if (child.savedOutputPath) artifacts.push({ kind: "output", path: child.savedOutputPath });
4421
- if (child.transcriptPath) artifacts.push({ kind: "other", path: child.transcriptPath, description: "Child transcript" });
4422
- if (child.structuredOutputPath) artifacts.push({ kind: "output", path: child.structuredOutputPath, description: "Structured output" });
4423
- }
4424
- if (result.details.parallelHandoff?.path) artifacts.push({ kind: "manifest", path: result.details.parallelHandoff.path });
4425
- return artifacts;
4426
- }
4427
- function firstText(result) {
4428
- const text = result.content.find((item) => item.type === "text")?.text.trim();
4429
- if (!text) return void 0;
4430
- return text.length > 2e3 ? `${text.slice(0, 1997)}...` : text;
4431
- }
4432
- function persistedBinding(binding) {
4433
- return {
4434
- schemaVersion: 1,
4435
- missionId: binding.missionId,
4436
- projectRoot: binding.location.projectRoot,
4437
- missionDir: binding.location.missionDir,
4438
- globalIndexDir: binding.location.globalIndexDir,
4439
- writeGlobalIndex: binding.location.writeGlobalIndex,
4440
- ...binding.location.retainTerminal !== void 0 ? { retainTerminal: binding.location.retainTerminal } : {}
4441
- };
4442
- }
4443
- function writeMissionAsyncBinding(asyncDir, binding) {
4444
- writePrivateAtomicJson(path12.join(asyncDir, MISSION_BINDING_FILE), persistedBinding(binding));
4445
- }
4446
- function attachMissionToLaunchResult(input) {
4447
- const runId = input.result.details.runId ?? input.result.details.asyncId;
4448
- if (!runId) {
4449
- const current2 = readMission(input.binding.location, input.binding.missionId);
4450
- const activeRunExists = current2.runs.some((run2) => run2.status === "active" || run2.status === "queued" || run2.status === "running");
4451
- const mission2 = toolResultIsError(input.result) ? updateMission(input.binding.location, input.binding.missionId, {
4452
- status: activeRunExists ? "active" : "failed",
4453
- ...firstText(input.result) ? { summary: firstText(input.result) } : {}
4454
- }) : current2;
4455
- return {
4456
- ...input.result,
4457
- details: {
4458
- ...input.result.details,
4459
- missionId: input.binding.missionId,
4460
- missionPath: missionRecordPath(input.binding.location, input.binding.missionId),
4461
- mission: mission2
4462
- }
4463
- };
4464
- }
4465
- const runStatus = runStatusForResult(input.result);
4466
- const current = readMission(input.binding.location, input.binding.missionId);
4467
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
4468
- const usage = usageForResult(input.result);
4469
- const run = {
4470
- runId,
4471
- mode: missionRunModeForResult(input.result.details.mode),
4472
- status: runStatus,
4473
- startedAt,
4474
- ...input.result.details.asyncDir ? { asyncDir: input.result.details.asyncDir } : {},
4475
- ...input.result.details.results.length === 1 && input.result.details.results[0]?.agent ? { agent: input.result.details.results[0].agent } : {},
4476
- ...runStatus !== "active" ? { completedAt: startedAt } : {},
4477
- ...usage ? { usage } : {}
4478
- };
4479
- let mission = updateMission(input.binding.location, input.binding.missionId, {
4480
- status: missionStatusForRun(current, runId, runStatus),
4481
- addRuns: [run],
4482
- addArtifacts: artifactsForResult(input.result),
4483
- ...firstText(input.result) && runStatus !== "active" ? { summary: firstText(input.result) } : {},
4484
- ...input.result.details.results.length === 1 && input.result.details.results[0]?.acceptance ? { acceptance: input.result.details.results[0].acceptance } : {}
4485
- });
4486
- if (input.result.details.asyncDir) {
4487
- writeMissionAsyncBinding(input.result.details.asyncDir, input.binding);
4488
- const statusPath = path12.join(input.result.details.asyncDir, "status.json");
4489
- if (fs11.existsSync(statusPath)) {
4490
- try {
4491
- const status = JSON.parse(fs11.readFileSync(statusPath, "utf-8"));
4492
- if (typeof status.state === "string" && !["queued", "running"].includes(status.state)) {
4493
- mission = syncMissionFromAsyncCompletion({
4494
- runId,
4495
- asyncDir: input.result.details.asyncDir,
4496
- mode: input.result.details.mode,
4497
- state: status.state,
4498
- ...firstText(input.result) ? { summary: firstText(input.result) } : {}
4499
- }) ?? mission;
4500
- }
4501
- } catch (error) {
4502
- throw new Error(`Failed to reconcile mission from terminal async status '${statusPath}': ${error instanceof Error ? error.message : String(error)}`);
4503
- }
4504
- }
4505
- }
4506
- const lastTextIndex = input.result.content.findLastIndex((item) => item.type === "text");
4507
- const hasStructuredOutput = input.result.details.results.some((child) => child.structuredOutputPath !== void 0);
4508
- const textIsJson = lastTextIndex >= 0 && (() => {
4509
- try {
4510
- JSON.parse(input.result.content[lastTextIndex].text);
4511
- return true;
4512
- } catch {
4513
- return false;
4514
- }
4515
- })();
4516
- return {
4517
- ...input.result,
4518
- content: input.binding.announceInContent === true && lastTextIndex >= 0 && !hasStructuredOutput && !textIsJson ? input.result.content.map((item, index) => index === lastTextIndex && item.type === "text" ? { ...item, text: `${item.text}
4519
- Mission: ${mission.id} (${mission.status})` } : item) : input.result.content,
4520
- details: {
4521
- ...input.result.details,
4522
- missionId: mission.id,
4523
- missionPath: missionRecordPath(input.binding.location, mission.id),
4524
- mission
4525
- }
4526
- };
4527
- }
4528
- function parsePersistedBinding(value, source) {
4529
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${source} must be an object`);
4530
- const input = value;
4531
- if (input.schemaVersion !== 1) throw new Error(`${source}.schemaVersion must be 1`);
4532
- for (const field of ["projectRoot", "missionDir", "globalIndexDir"]) {
4533
- if (typeof input[field] !== "string" || !input[field].trim()) throw new Error(`${source}.${field} must be a non-empty string`);
4534
- }
4535
- if (typeof input.writeGlobalIndex !== "boolean") throw new Error(`${source}.writeGlobalIndex must be boolean`);
4536
- if (input.retainTerminal !== void 0 && (!Number.isInteger(input.retainTerminal) || input.retainTerminal < 1)) throw new Error(`${source}.retainTerminal must be a positive integer`);
4537
- return {
4538
- missionId: validateMissionId(input.missionId, `${source}.missionId`),
4539
- autoCreated: false,
4540
- location: {
4541
- projectRoot: input.projectRoot,
4542
- missionDir: input.missionDir,
4543
- globalIndexDir: input.globalIndexDir,
4544
- writeGlobalIndex: input.writeGlobalIndex,
4545
- ...input.retainTerminal !== void 0 ? { retainTerminal: input.retainTerminal } : {}
4546
- }
4547
- };
4548
- }
4549
- function readMissionBinding(asyncDir) {
4550
- const bindingPath = path12.join(asyncDir, MISSION_BINDING_FILE);
4551
- if (!fs11.existsSync(bindingPath)) return void 0;
4552
- const raw = fs11.readFileSync(bindingPath, "utf-8");
4553
- try {
4554
- return parsePersistedBinding(JSON.parse(raw), bindingPath);
4555
- } catch (error) {
4556
- throw new Error(`Invalid mission binding file '${bindingPath}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
4557
- }
4558
- }
4559
- function syncMissionFromAsyncCompletion(value) {
4560
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
4561
- const event = value;
4562
- if (typeof event.asyncDir !== "string" || !event.asyncDir.trim()) return void 0;
4563
- const binding = readMissionBinding(event.asyncDir);
4564
- if (!binding) return void 0;
4565
- const runId = typeof event.runId === "string" ? event.runId : typeof event.id === "string" ? event.id : void 0;
4566
- if (!runId) throw new Error("Async mission completion is missing runId");
4567
- const runStatus = typeof event.state === "string" ? event.state : event.success === true ? "completed" : "failed";
4568
- let current;
4569
- try {
4570
- current = readMission(binding.location, binding.missionId);
4571
- } catch (error) {
4572
- if (!(error instanceof MissionNotFoundError)) throw error;
4573
- const reason = "mission-record-missing";
4574
- const markerId = createHash4("sha256").update(JSON.stringify([runId, binding.missionId, reason])).digest("hex");
4575
- const markerPath3 = path12.join(event.asyncDir, `.mission-sync-skipped-${markerId}.json`);
4576
- try {
4577
- fs11.writeFileSync(markerPath3, `${JSON.stringify({ runId, missionId: binding.missionId, reason })}
4578
- `, { encoding: "utf-8", mode: 384, flag: "wx" });
4579
- fs11.appendFileSync(path12.join(event.asyncDir, "events.jsonl"), `${JSON.stringify({
4580
- type: "subagent.mission.sync.skipped",
4581
- ts: Date.now(),
4582
- runId,
4583
- missionId: binding.missionId,
4584
- reason,
4585
- missionPath: missionRecordPath(binding.location, binding.missionId)
4586
- })}
4587
- `, "utf-8");
4588
- } catch {
4589
- }
4590
- return void 0;
4591
- }
4592
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
4593
- const artifacts = [
4594
- { kind: "status", path: path12.join(event.asyncDir, "status.json") },
4595
- { kind: "other", path: path12.join(event.asyncDir, "events.jsonl"), description: "Lifecycle events" }
4596
- ];
4597
- if (event.parallelHandoff && typeof event.parallelHandoff === "object" && typeof event.parallelHandoff.path === "string") {
4598
- artifacts.push({ kind: "manifest", path: event.parallelHandoff.path });
4599
- }
4600
- if (Array.isArray(event.results)) {
4601
- for (const result of event.results) {
4602
- if (!result || typeof result !== "object") continue;
4603
- const child = result;
4604
- if (typeof child.artifactPath === "string") artifacts.push({ kind: "output", path: child.artifactPath });
4605
- if (child.artifactPaths && typeof child.artifactPaths === "object" && typeof child.artifactPaths.outputPath === "string") {
4606
- artifacts.push({ kind: "output", path: child.artifactPaths.outputPath });
4607
- }
4608
- }
4609
- }
4610
- const summary = typeof event.summary === "string" && event.summary.trim() ? event.summary.slice(0, 2e3) : void 0;
4611
- const usage = usageFromUnknown(event.totalTokens) ?? (Array.isArray(event.results) ? { tokens: event.results.reduce((total, result) => {
4612
- if (!result || typeof result !== "object") return total;
4613
- return total + (usageFromUnknown(result.tokens)?.tokens ?? 0);
4614
- }, 0) } : void 0);
4615
- const workflowRunId = typeof event.parentWorkflowRunId === "string" && event.parentWorkflowRunId.trim() ? event.parentWorkflowRunId.trim() : void 0;
4616
- const workflowKey = typeof event.workflowKey === "string" && event.workflowKey.trim() ? event.workflowKey.trim() : void 0;
4617
- const workflowChildStatus = runStatus === "complete" || runStatus === "completed" || event.success === true ? "completed" : runStatus === "paused" ? "paused" : runStatus === "stopped" ? "stopped" : "failed";
4618
- const workflowChildTerminal = !["running", "queued", "active", "paused"].includes(workflowChildStatus);
4619
- return updateMission(binding.location, binding.missionId, {
4620
- status: missionStatusForRun(current, runId, runStatus),
4621
- addRuns: [{ runId, mode: typeof event.mode === "string" && ["single", "parallel", "chain", "workflow"].includes(event.mode) ? event.mode : "external", asyncDir: event.asyncDir, status: runStatus, completedAt, ...usage && usage.tokens > 0 ? { usage } : {} }],
4622
- addArtifacts: artifacts,
4623
- ...workflowRunId && workflowKey ? { upsertWorkflowChildren: [{
4624
- workflowRunId,
4625
- key: workflowKey,
4626
- runId,
4627
- status: workflowChildStatus,
4628
- artifactPaths: artifacts.map((artifact) => artifact.path),
4629
- ...workflowChildTerminal ? { completedAt } : {},
4630
- heartbeat: { status: workflowChildStatus, ...summary ? { message: summary } : {} }
4631
- }] } : {},
4632
- ...summary ? { summary } : {}
4633
- });
4634
- }
4635
-
4636
4662
  // src/runs/background/result-files.ts
4637
4663
  import { createHash as createHash5 } from "node:crypto";
4638
4664
  import * as fs12 from "node:fs";
@@ -7229,6 +7255,7 @@ export {
7229
7255
  MISSION_ACTIONS,
7230
7256
  handleMissionAction,
7231
7257
  prepareMissionLaunch,
7258
+ missionAcceptanceStatus,
7232
7259
  writeMissionAsyncBinding,
7233
7260
  attachMissionToLaunchResult,
7234
7261
  readMissionBinding,