sortie-dogs 0.12.15 → 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.15-review-lineage-model-gauges-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.15-review-lineage-model-gauges-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,6 +71,7 @@ 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;
@@ -61,6 +91,15 @@ export declare const MISSION_EVIDENCE_GAP_REVIEW_LIMIT = 2;
61
91
  export declare function missionReviewVerdict(text: string): "PASS" | "evidence-gaps" | "findings";
62
92
  /** Whether the recorded review permits submission and acceptance of the current candidate. */
63
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[];
64
103
  /** Durable user intent and dispatch ownership. Execution/evidence still belong to the v0.10 engine. */
65
104
  export declare class OperatorMissionRuntime {
66
105
  readonly projectRoot: string;
@@ -78,7 +117,10 @@ export declare class OperatorMissionRuntime {
78
117
  read(root: string): Promise<OperatorMission | undefined>;
79
118
  required(root: string): Promise<OperatorMission>;
80
119
  capture(root: string, request: MissionRequest): Promise<void>;
81
- 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>;
82
124
  update(root: string, change: (state: OperatorMission) => void): Promise<OperatorMission>;
83
125
  /** Keep host-accepted criteria ahead of new text, including an exactly linked settled predecessor. */
84
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,5 +1,5 @@
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
4
  /** A child ID alone cannot establish the initial phase after a restart or failed dispatch. */
5
5
  export declare function initialMissionReviewPrompt(mission: OperatorMission, prompts: readonly string[]): string | undefined;
@@ -9,7 +9,7 @@ export declare function completedMissionReviewPrompts(mission: OperatorMission |
9
9
  messages(id: string): Promise<readonly Record<string, unknown>[]>;
10
10
  }): Promise<string[]>;
11
11
  /** Pin all scoped tracked/untracked source bytes, including deletions; display a bounded excerpt only. */
12
- export declare function missionReviewSource(directory: string, run: OperatorState): Promise<{
12
+ export declare function missionReviewSource(directory: string, run: OperatorState, evidence?: readonly MissionEvidenceExcerpt[]): Promise<{
13
13
  fingerprint: string;
14
14
  excerpt: string;
15
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";
@@ -85,12 +86,38 @@ export async function completedMissionReviewPrompts(mission, profile, root, requ
85
86
  return prompts;
86
87
  }
87
88
  /** Pin all scoped tracked/untracked source bytes, including deletions; display a bounded excerpt only. */
88
- export async function missionReviewSource(directory, run) {
89
+ export async function missionReviewSource(directory, run, evidence = []) {
89
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
+ }
90
117
  const writes = [...new Set(run.units.flatMap(unit => unit.unit.write).map(path => path === "." ? path : normalizeManifestScope(path).path))];
91
118
  if (writes.length === 0)
92
119
  return { fingerprint: `sha256:${hash.digest("hex")}`,
93
- 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.]" };
94
121
  // The shared dependency environment is local tooling, never reviewed or pinned source.
95
122
  const external = [], local = [];
96
123
  for (const path of writes) {
@@ -109,8 +136,9 @@ export async function missionReviewSource(directory, run) {
109
136
  const ignored = local.length ? await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", ...scopes]) : "";
110
137
  for (const path of ignored.split("\0").filter(Boolean))
111
138
  untracked.add(path);
112
- let excerpt = local.length ? await git(["diff", "--no-ext-diff", "--no-textconv", "HEAD", "--", ...scopes]).catch(() => git(["diff", "--no-ext-diff", "--no-textconv", "--", ...scopes])) : "";
113
- 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;
114
142
  const paths = [...new Set([...names.split("\0"), ...ignored.split("\0")].filter(Boolean))].sort();
115
143
  const omitted = [];
116
144
  for (const path of paths) {
@@ -12,7 +12,7 @@ import { decoratePreviewHeadings, returnReportPanel } from "./receipt-presentati
12
12
  import { sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
13
13
  import { normalizeCommand } from "./gate.js";
14
14
  import { normalizeRelativePath } from "../core/path.js";
15
- import { MISSION_EVIDENCE_GAP_REVIEW_LIMIT, OperatorMissionRuntime, missionPacket, missionPlan, missionReviewAccepted, missionReviewTask, missionReviewTraces, missionReviewVerdict } from "../core/operator-mission.js";
15
+ import { MISSION_EVIDENCE_GAP_REVIEW_LIMIT, OperatorMissionRuntime, missionPacket, missionPlan, missionReviewAccepted, missionReviewTask, missionCommandOutcome, missionConversationContext, missionExecutionStatus, missionValidationCommand, missionReviewTraces, missionReviewVerdict } from "../core/operator-mission.js";
16
16
  import { publishMissionProgress } from "./mission-progress.js";
17
17
  import { completedMissionReviewPrompts, initialMissionReviewPrompt, missionReviewSource } from "./mission-review.js";
18
18
  import { missionLocations, missionLocationPacket } from "./mission-location.js";
@@ -988,6 +988,12 @@ export function createProfiledPlugin(profile, assetVersion) {
988
988
  await requireRoot(context.sessionID);
989
989
  const state = await operators.required(context.sessionID);
990
990
  const mission = await missions.read(context.sessionID);
991
+ if (mission?.kind === "operation" && missionExecutionStatus(mission) !== "executed")
992
+ return JSON.stringify({
993
+ status: "not-ready", operation_status: missionExecutionStatus(mission),
994
+ 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.",
995
+ packet: missionPacket(mission, state)
996
+ });
991
997
  if (mission && !["completed", "cancelled"].includes(mission.phase))
992
998
  await assertMissionReview(context.sessionID, mission);
993
999
  if (args.run_id !== state.runID || args.acceptance_fingerprint !== state.acceptanceFingerprint)
@@ -1005,7 +1011,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1005
1011
  await operators.terminal(context.sessionID, result.receipt);
1006
1012
  let panel;
1007
1013
  if (input.returnReportTransport === "tool-result" && result.receipt?.status === "succeeded") {
1008
- const text = `✅ **DONE** \`${state.runID}\` — ${state.acceptance.length} acceptance requirements verified.\n\n` +
1014
+ const text = `✅ **DONE** \`${state.runID}\` — declared checks passed; Operator accepted the result.\n\n` +
1009
1015
  `**変更点:** ${state.units.map(unit => unit.unit.title).join("; ")}\n\n` +
1010
1016
  `**確認結果:** 宣言検証合格 — ${[...new Set(state.units.flatMap(unit => unit.unit.validation))].join("; ")}` +
1011
1017
  (mission?.review ? `\n独立レビュー: ${mission.review.verdict === "evidence-gaps" ? "証拠不足を残して受入れ(レビューPASSではない)" : mission.review.verdict}.` : "") + "\n\n**次:** なし";
@@ -1303,6 +1309,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1303
1309
  const stringList = { type: "array", items: { type: "string" } };
1304
1310
  tools[startMission] = { description: "Operator: save the current user requirements. Use intent=replace when the user changes an existing request (version, parallelism, target): host cancels the previous run and archives its requirements/results while retaining spend. Supply the complete current requirements, retaining constraints the user has not changed. For separate work in a different location use intent=new. Dispatch the Coordinator immediately, or plan_units for a known single-unit procedure; item count and runtime alone do not require a Coordinator.",
1305
1311
  args: { requirements: { ...stringList, minItems: 1, maxItems: 64 },
1312
+ 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 },
1306
1313
  intent: { type: "string", enum: ["", "continue", "new", "replace"], "x-sortie-optional": true } }, execute: async (args, context) => {
1307
1314
  await requireRoot(context.sessionID);
1308
1315
  if (args.intent !== undefined && !["", "continue", "new", "replace"].includes(args.intent))
@@ -1317,16 +1324,34 @@ export function createProfiledPlugin(profile, assetVersion) {
1317
1324
  if (relocated)
1318
1325
  return JSON.stringify({ ...relocated, budget: await control.currentBudget(context.sessionID) });
1319
1326
  }
1320
- const mission = await retainCancelledMissionAcceptance(context.sessionID, await missions.start(context.sessionID, args.requirements, args.intent === "replace" || args.intent === "new"));
1327
+ const mission = await retainCancelledMissionAcceptance(context.sessionID, await missions.start(context.sessionID, args.requirements, args.intent === "replace" || args.intent === "new", {
1328
+ kind: args.kind === "operation" ? "operation" : "implementation",
1329
+ context: missionConversationContext(await messages(context.sessionID)),
1330
+ }));
1321
1331
  return JSON.stringify({ ...locationObservation(context.sessionID), ...(mission.dispatchOpen
1322
1332
  ? missionDispatchPacket(mission, await operators.read(context.sessionID))
1323
1333
  : { mission_id: mission.id, requirements: mission.requirements, task: missions.task(mission),
1324
1334
  next_action: "Nontrivial: dispatch task now. Simple single-unit work with known scope/check: call plan_units directly. Do not create a proposal or ask for plan approval." }) });
1325
1335
  });
1326
1336
  } };
1327
- async function declareMissionUnits(root, actor, mission, raw, reason) {
1337
+ async function declareMissionUnits(root, actor, mission, raw, reason, execution) {
1328
1338
  const previous = await operators.read(root);
1329
1339
  mission = await retainCancelledMissionAcceptance(root, mission, previous);
1340
+ let operation = mission.execution;
1341
+ // Models can serialize an optional operation field as an empty object for a normal edit.
1342
+ // Do not turn an implementation mission into an operation (or block its first Worker).
1343
+ const emptyImplementationExecution = mission.kind === "implementation" && record(execution) &&
1344
+ Array.isArray(execution.commands) && execution.commands.length === 0;
1345
+ if (execution !== undefined && !emptyImplementationExecution) {
1346
+ if (!record(execution) || typeof execution.directory !== "string" || !execution.directory.trim() ||
1347
+ !Array.isArray(execution.commands) || execution.commands.length === 0 || execution.commands.length > 16 ||
1348
+ !execution.commands.every(command => typeof command === "string" && command.trim()))
1349
+ throw new Error("mission-operation-input: supply the actual operation commands and their working directory");
1350
+ const commands = [...new Set(execution.commands.map(missionValidationCommand).map(normalizeCommand))];
1351
+ const directory = resolve(input.directory, execution.directory);
1352
+ // A corrected command is allowed on replan; retain prior observations for accounting.
1353
+ operation = { commands, directory, observations: operation?.observations ?? [] };
1354
+ }
1330
1355
  const plan = missionPlan(mission, raw);
1331
1356
  if (actor === root && (mission.coordinator !== null || plan.units.length !== 1))
1332
1357
  throw new Error("mission-coordinator-required: dispatch the returned Coordinator task");
@@ -1376,6 +1401,10 @@ export function createProfiledPlugin(profile, assetVersion) {
1376
1401
  item.phase = "running";
1377
1402
  item.submission = null;
1378
1403
  delete item.supersededRunID;
1404
+ if (operation) {
1405
+ item.kind = "operation";
1406
+ item.execution = operation;
1407
+ }
1379
1408
  });
1380
1409
  return JSON.stringify(await operators.next(root, actor));
1381
1410
  }
@@ -1384,9 +1413,12 @@ export function createProfiledPlugin(profile, assetVersion) {
1384
1413
  properties: { title: { type: "string" }, objective: { type: "string" },
1385
1414
  read: { ...stringList, description: "Inputs that affect validation, not an allowlist for observation. Do not include whole live session/database/log trees just to inspect them." }, write: stringList,
1386
1415
  validation: stringList, requirement_ids: stringList }, required: ["title", "objective", "write", "validation"] } },
1416
+ execution: { type: "object", properties: { commands: stringList, directory: { type: "string" } },
1417
+ required: ["commands", "directory"], additionalProperties: false,
1418
+ 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 },
1387
1419
  reason: { type: "string", "x-sortie-optional": true } }, execute: async (args, context) => {
1388
1420
  const { root, mission } = await missionAuthority(context.sessionID);
1389
- return serializeDispatchTransition(root, () => declareMissionUnits(root, context.sessionID, mission, args.units, args.reason));
1421
+ return serializeDispatchTransition(root, () => declareMissionUnits(root, context.sessionID, mission, args.units, args.reason, args.execution));
1390
1422
  } };
1391
1423
  tools[expandUnit] = { description: "Coordinator: extend the stopped unit's write scope immediately within the original request. Keeps original requirements and cumulative budget; generates a replacement Worker contract. No Operator approval or handwritten manifest repair is needed. Use only after the Worker has returned.",
1392
1424
  args: { unit_id: stringSchema, paths: stringList, reason: stringSchema }, execute: async (args, context) => {
@@ -1405,6 +1437,9 @@ export function createProfiledPlugin(profile, assetVersion) {
1405
1437
  } };
1406
1438
  tools[reviewMission] = { description: "Coordinator: prepare the independent Reviewer task from current source, requirements and observed checks. Supply risk_tags (empty only for genuinely low risk) and concise criterion-level changed-code/test traces. Host supplies diff, IDs, manifest, mappings and evidence; keep candidate lineage across corrections. A low-risk skip is recorded, never inferred from a missing review.",
1407
1439
  args: { risk_tags: { type: "array", items: { type: "string", enum: SOURCE_REVIEW_RISK_TAGS } },
1440
+ evidence: { type: "array", maxItems: 6, items: { type: "object", additionalProperties: false,
1441
+ properties: { path: { type: "string" }, offset: { type: "integer", minimum: 1 }, limit: { type: "integer", minimum: 1, maximum: 200 } },
1442
+ required: ["path", "offset", "limit"] }, description: "Focused excerpts from existing declared inputs/outputs. Use for missing review evidence instead of creating another evidence-copying Worker.", "x-sortie-optional": true },
1408
1443
  traces: stringList }, execute: async (args, context) => {
1409
1444
  const { root, mission } = await missionAuthority(context.sessionID);
1410
1445
  const run = await operators.required(root);
@@ -1415,7 +1450,10 @@ export function createProfiledPlugin(profile, assetVersion) {
1415
1450
  if (!Array.isArray(risk) || !risk.every(tag => SOURCE_REVIEW_RISK_TAGS.includes(tag))) {
1416
1451
  throw new Error("mission-review-input: use recognized risk tags");
1417
1452
  }
1418
- const source = await missionReviewSource(input.directory, run);
1453
+ const evidence = args.evidence;
1454
+ if (evidence !== undefined && (!Array.isArray(evidence) || evidence.length > 6))
1455
+ throw new Error("mission-review-evidence: select at most six focused excerpts");
1456
+ const source = await missionReviewSource(input.directory, run, evidence);
1419
1457
  const requestFingerprint = goalFingerprint({ run: run.runID, source: source.fingerprint, risk, traces });
1420
1458
  if (mission.review?.requestFingerprint === requestFingerprint && mission.review.verdict !== "pending") {
1421
1459
  return JSON.stringify({ ...missionPacket(mission, run), status: "review-recorded",
@@ -1437,7 +1475,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1437
1475
  description: `🔎 ${run.units[0].unit.title}`, prompt: [
1438
1476
  `candidate_id: ${mission.id}`, `review_phase: ${phase}`, "canonical_validation_exit: 0", `risk_tags: [${risk.join(", ")}]`,
1439
1477
  "Review this candidate independently. Use the language of the requirements/traces. Invoke no tools. First line: exactly PASS, FINDINGS or EVIDENCE_GAPS.",
1440
- "Use EVIDENCE_GAPS only when no finding establishes a source/test defect and every finding is proof the supplied artifact cannot settle, including process history later established on the base. Any concrete defect uses FINDINGS.",
1478
+ "Use EVIDENCE_GAPS only when no concrete source/test defect is established and a specific acceptance-relevant behavior or required validation cannot be settled by the supplied artifact. Name the affected path and why the missing evidence matters; do not request a generic route inventory or raw history for incidental workflow constraints already covered by a concise Worker trace. Any concrete defect uses FINDINGS.",
1441
1479
  "This Reviewer's native outcome and final acceptance can only be observed after this review. List those as deferred Operator checks, not as a reason to request another review. Still assess all available source, validation and historical evidence independently.",
1442
1480
  `acceptance: ${JSON.stringify(run.acceptance)}`, `changedLogicSummary: ${JSON.stringify(traces)}`,
1443
1481
  ...run.acceptance.map((_, i) => `acceptance[${i}] -> changedLogicSummary[${i}]`),
@@ -1447,7 +1485,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1447
1485
  ].join("\n") };
1448
1486
  const reviewed = await missions.update(root, item => {
1449
1487
  item.review = { runID: run.runID, risk: risk, source: source.fingerprint, requestFingerprint,
1450
- task, verdict: task ? "pending" : "skipped-low-risk", ...(mission.review?.child ? { child: mission.review.child } : {}),
1488
+ task, evidence, verdict: task ? "pending" : "skipped-low-risk", ...(mission.review?.child ? { child: mission.review.child } : {}),
1451
1489
  ...(initialPrompt ? { initialPrompt } : {}),
1452
1490
  ...(mission.review?.evidenceGapReviews ? { evidenceGapReviews: mission.review.evidenceGapReviews } : {}) };
1453
1491
  });
@@ -1456,13 +1494,17 @@ export function createProfiledPlugin(profile, assetVersion) {
1456
1494
  async function assertMissionReview(root, mission) {
1457
1495
  const run = await operators.required(root), review = mission.review;
1458
1496
  if (!review || review.runID !== run.runID || !missionReviewAccepted(review) ||
1459
- review.source !== (await missionReviewSource(input.directory, run)).fingerprint)
1497
+ review.source !== (await missionReviewSource(input.directory, run, review.evidence)).fingerprint)
1460
1498
  throw new Error("mission-review-required-or-stale");
1461
1499
  }
1462
1500
  tools[submitMission] = { description: "Coordinator: return only a completion candidate, a user-only decision, or a proven external/scope/budget blocker. Continue ordinary investigation, scope extensions and corrections yourself. Unit progress is published automatically without waking Operator.",
1463
1501
  args: { status: { type: "string", enum: ["ready", "needs-decision", "blocked"] }, summary: stringSchema }, execute: async (args, context) => {
1464
1502
  const { root, mission } = await missionAuthority(context.sessionID);
1465
1503
  if (args.status === "ready") {
1504
+ if (mission.kind === "operation" && missionExecutionStatus(mission) !== "executed") {
1505
+ return JSON.stringify({ ...missionPacket(mission, await operators.read(root)), status: "operation-incomplete",
1506
+ next_action: "Continue the requested operation or submit its actual blocker. A successful setup/NO_START check does not authorize ready." });
1507
+ }
1466
1508
  const run = await operators.required(root);
1467
1509
  if (run.phase !== "awaiting-acceptance")
1468
1510
  throw new Error("mission-units-incomplete");
@@ -1957,6 +1999,17 @@ export function createProfiledPlugin(profile, assetVersion) {
1957
1999
  }
1958
2000
  else
1959
2001
  Object.assign(output, outward);
2002
+ if (who.role === "dog-worker" && ["bash", "shell"].includes(request.tool) && mission?.execution &&
2003
+ typeof args.command === "string" && mission.execution.commands.includes(normalizeCommand(args.command)) &&
2004
+ resolve(input.directory, typeof args.workdir === "string" ? args.workdir : ".") === mission.execution.directory) {
2005
+ const command = normalizeCommand(args.command);
2006
+ await missions.update(root, item => {
2007
+ if (!item.execution.observations.some(observation => observation.callID === request.callID)) {
2008
+ item.execution.observations.push({ command, directory: item.execution.directory,
2009
+ callID: request.callID, sessionID: request.sessionID, startedAt: new Date().toISOString() });
2010
+ }
2011
+ });
2012
+ }
1960
2013
  }
1961
2014
  catch (error) {
1962
2015
  if (delegated) {
@@ -2005,8 +2058,8 @@ export function createProfiledPlugin(profile, assetVersion) {
2005
2058
  if (reviewed.review?.verdict === "evidence-gaps" && typeof output.output === "string") {
2006
2059
  const count = reviewed.review.evidenceGapReviews ?? 0;
2007
2060
  output.output += missionReviewAccepted(reviewed.review)
2008
- ? `\n\nHOST: evidence-gap review ${count}/${MISSION_EVIDENCE_GAP_REVIEW_LIMIT} reached the limit. No defect was found; submit_mission ready is permitted. List the remaining evidence gaps in the summary.`
2009
- : `\n\nHOST: evidence-gap review ${count}/${MISSION_EVIDENCE_GAP_REVIEW_LIMIT}. No defect was found; supply the requested evidence through review_mission traces (or at most one evidence-only unit) and re-review. Do not re-implement.`;
2061
+ ? `\n\nHOST: evidence-gap review ${count}/${MISSION_EVIDENCE_GAP_REVIEW_LIMIT} reached the limit. Review is closed with gaps; this does not prove execution or requirement completion. Submit ready only when the requested result is complete, and list the gaps.`
2062
+ : `\n\nHOST: evidence-gap review ${count}/${MISSION_EVIDENCE_GAP_REVIEW_LIMIT}. Supply focused original-file excerpts through review_mission evidence and traces. Do not create an evidence-copying Worker or re-implement.`;
2010
2063
  }
2011
2064
  }
2012
2065
  taskOwners.delete(request.callID);
@@ -2034,6 +2087,21 @@ export function createProfiledPlugin(profile, assetVersion) {
2034
2087
  await core["tool.execute.after"]?.({ ...translate(request, false),
2035
2088
  ...(ownership ? { sessionID: ownership.root } : {}) }, mapped);
2036
2089
  Object.assign(output, translate(mapped, true));
2090
+ if (!ownership && ["bash", "shell"].includes(request.tool.toLowerCase())) {
2091
+ const root = await rootFor(id);
2092
+ const mission = root ? await missions.read(root) : undefined;
2093
+ if (root && mission?.execution?.observations.some(item => item.callID === request.callID && item.sessionID === id)) {
2094
+ const metadata = record(output.metadata) ? output.metadata : {};
2095
+ await missions.update(root, item => {
2096
+ const observation = item.execution.observations.find(value => value.callID === request.callID && value.sessionID === id);
2097
+ observation.completedAt = new Date().toISOString();
2098
+ observation.status = output.status === "error" || metadata.status === "error" ? "error" : "completed";
2099
+ if (Number.isSafeInteger(metadata.exit))
2100
+ observation.exit = metadata.exit;
2101
+ Object.assign(observation, missionCommandOutcome(returnedOutput, metadata.exit, observation.status));
2102
+ });
2103
+ }
2104
+ }
2037
2105
  if (repairAccess !== null && ["bash", "shell"].includes(request.tool.toLowerCase())) {
2038
2106
  const metadata = record(output.metadata) ? output.metadata : undefined;
2039
2107
  const exit = metadata?.exit;
@@ -2,6 +2,9 @@ import { type GoalEvidence } from "../core/goal-bound.js";
2
2
  type Binding = NonNullable<GoalEvidence["protected_binding"]>;
3
3
  /** Live orchestration/Git bookkeeping is readable context, not the source being validated. */
4
4
  export declare function isRuntimeControlPath(path: string): boolean;
5
+ /** An existing operation may create its declared outputs. Compare its other inputs during execution;
6
+ * ordinary evidence still pins the full post-operation source and outputs for later acceptance. */
7
+ export declare function operationInputSnapshot(projectRoot: string, binding: Binding): Promise<string | undefined>;
5
8
  export declare function protectedSnapshot(authorization: {
6
9
  manifestPath: string;
7
10
  manifestHash: string;
@@ -11,10 +11,12 @@ export function isRuntimeControlPath(path) {
11
11
  const first = path.replaceAll("\\", "/").split("/")[0];
12
12
  return controlRoots.has(process.platform === "win32" ? first.toLowerCase() : first);
13
13
  }
14
- async function protectedScopeDigest(projectRoot, paths, manifestHash, sourcePolicy) {
14
+ async function protectedScopeDigest(projectRoot, paths, manifestHash, sourcePolicy, excluded = []) {
15
15
  const entries = [];
16
16
  const canonicalRoot = await realpath(projectRoot);
17
17
  const visit = async (absolute) => {
18
+ if (excluded.some(root => !outside(root, absolute)))
19
+ return true;
18
20
  const scoped = relative(projectRoot, absolute).replaceAll("\\", "/");
19
21
  if (scoped === ".." || scoped.startsWith("../") || isAbsolute(scoped))
20
22
  return false;
@@ -62,14 +64,27 @@ function outside(projectRoot, path) {
62
64
  const scoped = relative(projectRoot, path).replaceAll("\\", "/");
63
65
  return scoped === ".." || scoped.startsWith("../") || isAbsolute(scoped);
64
66
  }
65
- async function declaredScopeDigest(projectRoot, paths, manifestHash, source) {
67
+ async function declaredScopeDigest(projectRoot, paths, manifestHash, source, excluded = []) {
66
68
  const local = paths.filter(path => !outside(projectRoot, path));
67
69
  const external = paths.filter(path => outside(projectRoot, path));
68
- const project = await protectedScopeDigest(projectRoot, local, manifestHash, source ? "project-files-v1" : undefined);
70
+ const project = await protectedScopeDigest(projectRoot, local, manifestHash, source ? "project-files-v1" : undefined, excluded);
69
71
  if (project === undefined)
70
72
  return undefined;
71
73
  const artifacts = await declaredArtifacts(external).catch(() => undefined);
72
- return artifacts && goalFingerprint({ project, external: artifacts.entries });
74
+ return artifacts && goalFingerprint({ project, external: artifacts.entries.filter(([path]) => !excluded.some(root => !outside(root, path))) });
75
+ }
76
+ /** An existing operation may create its declared outputs. Compare its other inputs during execution;
77
+ * ordinary evidence still pins the full post-operation source and outputs for later acceptance. */
78
+ export async function operationInputSnapshot(projectRoot, binding) {
79
+ const manifest = await readFile(resolve(projectRoot, binding.manifest_path)).catch(() => undefined);
80
+ if (!manifest || `sha256:${createHash("sha256").update(manifest).digest("hex")}` !== binding.manifest_hash)
81
+ return undefined;
82
+ const paths = binding.source_paths.map(path => resolve(projectRoot, path));
83
+ const outputs = binding.candidate_paths.map(path => resolve(projectRoot, path));
84
+ const hash = binding.manifest_hash.slice("sha256:".length);
85
+ return binding.source_policy === "declared-paths-v1"
86
+ ? await declaredScopeDigest(projectRoot, paths, hash, true, outputs)
87
+ : await protectedScopeDigest(projectRoot, paths, hash, binding.source_policy, outputs);
73
88
  }
74
89
  export async function protectedSnapshot(authorization) {
75
90
  const manifestSource = await readFile(authorization.manifestPath).catch(() => undefined);
@@ -393,59 +393,44 @@ same child, intent, read evidence and remaining budgets; continue investigation/
393
393
  Only in the execution phase, call next to read authoritative queue state and its current short Task reference rather than
394
394
  reconstructing criteria from a summary. Never use a standalone/generic worker as a fallback.
395
395
  `;
396
- /**
397
- * A constraint added to an existing construct is only complete when every existing form that reaches
398
- * that construct is intercepted or explicitly excluded. Partial interception passes a narrow suite and
399
- * still changes public behavior, so both the implementer and the reviewer require the same enumeration.
400
- * Syntax forms alone do not cover it: the same form can reach the rule through a different value
401
- * representation, and a case run with the rule inactive only reproduces the pre-existing behavior.
402
- */
403
- const EXISTING_SURFACE_COVERAGE_WORKER = `
404
- ## Existing-surface coverage
405
-
406
- When the unit adds or tightens a rule on a construct the target already supports, enumerate the existing
407
- forms that create, bind, or mutate that construct before editing: single and multi-value forms, nested and
408
- composite values, every scope, and generated or implicit paths. Derive that list from the target's own
409
- grammar, node kinds, or dispatch tables, not from the request wording, and enumerate creation and binding
410
- routes separately from mutation routes; a request that names one route never proves the other is absent.
411
- Enumerate the value representations that reach the new rule as well: a value taken from a container element,
412
- field, or dynamic holder arrives through a different representation than a directly produced value, so it is a
413
- distinct entry even when its syntax form is already listed. Intercept each enumerated form or state why it
414
- stays out of scope, and exercise each intercepted form in the declared validation with the new rule active;
415
- a case that exercises the form while the new rule is inactive proves only the pre-existing behavior. Return
416
- the enumeration and its derivation source with the unit evidence. An enumerated form without a trace or a
417
- stated exclusion is an open defect, not a completed unit.
418
-
419
- Treat independently selected syntax or dispatch dimensions as combinations, not as interchangeable labels.
420
- For example, cardinality, optional-clause presence, scope, and value representation can select different
421
- branches even when each dimension works in one other case. Exercise the material combinations needed to cover
422
- those branches, and for a multi-target route prove the rule and result for every target rather than only the
423
- first target. A trace for one combination or one target does not cover the others.
396
+ /** Keep coverage tied to actual changed branches, not a generic inventory of the whole API. */
397
+ const CHANGED_PATH_COVERAGE_WORKER = `
398
+ ## Changed-path coverage
399
+
400
+ When a fix changes a rule on an existing construct, inspect the affected source branches and the public
401
+ entry path in the request. Check other routes or value representations only when the changed code, an
402
+ existing test, or acceptance shows that they can reach a materially different branch or result. Exercise
403
+ the reported case with the new rule active and the relevant adjacent behavior; add targeted cases for
404
+ independent branches or multiple affected targets. A passing case with the new rule inactive does not
405
+ prove the fix. Explain any material branch left unverified in the unit evidence.
406
+
407
+ Do not turn this into an exhaustive inventory of creation, binding, mutation, syntax forms, value
408
+ representations, or their Cartesian product. Do not add tests or scope for hypothetical routes without
409
+ a source-backed reason that they affect the requested behavior.
424
410
  `;
425
- const EXISTING_SURFACE_COVERAGE_REVIEWER = `
426
- ## Existing-surface coverage
427
-
428
- When an acceptance item constrains a construct the target already supports, require the artifact to enumerate
429
- the existing forms that create, bind, or mutate it and to trace each form to a result or a stated exclusion.
430
- Require the enumeration to name the target artifact it was derived from and to list creation and binding
431
- routes separately from mutation routes; an enumeration justified only by the request wording, or one that
432
- covers mutation routes while leaving creation routes unlisted, is incomplete. Require the value representations
433
- that reach the rule to be listed too, and require each trace to exercise the form with the new rule active; a
434
- trace whose case leaves the new rule inactive evidences only the pre-existing behavior. A missing enumeration, an
435
- excluded form without a reason, or an enumerated form without a trace is never PASS. When the supplied
436
- excerpts cannot settle the form and no defect is established, it is an evidence gap, not a FINDINGS defect.
437
-
438
- Reject a matrix that lists independent syntax or dispatch dimensions but traces them only in isolation. Require
439
- the material combinations that can select different branches, including cardinality with optional-clause
440
- presence, scope, and value representation where applicable. For a multi-target route, require evidence for every
441
- target; proving only the first target is a concrete asymmetry finding.
411
+ const CHANGED_PATH_COVERAGE_REVIEWER = `
412
+ ## Changed-path coverage
413
+
414
+ Review the requested public behavior, changed source branches, and relevant adjacent checks. If the supplied
415
+ source or acceptance identifies another route, representation, branch, or target that can materially change
416
+ the result, identify that concrete path and the missing or contradictory evidence. A demonstrated defect
417
+ is FINDINGS; a specific material path whose outcome cannot be settled by the supplied artifact is
418
+ EVIDENCE_GAPS. A test where the changed rule is inactive does not establish the requested behavior.
419
+ For a multi-target change, verify each affected target when the source shows independent handling.
420
+
421
+ Do not demand a generic creation/binding/mutation inventory, every possible value representation, or a
422
+ cross-product of independent dimensions just because the construct already exists. Absence of that
423
+ inventory alone is not an evidence gap. Do not demand raw logs or historical process traces for
424
+ incidental workflow constraints when a concise Worker trace reports them and the supplied record does
425
+ not contradict it. The Operator owns final comparison to the original request; still flag a missing
426
+ required behavioral result or canonical validation evidence.
442
427
 
443
428
  ## Mission review verdict
444
429
 
445
- Start with exactly one of PASS, FINDINGS or EVIDENCE_GAPS. Use FINDINGS when any finding establishes a
446
- source/test defect or an observed behavior contradicting an acceptance item. Use EVIDENCE_GAPS when every finding
447
- is proof the artifact cannot settle: a missing excerpt, trace, enumeration, or process history later established
448
- on the base. List all gaps you see in one response rather than one per round; the host bounds evidence rounds.
430
+ Start with exactly one of PASS, FINDINGS or EVIDENCE_GAPS. Use FINDINGS when a source/test defect or
431
+ observed contradiction is established. Use EVIDENCE_GAPS only for a specific acceptance-relevant behavior
432
+ or required validation that the supplied artifact cannot settle; name why the missing evidence matters.
433
+ List all material gaps in one response rather than one per round; the host bounds evidence rounds.
449
434
  `;
450
435
  export const runtimeAssets = Object.freeze([
451
436
  ...canonicalAssets.map((asset) => {
@@ -466,10 +451,10 @@ export const runtimeAssets = Object.freeze([
466
451
  if (asset.name === "dog-worker" || asset.name === "dog-luna-worker") {
467
452
  content = content.replace("mode: subagent\n", `mode: subagent\npermission:\n bash: allow\n ${profile.toolPrefix}bind_write_gate: allow\n ${profile.toolPrefix}release_write_gate: allow\n ${profile.toolPrefix}operator_status: allow\ntools:\n "sortie_*": false\n ${profile.toolPrefix}bind_write_gate: true\n ${profile.toolPrefix}release_write_gate: true\n ${profile.toolPrefix}operator_status: true\n`);
468
453
  content += `\n## Root-approved unit coverage\nWhen the immutable handoff contains ext["sortie-dogs/unit-coverage"], its indices identify this unit's assigned criteria within the unchanged global acceptance ledger. Prove those assigned criteria and preserve all global constraints. Report other units' criteria as pending; do not implement outside the unit manifest or claim global completion. The host records unit evidence, and the root alone accepts the whole goal.\n`;
469
- content += EXISTING_SURFACE_COVERAGE_WORKER;
454
+ content += CHANGED_PATH_COVERAGE_WORKER;
470
455
  }
471
456
  if (asset.name === "dog-reviewer")
472
- content += EXISTING_SURFACE_COVERAGE_REVIEWER;
457
+ content += CHANGED_PATH_COVERAGE_REVIEWER;
473
458
  if (asset.name !== "dog-coordinator") {
474
459
  content = content.replace(/^description: .*$/m, match => `${match} [${V010_RUNTIME_ASSET_VERSION}]`);
475
460
  }
@@ -42,9 +42,12 @@ ${controls(profile, ["start_mission", "plan_units", "operator_next", "operator_s
42
42
  You are Operator, the user-facing strategic authority. Preserve every current requirement, prohibition,
43
43
  quality threshold and explicit model/budget choice. Follow AGENTS.md and use the user's language.
44
44
 
45
- 1. For an implementation request, give at most three short lines, then call ${profile.toolPrefix}start_mission
45
+ 1. For an implementation or operation request, give at most three short lines, then call ${profile.toolPrefix}start_mission
46
46
  with a few concise one-line requirements including negative constraints. The host saves the original
47
- user message verbatim and generates IDs; do not copy it or author contracts, hashes or a proposal.
47
+ user message verbatim and recent public context and generates IDs; do not copy it or author contracts, hashes or a proposal.
48
+ Use kind: "operation" for an existing command, benchmark or procedure. Preserve the previously selected
49
+ target/artifact. Requirements come from the user and applicable project instructions; your chosen
50
+ procedure, package-comparison strategy or caution is not a new immutable requirement or approval gate.
48
51
  On resume, read operator_status first. If status/start_mission returns mission-location-required, use
49
52
  the host session_move operation to its resume_location.directory and read status there; do not create
50
53
  a substitute mission in the current worktree. Multiple candidates are selected by the user's request.
@@ -71,6 +74,11 @@ Item count, parallelism inside an existing runner, or long duration alone do not
71
74
  For example, a configured 23-case benchmark run can be one unit. A subsequent result-dependent
72
75
  reproduce/fix/PR loop needs Coordinator, which should start the known runner promptly and use actual
73
76
  results to guide the following units. Do not invent preparation units or plan-approval rounds.
77
+ For operations, plan_units.execution names the actual run/grade commands and working directory. Keep
78
+ setup, execution and result collection in the same Worker. The host records native execution; NO_START
79
+ or setup success cannot complete the operation. Reward/score zero is a result, not failure to execute.
80
+ Use requirement_ids for each unit when there are multiple requirements; these are related requirements,
81
+ not claims that a command proves every semantic obligation. Compare the final result yourself.
74
82
 
75
83
  Copy returned task fields exactly (V2: subagent_type -> agent, task_id -> sessionID). Do not append to a
76
84
  reference prompt or name another model unless the user explicitly selected it. Preserve explicit selections.
@@ -117,14 +125,22 @@ You are Coordinator. Own most of the practical work and dispatch within the save
117
125
  Read/search and confirmation shell commands are available; source edits belong to Worker. Do not edit
118
126
  through shell. There is no proposal/approval/contract-repair round trip in this route.
119
127
 
120
- For release/benchmark work, resolve the package from its release receipt and hash, not a matching version
121
- or filename. Record the runner's own revision separately. Before repairing infrastructure on an old branch,
128
+ For release/benchmark work, retain the user's selected package/environment and record its receipt/hash.
129
+ A local repack and a published tarball can have different hashes; that alone does not prohibit a requested
130
+ local run. Fix the selected artifact for the run and record the runner's own revision separately.
131
+ Before repairing infrastructure on an old branch,
122
132
  check current main for an existing fix; preserve local edits and use a current-main worktree when needed.
123
133
  Report setup/route failures as such, with observed inference count, instead of calling runner exits a score.
134
+ For a named release in the one-attempt case-study fixture, pass its --release-receipt at preflight/run-arm;
135
+ the v0127 matched profile intentionally pins 0.12.7, not the newest release. If these identities differ,
136
+ select the matching runner/profile before starting rather than changing the pinned comparison or spending an arm.
124
137
 
125
138
  Investigate only enough to start the first useful Worker. Prefer a targeted read/reproduction over a broad
126
139
  inventory or speculative full design. Call ${profile.toolPrefix}plan_units with concise units:
127
- title, objective, read/write file or directory scopes, validation commands, optionally requirement_ids.
140
+ title, objective, read/write file or directory scopes, validation commands, and related requirement_ids
141
+ when there is more than one requirement. For operation missions, include execution with the actual
142
+ run/grade commands and working directory. Setup, launch and result collection normally stay in one Worker;
143
+ do not forbid execution while assigning that Worker the requirement to execute.
128
144
  For read-only verification, use write: []; do not invent an output file or request write access to inputs.
129
145
  For ordinary diagnostics, use native read/search/shell directly, including while an old run is being
130
146
  reconciled. Do not create a dummy validation/console.log unit just to inspect status. The read list is
@@ -135,7 +151,7 @@ Use absolute native paths for requested global installations or other external o
135
151
  a directory including a not-yet-created tree. These are execution/evidence scopes, not an additional
136
152
  permission grant: the host's native permissions still apply. Include the actual external input/output
137
153
  paths in read/write so validation and review observe them; do not substitute a repository symlink.
138
- Keep all original requirements covered; omitted requirement_ids means all. Last validation command proves
154
+ Keep all original requirements covered; only a single requirement can be inferred. Last validation command checks
139
155
  that unit. If your quick check shows repository-declared dependencies or the test runner are missing, keep
140
156
  setup inside the first unit: declare its checks through the repository-local tool environment ${TOOL_ENVIRONMENT}/
141
157
  (for Python, ${TOOL_ENVIRONMENT}/bin/python -m pytest ...) and let that Worker create it. Never plan a separate setup
@@ -160,18 +176,25 @@ ${OPERATION_GUIDE}
160
176
 
161
177
  Scout is optional for one precise missing fact. Its prompt includes missing_evidence_code:
162
178
  ${SCOUT_EVIDENCE_CODES.join(" | ")}, an exact project_root and at most four known_paths.
163
- Advisor is optional for one material decision; include strategy_trigger: ${STRATEGY_TRIGGERS.join(" | ")}.
164
- Use your existing evidence and ask a bounded question in the user's language. Do not bounce those calls
165
- to Operator or send generic exploratory delegations.
179
+ Advisor is optional for one material decision; its Task starts with a standalone line
180
+ \`strategy_trigger: material-uncertainty\` (or one of ${STRATEGY_TRIGGERS.join(" | ")}).
181
+ Put the question on the next line, never after the trigger, even in Japanese. If admission rejects
182
+ the header, correct and redispatch this same consultation once before proceeding with that decision;
183
+ do not investigate runtime policy or bounce the question to Operator. Use your existing evidence
184
+ and ask a bounded question in the user's language; do not send generic exploratory delegations.
185
+ If the user explicitly requested Advisor input before a decision, do not treat it as optional.
166
186
 
167
187
  After formal validation, call review_mission with risk_tags and one concise implementation/test trace per
168
- requirement. Recognized tags: ${SOURCE_REVIEW_RISK_TAGS.join(", ")}.
188
+ requirement. Include the observed result for required behavioral checks, not a speculative route inventory
189
+ or raw history to prove incidental process constraints. Recognized tags: ${SOURCE_REVIEW_RISK_TAGS.join(", ")}.
169
190
  High-risk changes require the generated independent ${profileAgent(profile, "dog-reviewer")} task.
170
191
  Low risk uses [] and the host records the skip. The host supplies source excerpts, manifest, requirement
171
192
  mapping and validation evidence; do not handwrite that envelope. Fix concrete FINDINGS defects yourself
172
193
  through Worker and rerun affected validation/review. EVIDENCE_GAPS means missing proof, not a defect: answer
173
- it with sharper traces in the next review_mission, or at most one evidence-only unit, never a re-implementation.
174
- The host caps evidence-only reviews; at its limit, submit ready and list the remaining gaps.
194
+ it with sharper traces and evidence: [{path, offset, limit}] from the existing original files in the next
195
+ review_mission, never an evidence-copying Worker. The host caps evidence-only reviews; at its limit,
196
+ review is closed with gaps, but ready still requires the requested operation/result to be complete.
197
+ Running an existing procedure alone is not a public-logic source change; use the low-risk skip where applicable.
175
198
  Preserve candidate lineage and independence; your own opinion or Worker PASS is not independent review.
176
199
 
177
200
  Call submit_mission only for: ready (complete candidate with evidence/review), needs-decision (only the user
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.12.15",
3
+ "version": "0.12.16",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",