pi-subagents 0.83.0 → 0.84.0

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 (73) hide show
  1. package/CHANGELOG.md +39 -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-HJJAVHFB.js → chunk-235AWJUQ.js} +35 -32
  6. package/bundle/{chunk-P2NCCTZP.js → chunk-2D52JFLF.js} +1 -1
  7. package/bundle/{chunk-WG2OTTAK.js → chunk-2DHZTCOL.js} +2 -2
  8. package/bundle/{chunk-MBDOYZ2V.js → chunk-2S4K53MV.js} +1 -1
  9. package/bundle/{chunk-527E3D2J.js → chunk-4E7BKDTX.js} +5 -5
  10. package/bundle/{chunk-CZTMUBUI.js → chunk-7BWGNDNK.js} +4 -4
  11. package/bundle/{chunk-HXVKGS33.js → chunk-AKOIVLA5.js} +4 -4
  12. package/bundle/{chunk-MHX22USZ.js → chunk-C5P5CHYK.js} +1 -1
  13. package/bundle/{chunk-5ESXK62X.js → chunk-CGN2TVE7.js} +417 -389
  14. package/bundle/{chunk-VTMNBPCQ.js → chunk-D3JAFGMO.js} +1 -1
  15. package/bundle/{chunk-CDI3GMNV.js → chunk-DZDETGRJ.js} +18 -15
  16. package/bundle/{chunk-BKFWQ642.js → chunk-F4ASL6OV.js} +1 -1
  17. package/bundle/{chunk-JHZ6DZ5M.js → chunk-G2Z6ZH26.js} +1 -1
  18. package/bundle/{chunk-XXKG3P2W.js → chunk-J5YL7LYR.js} +4 -4
  19. package/bundle/{chunk-LDMXJIA6.js → chunk-JG4QKI5M.js} +58 -67
  20. package/bundle/{chunk-OOFW75PV.js → chunk-L3WFKBEI.js} +15 -11
  21. package/bundle/{chunk-Y3I6H6DV.js → chunk-LFDBD4PW.js} +2 -2
  22. package/bundle/{chunk-7F5SK2U3.js → chunk-LNRJCP2Y.js} +17 -14
  23. package/bundle/{chunk-4BW55BTL.js → chunk-NENDSQUY.js} +4 -4
  24. package/bundle/{chunk-6B4ITO2W.js → chunk-NKQ6JNAS.js} +1 -1
  25. package/bundle/{chunk-RZJN63GU.js → chunk-PALJGGYC.js} +2 -2
  26. package/bundle/{chunk-TJQJ3MEA.js → chunk-PV63UFUR.js} +10 -10
  27. package/bundle/{chunk-Z7KVHQHL.js → chunk-QXASAHIA.js} +2 -2
  28. package/bundle/{chunk-HLE22FSH.js → chunk-SSG7IEP5.js} +2 -2
  29. package/bundle/{chunk-CSSZRTKY.js → chunk-V2BZYLEP.js} +2 -2
  30. package/bundle/{chunk-CNLDYEH4.js → chunk-VQVVHSY6.js} +11 -11
  31. package/bundle/{chunk-CGXWSNPA.js → chunk-XDQORLZ5.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 +4 -1
  53. package/src/runs/background/async-execution.js +18 -3
  54. package/src/runs/background/async-resume.js +3 -2
  55. package/src/runs/background/async-retention.d.ts +1 -0
  56. package/src/runs/background/async-retention.js +16 -10
  57. package/src/runs/background/retained-children.js +1 -1
  58. package/src/runs/background/run-child-session.d.ts +2 -0
  59. package/src/runs/background/run-child-session.js +9 -1
  60. package/src/runs/background/subagent-runner.js +1 -0
  61. package/src/runs/foreground/subagent-executor.js +29 -51
  62. package/src/runs/shared/capability-ceiling.d.ts +2 -1
  63. package/src/runs/shared/capability-ceiling.js +3 -2
  64. package/src/runs/shared/child-session.d.ts +2 -0
  65. package/src/runs/shared/child-session.js +1 -0
  66. package/src/runs/shared/model-resolution-diagnostic.d.ts +11 -0
  67. package/src/runs/shared/model-resolution-diagnostic.js +25 -0
  68. package/src/runs/shared/parallel-handoff.d.ts +1 -1
  69. package/src/runs/shared/parallel-handoff.js +10 -6
  70. package/src/runs/shared/parallel-utils.d.ts +2 -0
  71. package/src/shared/types.d.ts +1 -1
  72. package/src/workflows/scripted-workflow.js +6 -1
  73. package/workflow-script-worker.cjs +450 -175
@@ -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,16 +2511,19 @@ ${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) });
2518
2519
  }
