sortie-dogs 0.12.14 → 0.12.16

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.
@@ -3,5 +3,5 @@
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.12.14-worker-context-progress-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.12.16-operation-efficiency-v1";
7
7
  export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION;
@@ -3,4 +3,4 @@
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.12.14-worker-context-progress-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.12.16-operation-efficiency-v1";
@@ -8,11 +8,40 @@ export interface MissionRequest {
8
8
  id: string;
9
9
  text: string;
10
10
  }
11
+ export interface MissionContext extends MissionRequest {
12
+ role: "user" | "assistant";
13
+ }
14
+ export interface MissionEvidenceExcerpt {
15
+ path: string;
16
+ offset: number;
17
+ limit: number;
18
+ }
19
+ export interface MissionExecution {
20
+ commands: string[];
21
+ directory: string;
22
+ observations: {
23
+ command: string;
24
+ directory: string;
25
+ callID: string;
26
+ sessionID: string;
27
+ startedAt: string;
28
+ completedAt?: string;
29
+ exit?: number;
30
+ status?: "completed" | "error";
31
+ outcome?: "not-started" | "execution-failed" | "executed";
32
+ result?: Record<string, unknown>;
33
+ }[];
34
+ }
11
35
  export interface OperatorMission {
12
36
  version: "0.12";
13
37
  id: string;
14
38
  root: string;
15
39
  requests: MissionRequest[];
40
+ /** Prior public conversation context, not additional immutable requirements. */
41
+ context?: MissionContext[];
42
+ kind?: "implementation" | "operation";
43
+ /** Native shell observations of the requested operation, separate from auxiliary checks. */
44
+ execution?: MissionExecution;
16
45
  requirements: {
17
46
  id: string;
18
47
  text: string;
@@ -42,10 +71,13 @@ export interface OperatorMission {
42
71
  risk: string[];
43
72
  source: string;
44
73
  task: OperatorTask | null;
74
+ evidence?: MissionEvidenceExcerpt[];
45
75
  requestFingerprint?: string;
46
76
  verdict: "pending" | "PASS" | "findings" | "evidence-gaps" | "skipped-low-risk";
47
77
  result?: string;
48
78
  child?: string;
79
+ /** Completed, independent initial review for this mission, not merely an inherited child ID. */
80
+ initialPrompt?: string;
49
81
  /** Reviews on this mission that found only missing evidence; bounded by MISSION_EVIDENCE_GAP_REVIEW_LIMIT. */
50
82
  evidenceGapReviews?: number;
51
83
  };
@@ -59,6 +91,15 @@ export declare const MISSION_EVIDENCE_GAP_REVIEW_LIMIT = 2;
59
91
  export declare function missionReviewVerdict(text: string): "PASS" | "evidence-gaps" | "findings";
60
92
  /** Whether the recorded review permits submission and acceptance of the current candidate. */
61
93
  export declare function missionReviewAccepted(review: NonNullable<OperatorMission["review"]>): boolean;
94
+ export declare function missionExecutionStatus(mission: OperatorMission): "not-required" | "not-started" | "running" | "execution-failed" | "executed";
95
+ /** Observe the command's own terminal summary, never Worker/Reviewer prose. Scores are result data,
96
+ * not process success. Unstructured commands retain their native exit semantics. */
97
+ export declare function missionCommandOutcome(output: unknown, exit: unknown, status: unknown): {
98
+ outcome: "not-started" | "execution-failed" | "executed";
99
+ result?: Record<string, unknown>;
100
+ };
101
+ /** Bounded public text only: tool logs and private reasoning are not delegation context. */
102
+ export declare function missionConversationContext(messages: readonly Record<string, unknown>[]): MissionContext[];
62
103
  /** Durable user intent and dispatch ownership. Execution/evidence still belong to the v0.10 engine. */
63
104
  export declare class OperatorMissionRuntime {
64
105
  readonly projectRoot: string;
@@ -76,7 +117,10 @@ export declare class OperatorMissionRuntime {
76
117
  read(root: string): Promise<OperatorMission | undefined>;
77
118
  required(root: string): Promise<OperatorMission>;
78
119
  capture(root: string, request: MissionRequest): Promise<void>;
79
- start(root: string, requirements: unknown, replaceRequirements?: boolean): Promise<OperatorMission>;
120
+ start(root: string, requirements: unknown, replaceRequirements?: boolean, options?: {
121
+ kind?: OperatorMission["kind"];
122
+ context?: MissionContext[];
123
+ }): Promise<OperatorMission>;
80
124
  update(root: string, change: (state: OperatorMission) => void): Promise<OperatorMission>;
81
125
  /** Keep host-accepted criteria ahead of new text, including an exactly linked settled predecessor. */
82
126
  carryForward(root: string, missionID: string, acceptance: readonly string[], supersededRunID?: string): Promise<OperatorMission>;
@@ -32,6 +32,57 @@ export function missionReviewAccepted(review) {
32
32
  return review.verdict === "PASS" || review.verdict === "skipped-low-risk" ||
33
33
  (review.verdict === "evidence-gaps" && (review.evidenceGapReviews ?? 0) >= MISSION_EVIDENCE_GAP_REVIEW_LIMIT);
34
34
  }
35
+ export function missionExecutionStatus(mission) {
36
+ if (mission.kind !== "operation")
37
+ return "not-required";
38
+ const execution = mission.execution;
39
+ if (!execution || execution.observations.length === 0)
40
+ return "not-started";
41
+ const latest = execution.commands.map(command => [...execution.observations].reverse().find(item => item.command === command && item.directory === execution.directory));
42
+ if (latest.some(item => item && !item.completedAt))
43
+ return "running";
44
+ if (latest.some(item => item?.outcome === "execution-failed" || (item && (item.status === "error" || item.exit !== 0))))
45
+ return "execution-failed";
46
+ return latest.every(item => item?.outcome === "executed") ? "executed" : "not-started";
47
+ }
48
+ /** Observe the command's own terminal summary, never Worker/Reviewer prose. Scores are result data,
49
+ * not process success. Unstructured commands retain their native exit semantics. */
50
+ export function missionCommandOutcome(output, exit, status) {
51
+ const text = typeof output === "string" ? output.trim() : "";
52
+ let result;
53
+ for (const value of [text, ...text.split(/\r?\n/u).reverse().slice(0, 8)]) {
54
+ try {
55
+ const parsed = JSON.parse(value);
56
+ if (record(parsed)) {
57
+ result = parsed;
58
+ break;
59
+ }
60
+ }
61
+ catch { /* not a JSON summary */ }
62
+ }
63
+ const state = String(result?.status ?? "").toLowerCase().replaceAll("_", "-");
64
+ const summary = result ? Object.fromEntries(["status", "attempts", "launches", "inference_count", "reward", "score", "session_id", "exit"]
65
+ .filter(key => Object.hasOwn(result, key)).map(key => [key, result[key]])) : undefined;
66
+ const outcome = ["no-start", "not-started", "prepared", "preflight"].includes(state) || text === "NO_START" ||
67
+ [result?.attempts, result?.launches, result?.inference_count].some(value => value === 0) ? "not-started"
68
+ : status === "error" || exit !== 0 || ["execution-failed", "setup-failed", "route-failed"].includes(state)
69
+ ? "execution-failed" : "executed";
70
+ return { outcome, ...(summary ? { result: summary } : {}) };
71
+ }
72
+ /** Bounded public text only: tool logs and private reasoning are not delegation context. */
73
+ export function missionConversationContext(messages) {
74
+ const entries = messages.flatMap(message => {
75
+ const info = record(message.info) ? message.info : message;
76
+ if ((info.role !== "user" && info.role !== "assistant") || typeof info.id !== "string" || !Array.isArray(message.parts))
77
+ return [];
78
+ const text = message.parts.filter(record).filter(part => part.type === "text" && part.synthetic !== true && typeof part.text === "string")
79
+ .map(part => part.text).join("\n");
80
+ return text.trim() ? [{ id: info.id, role: info.role, text: text.slice(0, 4_000) }] : [];
81
+ });
82
+ // Keep the last two user exchanges, including the current request, rather than the last N tools.
83
+ const users = entries.flatMap((entry, i) => entry.role === "user" ? [i] : []);
84
+ return entries.slice(users.at(-2) ?? 0).slice(-8);
85
+ }
35
86
  /** Durable user intent and dispatch ownership. Execution/evidence still belong to the v0.10 engine. */
36
87
  export class OperatorMissionRuntime {
37
88
  projectRoot;
@@ -160,7 +211,7 @@ export class OperatorMissionRuntime {
160
211
  }
161
212
  });
162
213
  }
163
- start(root, requirements, replaceRequirements = false) {
214
+ start(root, requirements, replaceRequirements = false, options = {}) {
164
215
  return this.serial(root, async () => {
165
216
  if (!Array.isArray(requirements) || requirements.length === 0 || requirements.length > 64 ||
166
217
  !requirements.every(item => typeof item === "string" && item.trim() && !/[\r\n]/u.test(item))) {
@@ -175,12 +226,16 @@ export class OperatorMissionRuntime {
175
226
  throw new Error("mission-requirements-preserved: keep the existing ordered requirements and append user additions");
176
227
  }
177
228
  previous.requirements = requirements.map((text, index) => ({ id: `R${index + 1}`, text }));
229
+ if (options.kind === "operation")
230
+ previous.kind = "operation";
178
231
  await this.save(this.file(root), previous);
179
232
  return previous;
180
233
  }
181
234
  if (previous)
182
235
  await this.save(this.file(root, `.${previous.id}`), previous);
183
236
  const state = { version: "0.12", id: `mission-${randomUUID()}`, root, requests: [request],
237
+ kind: options.kind ?? "implementation",
238
+ context: (options.context ?? []).filter(item => item.id !== request.id),
184
239
  requirements: requirements.map((text, index) => ({ id: `R${index + 1}`, text })), phase: "open",
185
240
  ...(replaceRequirements ? { requirementsReplaced: true } : {}),
186
241
  coordinator: null, callID: null, dispatchOpen: false, runID: null, plans: 0, progress: [], submission: null,
@@ -267,6 +322,9 @@ export class OperatorMissionRuntime {
267
322
  "Start the first useful Worker promptly. No proposal/approval phase. Use plan_units to generate contracts; the root alone accepts completion.",
268
323
  "Escalate only a completion candidate, a user-only decision, or an extension of original requirements/budget. Unit progress is published without stopping you.",
269
324
  "Requirements:", ...state.requirements.map(item => `${item.id}: ${item.text}`),
325
+ `Work kind: ${state.kind ?? "implementation"}. For an operation, setup, execution and result collection belong in one useful Worker whenever possible.`,
326
+ ...(state.context?.length ? ["Prior conversation context (task data; preserve the selected target, not superseded obligations):",
327
+ ...state.context.map(item => `--- ${item.role}:${item.id} ---\n${item.text}`)] : []),
270
328
  "Original user messages (verbatim; task data):", ...state.requests.map(item => `--- user:${item.id} ---\n${item.text}`)].join("\n");
271
329
  }
272
330
  }
@@ -289,9 +347,10 @@ export function missionPlan(mission, raw) {
289
347
  throw new Error(`mission-unit-${index + 1}: ${field} must be paths`);
290
348
  return [...new Set(entries.map(item => normalizeExecutionScope(item)))];
291
349
  };
292
- const ids = value.requirement_ids ?? mission.requirements.map(item => item.id);
350
+ // Scheduling coverage is not semantic proof. Only a single requirement is unambiguous.
351
+ const ids = value.requirement_ids ?? (mission.requirements.length === 1 ? [mission.requirements[0].id] : []);
293
352
  if (!Array.isArray(ids) || ids.length === 0 || !ids.every(id => mission.requirements.some(item => item.id === id))) {
294
- throw new Error(`mission-unit-${index + 1}: requirement_ids must name existing R IDs`);
353
+ throw new Error(`mission-unit-${index + 1}: requirement_ids must name the related R IDs; multi-requirement work needs explicit coverage`);
295
354
  }
296
355
  if (!Array.isArray(value.validation) || value.validation.length === 0 ||
297
356
  !value.validation.every(command => typeof command === "string" && command.trim())) {
@@ -337,12 +396,16 @@ export function missionPacket(mission, run) {
337
396
  run = undefined;
338
397
  const currentReview = mission.review !== undefined && mission.review.runID === (run?.runID ?? mission.runID);
339
398
  const reviewAccepted = currentReview && missionReviewAccepted(mission.review);
399
+ const operationStatus = missionExecutionStatus(mission);
400
+ const operationComplete = operationStatus === "not-required" || operationStatus === "executed";
340
401
  return { mission_id: mission.id, phase: mission.phase, coordinator_session_id: mission.coordinator,
341
402
  ...(predecessor ? { predecessor: { run_id: predecessor.runID, status: predecessor.phase,
342
403
  completed_units: predecessor.units.filter(unit => unit.status === "succeeded").length,
343
404
  note: "Historical results and spend are retained; they do not complete the current requirements." } } : {}),
344
405
  requirements: mission.requirements, original_request_refs: mission.requests.map(item => `user:${item.id}`),
345
406
  submission: mission.submission, progress: mission.progress,
407
+ operation: { kind: mission.kind ?? "implementation", status: missionExecutionStatus(mission),
408
+ ...(mission.execution ? { ...mission.execution } : {}) },
346
409
  execution_summary: { completed_units: run?.units.filter(unit => unit.status === "succeeded").length ?? 0,
347
410
  running_units: run?.units.filter(unit => unit.status === "running").map(unit => ({ id: unit.unit.id, title: unit.unit.title, child_session_id: unit.childSessionID })) ?? [],
348
411
  pending_units: run?.units.filter(unit => unit.status === "pending").length ?? 0,
@@ -354,7 +417,7 @@ export function missionPacket(mission, run) {
354
417
  source_fingerprint: mission.review.source, reviewer_session_id: mission.review.child ?? null,
355
418
  result: mission.review.result ?? null, evidence_gap_reviews: mission.review.evidenceGapReviews ?? 0,
356
419
  evidence_gap_review_limit: MISSION_EVIDENCE_GAP_REVIEW_LIMIT, accepted: reviewAccepted,
357
- passed: currentReview && mission.review.verdict === "PASS", permits_submission: reviewAccepted } : null,
420
+ passed: currentReview && mission.review.verdict === "PASS", permits_submission: reviewAccepted && operationComplete } : null,
358
421
  ...(run ? { run_id: run.runID, status: run.phase, decision: run.decision,
359
422
  units: run.units.map(unit => ({ id: unit.unit.id, title: unit.unit.title, status: unit.status,
360
423
  child_session_id: unit.childSessionID, result_class: unit.resultClass, evidence: unit.evidence,
@@ -367,12 +430,14 @@ export function missionPacket(mission, run) {
367
430
  : run?.phase === "awaiting-decision" ? (run.units.some(unit => unit.dispatchDenial)
368
431
  ? "Coordinator: inspect units[].dispatch_denial before changing the plan. Correct only its diagnosed cause; do not repeat an unchanged refused Task or replan for a host-state mismatch. Report an unresolved runtime mismatch with the loaded runtime identity; preserve requirements and cumulative spend."
369
432
  : "Coordinator: correct the cause and call plan_units with the remaining work and all requirements; budget is cumulative.")
370
- : run?.phase === "awaiting-acceptance" ? (currentReview && mission.review?.verdict === "evidence-gaps" && !reviewAccepted
371
- ? "Coordinator: the Reviewer found only missing evidence. Supply it through review_mission traces (or at most one evidence-only unit) and re-review once; do not re-implement. At the evidence-gap limit, submit_mission ready with the gaps listed."
372
- : reviewAccepted
373
- ? "Coordinator: review permits submission. Submit the candidate with any remaining gaps; do not repeat passed validation or review. Operator performs final acceptance."
374
- : "Coordinator: address recorded findings or obtain the required independent review, then submit_mission. Operator compares all requirements with source/evidence before complete_mission.")
375
- : "Coordinator: continue the next useful unit within original requirements. Return only a completion candidate, user-only decision, or scope/budget extension." };
433
+ : run?.phase === "awaiting-acceptance" && !operationComplete
434
+ ? `Requested operation is ${operationStatus}. Continue the actual operation or report its blocker; auxiliary checks and review disposition cannot complete it.`
435
+ : run?.phase === "awaiting-acceptance" ? (currentReview && mission.review?.verdict === "evidence-gaps" && !reviewAccepted
436
+ ? "Coordinator: the Reviewer found only missing evidence. Supply focused original-file excerpts through review_mission evidence and traces; do not re-implement. At the evidence-gap limit, review ends with the gaps listed, not with execution proof."
437
+ : reviewAccepted
438
+ ? "Coordinator: review permits submission. Submit the candidate with any remaining gaps; do not repeat passed validation or review. Operator performs final acceptance."
439
+ : "Coordinator: address recorded findings or obtain the required independent review, then submit_mission. Operator compares all requirements with source/evidence before complete_mission.")
440
+ : "Coordinator: continue the next useful unit within original requirements. Return only a completion candidate, user-only decision, or scope/budget extension." };
376
441
  }
377
442
  /** Models forward a short capability, never recopy the host's evidence hashes and source packet. */
378
443
  export function missionReviewTask(mission) {
@@ -915,9 +915,9 @@ export class OperatorRuntime {
915
915
  "validation:", ...unit.validation.map(value => ` - ${value}`),
916
916
  mission ? "Read-only investigation commands are unrestricted. Use shell to reproduce and diagnose without asking for command registration. Keep all writes, including generated/transient outputs and cleanup, inside unit.write. Run formal validation exactly as listed, in order and in separate calls, so the host records its real result. If a write scope or formal check must change, return the precise change to your Coordinator; it can extend/redeclare immediately within the original requirements. Diagnostic success is not formal acceptance evidence."
917
917
  : "Execute validation in its declared order. Earlier entries may be approved generator, build, formatter, or exact cleanup commands required before canonical criterion tests. Every persistent or transient generator output must be declared in unit.write. Cleanup may remove only declared unit.write outputs and must be an explicit ordered command after generation and before post-commit or canonical validation; never add an ignore rule or remove an undeclared path. If any necessary command, input, output, or cleanup is missing, do not run an undeclared command or variant and do not use resume evidence tooling to invent permission; return a contract-repair decision.",
918
- "Preserve existing public API success and error return semantics unless acceptance explicitly changes them, and cover those compatibility boundaries in the declared validation.",
918
+ ...(mission ? [] : ["Preserve existing public API success and error return semantics unless acceptance explicitly changes them, and cover those compatibility boundaries in the declared validation."]),
919
919
  mission
920
- ? "Do not spawn nested subagents for consultation. Required technical or user decisions that must precede implementation belong to the root before dispatch; use the confirmed decisions and evidence declared in the unit objective and inputs. If a required prior decision is missing, return the exact contract gap to the parent instead of implementing dependent changes, inventing consent, or asking the user to repeat an already recorded decision. Independent SourceReview is a separate post-implementation and post-validation review: do not require it before implementation or formal validation, and do not stop this Task solely because it has not yet occurred. Complete the assigned implementation and all listed formal validation commands, then return the real evidence so the Coordinator can dispatch the independent review_mission with applicable risk_tags (for example, public-logic). Do not conduct or delegate that independent review yourself. Reflect supplied prior Reviewer FINDINGS within this unit's scope."
920
+ ? "Complete the assigned work and listed validation within this Task. For a known operation, proceed through setup, execution and result collection; preparation alone is not execution. Preserve existing public behavior when changing source. Do not spawn nested subagents. The Coordinator handles any applicable independent review after your return; review is not a prerequisite to execution. Return actual results, including failed or not-started operations, and any exact contract correction needed."
921
921
  : "Do not spawn nested subagents for consultation. Required consultations belong to the root before dispatch; use the confirmed decisions and evidence declared in the unit objective and inputs. If required consultation results or user decisions are missing, return the exact contract gap to the parent instead of attempting a deeper Task, inventing consent, or asking the user to repeat an already recorded decision.",
922
922
  ...(plan.git_lifecycle !== undefined && index === plan.units.length - 1 ? [
923
923
  `git_post_commit_validation: ${JSON.stringify(plan.git_lifecycle.post_commit_validation)}`,
@@ -28,6 +28,10 @@ function lineValue(prompt, key) {
28
28
  const values = [...prompt.matchAll(new RegExp(`^\\s*${escapedKey}\\s*:\\s*(.+?)\\s*$`, "gmu"))];
29
29
  return values.length === 1 ? values[0][1] : undefined;
30
30
  }
31
+ /** Keep an unambiguous, known Strategy trigger when Japanese sentence punctuation joins its prose. */
32
+ function normalizeAdvisorPrompt(prompt) {
33
+ return prompt.replace(/^([ \t]*strategy_trigger[ \t]*:[ \t]*(?:architecture-choice|cross-boundary-tradeoff|material-uncertainty))[。.][ \t]*(?=\S)/mu, "$1\n");
34
+ }
31
35
  function hasReviewEvidence(prompt) {
32
36
  if (lineValue(prompt, "canonical_validation_exit") !== "0")
33
37
  return false;
@@ -416,13 +420,17 @@ export class FastLaneController {
416
420
  return;
417
421
  }
418
422
  if (role === "dog-advisor") {
419
- const trigger = lineValue(prompt, "strategy_trigger");
423
+ const normalized = normalizeAdvisorPrompt(prompt);
424
+ const trigger = lineValue(normalized, "strategy_trigger");
420
425
  if (trigger === undefined || !STRATEGY_TRIGGER_SET.has(trigger)) {
421
426
  const error = new FastLaneDeniedError("ADVISOR_TRIGGER_REQUIRED");
422
- error.message += `; required: strategy_trigger: <${STRATEGY_TRIGGERS.join(" | ")}>; repair the request header, not runtime policy`;
427
+ error.message += `; required: strategy_trigger: <${STRATEGY_TRIGGERS.join(" | ")}> on its own line; ` +
428
+ "correct this Task header and redispatch the same consultation before proceeding, not runtime policy";
423
429
  throw error;
424
430
  }
425
- const basis = fallbackBasis(prompt);
431
+ if (normalized !== prompt)
432
+ args.prompt = normalized;
433
+ const basis = fallbackBasis(normalized);
426
434
  const dispatches = state.advisorRequests.get(basis) ?? 0;
427
435
  const retry = lineValue(prompt, "fallback_retry");
428
436
  if ((dispatches === 0 && retry !== undefined) || (dispatches === 1 && retry !== "true")) {
@@ -51,7 +51,7 @@ import { collectRunMetrics, createSortieResult, createGoalReport, insertRunMetri
51
51
  import { STABLE_RUNTIME_PROFILE } from "../core/runtime-profile.js";
52
52
  import { evidenceFromObservedExecution } from "../core/observed-goal-evidence.js";
53
53
  import { receiptBoundTerminalText } from "./receipt-presentation.js";
54
- import { protectedSnapshot, refreshProtectedSnapshot } from "./protected-snapshot.js";
54
+ import { operationInputSnapshot, protectedSnapshot, refreshProtectedSnapshot } from "./protected-snapshot.js";
55
55
  import { settledUnitUsage } from "./unit-usage.js";
56
56
  import { goalCompletionReadiness } from "./goal-completion.js";
57
57
  const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024, parallel: 512 * 1024 };
@@ -1477,10 +1477,15 @@ export const SortieDogsPlugin = async (input, options) => {
1477
1477
  }
1478
1478
  }
1479
1479
  }
1480
+ const mission = await new OperatorMissionRuntime(input.directory, runtimeProfile).read(root);
1481
+ const operationInputs = mission?.kind === "operation" && mission.execution?.commands.includes(rawCommand) &&
1482
+ resolve(input.directory, typeof args?.workdir === "string" ? args.workdir : ".") === mission.execution.directory
1483
+ ? await operationInputSnapshot(authorization.projectRoot, snapshot.binding).catch(() => undefined) : undefined;
1480
1484
  hostGoalExecutions.set(toolInput.callID, { root, projectRoot: authorization.projectRoot,
1481
1485
  sessionID: toolInput.sessionID,
1482
1486
  callID: toolInput.callID, tool: toolInput.tool, command: [rawCommand], startedAt: new Date().toISOString(),
1483
1487
  binding: snapshot.binding, source: snapshot.source, candidate: snapshot.candidate,
1488
+ ...(operationInputs === undefined ? {} : { operationInputs }),
1484
1489
  owner: validation?.request.owner ?? "worker", validation,
1485
1490
  ...(reusedEvidence === undefined ? {} : { reusedEvidence }) });
1486
1491
  }
@@ -1508,10 +1513,11 @@ export const SortieDogsPlugin = async (input, options) => {
1508
1513
  ? new Date(timing.start).toISOString() : execution.startedAt;
1509
1514
  const endedAt = typeof timing?.end === "number" && Number.isFinite(timing.end)
1510
1515
  ? new Date(timing.end).toISOString() : new Date().toISOString();
1511
- // The candidate is expected to change during a valid implementation task (the goal fixture
1512
- // itself is a declared write). Protect the source snapshot across the task, then bind evidence
1513
- // to the post-task candidate snapshot instead of rejecting the genuine mutation.
1514
- const fresh = refreshed !== undefined && refreshed.source === execution.source;
1516
+ // Existing operations produce declared outputs. Preserve their read-only inputs during execution,
1517
+ // then bind ordinary evidence to all post-operation bytes, including those outputs.
1518
+ const fresh = refreshed !== undefined && (execution.operationInputs === undefined
1519
+ ? refreshed.source === execution.source
1520
+ : await operationInputSnapshot(execution.projectRoot, execution.binding).catch(() => undefined) === execution.operationInputs);
1515
1521
  const immutableRef = outcome === undefined || execution.reusedEvidence !== undefined ? undefined : goalFingerprint({ root: execution.root,
1516
1522
  child_session_id: execution.sessionID, call_id: execution.callID, command: execution.command,
1517
1523
  started_at: observedStartedAt, ended_at: endedAt, exit_code: exitCode ?? null, outcome,
@@ -4770,6 +4776,7 @@ export const SortieDogsPlugin = async (input, options) => {
4770
4776
  remedy: "No write-gate state changed after one retry; stop this candidate and record the local blocker.",
4771
4777
  },
4772
4778
  };
4779
+ let conflicts = [];
4773
4780
  const deny = (reason, defects = []) => {
4774
4781
  const detail = remedies[reason] ?? {
4775
4782
  recoverable: false,
@@ -4798,6 +4805,7 @@ export const SortieDogsPlugin = async (input, options) => {
4798
4805
  reason,
4799
4806
  ...detail,
4800
4807
  ...(reported.length === 0 ? {} : { defects: reported }),
4808
+ ...(reason === "manifest-overlap" && conflicts.length ? { conflicts } : {}),
4801
4809
  escalation,
4802
4810
  });
4803
4811
  };
@@ -4884,10 +4892,23 @@ export const SortieDogsPlugin = async (input, options) => {
4884
4892
  if (diagnostics.some(({ severity }) => severity === "error") ||
4885
4893
  inspectionFingerprint(handoffValidation.value, diagnostics, validation.value) !== inspectedEntry.fingerprint)
4886
4894
  return denyHandoffMismatch(handoffValue, contractDefects(diagnostics));
4887
- const conflictsWithActiveAuthorization = (writeScopes, readScopes) => [...sessionAuthorizations.entries()].some(([ownerSessionID, authorization]) => ownerSessionID !== sessionID && !authorization.suspended &&
4888
- (writeScopesOverlap(writeScopes, authorization.writeScopes) ||
4889
- writeScopesOverlap(writeScopes, authorization.readScopes) ||
4890
- writeScopesOverlap(readScopes, authorization.writeScopes)));
4895
+ const conflictsWithActiveAuthorization = (writeScopes, readScopes) => {
4896
+ conflicts = [];
4897
+ for (const [ownerSessionID, authorization] of sessionAuthorizations) {
4898
+ if (ownerSessionID === sessionID || authorization.suspended)
4899
+ continue;
4900
+ for (const [requested, held, access] of [[writeScopes, authorization.writeScopes, "write/write"],
4901
+ [writeScopes, authorization.readScopes, "write/read"], [readScopes, authorization.writeScopes, "read/write"]]) {
4902
+ for (const left of requested)
4903
+ for (const right of held) {
4904
+ if (conflicts.length < 8 && writeScopesOverlap([left], [right])) {
4905
+ conflicts.push({ session_id: ownerSessionID, requested: left, held: right, access });
4906
+ }
4907
+ }
4908
+ }
4909
+ }
4910
+ return conflicts.length > 0;
4911
+ };
4891
4912
  const acquireDurableLease = async (readScopes, writeScopes) => {
4892
4913
  if (activeSessions.get(sessionID)?.parallel !== "valid")
4893
4914
  return undefined;
@@ -5103,7 +5124,7 @@ export const SortieDogsPlugin = async (input, options) => {
5103
5124
  for (const [sessionID, state] of activeSessions) {
5104
5125
  if (state.expiresAt > now)
5105
5126
  continue;
5106
- if (state.inFlightCalls.size > 0)
5127
+ if (state.inFlightCalls.size > 0 || childHasInFlightParentTask(sessionID))
5107
5128
  state.expiresAt = now + ACTIVE_SESSION_CACHE.ttlMilliseconds;
5108
5129
  else
5109
5130
  expireSession(sessionID);
@@ -5116,7 +5137,7 @@ export const SortieDogsPlugin = async (input, options) => {
5116
5137
  for (const [sessionID, state] of coordinatorRoots) {
5117
5138
  if (state.expiresAt > now)
5118
5139
  continue;
5119
- const hasInFlightChild = [...sessionRoots.entries()].some(([childID, rootID]) => rootID === sessionID && (activeSessions.get(childID)?.inFlightCalls.size ?? 0) > 0);
5140
+ const hasInFlightChild = [...sessionRoots.entries()].some(([childID, rootID]) => rootID === sessionID && ((activeSessions.get(childID)?.inFlightCalls.size ?? 0) > 0 || childHasInFlightParentTask(childID)));
5120
5141
  if (hasInFlightChild)
5121
5142
  state.expiresAt = now + ACTIVE_SESSION_CACHE.ttlMilliseconds;
5122
5143
  else {
@@ -6565,6 +6586,8 @@ export const SortieDogsPlugin = async (input, options) => {
6565
6586
  diagnosis.accepting = false;
6566
6587
  diagnosisCalls.delete(toolInput.callID ?? "");
6567
6588
  }
6589
+ const returnedMissionWorker = completedChildSessionID !== undefined &&
6590
+ await input.runtimeBridge?.allowsInvestigativeShell?.(completedChildSessionID);
6568
6591
  parallelCalls.delete(toolInput.callID ?? "");
6569
6592
  activeSessions.get(toolInput.sessionID ?? "")?.inFlightCalls.delete(toolInput.callID ?? "");
6570
6593
  if (coordinatorTaskFinished && diagnosis === undefined) {
@@ -6584,6 +6607,12 @@ export const SortieDogsPlugin = async (input, options) => {
6584
6607
  evictSession(completedChildSessionID);
6585
6608
  void childLifecycles.get(completedChildSessionID)?.check();
6586
6609
  }
6610
+ else if (completedChildSessionID !== undefined && activeSessions.get(completedChildSessionID)?.parallel !== "valid" &&
6611
+ returnedMissionWorker) {
6612
+ // Keep same-worker recovery identity, but a returned serial Task must not keep a live
6613
+ // write reservation. Rebinding the retained manifest reacquires it before any later write.
6614
+ await releaseWriteGate(completedChildSessionID);
6615
+ }
6587
6616
  }
6588
6617
  },
6589
6618
  "tool.execute.before": async (toolInput, output) => {
@@ -1,13 +1,15 @@
1
1
  import type { OperatorState } from "../core/operator-runtime.js";
2
- import { type OperatorMission } from "../core/operator-mission.js";
2
+ import { type MissionEvidenceExcerpt, type OperatorMission } from "../core/operator-mission.js";
3
3
  import { type RuntimeProfile } from "../core/runtime-profile.js";
4
+ /** A child ID alone cannot establish the initial phase after a restart or failed dispatch. */
5
+ export declare function initialMissionReviewPrompt(mission: OperatorMission, prompts: readonly string[]): string | undefined;
4
6
  /** Recover from native completion records, never from the pending review's inherited child ID. */
5
7
  export declare function completedMissionReviewPrompts(mission: OperatorMission | undefined, profile: RuntimeProfile, root: string, requestedPrompt: string, host: {
6
8
  get(id: string): Promise<unknown>;
7
9
  messages(id: string): Promise<readonly Record<string, unknown>[]>;
8
10
  }): Promise<string[]>;
9
11
  /** Pin all scoped tracked/untracked source bytes, including deletions; display a bounded excerpt only. */
10
- export declare function missionReviewSource(directory: string, run: OperatorState): Promise<{
12
+ export declare function missionReviewSource(directory: string, run: OperatorState, evidence?: readonly MissionEvidenceExcerpt[]): Promise<{
11
13
  fingerprint: string;
12
14
  excerpt: string;
13
15
  }>;
@@ -2,8 +2,9 @@ import { createHash } from "node:crypto";
2
2
  import { execFile } from "node:child_process";
3
3
  import { createReadStream } from "node:fs";
4
4
  import { lstat, readlink } from "node:fs/promises";
5
- import { isAbsolute, relative, resolve } from "node:path";
5
+ import { isAbsolute, relative, resolve, sep } from "node:path";
6
6
  import { promisify } from "node:util";
7
+ import { createInterface } from "node:readline";
7
8
  import { TOOL_ENVIRONMENT } from "../runtime-mission-assets.js";
8
9
  import { MISSION_REVIEW_REFERENCE } from "../core/operator-mission.js";
9
10
  import { canonicalAgent } from "../core/runtime-profile.js";
@@ -12,11 +13,21 @@ import { normalizeManifestScope } from "../core/path.js";
12
13
  import { declaredArtifacts } from "./declared-artifacts.js";
13
14
  const exec = promisify(execFile);
14
15
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
16
+ /** A child ID alone cannot establish the initial phase after a restart or failed dispatch. */
17
+ export function initialMissionReviewPrompt(mission, prompts) {
18
+ return [mission.review?.initialPrompt, ...prompts].find(prompt => typeof prompt === "string" &&
19
+ prompt.startsWith(`candidate_id: ${mission.id}\n`) && /^review_phase: initial$/mu.test(prompt) &&
20
+ /^canonical_validation_exit: 0$/mu.test(prompt));
21
+ }
15
22
  /** Recover from native completion records, never from the pending review's inherited child ID. */
16
23
  export async function completedMissionReviewPrompts(mission, profile, root, requestedPrompt, host) {
17
24
  if (!mission || mission.root !== root || !mission.coordinator || ["completed", "cancelled"].includes(mission.phase) ||
18
25
  mission.review?.task?.prompt !== requestedPrompt)
19
26
  return [];
27
+ // The completion hook has already bound this prompt to a real independent Reviewer child.
28
+ // Avoid a second V2 history read when its page/list API is temporarily unavailable.
29
+ if (initialMissionReviewPrompt(mission, []))
30
+ return [mission.review.initialPrompt];
20
31
  const coordinator = await host.get(mission.coordinator);
21
32
  if (!record(coordinator) || coordinator.parentID !== root || canonicalAgent(profile, coordinator.agent) !== "dog-operator")
22
33
  return [];
@@ -75,12 +86,38 @@ export async function completedMissionReviewPrompts(mission, profile, root, requ
75
86
  return prompts;
76
87
  }
77
88
  /** Pin all scoped tracked/untracked source bytes, including deletions; display a bounded excerpt only. */
78
- export async function missionReviewSource(directory, run) {
89
+ export async function missionReviewSource(directory, run, evidence = []) {
79
90
  const hash = createHash("sha256").update(JSON.stringify(run.units.map(unit => ({ unit: unit.unit, hashes: unit.hashes }))));
91
+ let selected = "";
92
+ for (const entry of evidence) {
93
+ const absolute = resolve(directory, entry.path);
94
+ const allowed = run.units.flatMap(unit => [...unit.unit.read, ...unit.unit.write]).some(scope => {
95
+ const root = resolve(directory, scope === "." ? scope : normalizeManifestScope(scope).path), rest = relative(root, absolute);
96
+ return rest === "" || (rest !== ".." && !rest.startsWith(`..${sep}`) && !isAbsolute(rest));
97
+ });
98
+ if (!allowed || !Number.isSafeInteger(entry.offset) || entry.offset < 1 ||
99
+ !Number.isSafeInteger(entry.limit) || entry.limit < 1 || entry.limit > 200)
100
+ throw new Error("mission-review-evidence: select up to 200 lines from a declared input/output");
101
+ const info = await lstat(absolute);
102
+ if (!info.isFile())
103
+ throw new Error("mission-review-evidence: select a regular file");
104
+ hash.update(JSON.stringify(entry)).update(String(info.size));
105
+ const stream = createReadStream(absolute);
106
+ stream.on("data", chunk => hash.update(chunk));
107
+ const lines = createInterface({ input: stream, crlfDelay: Infinity });
108
+ selected += `\n--- evidence: ${entry.path}:${entry.offset} ---\n`;
109
+ let number = 0;
110
+ for await (const line of lines) {
111
+ number++;
112
+ if (number >= entry.offset && number < entry.offset + entry.limit && Buffer.byteLength(selected) < 12_000) {
113
+ selected += `${number}: ${line.slice(0, 2_000)}\n`;
114
+ }
115
+ }
116
+ }
80
117
  const writes = [...new Set(run.units.flatMap(unit => unit.unit.write).map(path => path === "." ? path : normalizeManifestScope(path).path))];
81
118
  if (writes.length === 0)
82
119
  return { fingerprint: `sha256:${hash.digest("hex")}`,
83
- excerpt: "[Read-only units: no declared output files. Review the supplied observations, traces and validation evidence.]" };
120
+ excerpt: selected + "[Read-only units: no declared output files. Review the supplied observations, traces and validation evidence.]" };
84
121
  // The shared dependency environment is local tooling, never reviewed or pinned source.
85
122
  const external = [], local = [];
86
123
  for (const path of writes) {
@@ -99,8 +136,9 @@ export async function missionReviewSource(directory, run) {
99
136
  const ignored = local.length ? await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", ...scopes]) : "";
100
137
  for (const path of ignored.split("\0").filter(Boolean))
101
138
  untracked.add(path);
102
- let excerpt = local.length ? await git(["diff", "--no-ext-diff", "--no-textconv", "HEAD", "--", ...scopes]).catch(() => git(["diff", "--no-ext-diff", "--no-textconv", "--", ...scopes])) : "";
103
- const unchanged = excerpt.length === 0;
139
+ const diff = local.length ? await git(["diff", "--no-ext-diff", "--no-textconv", "HEAD", "--", ...scopes]).catch(() => git(["diff", "--no-ext-diff", "--no-textconv", "--", ...scopes])) : "";
140
+ const unchanged = diff.length === 0;
141
+ let excerpt = selected + diff;
104
142
  const paths = [...new Set([...names.split("\0"), ...ignored.split("\0")].filter(Boolean))].sort();
105
143
  const omitted = [];
106
144
  for (const path of paths) {