sortie-dogs 0.12.6 → 0.12.7

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.3-v2-zod-schema-compaction-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.12.7-review-convergence-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.3-v2-zod-schema-compaction-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.12.7-review-convergence-v1";
package/dist/cli/main.js CHANGED
@@ -258,6 +258,14 @@ export async function run(argv) {
258
258
  if (initialized.preservedLegacyPaths.length > 0) {
259
259
  process.stdout.write(`Preserved legacy runtime files: ${initialized.preservedLegacyPaths.join(", ")}.\n`);
260
260
  }
261
+ const override = profile === "v010"
262
+ ? await initializer.coordinatorModelOverride(global ? target : resolve(initArgs[0] ?? process.cwd()), global).catch(() => undefined)
263
+ : undefined;
264
+ if (override) {
265
+ process.stderr.write(`Warning: ${override.path} routes dogs-coordinator to ${override.model}. ` +
266
+ `Sortie-dogs expects ${initializer.V010_COORDINATOR_MODEL}; a gpt-6-luna-fast Coordinator was measured to repeat plans and reviews until timeout. ` +
267
+ "Remove the override unless it is intentional.\n");
268
+ }
261
269
  return 0;
262
270
  }
263
271
  catch (error) {
@@ -11,6 +11,15 @@ export declare class ProjectInitializationError extends Error {
11
11
  readonly code: ProjectInitializationErrorCode;
12
12
  constructor(code: ProjectInitializationErrorCode, message: string, options?: ErrorOptions);
13
13
  }
14
+ export declare const V010_COORDINATOR_MODEL = "openai/gpt-6-sol#xhigh";
15
+ /**
16
+ * Report a user config that routes the v0.10 Coordinator away from its packaged model. Measured Luna
17
+ * Coordinators repeated plans and review dispatches until timeout, so init surfaces it without editing it.
18
+ */
19
+ export declare function coordinatorModelOverride(root: string, global: boolean): Promise<{
20
+ path: string;
21
+ model: string;
22
+ } | undefined>;
14
23
  /** Resolves the OpenCode global configuration directory without platform-specific paths. */
15
24
  export declare function resolveGlobalConfigRoot(env?: NodeJS.ProcessEnv, home?: string): Promise<string>;
16
25
  export declare function initializeProject(projectRoot?: string, profile?: RuntimeProfileId): Promise<InitializeProjectResult>;
@@ -536,6 +536,34 @@ async function initializeRoot(requestedRoot, layout, installAssets = runtimeAsse
536
536
  preservedLegacyPaths,
537
537
  };
538
538
  }
539
+ export const V010_COORDINATOR_MODEL = "openai/gpt-6-sol#xhigh";
540
+ /**
541
+ * Report a user config that routes the v0.10 Coordinator away from its packaged model. Measured Luna
542
+ * Coordinators repeated plans and review dispatches until timeout, so init surfaces it without editing it.
543
+ */
544
+ export async function coordinatorModelOverride(root, global) {
545
+ const prefix = global ? "" : `${OPEN_CODE_DIRECTORY}/`;
546
+ for (const relativePath of [`${prefix}opencode.jsonc`, `${prefix}opencode.json`]) {
547
+ let content;
548
+ try {
549
+ content = await readFile(resolve(root, relativePath), "utf8");
550
+ }
551
+ catch (error) {
552
+ if (error.code === "ENOENT")
553
+ continue;
554
+ throw error;
555
+ }
556
+ const config = parse(content, [], { allowTrailingComma: true });
557
+ for (const key of ["agents", "agent"]) {
558
+ const agents = config?.[key];
559
+ const coordinator = agents !== null && typeof agents === "object" ? agents["dogs-coordinator"] : undefined;
560
+ const model = coordinator !== null && typeof coordinator === "object" ? coordinator.model : undefined;
561
+ if (typeof model === "string" && model !== V010_COORDINATOR_MODEL)
562
+ return { path: resolve(root, relativePath), model };
563
+ }
564
+ }
565
+ return undefined;
566
+ }
539
567
  /** Resolves the OpenCode global configuration directory without platform-specific paths. */
