sortie-dogs 0.13.10 → 0.13.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -194,6 +194,11 @@ host must reconcile any still-running process. Omission keeps native `command/ex
194
194
  executor never falls back to native execution after rejection or failure. Existing Mission before/after
195
195
  hooks, command identity and validation acceptance remain shared. CLI progress identifies the selected
196
196
  executor; the CLI itself does not attach a parent executor.
197
+ File writes use a short argv referencing a temporary UTF-8 payload, so large files and literal quotes,
198
+ newlines or NUL characters do not overflow Windows command-line limits. Staging does not write the
199
+ project file; the same configured executor performs that write, and the payload is removed afterward.
200
+ Status observations (without `confirmed_conditions`) can return during an active child Task. Condition
201
+ registration and other modifying tools remain serialized; status does not imply Task completion.
197
202
  The bash tool accepts `workdir` (absolute or relative to the project root). The shared hooks and both
198
203
  executors use that directory; progress and receipts include the effective cwd. A successful command in
199
204
  another directory does not satisfy a validation declared for the project root.
@@ -3,6 +3,6 @@
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
5
  export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
6
- export declare const V010_RUNTIME_ASSET_VERSION = "0.13.10-codex-windows-v1";
7
- export declare const CODEX_SKILL_ASSET_VERSION = "0.13.10-codex-skill-v2";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.13.11-operation-result-v1";
7
+ export declare const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
8
8
  export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION | typeof CODEX_SKILL_ASSET_VERSION;
@@ -3,5 +3,5 @@
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
5
  export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
6
- export const V010_RUNTIME_ASSET_VERSION = "0.13.10-codex-windows-v1";
7
- export const CODEX_SKILL_ASSET_VERSION = "0.13.10-codex-skill-v2";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.13.11-operation-result-v1";
7
+ export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
@@ -59,6 +59,7 @@ export declare class CodexMissionSession {
59
59
  private observations;
60
60
  private readonly ownedRoots;
61
61
  private readonly toolQueues;
62
+ private readonly observationsInFlight;
62
63
  readonly directory: string;
63
64
  private constructor();
64
65
  static create(options: CodexMissionSessionOptions): Promise<CodexMissionSession>;
@@ -9,6 +9,7 @@ import { runtimeAssets } from "../runtime-assets-v010.js";
9
9
  import { OperatorRuntime } from "../core/operator-runtime.js";
10
10
  import { OperatorMissionRuntime, codexProcessOwner, codexOwnerGone } from "../core/operator-mission.js";
11
11
  import { resolveCodexMissionShell } from "./mission-shell.js";
12
+ import { prepareCodexWrite } from "./mission-write.js";
12
13
  const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
13
14
  /** Native Codex transport for the existing Mission tools, roles and acceptance state. */
14
15
  export class CodexMissionSession {
@@ -24,6 +25,7 @@ export class CodexMissionSession {
24
25
  observations = [];
25
26
  ownedRoots = new Set();
26
27
  toolQueues = new Map();
28
+ observationsInFlight = new Set();
27
29
  directory;
28
30
  constructor(options, shell) {
29
31
  this.options = options;
@@ -80,7 +82,7 @@ export class CodexMissionSession {
80
82
  }
81
83
  async close() {
82
84
  await this.stopHosts();
83
- await Promise.allSettled([...this.toolQueues.values()]);
85
+ await Promise.allSettled([...this.toolQueues.values(), ...this.observationsInFlight]);
84
86
  const missions = new OperatorMissionRuntime(this.directory, profile);
85
87
  for (const root of this.ownedRoots)
86
88
  await missions.codexRecovery(root, async (state) => {
@@ -269,6 +271,15 @@ export class CodexMissionSession {
269
271
  return await this.options.permissionsApproval?.(request) ?? { permissions: {}, scope: "turn" };
270
272
  },
271
273
  dynamicTool: call => {
274
+ // Observations must remain available while a Task awaits its child. Status
275
+ // with confirmed_conditions writes launch conditions and stays serialized.
276
+ if (call.tool === `${profile.toolPrefix}operator_status` && object(call.arguments) &&
277
+ legacyToolArgs(call.arguments, this.hooks.tool?.[call.tool]?.args ?? {}).confirmed_conditions === undefined) {
278
+ const observation = this.execute(call);
279
+ this.observationsInFlight.add(observation);
280
+ void observation.finally(() => this.observationsInFlight.delete(observation)).catch(() => undefined);
281
+ return observation;
282
+ }
272
283
  const execution = (this.toolQueues.get(call.threadId) ?? Promise.resolve()).catch(() => undefined).then(() => this.execute(call));
273
284
  this.toolQueues.set(call.threadId, execution);
274
285
  return execution;
@@ -530,6 +541,7 @@ export class CodexMissionSession {
530
541
  let dispatched = false;
531
542
  let child;
532
543
  let metadata;
544
+ let payload;
533
545
  try {
534
546
  if (call.tool === "task") {
535
547
  child = typeof args.task_id === "string" && args.task_id ? this.required(args.task_id) : undefined;
@@ -547,9 +559,11 @@ export class CodexMissionSession {
547
559
  metadata = { sessionId: child.id, status: result.status };
548
560
  }
549
561
  else {
562
+ if (call.tool === "write")
563
+ payload = await prepareCodexWrite(resolve(this.directory, String(args.filePath)), String(args.content));
550
564
  const command = call.tool === "bash" ? this.shell.command(String(args.command))
551
565
  : call.tool === "read" ? [process.execPath, "-e", "const fs=require('node:fs'),p=process.argv[1];process.stdout.write(fs.statSync(p).isDirectory()?fs.readdirSync(p,{withFileTypes:true}).map(e=>e.name+(e.isDirectory()?'/':'')).sort().join('\\n')+'\\n':fs.readFileSync(p,'utf8'))", resolve(this.directory, String(args.filePath))]
552
- : [process.execPath, "-e", "require('node:fs').writeFileSync(process.argv[1],process.argv[2])", resolve(this.directory, String(args.filePath)), String(args.content)];
566
+ : payload.command;
553
567
  const executor = this.options.executeCommand ? "host" : "native";
554
568
  await this.options.onEvent?.({ method: "sortie/commandExecution", threadId: session.id,
555
569
  params: { turnId: call.turnId, callId: call.callId, tool: call.tool, executor, cwd, status: "started" } });
@@ -610,6 +624,16 @@ export class CodexMissionSession {
610
624
  await this.stopHosts().catch(() => undefined);
611
625
  throw error;
612
626
  }
627
+ finally {
628
+ if (payload)
629
+ await payload.dispose().catch(async (error) => {
630
+ try {
631
+ await this.options.onEvent?.({ method: "sortie/payloadCleanup", threadId: session.id,
632
+ params: { callId: call.callId, status: "failed", reason: String(error) } });
633
+ }
634
+ catch { /* Cleanup reporting cannot replace the authoritative command outcome. */ }
635
+ });
636
+ }
613
637
  const time = { start, end: Date.now() };
614
638
  metadata.sortie_execution = time;
615
639
  session.history.push({ info: { id: call.callId, role: "assistant", sessionID: session.id, codexToolReceipt: true,
@@ -0,0 +1,5 @@
1
+ /** Stage transport data only. The configured executor still performs the project write. */
2
+ export declare function prepareCodexWrite(filePath: string, content: string): Promise<{
3
+ command: readonly string[];
4
+ dispose: () => Promise<void>;
5
+ }>;
@@ -0,0 +1,13 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { writeFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ /** Stage transport data only. The configured executor still performs the project write. */
6
+ export async function prepareCodexWrite(filePath, content) {
7
+ const payload = join(tmpdir(), `sortie-codex-write-${randomUUID()}`);
8
+ await writeFile(payload, content, { encoding: "utf8", flag: "wx" });
9
+ return {
10
+ command: [process.execPath, "-e", "const fs=require('node:fs');fs.writeFileSync(process.argv[1],fs.readFileSync(process.argv[2]))", filePath, payload],
11
+ dispose: () => rm(payload, { force: true }),
12
+ };
13
+ }
@@ -301,6 +301,11 @@ export declare function missionReviewAccepted(review: NonNullable<OperatorMissio
301
301
  /** A short native report, not a tag/hash-based second-review policy or an approval checklist. */
302
302
  export declare function missionSelfRecheckReport(text: string, source: string, hostBound?: boolean): Pick<MissionSelfRecheck, "unresolvedFindings" | "residualMajor"> | undefined;
303
303
  export declare function missionExecutionStatus(mission: OperatorMission): "not-required" | "not-started" | "running" | "execution-failed" | "executed";
304
+ /** A terminal failure is a collected result, not a successful operation. Required success
305
+ * remains in the declared formal checks and Operator's original-request comparison. */
306
+ export declare function missionExecutionComplete(mission: OperatorMission): boolean;
307
+ /** Projection of existing observations only; never another check or inferred success. */
308
+ export declare function missionOperationSummary(mission: OperatorMission): Record<string, unknown>;
304
309
  /** Observe the command's own terminal summary, never Worker/Reviewer prose. Scores are result data,
305
310
  * not process success. Unstructured commands retain their native exit semantics. */
306
311
  export declare function missionCommandOutcome(output: unknown, exit: unknown, status: unknown): {
@@ -116,14 +116,28 @@ export function missionExecutionStatus(mission) {
116
116
  if (mission.kind !== "operation")
117
117
  return "not-required";
118
118
  const execution = mission.execution;
119
- if (!execution || execution.observations.length === 0)
119
+ if (!execution || execution.commands.length === 0 || execution.observations.length === 0)
120
120
  return "not-started";
121
121
  const latest = execution.commands.map(command => [...execution.observations].reverse().find(item => item.command === command && item.directory === execution.directory));
122
122
  if (latest.some(item => item && !item.completedAt))
123
123
  return "running";
124
+ if (latest.some(item => !item || !["executed", "execution-failed"].includes(item.outcome ?? "")))
125
+ return "not-started";
124
126
  if (latest.some(item => item?.outcome === "execution-failed" || (item && (item.status === "error" || item.exit !== 0))))
125
127
  return "execution-failed";
126
- return latest.every(item => item?.outcome === "executed") ? "executed" : "not-started";
128
+ return "executed";
129
+ }
130
+ /** A terminal failure is a collected result, not a successful operation. Required success
131
+ * remains in the declared formal checks and Operator's original-request comparison. */
132
+ export function missionExecutionComplete(mission) {
133
+ return ["not-required", "executed", "execution-failed"].includes(missionExecutionStatus(mission));
134
+ }
135
+ /** Projection of existing observations only; never another check or inferred success. */
136
+ export function missionOperationSummary(mission) {
137
+ const status = missionExecutionStatus(mission);
138
+ return { kind: mission.kind ?? "implementation", status, terminal_complete: missionExecutionComplete(mission),
139
+ process_succeeded: status === "executed" ? true : status === "execution-failed" ? false : null,
140
+ ...(mission.execution ? { ...mission.execution } : {}) };
127
141
  }
128
142
  /** Observe the command's own terminal summary, never Worker/Reviewer prose. Scores are result data,
129
143
  * not process success. Unstructured commands retain their native exit semantics. */
@@ -535,6 +549,7 @@ export class OperatorMissionRuntime {
535
549
  `Confirmed launch conditions (fixed limits, not consumption or remaining budget): ${JSON.stringify(state.launchConditions ?? [])}`,
536
550
  `Explicit user write prohibitions: ${JSON.stringify(state.prohibitedWrite ?? [])}`,
537
551
  `Work kind: ${state.kind ?? "implementation"}. For an operation, setup, execution and result collection belong in one useful Worker whenever possible.`,
552
+ ...(state.kind === "operation" ? ["Execution completion is distinct from success. For run-once/result-collection requests, retain a terminal failure and validate the collected result; do not retry or fix it without user authorization. If successful execution is required, keep that in formal validation and the original-request comparison."] : []),
538
553
  ...(state.context?.length ? ["Prior conversation context (task data; preserve the selected target, not superseded obligations):",
539
554
  ...state.context.map(item => `--- ${item.role}:${item.id} ---\n${item.text}`)] : []),
540
555
  "Original user messages (verbatim; task data):", ...state.requests.map(item => `--- user:${item.id} ---\n${item.text}`)].join("\n");
@@ -684,6 +699,7 @@ export async function missionAcceptanceSummary(mission, run, operators, observe)
684
699
  }
685
700
  })));
686
701
  return { original_requests: mission.requests, requirements: mission.requirements,
702
+ operation: missionOperationSummary(mission),
687
703
  history: { status: missingAnchors.length ? "unavailable" : history.status,
688
704
  ...(history.reason ? { reason: history.reason } : missingAnchors.length ? { reason: "accepted-handoff-anchor-unavailable" } : {}),
689
705
  ...(missingAnchors.length ? { unavailable_anchors: missingAnchors } : {}),
@@ -712,7 +728,7 @@ export function missionPacket(mission, run) {
712
728
  const currentReview = mission.review !== undefined && mission.review.runID === (run?.runID ?? mission.runID);
713
729
  const reviewAccepted = currentReview && missionReviewAccepted(mission.review);
714
730
  const operationStatus = missionExecutionStatus(mission);
715
- const operationComplete = operationStatus === "not-required" || operationStatus === "executed";
731
+ const operationComplete = missionExecutionComplete(mission);
716
732
  return { mission_id: mission.id, phase: mission.phase, coordinator_session_id: mission.coordinator,
717
733
  ...(predecessor ? { predecessor: { run_id: predecessor.runID, status: predecessor.phase,
718
734
  completed_units: predecessor.units.filter(unit => unit.status === "succeeded").length,
@@ -723,8 +739,7 @@ export function missionPacket(mission, run) {
723
739
  delivery_observation: mission.deliveryObservation?.run_id === (run?.runID ?? mission.runID) ? mission.deliveryObservation : null,
724
740
  submission: mission.submission, progress: mission.progress, consultations: mission.consultations ?? [],
725
741
  attempts: mission.attempts ?? [], corrections: (mission.corrections ?? []).map(({ findings: _findings, initialPrompt: _prompt, ...item }) => item), ...(mission.rescue ? { rescue: mission.rescue } : {}),
726
- operation: { kind: mission.kind ?? "implementation", status: missionExecutionStatus(mission),
727
- ...(mission.execution ? { ...mission.execution } : {}) },
742
+ operation: missionOperationSummary(mission),
728
743
  execution_summary: { completed_units: run?.units.filter(unit => unit.status === "succeeded").length ?? 0,
729
744
  running_units: run?.units.filter(unit => unit.status === "running").map(unit => ({ id: unit.unit.id, title: unit.unit.title, child_session_id: unit.childSessionID })) ?? [],
730
745
  pending_units: run?.units.filter(unit => unit.status === "pending").length ?? 0,
@@ -13,7 +13,7 @@ import { decoratePreviewHeadings, returnReportPanel } from "./receipt-presentati
13
13
  import { sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
14
14
  import { normalizeCommand, canonicalDeclaredValidationMembers, extractWritePaths } from "./gate.js";
15
15
  import { normalizeExecutionScope, normalizeManifestScope, normalizeRelativePath } from "../core/path.js";
16
- import { OperatorMissionRuntime, missionAcceptanceSummary, missionPacket, missionPlan, missionReviewAccepted, missionReviewIndependent, missionReviewScope, missionReviewTask, missionCommandOutcome, missionConversationContext, missionExecutionStatus, missionValidationCommand, missionReviewTraces, missionReviewVerdict, missionSelfRecheckReport } from "../core/operator-mission.js";
16
+ import { OperatorMissionRuntime, missionAcceptanceSummary, missionPacket, missionPlan, missionReviewAccepted, missionReviewIndependent, missionReviewScope, missionReviewTask, missionCommandOutcome, missionConversationContext, missionExecutionComplete, missionExecutionStatus, missionOperationSummary, missionValidationCommand, missionReviewTraces, missionReviewVerdict, missionSelfRecheckReport } from "../core/operator-mission.js";
17
17
  import { publishMissionProgress } from "./mission-progress.js";
18
18
  import { completedMissionReviewPrompts, initialMissionReviewPrompt, missionDeliveryObservation, missionReviewBaseline, missionReviewSource, observedMissionValidation, observedMissionValidationSummary, missionReviewValidation, reviewerCorrectionValidation, reviewerCorrectionValidationFresh, validationDirectoryMatches } from "./mission-review.js";
19
19
  import { missionLocations, missionLocationPacket } from "./mission-location.js";
@@ -983,6 +983,12 @@ export function createProfiledPlugin(profile, assetVersion) {
983
983
  taskOwners.delete(mission.callID);
984
984
  return reconciled;
985
985
  }
986
+ function withMissionOperationOutcome(text, mission) {
987
+ if (!mission || missionExecutionStatus(mission) !== "execution-failed" || terminalRunOutcome(text) !== "DONE")
988
+ return text;
989
+ const detail = "Operation result: execution-failed (result accepted; process did not succeed).";
990
+ return text.includes(detail) ? text : text.replace(/^((?:[ \t]*\r?\n)*[^\r\n]+)/u, `$1 ${detail}`);
991
+ }
986
992
  function missionDispatchPacket(mission, run) {
987
993
  const packet = { ...missionPacket(mission, run), project_root: input.directory,
988
994
  coordinator_dispatch: mission.dispatchOpen ? "active" : ["completed", "cancelled"].includes(mission.phase) ? "terminal" : "resumable" };
@@ -1006,7 +1012,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1006
1012
  next_action: "Fast-lane: plan one useful Worker unit with the meaningful formal check known from user, project or task context and estimated scope, then dispatch its Task immediately. Investigation/edit/check/requested commit belong inside Worker before independent Review. Objective: target 2000 characters; original requests are supplied separately. Use Coordinator for an unknown check or real unit decomposition." };
1007
1013
  }
1008
1014
  if (mission.coordinator === null && mission.runID === run?.runID && !mission.dispatchOpen &&
1009
- run?.phase === "awaiting-acceptance" && mission.kind === "operation" && missionExecutionStatus(mission) !== "executed") {
1015
+ run?.phase === "awaiting-acceptance" && mission.kind === "operation" && !missionExecutionComplete(mission)) {
1010
1016
  return { ...packet, task: missions.task(mission),
1011
1017
  next_action: "Fast-lane operation is not executed. Dispatch this same mission's Coordinator Task to finish or report its actual blocker; a setup or validation success is not operation completion." };
1012
1018
  }
@@ -1442,6 +1448,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1442
1448
  budget_remaining_units: budget?.remaining_units ?? null,
1443
1449
  next_action: typeof packet.next_action === "string" ? packet.next_action : null,
1444
1450
  ...(record(packet.acceptance_summary) ? { observations: {
1451
+ operation: packet.acceptance_summary.operation,
1445
1452
  formal_validation: packet.acceptance_summary.formal_validation,
1446
1453
  native_declared_validation: packet.acceptance_summary.native_declared_validation,
1447
1454
  worker_terminals: (mission.attempts ?? []).map(attempt => ({ run_id: attempt.runID, status: attempt.status, terminal: attempt.terminal ?? null })),
@@ -1627,7 +1634,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1627
1634
  await requireRoot(context.sessionID);
1628
1635
  const state = await operators.required(context.sessionID);
1629
1636
  let mission = await missions.read(context.sessionID);
1630
- if (mission?.kind === "operation" && missionExecutionStatus(mission) !== "executed")
1637
+ if (mission?.kind === "operation" && !missionExecutionComplete(mission))
1631
1638
  return JSON.stringify({
1632
1639
  status: "not-ready", operation_status: missionExecutionStatus(mission),
1633
1640
  next_action: "The requested operation has not completed. Preserve its real outcome; auxiliary checks and a bounded review do not complete it. Continue the declared operation or report its blocker.",
@@ -1658,7 +1665,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1658
1665
  const reviewGaps = missionReportReview(mission, state.runID) === "evidence-gaps"
1659
1666
  ? mission.review.result?.trim() || "独立Reviewの未解決証拠あり" : undefined;
1660
1667
  const gapSummary = reviewGaps?.replace(/^EVIDENCE_GAPS\s*/u, "").replace(/\s+/gu, " ").slice(0, 500);
1661
- const text = `✅ **DONE** \`${state.runID}\` — declared checks passed; Operator accepted the result.\n\n` +
1668
+ const text = withMissionOperationOutcome(`✅ **DONE** \`${state.runID}\` — declared checks passed; Operator accepted the result.`, mission) + `\n\n` +
1662
1669
  `**変更点:** ${state.units.map(unit => unit.unit.title).join("; ")}\n\n` +