2520
+ if (node.type === "ImportExpression") errors.push({ message: "workflowScript cannot load modules with import().", ...nodeLocation(node) });
2519
2521
  if (directRunsCall(node, "run")) {
2520
2522
  const args = Array.isArray(node.arguments) ? node.arguments : [];
2521
2523
  const keyNode = astNode(args[0]) ? args[0] : void 0;
2522
2524
  const key = literalString(keyNode);
2523
2525
  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) });
2526
+ 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
2527
  if (astNode(args[1])) {
2525
2528
  errors.push(...validateStaticBaseRef(args[1], "runs.run"));
2526
2529
  const message = definitelyNonJson(args[1]);
@@ -3365,6 +3368,7 @@ function parseWorkflowChild(value, label) {
3365
3368
  ...optionalString(input.phase, `${label}.phase`) ? { phase: input.phase } : {},
3366
3369
  ...input.completedAt !== void 0 ? { completedAt: timestamp(input.completedAt, `${label}.completedAt`) } : {},
3367
3370
  ...optionalString(input.sessionPath, `${label}.sessionPath`) ? { sessionPath: input.sessionPath } : {},
3371
+ ...optionalString(input.acceptanceStatus, `${label}.acceptanceStatus`) ? { acceptanceStatus: input.acceptanceStatus } : {},
3368
3372
  ...heartbeat ? { heartbeat: {
3369
3373
  updatedAt: timestamp(heartbeat.updatedAt, `${label}.heartbeat.updatedAt`),
3370
3374
  ...optionalString(heartbeat.status, `${label}.heartbeat.status`) ? { status: heartbeat.status } : {},
@@ -4005,103 +4009,425 @@ function createMissionWorkflowState(location, missionId, options = {}) {
4005
4009
  }
4006
4010
 
4007
4011
  // src/missions/actions.ts
4012
+ import * as fs11 from "node:fs";
4013
+ import * as path12 from "node:path";
4014
+
4015
+ // src/missions/lifecycle.ts
4008
4016
  import * as fs10 from "node:fs";
4009
4017
  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(", ")}`);
4018
+ import { createHash as createHash4 } from "node:crypto";
4019
+ var MISSION_BINDING_FILE = "mission.json";
4020
+ function workflowObjective(params) {
4021
+ const objective = params.task?.trim() || params.tasks?.find((task) => task.task?.trim())?.task?.trim() || params.chain?.find((step) => step.task?.trim())?.task?.trim();
4022
+ if (objective) return objective;
4023
+ for (const step of params.chain ?? []) {
4024
+ const parallel = Array.isArray(step.parallel) ? step.parallel : step.parallel ? [step.parallel] : [];
4025
+ const task = parallel.find((child) => child.task?.trim())?.task?.trim();
4026
+ if (task) return task;
4028
4027
  }
4029
- return value;
4028
+ return void 0;
4030
4029
  }
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`);
4030
+ function prepareMissionLaunch(input) {
4031
+ const hasMissionId = input.params.missionId !== void 0;
4032
+ if (hasMissionId && input.params.mission !== void 0) throw new Error("Use missionId or mission, not both");
4033
+ if (input.params.mission === false) return void 0;
4034
+ const objective = workflowObjective(input.params);
4035
+ const missionsEnabled = input.config?.enabled !== false;
4036
+ const shouldCreate = input.params.mission !== void 0 || missionsEnabled && objective !== void 0;
4037
+ if (!hasMissionId && !shouldCreate) return void 0;
4038
+ const location = resolveMissionStoreLocation({ projectRoot: input.projectRoot, ...input.config ? { config: input.config } : {} });
4039
+ if (hasMissionId) {
4040
+ const missionId = validateMissionId(input.params.missionId);
4041
+ readMission(location, missionId);
4042
+ updateMission(location, missionId, { status: "active" });
4043
+ return { missionId, location, autoCreated: false, announceInContent: true };
4036
4044
  }
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");
4045
+ const mission = input.params.mission !== void 0 ? validateMissionLaunch(input.params.mission) : void 0;
4046
+ const promptDerivedObjective = mission?.objective ?? (mission ? mission.title : objective ? PROMPT_REDACTED : void 0);
4047
+ const title = mission?.title || PROMPT_REDACTED;
4048
+ const record = createMission(location, {
4049
+ title,
4050
+ objective: promptDerivedObjective || title,
4051
+ ...mission?.goal === true ? { goal: true } : {},
4052
+ ...mission?.budget ? { budget: mission.budget } : {},
4053
+ status: "active",
4054
+ ...mission?.labels ? { labels: mission.labels } : {},
4055
+ ...input.ownerSessionId ? { ownerSessionId: input.ownerSessionId } : {}
4056
+ }, /* @__PURE__ */ new Date(), input.config?.retainTerminal);
4057
+ return { missionId: record.id, location, autoCreated: input.params.mission === void 0, announceInContent: true };
4058
+ }
4059
+ function toolResultIsError(result) {
4060
+ return "isError" in result && result.isError === true;
4061
+ }
4062
+ function missionRunModeForResult(mode) {
4063
+ return mode === "management" ? "external" : mode;
4064
+ }
4065
+ function runStatusForResult(result) {
4066
+ if (result.details.asyncDir) return "active";
4067
+ if (result.details.results.some((child) => child.interrupted || child.detached)) return "paused";
4068
+ if (toolResultIsError(result) || result.details.results.some((child) => child.exitCode !== 0)) return "failed";
4069
+ return "completed";
4070
+ }
4071
+ var UNACCEPTED_ACCEPTANCE_STATUSES = /* @__PURE__ */ new Set(["pending", "claimed", "review-required", "rejected"]);
4072
+ function missionAcceptanceStatus(results) {
4073
+ const statuses = (Array.isArray(results) ? results : []).map((result) => result?.acceptance?.status).filter((status) => typeof status === "string" && status.trim() !== "");
4074
+ return statuses.find((status) => UNACCEPTED_ACCEPTANCE_STATUSES.has(status)) ?? statuses[0];
4075
+ }
4076
+ function finishedMissionStatus(record, autoCreated, runAcceptance) {
4077
+ if (autoCreated) return "completed";
4078
+ if (runAcceptance.some((run) => UNACCEPTED_ACCEPTANCE_STATUSES.has(run.status))) return "waiting";
4079
+ const reported = new Set(runAcceptance.map((run) => run.runId));
4080
+ return record.workflowChildren.some((child) => !reported.has(child.runId) && UNACCEPTED_ACCEPTANCE_STATUSES.has(child.acceptanceStatus)) ? "waiting" : "completed";
4081
+ }
4082
+ function missionStatusForRun(record, runId, runStatus, acceptanceStatus, autoCreated) {
4083
+ if (record.status === "completed" || record.status === "failed" || record.status === "cancelled") return record.status;
4084
+ if (runStatus === "active" || runStatus === "queued" || runStatus === "running") return "active";
4085
+ if (record.goal) return "active";
4086
+ if (runStatus === "paused") return "waiting";
4087
+ const otherActive = record.runs.some((run) => run.runId !== runId && (run.status === "active" || run.status === "queued" || run.status === "running"));
4088
+ if (otherActive) return "active";
4089
+ if (runStatus === "completed" || runStatus === "complete") return finishedMissionStatus(record, autoCreated, [{ runId, status: acceptanceStatus }]);
4090
+ if (runStatus === "stopped" || runStatus === "rejected" || runStatus === "cancelled") return "cancelled";
4091
+ return "failed";
4092
+ }
4093
+ function usageForResult(result) {
4094
+ const tokens = result.details.results.reduce((total, child) => total + child.usage.input + child.usage.output, 0);
4095
+ return tokens > 0 ? { tokens } : void 0;
4096
+ }
4097
+ function usageFromUnknown(value) {
4098
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
4099
+ const total = value.total;
4100
+ return Number.isSafeInteger(total) && total >= 0 ? { tokens: total } : void 0;
4101
+ }
4102
+ function artifactsForResult(result) {
4103
+ const artifacts = [];
4104
+ if (result.details.asyncDir) {
4105
+ artifacts.push(
4106
+ { kind: "status", path: path11.join(result.details.asyncDir, "status.json") },
4107
+ { kind: "other", path: path11.join(result.details.asyncDir, "events.jsonl"), description: "Lifecycle events" }
4108
+ );
4045
4109
  }
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");
4110
+ for (const child of result.details.results) {
4111
+ if (child.artifactPaths?.outputPath) artifacts.push({ kind: "output", path: child.artifactPaths.outputPath });
4112
+ if (child.savedOutputPath) artifacts.push({ kind: "output", path: child.savedOutputPath });
4113
+ if (child.transcriptPath) artifacts.push({ kind: "other", path: child.transcriptPath, description: "Child transcript" });
4114
+ if (child.structuredOutputPath) artifacts.push({ kind: "output", path: child.structuredOutputPath, description: "Structured output" });
4049
4115
  }
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
- };
4116
+ if (result.details.parallelHandoff?.path) artifacts.push({ kind: "manifest", path: result.details.parallelHandoff.path });
4117
+ return artifacts;
4057
4118
  }
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`);
4119
+ function firstText(result) {
4120
+ const text = result.content.find((item) => item.type === "text")?.text.trim();
4121
+ if (!text) return void 0;
4122
+ return text.length > 2e3 ? `${text.slice(0, 1997)}...` : text;
4123
+ }
4124
+ function persistedBinding(binding) {
4065
4125
  return {
4066
- kind: input.kind,
4067
- path: input.path,
4068
- ...typeof input.description === "string" ? { description: input.description } : {}
4126
+ schemaVersion: 1,
4127
+ missionId: binding.missionId,
4128
+ projectRoot: binding.location.projectRoot,
4129
+ missionDir: binding.location.missionDir,
4130
+ globalIndexDir: binding.location.globalIndexDir,
4131
+ writeGlobalIndex: binding.location.writeGlobalIndex,
4132
+ ...binding.location.retainTerminal !== void 0 ? { retainTerminal: binding.location.retainTerminal } : {},
4133
+ autoCreated: binding.autoCreated
4069
4134
  };
4070
4135
  }
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`);
4136
+ function writeMissionAsyncBinding(asyncDir, binding) {
4137
+ writePrivateAtomicJson(path11.join(asyncDir, MISSION_BINDING_FILE), persistedBinding(binding));
4138
+ }
4139
+ function attachMissionToLaunchResult(input) {
4140
+ const runId = input.result.details.runId ?? input.result.details.asyncId;
4141
+ if (!runId) {
4142
+ const current2 = readMission(input.binding.location, input.binding.missionId);
4143
+ const activeRunExists = current2.runs.some((run2) => run2.status === "active" || run2.status === "queued" || run2.status === "running");
4144
+ const mission2 = toolResultIsError(input.result) ? updateMission(input.binding.location, input.binding.missionId, {
4145
+ status: activeRunExists ? "active" : "failed",
4146
+ ...firstText(input.result) ? { summary: firstText(input.result) } : {}
4147
+ }) : current2;
4148
+ return {
4149
+ ...input.result,
4150
+ details: {
4151
+ ...input.result.details,
4152
+ missionId: input.binding.missionId,
4153
+ missionPath: missionRecordPath(input.binding.location, input.binding.missionId),
4154
+ mission: mission2
4155
+ }
4156
+ };
4083
4157
  }
4084
- try {
4085
- new URL(input.url);
4086
- } catch {
4087
- throw new Error(`missionUpdate.receipts[${index}].url must be an absolute URL`);
4158
+ const runStatus = runStatusForResult(input.result);
4159
+ const current = readMission(input.binding.location, input.binding.missionId);
4160
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
4161
+ const usage = usageForResult(input.result);
4162
+ const run = {
4163
+ runId,
4164
+ mode: missionRunModeForResult(input.result.details.mode),
4165
+ status: runStatus,
4166
+ startedAt,
4167
+ ...input.result.details.asyncDir ? { asyncDir: input.result.details.asyncDir } : {},
4168
+ ...input.result.details.results.length === 1 && input.result.details.results[0]?.agent ? { agent: input.result.details.results[0].agent } : {},
4169
+ ...runStatus !== "active" ? { completedAt: startedAt } : {},
4170
+ ...usage ? { usage } : {}
4171
+ };
4172
+ let mission = updateMission(input.binding.location, input.binding.missionId, {
4173
+ status: missionStatusForRun(current, runId, runStatus, missionAcceptanceStatus(input.result.details.results), input.binding.autoCreated),
4174
+ addRuns: [run],
4175
+ addArtifacts: artifactsForResult(input.result),
4176
+ ...firstText(input.result) && runStatus !== "active" ? { summary: firstText(input.result) } : {},
4177
+ ...input.result.details.results.length === 1 && input.result.details.results[0]?.acceptance ? { acceptance: input.result.details.results[0].acceptance } : {}
4178
+ });
4179
+ if (input.result.details.asyncDir) {
4180
+ writeMissionAsyncBinding(input.result.details.asyncDir, input.binding);
4181
+ const statusPath = path11.join(input.result.details.asyncDir, "status.json");
4182
+ if (fs10.existsSync(statusPath)) {
4183
+ try {
4184
+ const status = JSON.parse(fs10.readFileSync(statusPath, "utf-8"));
4185
+ if (typeof status.state === "string" && !["queued", "running"].includes(status.state)) {
4186
+ mission = syncMissionFromAsyncCompletion({
4187
+ runId,
4188
+ asyncDir: input.result.details.asyncDir,
4189
+ mode: input.result.details.mode,
4190
+ state: status.state,
4191
+ // A run that finished before this launch result was attached keeps its acceptance in the status steps.
4192
+ ...Array.isArray(status.steps) ? { results: status.steps.map((step) => ({ acceptance: step?.acceptance })) } : {},
4193
+ ...firstText(input.result) ? { summary: firstText(input.result) } : {}
4194
+ }) ?? mission;
4195
+ }
4196
+ } catch (error) {
4197
+ throw new Error(`Failed to reconcile mission from terminal async status '${statusPath}': ${error instanceof Error ? error.message : String(error)}`);
4198
+ }
4199
+ }
4088
4200
  }
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`);
4201
+ const lastTextIndex = input.result.content.findLastIndex((item) => item.type === "text");
4202
+ const hasStructuredOutput = input.result.details.results.some((child) => child.structuredOutputPath !== void 0);
4203
+ const textIsJson = lastTextIndex >= 0 && (() => {
4204
+ try {
4205
+ JSON.parse(input.result.content[lastTextIndex].text);
4206
+ return true;
4207
+ } catch {
4208
+ return false;
4209
+ }
4210
+ })();
4090
4211
  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() } : {}
4212
+ ...input.result,
4213
+ content: input.binding.announceInContent === true && lastTextIndex >= 0 && !hasStructuredOutput && !textIsJson ? input.result.content.map((item, index) => index === lastTextIndex && item.type === "text" ? { ...item, text: `${item.text}
4214
+ Mission: ${mission.id} (${mission.status})` } : item) : input.result.content,
4215
+ details: {
4216
+ ...input.result.details,
4217
+ missionId: mission.id,
4218
+ missionPath: missionRecordPath(input.binding.location, mission.id),
4219
+ mission
4220
+ }
4096
4221
  };
4097
4222
  }
4098
- function validateMissionUpdate(value) {
4099
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("missionUpdate must be an object");
4223
+ function parsePersistedBinding(value, source) {
4224
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${source} must be an object`);
4100
4225
  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`);
4226
+ if (input.schemaVersion !== 1) throw new Error(`${source}.schemaVersion must be 1`);
4227
+ for (const field of ["projectRoot", "missionDir", "globalIndexDir"]) {
4228
+ if (typeof input[field] !== "string" || !input[field].trim()) throw new Error(`${source}.${field} must be a non-empty string`);
4103
4229
  }
4104
- const update = {};
4230
+ if (typeof input.writeGlobalIndex !== "boolean") throw new Error(`${source}.writeGlobalIndex must be boolean`);
4231
+ if (input.retainTerminal !== void 0 && (!Number.isInteger(input.retainTerminal) || input.retainTerminal < 1)) throw new Error(`${source}.retainTerminal must be a positive integer`);
4232
+ if (input.autoCreated !== void 0 && typeof input.autoCreated !== "boolean") throw new Error(`${source}.autoCreated must be boolean`);
4233
+ return {
4234
+ missionId: validateMissionId(input.missionId, `${source}.missionId`),
4235
+ // Bindings saved before this field existed belong to runs that completed their mission without an acceptance check.
4236
+ autoCreated: input.autoCreated !== false,
4237
+ location: {
4238
+ projectRoot: input.projectRoot,
4239
+ missionDir: input.missionDir,
4240
+ globalIndexDir: input.globalIndexDir,
4241
+ writeGlobalIndex: input.writeGlobalIndex,
4242
+ ...input.retainTerminal !== void 0 ? { retainTerminal: input.retainTerminal } : {}
4243
+ }
4244
+ };
4245
+ }
4246
+ function readMissionBinding(asyncDir) {
4247
+ const bindingPath = path11.join(asyncDir, MISSION_BINDING_FILE);
4248
+ if (!fs10.existsSync(bindingPath)) return void 0;
4249
+ const raw = fs10.readFileSync(bindingPath, "utf-8");
4250
+ try {
4251
+ return parsePersistedBinding(JSON.parse(raw), bindingPath);
4252
+ } catch (error) {
4253
+ throw new Error(`Invalid mission binding file '${bindingPath}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
4254
+ }
4255
+ }
4256
+ function syncMissionFromAsyncCompletion(value) {
4257
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
4258
+ const event = value;
4259
+ if (typeof event.asyncDir !== "string" || !event.asyncDir.trim()) return void 0;
4260
+ const binding = readMissionBinding(event.asyncDir);
4261
+ if (!binding) return void 0;
4262
+ const runId = typeof event.runId === "string" ? event.runId : typeof event.id === "string" ? event.id : void 0;
4263
+ if (!runId) throw new Error("Async mission completion is missing runId");
4264
+ const runStatus = typeof event.state === "string" ? event.state : event.success === true ? "completed" : "failed";
4265
+ let current;
4266
+ try {
4267
+ current = readMission(binding.location, binding.missionId);
4268
+ } catch (error) {
4269
+ if (!(error instanceof MissionNotFoundError)) throw error;
4270
+ const reason = "mission-record-missing";
4271
+ const markerId = createHash4("sha256").update(JSON.stringify([runId, binding.missionId, reason])).digest("hex");
4272
+ const markerPath3 = path11.join(event.asyncDir, `.mission-sync-skipped-${markerId}.json`);
4273
+ try {
4274
+ fs10.writeFileSync(markerPath3, `${JSON.stringify({ runId, missionId: binding.missionId, reason })}
4275
+ `, { encoding: "utf-8", mode: 384, flag: "wx" });
4276
+ fs10.appendFileSync(path11.join(event.asyncDir, "events.jsonl"), `${JSON.stringify({
4277
+ type: "subagent.mission.sync.skipped",
4278
+ ts: Date.now(),
4279
+ runId,
4280
+ missionId: binding.missionId,
4281
+ reason,
4282
+ missionPath: missionRecordPath(binding.location, binding.missionId)
4283
+ })}
4284
+ `, "utf-8");
4285
+ } catch {
4286
+ }
4287
+ return void 0;
4288
+ }
4289
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
4290
+ const artifacts = [
4291
+ { kind: "status", path: path11.join(event.asyncDir, "status.json") },
4292
+ { kind: "other", path: path11.join(event.asyncDir, "events.jsonl"), description: "Lifecycle events" }
4293
+ ];
4294
+ if (event.parallelHandoff && typeof event.parallelHandoff === "object" && typeof event.parallelHandoff.path === "string") {
4295
+ artifacts.push({ kind: "manifest", path: event.parallelHandoff.path });
4296
+ }
4297
+ if (Array.isArray(event.results)) {
4298
+ for (const result of event.results) {
4299
+ if (!result || typeof result !== "object") continue;
4300
+ const child = result;
4301
+ if (typeof child.artifactPath === "string") artifacts.push({ kind: "output", path: child.artifactPath });
4302
+ if (child.artifactPaths && typeof child.artifactPaths === "object" && typeof child.artifactPaths.outputPath === "string") {
4303
+ artifacts.push({ kind: "output", path: child.artifactPaths.outputPath });
4304
+ }
4305
+ }
4306
+ }
4307
+ const summary = typeof event.summary === "string" && event.summary.trim() ? event.summary.slice(0, 2e3) : void 0;
4308
+ const usage = usageFromUnknown(event.totalTokens) ?? (Array.isArray(event.results) ? { tokens: event.results.reduce((total, result) => {
4309
+ if (!result || typeof result !== "object") return total;
4310
+ return total + (usageFromUnknown(result.tokens)?.tokens ?? 0);
4311
+ }, 0) } : void 0);
4312
+ const workflowRunId = typeof event.parentWorkflowRunId === "string" && event.parentWorkflowRunId.trim() ? event.parentWorkflowRunId.trim() : void 0;
4313
+ const workflowKey = typeof event.workflowKey === "string" && event.workflowKey.trim() ? event.workflowKey.trim() : void 0;
4314
+ const workflowChildStatus = runStatus === "complete" || runStatus === "completed" || event.success === true ? "completed" : runStatus === "paused" ? "paused" : runStatus === "stopped" ? "stopped" : "failed";
4315
+ const workflowChildTerminal = !["running", "queued", "active", "paused"].includes(workflowChildStatus);
4316
+ const acceptanceStatus = missionAcceptanceStatus(event.results);
4317
+ return updateMission(binding.location, binding.missionId, {
4318
+ status: missionStatusForRun(current, runId, runStatus, acceptanceStatus, binding.autoCreated),
4319
+ 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 } : {} }],
4320
+ addArtifacts: artifacts,
4321
+ ...workflowRunId && workflowKey ? { upsertWorkflowChildren: [{
4322
+ workflowRunId,
4323
+ key: workflowKey,
4324
+ runId,
4325
+ status: workflowChildStatus,
4326
+ artifactPaths: artifacts.map((artifact) => artifact.path),
4327
+ ...workflowChildTerminal ? { completedAt } : {},
4328
+ ...acceptanceStatus ? { acceptanceStatus } : {},
4329
+ heartbeat: { status: workflowChildStatus, ...summary ? { message: summary } : {} }
4330
+ }] } : {},
4331
+ ...summary ? { summary } : {}
4332
+ });
4333
+ }
4334
+
4335
+ // src/missions/actions.ts
4336
+ var MISSION_ACTIONS = [
4337
+ "mission.create",
4338
+ "mission.list",
4339
+ "mission.show",
4340
+ "mission.update",
4341
+ "mission.resolve-decision",
4342
+ "mission.attach-run",
4343
+ "mission.close"
4344
+ ];
4345
+ function textResult(text, details) {
4346
+ return { content: [{ type: "text", text }], details };
4347
+ }
4348
+ function requireMissionId(params) {
4349
+ return validateMissionId(params.missionId, "missionId");
4350
+ }
4351
+ function validateStatus(value, label) {
4352
+ if (typeof value !== "string" || !MISSION_STATUSES.includes(value)) {
4353
+ throw new Error(`${label} must be one of ${MISSION_STATUSES.join(", ")}`);
4354
+ }
4355
+ return value;
4356
+ }
4357
+ function validateMissionLaunch(value) {
4358
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("mission must be an object");
4359
+ const input = value;
4360
+ for (const key of Object.keys(input)) {
4361
+ if (key !== "title" && key !== "summary" && key !== "objective" && key !== "goal" && key !== "budget" && key !== "labels") throw new Error(`mission.${key} is unknown`);
4362
+ }
4363
+ if (input.title !== void 0 && input.summary !== void 0) throw new Error("mission.title and mission.summary cannot both be set");
4364
+ const title = input.title ?? input.summary;
4365
+ if (typeof title !== "string" || !title.trim()) throw new Error("mission.title or mission.summary must be a non-empty string");
4366
+ if (input.objective !== void 0 && (typeof input.objective !== "string" || !input.objective.trim())) throw new Error("mission.objective must be a non-empty string");
4367
+ if (input.goal !== void 0 && input.goal !== true) throw new Error("mission.goal must be true when supplied");
4368
+ const budget = input.budget;
4369
+ if (budget !== void 0 && (!budget || typeof budget !== "object" || Array.isArray(budget) || !Number.isSafeInteger(budget.tokens) || budget.tokens < 1)) {
4370
+ throw new Error("mission.budget.tokens must be a positive integer");
4371
+ }
4372
+ if (input.goal === true && budget === void 0) throw new Error("mission.budget is required when mission.goal is true");
4373
+ if (input.labels !== void 0 && (!Array.isArray(input.labels) || input.labels.some((label) => typeof label !== "string" || !label.trim()))) {
4374
+ throw new Error("mission.labels must contain only non-empty strings");
4375
+ }
4376
+ return {
4377
+ title: title.trim(),
4378
+ ...typeof input.objective === "string" ? { objective: input.objective.trim() } : {},
4379
+ ...input.goal === true ? { goal: true } : {},
4380
+ ...budget !== void 0 ? { budget: { tokens: budget.tokens } } : {},
4381
+ ...input.labels !== void 0 ? { labels: input.labels.map((label) => label.trim()) } : {}
4382
+ };
4383
+ }
4384
+ function validateArtifact(value, index) {
4385
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`missionUpdate.artifacts[${index}] must be an object`);
4386
+ const input = value;
4387
+ const kinds = ["status", "output", "patch", "manifest", "review", "note", "other"];
4388
+ if (typeof input.kind !== "string" || !kinds.includes(input.kind)) throw new Error(`missionUpdate.artifacts[${index}].kind is invalid`);
4389
+ if (typeof input.path !== "string" || !input.path.trim()) throw new Error(`missionUpdate.artifacts[${index}].path must be a non-empty string`);
4390
+ 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`);
4391
+ return {
4392
+ kind: input.kind,
4393
+ path: input.path,
4394
+ ...typeof input.description === "string" ? { description: input.description } : {}
4395
+ };
4396
+ }
4397
+ function validateReceipt(value, index) {
4398
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`missionUpdate.receipts[${index}] must be an object`);
4399
+ const input = value;
4400
+ for (const key of Object.keys(input)) {
4401
+ if (!["kind", "status", "title", "url", "description"].includes(key)) throw new Error(`missionUpdate.receipts[${index}].${key} is unknown`);
4402
+ }
4403
+ const kinds = ["pull_request", "ci", "deployment", "release"];
4404
+ const statuses = ["pending", "ready", "succeeded", "failed"];
4405
+ if (typeof input.kind !== "string" || !kinds.includes(input.kind)) throw new Error(`missionUpdate.receipts[${index}].kind is invalid`);
4406
+ if (typeof input.status !== "string" || !statuses.includes(input.status)) throw new Error(`missionUpdate.receipts[${index}].status is invalid`);
4407
+ for (const field of ["title", "url"]) {
4408
+ if (typeof input[field] !== "string" || !input[field].trim()) throw new Error(`missionUpdate.receipts[${index}].${field} must be a non-empty string`);
4409
+ }
4410
+ try {
4411
+ new URL(input.url);
4412
+ } catch {
4413
+ throw new Error(`missionUpdate.receipts[${index}].url must be an absolute URL`);
4414
+ }
4415
+ 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`);
4416
+ return {
4417
+ kind: input.kind,
4418
+ status: input.status,
4419
+ title: input.title.trim(),
4420
+ url: input.url.trim(),
4421
+ ...typeof input.description === "string" ? { description: input.description.trim() } : {}
4422
+ };
4423
+ }
4424
+ function validateMissionUpdate(value) {
4425
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("missionUpdate must be an object");
4426
+ const input = value;
4427
+ for (const key of Object.keys(input)) {
4428
+ if (!["title", "objective", "goal", "budget", "status", "summary", "labels", "artifacts", "receipts", "decisions"].includes(key)) throw new Error(`missionUpdate.${key} is unknown`);
4429
+ }
4430
+ const update = {};
4105
4431
  for (const field of ["title", "objective", "summary"]) {
4106
4432
  const candidate = input[field];
4107
4433
  if (candidate === void 0) continue;
@@ -4156,13 +4482,14 @@ function validateMissionUpdate(value) {
4156
4482
  }
4157
4483
  function refreshLinkedRunStatus(location, record) {
4158
4484
  const warnings = [];
4485
+ const runAcceptance = [];
4159
4486
  const updates = record.runs.flatMap((run) => {
4160
4487
  if (!run.asyncDir) return [];
4161
- const statusPath = path11.join(run.asyncDir, "status.json");
4162
- if (!fs10.existsSync(statusPath)) return [];
4488
+ const statusPath = path12.join(run.asyncDir, "status.json");
4489
+ if (!fs11.existsSync(statusPath)) return [];
4163
4490
  let status2;
4164
4491
  try {
4165
- status2 = JSON.parse(fs10.readFileSync(statusPath, "utf-8"));
4492
+ status2 = JSON.parse(fs11.readFileSync(statusPath, "utf-8"));
4166
4493
  } catch (error) {
4167
4494
  warnings.push(`Failed to read linked run status '${statusPath}': ${error instanceof Error ? error.message : String(error)}`);
4168
4495
  return [];
@@ -4176,6 +4503,7 @@ function refreshLinkedRunStatus(location, record) {
4176
4503
  warnings.push(`Linked run status '${statusPath}' is missing state`);
4177
4504
  return [];
4178
4505
  }
4506
+ runAcceptance.push({ runId: run.runId, status: missionAcceptanceStatus(status2.steps) });
4179
4507
  if (state === run.status) return [];
4180
4508
  return [{
4181
4509
  ...run,
@@ -4186,7 +4514,7 @@ function refreshLinkedRunStatus(location, record) {
4186
4514
  if (updates.length === 0) return { record, warnings };
4187
4515
  const merged = record.runs.map((run) => updates.find((update) => update.runId === run.runId && update.childIndex === run.childIndex) ?? run);
4188
4516
  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;
4517
+ 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
4518
  return { record: updateMission(location, record.id, { status, addRuns: updates }), warnings };
4191
4519
  }
4192
4520
  function formatMission(record) {
@@ -4210,7 +4538,8 @@ function formatMission(record) {
4210
4538
  const identity = child.runId ? `${child.key} (${child.runId})` : child.key;
4211
4539
  const heartbeat = child.heartbeat ? `; heartbeat ${child.heartbeat.status ?? child.status}${child.heartbeat.phase ? `/${child.heartbeat.phase}` : ""} at ${child.heartbeat.updatedAt}` : "";
4212
4540
  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}`);
4541
+ const acceptance = child.acceptanceStatus ? `; acceptance ${child.acceptanceStatus}` : "";
4542
+ lines.push(` ${identity}: ${child.status}${child.agent ? ` \u2014 ${child.agent}` : ""}${child.phase ? ` [${child.phase}]` : ""}${acceptance}; updated ${child.updatedAt}${heartbeat}${recovery}`);
4214
4543
  }
4215
4544
  }
4216
4545
  if (record.decisions.length) {
@@ -4331,308 +4660,6 @@ ${formatMission(record2)}`, { mode: "management", results: [], missionId: record
4331
4660
  }
4332
4661
  }
4333
4662
 
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
4663
  // src/runs/background/result-files.ts
4637
4664
  import { createHash as createHash5 } from "node:crypto";
4638
4665
  import * as fs12 from "node:fs";
@@ -7229,6 +7256,7 @@ export {
7229
7256
  MISSION_ACTIONS,
7230
7257
  handleMissionAction,
7231
7258
  prepareMissionLaunch,
7259
+ missionAcceptanceStatus,
7232
7260
  writeMissionAsyncBinding,
7233
7261
  attachMissionToLaunchResult,
7234
7262
  readMissionBinding,