540
568
  export async function resolveGlobalConfigRoot(env = process.env, home = homedir()) {
541
569
  if (env.OPENCODE_CONFIG_DIR)
@@ -40,11 +40,22 @@ export interface OperatorMission {
40
40
  risk: string[];
41
41
  source: string;
42
42
  task: OperatorTask | null;
43
- verdict: "pending" | "PASS" | "findings" | "skipped-low-risk";
43
+ verdict: "pending" | "PASS" | "findings" | "evidence-gaps" | "skipped-low-risk";
44
44
  result?: string;
45
45
  child?: string;
46
+ /** Reviews on this mission that found only missing evidence; bounded by MISSION_EVIDENCE_GAP_REVIEW_LIMIT. */
47
+ evidenceGapReviews?: number;
46
48
  };
47
49
  }
50
+ /**
51
+ * Evidence-only findings do not establish a defect. Each extra round costs a full Reviewer pass and often a
52
+ * Worker unit, so after this many evidence-only reviews the candidate may be submitted with the gaps listed.
53
+ */
54
+ export declare const MISSION_EVIDENCE_GAP_REVIEW_LIMIT = 2;
55
+ /** Classify an independent Reviewer's first line. Anything else is a finding. */
56
+ export declare function missionReviewVerdict(text: string): "PASS" | "evidence-gaps" | "findings";
57
+ /** Whether the recorded review permits submission and acceptance of the current candidate. */
58
+ export declare function missionReviewAccepted(review: NonNullable<OperatorMission["review"]>): boolean;
48
59
  /** Durable user intent and dispatch ownership. Execution/evidence still belong to the v0.10 engine. */
49
60
  export declare class OperatorMissionRuntime {
50
61
  readonly projectRoot: string;
@@ -18,6 +18,20 @@ export function missionValidationCommand(command) {
18
18
  const expression = `exec(${JSON.stringify(heredoc[4])})`;
19
19
  return `${heredoc[1]} -c '${expression.replaceAll("'", "'\\''")}'`;
20
20
  }
21
+ /**
22
+ * Evidence-only findings do not establish a defect. Each extra round costs a full Reviewer pass and often a
23
+ * Worker unit, so after this many evidence-only reviews the candidate may be submitted with the gaps listed.
24
+ */
25
+ export const MISSION_EVIDENCE_GAP_REVIEW_LIMIT = 2;
26
+ /** Classify an independent Reviewer's first line. Anything else is a finding. */
27
+ export function missionReviewVerdict(text) {
28
+ return /^\s*PASS(?:\s|$)/u.test(text) ? "PASS" : /^\s*EVIDENCE_GAPS(?:\s|$)/u.test(text) ? "evidence-gaps" : "findings";
29
+ }
30
+ /** Whether the recorded review permits submission and acceptance of the current candidate. */
31
+ export function missionReviewAccepted(review) {
32
+ return review.verdict === "PASS" || review.verdict === "skipped-low-risk" ||
33
+ (review.verdict === "evidence-gaps" && (review.evidenceGapReviews ?? 0) >= MISSION_EVIDENCE_GAP_REVIEW_LIMIT);
34
+ }
21
35
  /** Durable user intent and dispatch ownership. Execution/evidence still belong to the v0.10 engine. */
22
36
  export class OperatorMissionRuntime {
23
37
  projectRoot;
@@ -294,12 +308,15 @@ export function missionPacket(mission, run) {
294
308
  submission: mission.submission, progress: mission.progress,
295
309
  review: mission.review ? { risk_tags: mission.review.risk, verdict: mission.review.verdict,
296
310
  source_fingerprint: mission.review.source, reviewer_session_id: mission.review.child ?? null,
297
- result: mission.review.result ?? null } : null,
311
+ result: mission.review.result ?? null, evidence_gap_reviews: mission.review.evidenceGapReviews ?? 0,
312
+ evidence_gap_review_limit: MISSION_EVIDENCE_GAP_REVIEW_LIMIT, accepted: missionReviewAccepted(mission.review) } : null,
298
313
  ...(run ? { run_id: run.runID, status: run.phase, decision: run.decision,
299
314
  units: run.units.map(unit => ({ id: unit.unit.id, title: unit.unit.title, status: unit.status,
300
315
  child_session_id: unit.childSessionID, result_class: unit.resultClass, evidence: unit.evidence })) } : {}),
301
316
  next_action: run?.phase === "awaiting-decision" ? "Coordinator: correct the cause and call plan_units with the remaining work and all requirements; budget is cumulative."
302
- : run?.phase === "awaiting-acceptance" ? "Coordinator: obtain required independent review, then submit_mission. Operator: compare all requirements with source/evidence before complete_mission."
317
+ : run?.phase === "awaiting-acceptance" ? (mission.review?.verdict === "evidence-gaps" && !missionReviewAccepted(mission.review)
318
+ ? "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."
319
+ : "Coordinator: obtain required independent review, then submit_mission. Operator: compare all requirements with source/evidence before complete_mission.")
303
320
  : "Coordinator: continue the next useful unit within original requirements. Return only a completion candidate, user-only decision, or scope/budget extension." };
304
321
  }