1663
1670
  `**確認結果:** 宣言検証合格 — ${[...new Set(state.units.flatMap(unit => unit.unit.validation))].join("; ")}` +
1664
1671
  (mission?.review ? `\nレビュー: ${mission.review.verdict === "self-rechecked" ? "修正著者の自己再確認(独立PASSではない)" : mission.review.verdict === "evidence-gaps" ? "証拠不足を残して受入れ(レビューPASSではない)" : mission.review.verdict}.` : "") +
@@ -1670,6 +1677,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1670
1677
  panel = returnReportPanel(rendered);
1671
1678
  }
1672
1679
  return JSON.stringify({ status: result.status, run_id: state.runID,
1680
+ ...(mission?.kind === "operation" ? { operation: missionOperationSummary(mission) } : {}),
1673
1681
  acceptance_fingerprint: state.acceptanceFingerprint, receipt: result.receipt ?? null,
1674
1682
  ...(result.receipt?.status === "succeeded" && missionReportReview(mission, state.runID) === "evidence-gaps"
1675
1683
  ? { review_evidence_gaps: mission.review.result ?? null } : {}),
@@ -2032,9 +2040,13 @@ export function createProfiledPlugin(profile, assetVersion) {
2032
2040
  next_action: "Read operator_status: if the Coordinator dispatch is active, continue it; otherwise dispatch the returned task to resume the same Coordinator. Retain requirements, spend and candidate; no new mission or plan approval." });
2033
2041
  });