305
322
  /** Models forward a short capability, never recopy the host's evidence hashes and source packet. */
@@ -109,7 +109,7 @@ interface UnitState {
109
109
  readonly task: OperatorTask;
110
110
  readonly handoffPath: string;
111
111
  readonly manifestPath: string;
112
- readonly hashes: readonly string[];
112
+ hashes: readonly string[];
113
113
  status: "pending" | "running" | "succeeded" | "failed" | "cancelled";
114
114
  callID: string | null;
115
115
  childSessionID: string | null;
@@ -332,6 +332,8 @@ export declare class OperatorRuntime {
332
332
  packet(state: OperatorState): unknown;
333
333
  continuationCheckpoint(root: string): Promise<string | undefined>;
334
334
  private verifyControls;
335
+ /** Follow only the host's authorized parent-link repair; keep the durable control pin in sync. */
336
+ acknowledgeHostHandoffRepair(root: string, taskID: string, path: string, original: string, repaired: string): Promise<void>;
335
337
  private gitPaths;
336
338
  private pathAuthorized;
337
339
  /**
@@ -1893,6 +1893,30 @@ export class OperatorRuntime {
1893
1893
  if (contents.some((value, index) => hash(value) !== unit.hashes[index]))
1894
1894
  throw new Error("operator-contract-changed");
1895
1895
  }
1896
+ /** Follow only the host's authorized parent-link repair; keep the durable control pin in sync. */
1897
+ acknowledgeHostHandoffRepair(root, taskID, path, original, repaired) {
1898
+ return this.serial(root, async () => {
1899
+ const state = await this.read(root);
1900
+ const unit = state?.units.find(candidate => candidate.status === "running" && candidate.handoffPath === path &&
1901
+ /^task_id: (.+)$/m.exec(candidate.task.prompt)?.[1] === taskID);
1902
+ if (!unit)
1903
+ return; // The same host repair also serves non-Operator handoffs.
1904
+ if (hash(original) !== unit.hashes[0] || await readFile(path, "utf8") !== repaired) {
1905
+ throw new Error("operator-host-handoff-repair-unpinned");
1906
+ }
1907
+ const before = JSON.parse(original), after = JSON.parse(repaired);
1908
+ const oldLink = inspectAcceptanceContinuity(before).ledger;
1909
+ const newLink = inspectAcceptanceContinuity(after).ledger;
1910
+ if (!oldLink || !newLink || oldLink.task_id !== taskID || oldLink.parent_fingerprint !== "none" ||
1911
+ newLink.task_id !== taskID || newLink.fingerprint !== oldLink.fingerprint ||
1912
+ newLink.parent_fingerprint === "none" ||
1913
+ JSON.stringify({ ...before, ext: { ...before.ext, [ACCEPTANCE_CONTINUITY_EXTENSION]: { ...oldLink, parent_fingerprint: newLink.parent_fingerprint } } }, null, 2) + "\n" !== repaired) {
1914
+ throw new Error("operator-host-handoff-repair-invalid");
1915
+ }
1916
+ unit.hashes = [hash(repaired), ...unit.hashes.slice(1)];
1917
+ await this.save(state);
1918
+ });
1919
+ }
1896
1920
  gitPaths(output) {
1897
1921
  return output.split("\0").filter(Boolean).map(path => normalizeRelativePath(path.replaceAll("\\", "/")));
1898
1922
  }
@@ -6953,7 +6953,8 @@ export const SortieDogsPlugin = async (input, options) => {
6953
6953
  // Persist it so the worker's Read and later recovery see the same contract.
6954
6954
  if (reservedParallelDescriptor === undefined && ledger.parent_fingerprint === "none" &&
6955
6955
  (exactCarryForward || strictAppend)) {
6956
- const handoff = JSON.parse(await readFile(handoffPaths[0], "utf8"));
6956
+ const source = await readFile(handoffPaths[0], "utf8");
6957
+ const handoff = JSON.parse(source);
6957
6958
  const validated = validateHandoffSchema(handoff);
6958
6959
  const current = validated.ok ? inspectAcceptanceContinuity(validated.value).ledger : undefined;
6959
6960
  if (current === undefined || JSON.stringify(current) !== JSON.stringify(ledger)) {
@@ -6963,7 +6964,15 @@ export const SortieDogsPlugin = async (input, options) => {
6963
6964
  }
6964
6965
  ledger = { ...ledger, parent_fingerprint: previous.fingerprint };
6965
6966
  handoff.ext[ACCEPTANCE_CONTINUITY_EXTENSION] = ledger;
6966
- await writeFile(handoffPaths[0], `${JSON.stringify(handoff, null, 2)}\n`);
6967
+ const repaired = `${JSON.stringify(handoff, null, 2)}\n`;
6968
+ await writeFile(handoffPaths[0], repaired);
6969
+ try {
6970
+ await input.runtimeBridge?.onHostHandoffRepaired?.(toolInput.sessionID, ledger.task_id, handoffPaths[0], source, repaired);
6971
+ }
6972
+ catch (error) {
6973
+ await writeFile(handoffPaths[0], source);
6974
+ throw error;
6975
+ }
6967
6976
  await inspect(handoffPaths[0], undefined, { report: true });
6968
6977
  }
6969
6978
  if (ledger.parent_fingerprint !== previous.fingerprint || (!exactCarryForward && !strictAppend)) {
@@ -3,10 +3,12 @@ import { execFile } from "node:child_process";
3
3
  import { lstat, readFile, readlink } from "node:fs/promises";
4
4
  import { resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
+ import { TOOL_ENVIRONMENT } from "../runtime-mission-assets.js";
6
7
  const exec = promisify(execFile);
7
8
  /** Pin all scoped tracked/untracked source bytes, including deletions; display a bounded excerpt only. */
8
9
  export async function missionReviewSource(directory, run) {
9
- const scopes = [...new Set(run.units.flatMap(unit => unit.unit.write))];
10
+ // The shared dependency environment is local tooling, never reviewed or pinned source.
11
+ const scopes = [...new Set(run.units.flatMap(unit => unit.unit.write)), `:(exclude)${TOOL_ENVIRONMENT}`];
10
12
  const git = async (args) => (await exec("git", args, { cwd: directory, maxBuffer: 8 * 1024 * 1024 })).stdout;
11
13
  const names = await git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...scopes]);
12
14
  const untracked = new Set((await git(["ls-files", "-z", "--others", "--exclude-standard", "--", ...scopes])).split("\0").filter(Boolean));
@@ -12,7 +12,7 @@ import { decoratePreviewHeadings, returnReportPanel } from "./receipt-presentati
12
12
  import { sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
13
13
  import { extractWritePaths, normalizeCommand } from "./gate.js";
14
14
  import { normalizeRelativePath } from "../core/path.js";
15
- import { OperatorMissionRuntime, missionPacket, missionPlan, missionReviewTask, missionReviewTraces } from "../core/operator-mission.js";
15
+ import { MISSION_EVIDENCE_GAP_REVIEW_LIMIT, OperatorMissionRuntime, missionPacket, missionPlan, missionReviewAccepted, missionReviewTask, missionReviewTraces, missionReviewVerdict } from "../core/operator-mission.js";
16
16
  import { publishMissionProgress } from "./mission-progress.js";
17
17
  import { missionReviewSource } from "./mission-review.js";
18
18
  import { SOURCE_REVIEW_RISK_TAGS } from "../core/consultation.js";
@@ -284,6 +284,7 @@ export function createProfiledPlugin(profile, assetVersion) {
284
284
  });
285
285
  const runtimeBridge = {
286
286
  profile, assetVersion,
287
+ onHostHandoffRepaired: (root, taskID, path, original, repaired) => operators.acknowledgeHostHandoffRepair(root, taskID, path, original, repaired),
287
288
  continuationCheckpoint: async (root) => {
288
289
  const mission = await missions.read(root);
289
290
  if (mission && !["completed", "cancelled"].includes(mission.phase))
@@ -1193,7 +1194,8 @@ export function createProfiledPlugin(profile, assetVersion) {
1193
1194
  const task = risk.length === 0 ? null : { subagent_type: profileAgent(profile, "dog-reviewer"),
1194
1195
  description: `🔎 ${run.units[0].unit.title}`, prompt: [
1195
1196
  `candidate_id: ${mission.id}`, `review_phase: ${phase}`, "canonical_validation_exit: 0", `risk_tags: [${risk.join(", ")}]`,
1196
- "Review this candidate independently. Use the language of the requirements/traces. Invoke no tools. First line: exactly PASS or FINDINGS.",
1197
+ "Review this candidate independently. Use the language of the requirements/traces. Invoke no tools. First line: exactly PASS, FINDINGS or EVIDENCE_GAPS.",
1198
+ "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.",
1197
1199
  `acceptance: ${JSON.stringify(run.acceptance)}`, `changedLogicSummary: ${JSON.stringify(traces)}`,
1198
1200
  ...run.acceptance.map((_, i) => `acceptance[${i}] -> changedLogicSummary[${i}]`),
1199
1201
  `manifest: ${JSON.stringify(run.units.map(unit => unit.unit))}`, `sourceFingerprint: ${source.fingerprint}`,
@@ -1202,13 +1204,14 @@ export function createProfiledPlugin(profile, assetVersion) {
1202
1204
  ].join("\n") };
1203
1205
  const reviewed = await missions.update(root, item => {
1204
1206
  item.review = { runID: run.runID, risk: risk, source: source.fingerprint,
1205
- task, verdict: task ? "pending" : "skipped-low-risk", ...(mission.review?.child ? { child: mission.review.child } : {}) };
1207
+ task, verdict: task ? "pending" : "skipped-low-risk", ...(mission.review?.child ? { child: mission.review.child } : {}),
1208
+ ...(mission.review?.evidenceGapReviews ? { evidenceGapReviews: mission.review.evidenceGapReviews } : {}) };
1206
1209
  });
1207
1210
  return JSON.stringify(task ? { status: "review-required", task: missionReviewTask(reviewed) } : { status: "skipped-low-risk" });
1208
1211
  } };
1209
1212
  async function assertMissionReview(root, mission) {
1210
1213
  const run = await operators.required(root), review = mission.review;
1211
- if (!review || review.runID !== run.runID || !["PASS", "skipped-low-risk"].includes(review.verdict) ||
1214
+ if (!review || review.runID !== run.runID || !missionReviewAccepted(review) ||
1212
1215
  review.source !== (await missionReviewSource(input.directory, run)).fingerprint)
1213
1216
  throw new Error("mission-review-required-or-stale");
1214
1217
  }
@@ -1736,13 +1739,23 @@ export function createProfiledPlugin(profile, assetVersion) {
1736
1739
  const history = child ? await messages(child) : [];
1737
1740
  const last = [...history].reverse().find(message => (record(message.info) ? message.info.role : message.role) === "assistant");
1738
1741
  const text = Array.isArray(last?.parts) ? last.parts.filter(record).filter(part => part.type === "text").map(part => part.text).join("\n") : String(raw);
1739
- await missions.update(ownership.root, mission => {
1742
+ const independent = who?.role === "dog-reviewer" && who.parent === ownership.actor;
1743
+ const reviewed = await missions.update(ownership.root, mission => {
1740
1744
  if (mission.review) {
1741
1745
  mission.review.result = text.slice(0, 16_000);
1742
- mission.review.verdict = /^\s*PASS(?:\s|$)/u.test(text) && who?.role === "dog-reviewer" && who.parent === ownership.actor ? "PASS" : "findings";
1746
+ const verdict = missionReviewVerdict(text);
1747
+ mission.review.verdict = independent ? verdict : "findings";
1748
+ if (mission.review.verdict === "evidence-gaps")
1749
+ mission.review.evidenceGapReviews = (mission.review.evidenceGapReviews ?? 0) + 1;
1743
1750
  mission.review.child = child;
1744
1751
  }
1745
1752
  });
1753
+ if (reviewed.review?.verdict === "evidence-gaps" && typeof output.output === "string") {
1754
+ const count = reviewed.review.evidenceGapReviews ?? 0;
1755
+ output.output += missionReviewAccepted(reviewed.review)
1756
+ ? `\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.`
1757
+ : `\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.`;
1758
+ }
1746
1759
  }
1747
1760
  taskOwners.delete(request.callID);
1748
1761
  return;
@@ -27,6 +27,7 @@ export interface RuntimeBridge {
27
27
  continuationCheckpoint?(rootSessionID: string): Promise<string | undefined>;
28
28
  requiresExplicitAcceptance?(rootSessionID: string): Promise<boolean>;
29
29
  ownsCanonicalValidation?(rootSessionID: string, unitID: string, childSessionID: string, command: string): Promise<boolean>;
30
+ onHostHandoffRepaired?(rootSessionID: string, taskID: string, handoffPath: string, original: string, repaired: string): Promise<void>;
30
31
  allowsInvestigativeShell?(sessionID: string): Promise<boolean>;
31
32
  onSerialSettlement?(settlement: SerialDispatchSettlement): Promise<void>;
32
33
  onRootTerminal?(rootSessionID: string, receipt: GoalTerminalReceipt): Promise<void>;
@@ -432,13 +432,20 @@ routes separately from mutation routes; an enumeration justified only by the req
432
432
  covers mutation routes while leaving creation routes unlisted, is incomplete. Require the value representations
433
433
  that reach the rule to be listed too, and require each trace to exercise the form with the new rule active; a
434
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 a concrete finding, never PASS.
436
- Report it as an evidence gap when the supplied excerpts cannot settle the form.
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
437
 
438
438
  Reject a matrix that lists independent syntax or dispatch dimensions but traces them only in isolation. Require
439
439
  the material combinations that can select different branches, including cardinality with optional-clause
440
440
  presence, scope, and value representation where applicable. For a multi-target route, require evidence for every
441
441
  target; proving only the first target is a concrete asymmetry finding.
442
+
443
+ ## Mission review verdict
444
+
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.
442
449
  `;
443
450
  export const runtimeAssets = Object.freeze([
444
451
  ...canonicalAssets.map((asset) => {
@@ -1,4 +1,6 @@
1
1
  import { type RuntimeProfile } from "./core/runtime-profile.ts";
2
+ /** Repository-local dependency environment shared by all units; excluded from reviewed and captured source. */
3
+ export declare const TOOL_ENVIRONMENT = ".sortie-env";
2
4
  export declare function missionOperatorContent(profile: RuntimeProfile, version: string): string;
3
5
  export declare function missionCoordinatorContent(profile: RuntimeProfile, version: string): string;
4
6
  export declare function missionWorkerContent(profile: RuntimeProfile): string;
@@ -1,6 +1,8 @@
1
1
  import { profileAgent } from "./core/runtime-profile.js";
2
2
  import { STRATEGY_TRIGGERS, SOURCE_REVIEW_RISK_TAGS } from "./core/consultation.js";
3
3
  import { SCOUT_EVIDENCE_CODES } from "./core/scout-contract.js";
4
+ /** Repository-local dependency environment shared by all units; excluded from reviewed and captured source. */
5
+ export const TOOL_ENVIRONMENT = ".sortie-env";
4
6
  function controls(profile, names) {
5
7
  return names.map(name => ` ${profile.toolPrefix}${name}: true`).join("\n");
6
8
  }
@@ -36,8 +38,9 @@ quality threshold and explicit model/budget choice. Follow AGENTS.md and use the
36
38
  unit boundaries, Worker/Scout/Advisor/independent Reviewer calls, write-scope extensions and corrections
37
39
  within the original request and cumulative budget. Do not investigate or approve each unit at the root.
38
40
  3. Compare the returned completion candidate against the original request, real source and observed evidence.
39
- If incomplete, resume the SAME Coordinator with concrete feedback. If complete and required review passed,
40
- call ${profile.toolPrefix}complete_mission. Only its succeeded receipt authorizes DONE.
41
+ If incomplete, resume the SAME Coordinator with concrete feedback. If complete and required review passed
42
+ (or the host accepted it at the evidence-gap limit, with the gaps reported), call
43
+ ${profile.toolPrefix}complete_mission. Only its succeeded receipt authorizes DONE.
41
44
 
42
45
  Fast-lane exception: simple, low-risk, single-unit work with known files and validation may use
43
46
  ${profile.toolPrefix}plan_units directly after start_mission, then dispatch its exact Worker task.
@@ -91,7 +94,10 @@ Investigate only enough to start the first useful Worker. Prefer a targeted read
91
94
  inventory or speculative full design. Call ${profile.toolPrefix}plan_units with concise units:
92
95
  title, objective, read/write file or directory scopes, validation commands, optionally requirement_ids.
93
96
  Keep all original requirements covered; omitted requirement_ids means all. Last validation command proves
94
- that unit. Host generates IDs, proof mapping, handoff, manifest and Task references. Dispatch the returned
97
+ that unit. If your quick check shows repository-declared dependencies or the test runner are missing, keep
98
+ setup inside the first unit: declare its checks through the repository-local tool environment ${TOOL_ENVIRONMENT}/
99
+ (for Python, ${TOOL_ENVIRONMENT}/bin/python -m pytest ...) and let that Worker create it. Never plan a separate setup
100
+ unit or put ${TOOL_ENVIRONMENT}/ in a write scope. Host generates IDs, proof mapping, handoff, manifest and Task references. Dispatch the returned
95
101
  ${profileAgent(profile, "dog-worker")} task verbatim, in foreground. V2 maps subagent_type to agent and
96
102
  task_id to sessionID. Do not insert model overrides unless the user explicitly selected them.
97
103
 
@@ -114,8 +120,10 @@ After formal validation, call review_mission with risk_tags and one concise impl
114
120
  requirement. Recognized tags: ${SOURCE_REVIEW_RISK_TAGS.join(", ")}.
115
121
  High-risk changes require the generated independent ${profileAgent(profile, "dog-reviewer")} task.
116
122
  Low risk uses [] and the host records the skip. The host supplies source excerpts, manifest, requirement
117
- mapping and validation evidence; do not handwrite that envelope. Fix concrete findings yourself through
118
- Worker and rerun affected validation/review. A missing excerpt calls for evidence, not an invented source bug.
123
+ mapping and validation evidence; do not handwrite that envelope. Fix concrete FINDINGS defects yourself
124
+ through Worker and rerun affected validation/review. EVIDENCE_GAPS means missing proof, not a defect: answer
125
+ it with sharper traces in the next review_mission, or at most one evidence-only unit, never a re-implementation.
126
+ The host caps evidence-only reviews; at its limit, submit ready and list the remaining gaps.
119
127
  Preserve candidate lineage and independence; your own opinion or Worker PASS is not independent review.
120
128
 
121
129
  Call submit_mission only for: ready (complete candidate with evidence/review), needs-decision (only the user
@@ -140,7 +148,8 @@ The binding remains valid throughout this Task until return or a control/source
140
148
 
141
149
  Read/search and read-only investigation commands are unrestricted. Use targeted reproduction/diagnosis
142
150
  without registering every exploratory command. All writes, generated/transient files and cleanup stay
143
- inside the unit's file/directory scopes. Do not write outside them through scripts or tools. If scope must
151
+ inside the unit's file/directory scopes, except the tool environment below. Do not write outside them through
152
+ scripts or tools. If scope must
144
153
  expand or a formal check must change, return the exact paths/command and reason to Coordinator; it can
145
154
  approve an in-request extension immediately. Do not ask the user or delegate to another agent.
146
155
 
@@ -150,6 +159,12 @@ and separate shell calls; the host records actual command, source and exit. Diag
150
159
  formal acceptance evidence. Do not repeat a failed command without a concrete source/setup correction or
151
160
  repeat passed checks on unchanged source. Add meaningful tests only when needed by the change/request.
152
161
 
162
+ Missing repository-declared dependencies or test runner are setup, not a result. Make one bounded,
163
+ repository-documented setup attempt in the repository-local tool environment ${TOOL_ENVIRONMENT}/ (for Python:
164
+ python -m venv ${TOOL_ENVIRONMENT}, then install the declared dependencies with its pip), then run the checks.
165
+ Reuse an existing ${TOOL_ENVIRONMENT}/ and never delete it; it is local tooling, not a change, and needs no write
166
+ scope. Return to Coordinator for setup only when it is externally blocked or a formal command must change.
167
+
153
168
  Return changed paths and concise criterion-level implementation/test/input evidence with the real check
154
169
  results. Respect negative constraints and API success/error compatibility. A broad suite PASS does not
155
170
  alone prove every requirement; leave unproven items explicit. Never claim other units or the whole mission
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.12.6",
3
+ "version": "0.12.7",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",