2034
2042
  } };
2035
- tools[startMission] = { description: `Operator: save current requirements; the host retains the original request verbatim. With a meaningful formal check known from user, project or task context and one useful unit, include unit NOW and dispatch its Worker directly; plan_units remains available after start. A literal command in the original request is not required. Worker owns investigation/edit/check/requested commit before independent Review; no routine preparation or commit-only handoff. ${MISSION_GIT_SCOPE} Unknown checks or real unit decomposition use Coordinator. intent=replace follows an actual user requirement change, retaining unchanged constraints and cumulative spend; for mission-source-reconciliation-required use the saved requirements when they reflect that change. intent=new is separate work in another location.`,
2043
+ const missionExecutionSchema = { type: "object", properties: { commands: stringList, directory: { type: "string" } },
2044
+ required: ["commands", "directory"], additionalProperties: false,
2045
+ description: "For an operation: the actual run/grade commands, not a preflight or NO_START check. Host observes their native shell completion separately from successful exits; no handwritten proof file is needed. For run-once/result collection, validate the collected result without rerunning the operation.", "x-sortie-optional": true };
2046
+ tools[startMission] = { description: `Operator: save current requirements; the host retains the original request verbatim. With a meaningful formal check known from user, project or task context and one useful unit, include unit NOW (and execution for an operation) and dispatch its Worker directly; plan_units remains available after start. A literal command in the original request is not required. Worker owns investigation/edit/check/requested commit before independent Review; no routine preparation or commit-only handoff. ${MISSION_GIT_SCOPE} Unknown checks or real unit decomposition use Coordinator. intent=replace follows an actual user requirement change, retaining unchanged constraints and cumulative spend; for mission-source-reconciliation-required use the saved requirements when they reflect that change. intent=new is separate work in another location.`,
2036
2047
  args: { requirements: { ...stringList, minItems: 1, maxItems: 64 },
2037
2048
  unit: { ...missionUnitSchema, description: "When the meaningful check and single-unit scope are already known, include this unit now. Returns its configured Worker directly, combining start_mission and plan_units without another model round trip.", "x-sortie-optional": true },
2049
+ execution: missionExecutionSchema,
2038
2050
  confirmed_conditions: conditionsSchema,
2039
2051
  prohibited_write: { ...stringList, description: "Only explicit path prohibitions from the user or applicable instructions. Never infer a parent glob from project/repository names, semantic 'do not modify the product', or the complement of estimated unit.write. Preserve the authorized clone and exact prohibited paths; semantic constraints stay in requirements.", "x-sortie-optional": true },
2040
2052
  kind: { type: "string", enum: ["implementation", "operation"], description: "Use operation for running an existing benchmark, command or procedure. The host records its actual execution separately from setup and checks.", "x-sortie-optional": true },
@@ -2123,10 +2135,16 @@ export function createProfiledPlugin(profile, assetVersion) {
2123
2135
  if (baseline)
2124
2136
  mission = await missions.update(context.sessionID, state => { state.reviewBaseline = baseline; });
2125
2137
  }
2126
- if (args.unit !== undefined && mission.runID === null && !mission.dispatchOpen && mission.kind !== "operation") {
2127
- const planned = JSON.parse(await declareMissionUnits(context.sessionID, context.sessionID, mission, [args.unit]));
2138
+ if (args.unit !== undefined && mission.runID === null && !mission.dispatchOpen &&
2139
+ (mission.kind !== "operation" || args.execution !== undefined || mission.execution)) {
2140
+ const planned = JSON.parse(await declareMissionUnits(context.sessionID, context.sessionID, mission, [args.unit], undefined, args.execution));
2128
2141
  return JSON.stringify({ ...locationObservation(context.sessionID), mission_id: mission.id, requirements: mission.requirements, ...planned });
2129
2142
  }
2143
+ if (args.execution !== undefined && mission.runID === null && !mission.dispatchOpen) {
2144
+ const operation = declaredMissionExecution(mission, args.execution);
2145
+ if (operation)
2146
+ mission = await missions.update(context.sessionID, state => { state.kind = "operation"; state.execution = operation; });
2147
+ }
2130
2148
  return JSON.stringify({ ...locationObservation(context.sessionID), ...(mission.dispatchOpen
2131
2149
  ? missionDispatchPacket(mission, await operators.read(context.sessionID))
2132
2150
  : { mission_id: mission.id, requirements: mission.requirements, task: missions.task(mission),
@@ -2267,6 +2285,19 @@ export function createProfiledPlugin(profile, assetVersion) {
2267
2285
  obligations: policy.proposal.obligations }, run_id: prepared.runID, unit_id: unitID,
2268
2286
  task: record(next) ? next.task : undefined, budget: await control.currentBudget(root) });
2269
2287
  } };
2288
+ function declaredMissionExecution(mission, execution) {
2289
+ const operation = mission.execution;
2290
+ // Optional empty operation fields must not turn a normal edit into an operation.
2291
+ if (execution === undefined || mission.kind === "implementation" && record(execution) &&
2292
+ Array.isArray(execution.commands) && execution.commands.length === 0)
2293
+ return operation;
2294
+ if (!record(execution) || typeof execution.directory !== "string" || !execution.directory.trim() ||
2295
+ !Array.isArray(execution.commands) || execution.commands.length === 0 || execution.commands.length > 16 ||
2296
+ !execution.commands.every(command => typeof command === "string" && command.trim()))
2297
+ throw new Error("mission-operation-input: supply the actual operation commands and their working directory");
2298
+ const commands = [...new Set(execution.commands.map(missionValidationCommand).map(normalizeCommand))];
2299
+ return { commands, directory: resolve(input.directory, execution.directory), observations: operation?.observations ?? [] };
2300
+ }
2270
2301
  async function declareMissionUnits(root, actor, mission, raw, reason, execution, planningCallID) {
2271
2302
  await control.currentBudget(root);
2272
2303
  mission = await missions.required(root);
@@ -2281,21 +2312,11 @@ export function createProfiledPlugin(profile, assetVersion) {
2281
2312
  `requirements to Operator. Operator can relink this mission in place via ${startMission} intent=replace ` +
2282
2313
  `with those exact requirements when they match the user's changed scope; no Worker can start before that decision.` });
2283
2314
  mission = await retainCancelledMissionAcceptance(root, mission, previous);
2284
- let operation = mission.execution;
2315
+ const operation = declaredMissionExecution(mission, execution);
2285
2316
  // Models can serialize an optional operation field as an empty object for a normal edit.
2286
2317
  // Do not turn an implementation mission into an operation (or block its first Worker).
2287
2318
  const emptyImplementationExecution = mission.kind === "implementation" && record(execution) &&
2288
2319
  Array.isArray(execution.commands) && execution.commands.length === 0;
2289
- if (execution !== undefined && !emptyImplementationExecution) {
2290
- if (!record(execution) || typeof execution.directory !== "string" || !execution.directory.trim() ||
2291
- !Array.isArray(execution.commands) || execution.commands.length === 0 || execution.commands.length > 16 ||
2292
- !execution.commands.every(command => typeof command === "string" && command.trim()))
2293
- throw new Error("mission-operation-input: supply the actual operation commands and their working directory");
2294
- const commands = [...new Set(execution.commands.map(missionValidationCommand).map(normalizeCommand))];
2295
- const directory = resolve(input.directory, execution.directory);
2296
- // A corrected command is allowed on replan; retain prior observations for accounting.
2297
- operation = { commands, directory, observations: operation?.observations ?? [] };
2298
- }
2299
2320
  const plan = missionPlan(mission, raw, input.directory);
2300
2321
  assertMissionWritePaths(mission, plan.units.flatMap(unit => unit.write));
2301
2322
  if (actor === root && (mission.coordinator !== null || plan.units.length !== 1))
@@ -2462,9 +2483,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2462
2483
  }
2463
2484
  tools[planUnits] = { description: `Coordinator or single-unit Fast-lane Operator: declare useful work with estimated read/write scope and meaningful formal checks. executor=self starts implementation/correction and formal validation HERE without a Worker or handoff read; otherwise dispatch the returned configured Worker promptly. Keep investigation/edit/check/requested commit with the same author before independent Review, not a commit-only handoff. ${MISSION_GIT_SCOPE} Native scope reconciliation/expand_unit retain host permissions and explicit path prohibitions. Objective is the target or corrective delta: aim for 2000 characters; the full original request/public reproduction is supplied separately, not copied here. Oversized unit instructions are retained verbatim in handoff Mission context, not rejected for another planning round. Keep every requirement covered. Final validation proves the unit; empty or dummy checks do not qualify. Diagnostics need no registration. write: [] is read-only; dir/** and native absolute paths support actual outputs. reason replans settled work, or ends your own quiescent direct unit without acceptance to correct its declaration, in the same requirements/budget; same-scope failed validation uses retry_mission_unit. Reviewer FINDINGS stay with the same Reviewer for correction, formal validation and self-recheck.`,
2464
2485
  args: { units: { type: "array", minItems: 1, maxItems: 32, items: missionUnitSchema },
2465
- execution: { type: "object", properties: { commands: stringList, directory: { type: "string" } },
2466
- required: ["commands", "directory"], additionalProperties: false,
2467
- description: "For an operation: the actual run/grade commands, not a preflight or NO_START check. Host observes their native shell completion; no handwritten proof file is needed.", "x-sortie-optional": true },
2486
+ execution: missionExecutionSchema,
2468
2487
  reason: { type: "string", "x-sortie-optional": true },
2469
2488
  executor: { type: "string", enum: ["worker", "self"], description: "Use self to implement/correct and formally validate here without a Worker handoff. Default worker retains configured Worker routing.", "x-sortie-optional": true } }, execute: async (args, context) => {
2470
2489
  const { root, mission } = await missionAuthority(context.sessionID);
@@ -2870,7 +2889,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2870
2889
  args: { status: { type: "string", enum: ["ready", "needs-decision", "blocked"] }, summary: stringSchema }, execute: async (args, context) => {
2871
2890
  const { root, mission } = await missionAuthority(context.sessionID);
2872
2891
  if (args.status === "ready") {
2873
- if (mission.kind === "operation" && missionExecutionStatus(mission) !== "executed") {
2892
+ if (mission.kind === "operation" && !missionExecutionComplete(mission)) {
2874
2893
  return JSON.stringify({ ...missionPacket(mission, await operators.read(root)), status: "operation-incomplete",
2875
2894
  next_action: missionExecutionStatus(mission) === "running"
2876
2895
  ? "The declared operation is already running. Inspect its native shell/progress; do not start another Worker or run. Wait for a terminal result, or report the existing run as blocked if its completion cannot be observed."
@@ -2890,7 +2909,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2890
2909
  ...missionPacket(updated, submittedRun),
2891
2910
  next_action: "Return this result and acceptance_summary to Operator now. Compare the original requests with the candidate and actual evidence; inspect concrete gaps only, then complete_mission if satisfied. No routine archive search or full source reread. Completion still requires its final comparison and complete_mission receipt." });
2892
2911
  } };
2893
- tools[completeMission] = { description: "Operator only: after comparing the original request, source, actual checks and current review disposition (explicit native author self-recheck is not independent PASS), explicitly accept the whole mission. Returns the measured 🐾 report on success; never treat Worker start, CORRECTION_READY or one passing check as completion.",
2912
+ tools[completeMission] = { description: "Operator only: after comparing the original request, source, actual checks and current review disposition (explicit native author self-recheck is not independent PASS), explicitly accept the whole mission. A collected terminal failure can satisfy a run-once/report request, not a request for successful execution. Returns operation outcome separately from the measured 🐾 Mission receipt; never treat Worker start, CORRECTION_READY or one passing check as completion.",
2894
2913
  args: {}, execute: async (_args, context) => {
2895
2914
  await requireRoot(context.sessionID);
2896
2915
  await missions.required(context.sessionID);
@@ -2941,7 +2960,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2941
2960
  return;
2942
2961
  const run = await operators.read(sessionID);
2943
2962
  const mission = await missions.read(sessionID);
2944
- const text = await control.renderReturnReport(sessionID, decoratePreviewHeadings(part.text), goalFingerprint(receipt), run ? missionReportReview(mission, run.runID) : undefined, run ? missionReportReviewGaps(mission, run.runID) : undefined);
2963
+ const text = await control.renderReturnReport(sessionID, withMissionOperationOutcome(decoratePreviewHeadings(part.text), mission), goalFingerprint(receipt), run ? missionReportReview(mission, run.runID) : undefined, run ? missionReportReviewGaps(mission, run.runID) : undefined);
2945
2964
  if (text === undefined)
2946
2965
  return;
2947
2966
  if (text === part.text) {
@@ -3606,7 +3625,9 @@ export function createProfiledPlugin(profile, assetVersion) {
3606
3625
  }
3607
3626
  if (ownership?.mission) {
3608
3627
  const current = await missions.required(ownership.root);
3609
- if (current.callID !== request.callID) {
3628
+ // Native completion can arrive after cancellation/acceptance or an agent switch.
3629
+ // Close its historical owner without reviving the Mission or requiring revoked root authority.
3630
+ if (current.callID !== request.callID || ["completed", "cancelled"].includes(current.phase)) {
3610
3631
  taskOwners.delete(request.callID);
3611
3632
  return;
3612
3633
  }
@@ -3941,7 +3962,11 @@ export function createProfiledPlugin(profile, assetVersion) {
3941
3962
  const role = (await identity(request.sessionID)).role;
3942
3963
  if (role === "dog-operator" || !await rootFor(request.sessionID))
3943
3964
  return;
3944
- const forwardedText = role === "dog-coordinator" ? forwardTerminalText(output.text) : output.text;
3965
+ let forwardedText = role === "dog-coordinator" ? forwardTerminalText(output.text) : output.text;
3966
+ if (role === "dog-coordinator" && terminalRunOutcome(forwardedText) === "DONE" &&
3967
+ (await control.currentReceipt(request.sessionID))?.status === "succeeded") {
3968
+ forwardedText = withMissionOperationOutcome(forwardedText, await missions.read(request.sessionID));
3969
+ }
3945
3970
  // Ordinary Operator chat is not a request for a historical Mission return panel.
3946
3971
  if (role === "dog-coordinator" && terminalRunOutcome(forwardedText) === undefined)
3947
3972
  return;
@@ -54,6 +54,11 @@ not its exit; it cannot complete the declared operation. This does not restrict
54
54
  For a declared operation, do not append a tee pipeline, redirection or wrapper that was not declared. Save output separately
55
55
  when needed. Use that same run's terminal state and result for completion. If a run already started,
56
56
  inspect its progress and report a missing terminal observation instead of launching it again.
57
+ Execution completion is not successful execution. For a run-once/result-collection request, preserve
58
+ a terminal failure, validate the collected result and return it without a repair loop or another run.
59
+ Successful execution requirements stay in meaningful formal validation and Operator's comparison
60
+ with the original request. Mission acceptance must report the operation's exit/outcome separately;
61
+ a succeeded Mission receipt never changes a failed benchmark into a successful benchmark.
57
62
  `;
58
63
  function controls(profile, names) {
59
64
  return names.map(name => ` ${profile.toolPrefix}${name}: true`).join("\n");
@@ -114,7 +119,8 @@ quality threshold and explicit model/budget choice. Follow AGENTS.md and use the
114
119
  ${profile.toolPrefix}start_mission with intent: "replace" and the saved requirements. The cancelled
115
120
  Mission is archived; cumulative spend is retained. Dispatch only the returned Coordinator Task.
116
121
  2. If a meaningful formal check is known from the user, project or task context and the work fits one unit,
117
- include unit in start_mission to receive its Worker immediately in the same call. If already started,
122
+ include unit in start_mission to receive its Worker immediately in the same call. For a known operation,
123
+ include execution commands/directory alongside unit. If already started,
118
124
  call plan_units and dispatch its Worker. Source investigation, shell/environment checks,
119
125
  fix design and output inventory belong inside that Worker, not a routine Operator preflight.
120
126
  Use estimated read/write paths; native scope reconciliation and expand_unit cover actual outputs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.13.10",
3
+ "version": "0.13.11",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",