sortie-dogs 0.12.16 → 0.12.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -87,19 +87,29 @@ goal; preserving quality while reducing unnecessary expensive work is.
87
87
 
88
88
  ### SWE-bench evaluation
89
89
 
90
- A frozen Sortie-dogs v0.10.14 build was evaluated on 23 fixed tasks.
91
-
92
- - Official verifier PASS: 6 / 23
93
- - Scored FAIL: 17 / 23
94
- - Infrastructure blocked: 0 / 23
95
- - Total estimated model cost: $15.75
96
- - Median agent runtime: 15.2 min
97
-
98
- The same Sortie-dogs candidate, model routes, budgets, tools, and verification
99
- procedure were held fixed for all tasks. This is one frozen evaluation, not a
100
- general success-rate claim.
101
-
102
- Full methodology and per-task results: [benchmark details](docs/benchmark-v0.10.14-dev23.md)
90
+ Official SWE-bench Lite `dev` results on the same 23 public instances:
91
+
92
+ | Candidate / benchmark adapter | Official resolved | Empty patches | Run conditions | Details |
93
+ | --- | ---: | ---: | --- | --- |
94
+ | v0.10.6 candidate (`859c396`) | 4 / 23 (17.4%) | 5 | Four inference slots | [Handoff](docs/swebench-handoff-2026-09-21.md) |
95
+ | Frozen v0.10.14 build | 6 / 23 (26.1%) | — | Four slots; $1.50/task | [Per-task results](docs/benchmark-v0.10.14-dev23.md) |
96
+ | v0.12.8 (`e0f8cef` adapter) | 5 / 23 (21.7%) | 9 | Four slots; budget amended across two batches; 30-minute timeout | [Campaign](docs/swebench-v0128-dev23-2026-09-26.md) |
97
+ | v0.12.8 (`84ccdf1` main adapter) | 6 / 23 (26.1%) | 2 | Fresh 23-task run; four slots; 30-minute timeout | [Main-integrated run](docs/swebench-main-84ccdf1-dev23-2026-09-26.md) |
98
+ | v0.12.15 (`0c9690d`) | 5 / 23 (21.7%) | 3 | Fresh 23-task ext4 retry; four slots; 40-minute timeout | [Campaign](docs/swebench-v01215-dev23-2026-09-27.md) |
99
+ | v0.12.16 (`9b05a34` release; `b1a6c0e` runner) | 4 / 23 (17.4%) | 5 | Fresh 23-task run; eight slots; 40-minute timeout | [Campaign](docs/swebench-v01216-dev23-2026-09-27.md) |
100
+
101
+ Every row has 23 submitted official predictions; an empty patch counts against
102
+ the score, not as a missing evaluation. The v0.10.14 report does not separately
103
+ summarize empty patches. The v0.12.15 row is the separately approved fresh
104
+ run after an initial `/tmp` quota failure, not an additional score for that
105
+ failed attempt. Inference completion is **not** official resolution.
106
+ Budgets, runtime/adapter versions, and execution conditions changed between
107
+ campaigns, so this table is a history of observed results, not a controlled
108
+ head-to-head comparison or a general success-rate claim. The v0.10.14 run
109
+ estimated $15.75 in model cost and a 15.2-minute median agent runtime.
110
+ The v0.12.16 run is the first eight-slot inference score in this table;
111
+ five runners timed out, and this does not establish a model-quality regression
112
+ against runs with different concurrency and conditions.
103
113
 
104
114
  Historical qualification references remain in [benchmark reference](docs/benchmark-reference.md).
105
115
 
@@ -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.16-operation-efficiency-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.12.17-review-cost-paths-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.16-operation-efficiency-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.12.17-review-cost-paths-v1";
@@ -53,6 +53,8 @@ export interface OperatorMission {
53
53
  callID: string | null;
54
54
  dispatchOpen: boolean;
55
55
  runID: string | null;
56
+ /** Git HEAD before this mission's first implementation unit, retained across replans and commits. */
57
+ reviewBaseline?: string;
56
58
  /** A cancelled run replaced by a later real user turn; never reuse its acceptance or evidence. */
57
59
  supersededRunID?: string;
58
60
  plans: number;
@@ -283,8 +283,10 @@ export class OperatorMissionRuntime {
283
283
  return this.update(root, state => {
284
284
  const expected = this.task(state);
285
285
  const resume = state.coordinator !== null && args.task_id === state.coordinator && typeof args.prompt === "string" && args.prompt.trim();
286
+ // The description is a presentation label. The opaque prompt binds the original request and
287
+ // requirements; changing only the label must not force a second Coordinator dispatch.
286
288
  if (["cancelled", "completed"].includes(state.phase) || state.dispatchOpen || args.subagent_type !== expected.subagent_type ||
287
- (!resume && (args.prompt !== expected.prompt || args.description !== expected.description || args.task_id))) {
289
+ (!resume && (args.prompt !== expected.prompt || args.task_id))) {
288
290
  const reason = ["cancelled", "completed"].includes(state.phase) ? `mission is ${state.phase}`
289
291
  : state.dispatchOpen ? "Coordinator Task is still admitted; read operator_status to reconcile its native completion"
290
292
  : args.task_id && args.task_id !== state.coordinator ? `this location owns Coordinator ${state.coordinator ?? "not yet claimed"}, not ${String(args.task_id)}`
@@ -343,6 +343,7 @@ export declare class OperatorRuntime {
343
343
  /** Reconstruct the admitted child's instructions without trusting a lossy conversation summary. */
344
344
  workerContext(root: string, child: string): Promise<string | undefined>;
345
345
  continuationCheckpoint(root: string): Promise<string | undefined>;
346
+ private controlReference;
346
347
  private verifyControls;
347
348
  /** Follow only the host's authorized parent-link repair; keep the durable control pin in sync. */
348
349
  acknowledgeHostHandoffRepair(root: string, taskID: string, path: string, original: string, repaired: string): Promise<void>;
@@ -910,9 +910,11 @@ export class OperatorRuntime {
910
910
  const contents = [JSON.stringify(handoff), JSON.stringify(manifest)];
911
911
  controls.push({ path: handoffPath, content: contents[0] }, { path: manifestPath, content: contents[1] });
912
912
  const prompt = ["role: implementation", `task_id: ${taskID}`, `project_root: ${this.projectRoot}`,
913
- `source_manifest: ${(unit.write.length ? unit.write : unit.read).join(", ") || "none"}`, `operation_manifest: ${manifestRelative}`, `handoff_path: ${handoffPath}`,
913
+ `source_manifest: ${(unit.write.length ? unit.write : unit.read).join(", ") || "none"}`, `operation_manifest: ${manifestRelative}`,
914
+ `handoff_path: ${handoffPath}`,
914
915
  `goal_declaration_path: ${declarationPath}`, "acceptance:", ...plan.acceptance.map(value => ` - ${value}`),
915
916
  "validation:", ...unit.validation.map(value => ` - ${value}`),
917
+ ...(mission ? ["Read handoff_path and other repository files using their project-relative paths as supplied. The native working directory is project_root; do not prepend or reconstruct its absolute path for read/search/shell. Copy project_root only when binding the write gate. Preserve explicitly declared external paths."] : []),
916
918
  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
919
  : "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
920
  ...(mission ? [] : ["Preserve existing public API success and error return semantics unless acceptance explicitly changes them, and cover those compatibility boundaries in the declared validation."]),
@@ -1930,12 +1932,12 @@ export class OperatorRuntime {
1930
1932
  // Keep this prefix stable across reads/edits: changing counters belong in status/tool results.
1931
1933
  return `SORTIE_WORKER_CONTEXT\n${JSON.stringify({ root_session_id: root, run_id: state.runID,
1932
1934
  unit_id: unit.unit.id, task_id: /^task_id: (.+)$/m.exec(unit.task.prompt)?.[1],
1933
- project_root: this.projectRoot, handoff_path: unit.handoffPath, operation_manifest: unit.manifestPath,
1935
+ project_root: this.projectRoot, handoff_path: this.controlReference(unit.handoffPath), operation_manifest: this.controlReference(unit.manifestPath),
1934
1936
  objective: unit.unit.objective, read: unit.unit.read, write: unit.unit.write,
1935
1937
  validation: unit.unit.validation, acceptance: state.acceptance,
1936
1938
  ...(unit.repairValidation === null ? {} : { validation_only: true }),
1937
1939
  })}\nThis is the durable assignment for this same Worker, including after compaction or reload. ` +
1938
- "A missing path in a summary does not mean the handoff is missing. Read the retained handoff/manifest and inspect the actual diff and outputs before claiming no edits or replaying work. " +
1940
+ "A missing path in a summary does not mean the handoff is missing. Read the retained handoff/manifest using these project-relative paths without prepending project_root, and inspect the actual diff and outputs before claiming no edits or replaying work. " +
1939
1941
  `Use ${this.profile.toolPrefix}operator_status for observed validation and remaining work; do not repeat successful checks for an unchanged candidate. ` +
1940
1942
  "This context is an assignment, not proof of execution or acceptance.";
1941
1943
  }
@@ -1961,6 +1963,11 @@ export class OperatorRuntime {
1961
1963
  ? `call ${this.profile.toolPrefix}operator_status and execute its prepare_operator next_action in this turn; do not stop after reporting status`
1962
1964
  : `call ${this.profile.toolPrefix}operator_status and execute its exact next_action in this turn; do not stop after reporting status` });
1963
1965
  }
1966
+ controlReference(path) {
1967
+ const local = relative(this.projectRoot, path);
1968
+ return local === ".." || local.startsWith(`..${sep}`) || isAbsolute(local)
1969
+ ? path : local.replaceAll("\\", "/");
1970
+ }
1964
1971
  async verifyControls(unit) {
1965
1972
  if (!isAbsolute(unit.handoffPath) || !isAbsolute(unit.manifestPath))
1966
1973
  throw new Error("operator-control-path-invalid");
@@ -1,6 +1,7 @@
1
1
  import type { OperatorState } from "../core/operator-runtime.js";
2
2
  import { type MissionEvidenceExcerpt, type OperatorMission } from "../core/operator-mission.js";
3
3
  import { type RuntimeProfile } from "../core/runtime-profile.js";
4
+ export declare function missionReviewBaseline(directory: string): Promise<string | undefined>;
4
5
  /** A child ID alone cannot establish the initial phase after a restart or failed dispatch. */
5
6
  export declare function initialMissionReviewPrompt(mission: OperatorMission, prompts: readonly string[]): string | undefined;
6
7
  /** Recover from native completion records, never from the pending review's inherited child ID. */
@@ -9,7 +10,7 @@ export declare function completedMissionReviewPrompts(mission: OperatorMission |
9
10
  messages(id: string): Promise<readonly Record<string, unknown>[]>;
10
11
  }): Promise<string[]>;
11
12
  /** Pin all scoped tracked/untracked source bytes, including deletions; display a bounded excerpt only. */
12
- export declare function missionReviewSource(directory: string, run: OperatorState, evidence?: readonly MissionEvidenceExcerpt[]): Promise<{
13
+ export declare function missionReviewSource(directory: string, run: OperatorState, evidence?: readonly MissionEvidenceExcerpt[], baseline?: string): Promise<{
13
14
  fingerprint: string;
14
15
  excerpt: string;
15
16
  }>;
@@ -13,6 +13,16 @@ import { normalizeManifestScope } from "../core/path.js";
13
13
  import { declaredArtifacts } from "./declared-artifacts.js";
14
14
  const exec = promisify(execFile);
15
15
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
16
+ export async function missionReviewBaseline(directory) {
17
+ try {
18
+ const { stdout } = await exec("git", ["rev-parse", "--verify", "HEAD^{commit}"], { cwd: directory });
19
+ const oid = stdout.trim();
20
+ return /^[a-f0-9]{40,64}$/u.test(oid) ? oid : undefined;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ } // A mission can start before Git is initialized.
25
+ }
16
26
  /** A child ID alone cannot establish the initial phase after a restart or failed dispatch. */
17
27
  export function initialMissionReviewPrompt(mission, prompts) {
18
28
  return [mission.review?.initialPrompt, ...prompts].find(prompt => typeof prompt === "string" &&
@@ -86,8 +96,8 @@ export async function completedMissionReviewPrompts(mission, profile, root, requ
86
96
  return prompts;
87
97
  }
88
98
  /** Pin all scoped tracked/untracked source bytes, including deletions; display a bounded excerpt only. */
89
- export async function missionReviewSource(directory, run, evidence = []) {
90
- const hash = createHash("sha256").update(JSON.stringify(run.units.map(unit => ({ unit: unit.unit, hashes: unit.hashes }))));
99
+ export async function missionReviewSource(directory, run, evidence = [], baseline) {
100
+ const hash = createHash("sha256").update(JSON.stringify({ baseline, units: run.units.map(unit => ({ unit: unit.unit, hashes: unit.hashes })) }));
91
101
  let selected = "";
92
102
  for (const entry of evidence) {
93
103
  const absolute = resolve(directory, entry.path);
@@ -136,11 +146,40 @@ export async function missionReviewSource(directory, run, evidence = []) {
136
146
  const ignored = local.length ? await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", ...scopes]) : "";
137
147
  for (const path of ignored.split("\0").filter(Boolean))
138
148
  untracked.add(path);
139
- const diff = local.length ? await git(["diff", "--no-ext-diff", "--no-textconv", "HEAD", "--", ...scopes]).catch(() => git(["diff", "--no-ext-diff", "--no-textconv", "--", ...scopes])) : "";
149
+ const omitted = [];
150
+ let diff = "";
151
+ if (local.length && baseline) {
152
+ // HEAD-only diffs are empty once the Worker commits. Show bounded changes from the
153
+ // mission's original HEAD across all replans, not arbitrary alphabetical repository files.
154
+ const changed = (await git(["diff", "--name-only", "-z", "--no-ext-diff", baseline, "--", ...scopes]))
155
+ .split("\0").filter(Boolean);
156
+ const shown = changed.slice(0, 16);
157
+ const heading = changed.length ? `Changed since mission baseline (${baseline}; ${changed.length} paths):\n` : "";
158
+ const headers = shown.map(path => `\n--- changed: ${path} ---\n`);
159
+ const available = Math.max(0, 24_000 - Buffer.byteLength(selected + heading) -
160
+ headers.reduce((size, title) => size + Buffer.byteLength(title), 0) - 500);
161
+ const allowance = shown.length ? Math.min(3_000, Math.floor(available / shown.length)) : 0;
162
+ diff = heading;
163
+ for (const [index, path] of shown.entries()) {
164
+ const patch = await git(["diff", "--no-ext-diff", "--no-textconv", baseline, "--", path]);
165
+ const bytes = Buffer.from(patch);
166
+ if (allowance <= 0) {
167
+ omitted.push(path);
168
+ continue;
169
+ }
170
+ diff += `${headers[index]}${bytes.subarray(0, allowance).toString("utf8")}`;
171
+ if (bytes.length > allowance)
172
+ omitted.push(path);
173
+ }
174
+ omitted.push(...changed.slice(shown.length));
175
+ }
176
+ else if (local.length) {
177
+ diff = await git(["diff", "--no-ext-diff", "--no-textconv", "HEAD", "--", ...scopes])
178
+ .catch(() => git(["diff", "--no-ext-diff", "--no-textconv", "--", ...scopes]));
179
+ }
140
180
  const unchanged = diff.length === 0;
141
181
  let excerpt = selected + diff;
142
182
  const paths = [...new Set([...names.split("\0"), ...ignored.split("\0")].filter(Boolean))].sort();
143
- const omitted = [];
144
183
  for (const path of paths) {
145
184
  hash.update(JSON.stringify(path));
146
185
  try {
@@ -4,7 +4,7 @@ import { DEFAULT_OPERATOR_PROPOSAL_BUDGET, OPERATOR_APPROVAL_CONTRACT, OPERATOR_
4
4
  import { CANONICAL_AGENT_ROLES, canonicalAgent, profileAgent, profileTool, V010_RUNTIME_PROFILE } from "../core/runtime-profile.js";
5
5
  import { SortieDogsPlugin as canonicalPlugin } from "./index.js";
6
6
  import { taskChildSessionID } from "./task-result-repair.js";
7
- import { relative, resolve, sep } from "node:path";
7
+ import { isAbsolute, relative, resolve, sep } from "node:path";
8
8
  import { readFile, realpath } from "node:fs/promises";
9
9
  import { BUILT_IN_MODEL_CATALOG } from "./model-routing.js";
10
10
  import { goalFingerprint } from "../core/goal-bound.js";
@@ -14,7 +14,7 @@ import { normalizeCommand } from "./gate.js";
14
14
  import { normalizeRelativePath } from "../core/path.js";
15
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
- import { completedMissionReviewPrompts, initialMissionReviewPrompt, missionReviewSource } from "./mission-review.js";
17
+ import { completedMissionReviewPrompts, initialMissionReviewPrompt, missionReviewBaseline, missionReviewSource } from "./mission-review.js";
18
18
  import { missionLocations, missionLocationPacket } from "./mission-location.js";
19
19
  import { SOURCE_REVIEW_RISK_TAGS } from "../core/consultation.js";
20
20
  import { createHash } from "node:crypto";
@@ -280,6 +280,21 @@ export function createProfiledPlugin(profile, assetVersion) {
280
280
  return role;
281
281
  return CANONICAL_AGENT_ROLES.includes(value) ? `foreign/${value}` : value;
282
282
  }
283
+ function workerControlPrompt(text, outward) {
284
+ // Project only the generated header, never acceptance, commands or the user's objective.
285
+ // Durable Tasks and the shared engine retain their canonical absolute identities.
286
+ if (!/^role: implementation\ntask_id: operator-/u.test(text))
287
+ return text;
288
+ const end = text.indexOf("\nacceptance:\n");
289
+ if (end < 0)
290
+ return text;
291
+ return text.slice(0, end).replace(/^(handoff_path|goal_declaration_path): (.+)$/gm, (_line, name, path) => {
292
+ const absolute = resolve(input.directory, path);
293
+ const local = relative(input.directory, absolute);
294
+ const inside = local !== ".." && !local.startsWith(`..${sep}`) && !isAbsolute(local);
295
+ return `${name}: ${outward && inside ? local.replaceAll("\\", "/") : absolute}`;
296
+ }) + text.slice(end);
297
+ }
283
298
  function translate(value, outward) {
284
299
  if (Array.isArray(value))
285
300
  return value.map(item => translate(item, outward));
@@ -315,7 +330,23 @@ export function createProfiledPlugin(profile, assetVersion) {
315
330
  if ((method === "prompt" || method === "promptAsync") && record(request) && record(request.path) &&
316
331
  typeof request.path.id === "string" && retired.has(request.path.id))
317
332
  throw new Error("runtime-profile-revoked");
318
- return translate(await operation.apply(group, args.map(argument => translate(argument, true))), false);
333
+ const result = translate(await operation.apply(group, args.map(argument => translate(argument, true))), false);
334
+ // Native Worker history stores short references. Only its generated user
335
+ // header is projected back for core recovery; root requests and tool evidence
336
+ // keep their original text, including any path-like lines in task data.
337
+ if (method === "messages" || method === "message") {
338
+ const data = payload(result);
339
+ for (const message of Array.isArray(data) ? data : [data]) {
340
+ if (!record(message) || !record(message.info) || message.info.role !== "user" ||
341
+ message.info.agent !== "dog-worker" || !Array.isArray(message.parts))
342
+ continue;
343
+ for (const part of message.parts)
344
+ if (record(part) && part.type === "text" && typeof part.text === "string") {
345
+ part.text = workerControlPrompt(part.text, false);
346
+ }
347
+ }
348
+ }
349
+ return result;
319
350
  };
320
351
  },
321
352
  });
@@ -1324,10 +1355,15 @@ export function createProfiledPlugin(profile, assetVersion) {
1324
1355
  if (relocated)
1325
1356
  return JSON.stringify({ ...relocated, budget: await control.currentBudget(context.sessionID) });
1326
1357
  }
1327
- const mission = await retainCancelledMissionAcceptance(context.sessionID, await missions.start(context.sessionID, args.requirements, args.intent === "replace" || args.intent === "new", {
1358
+ let mission = await retainCancelledMissionAcceptance(context.sessionID, await missions.start(context.sessionID, args.requirements, args.intent === "replace" || args.intent === "new", {
1328
1359
  kind: args.kind === "operation" ? "operation" : "implementation",
1329
1360
  context: missionConversationContext(await messages(context.sessionID)),
1330
1361
  }));
1362
+ if (!mission.reviewBaseline) {
1363
+ const baseline = await missionReviewBaseline(input.directory);
1364
+ if (baseline)
1365
+ mission = await missions.update(context.sessionID, state => { state.reviewBaseline = baseline; });
1366
+ }
1331
1367
  return JSON.stringify({ ...locationObservation(context.sessionID), ...(mission.dispatchOpen
1332
1368
  ? missionDispatchPacket(mission, await operators.read(context.sessionID))
1333
1369
  : { mission_id: mission.id, requirements: mission.requirements, task: missions.task(mission),
@@ -1365,6 +1401,11 @@ export function createProfiledPlugin(profile, assetVersion) {
1365
1401
  if (budget && budget.remaining_units < plan.units.length && !same)
1366
1402
  throw new Error(`mission-budget-exhausted: plan needs ${plan.units.length} units; ${budget.remaining_units} remain. ` +
1367
1403
  "The current run is retained. Correct the plan within the remaining budget, or report a necessary cumulative extension to Operator.");
1404
+ if (!mission.reviewBaseline) {
1405
+ const baseline = await missionReviewBaseline(input.directory);
1406
+ if (baseline)
1407
+ mission = await missions.update(root, state => { state.reviewBaseline ??= baseline; });
1408
+ }
1368
1409
  // Cancellation marks the durable units before interrupting their native sessions. A cancelled
1369
1410
  // status alone therefore cannot prove the old Worker stopped or its reservation settled.
1370
1411
  const cancelledPredecessor = previous?.phase === "cancelled" &&
@@ -1453,7 +1494,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1453
1494
  const evidence = args.evidence;
1454
1495
  if (evidence !== undefined && (!Array.isArray(evidence) || evidence.length > 6))
1455
1496
  throw new Error("mission-review-evidence: select at most six focused excerpts");
1456
- const source = await missionReviewSource(input.directory, run, evidence);
1497
+ const source = await missionReviewSource(input.directory, run, evidence, mission.reviewBaseline);
1457
1498
  const requestFingerprint = goalFingerprint({ run: run.runID, source: source.fingerprint, risk, traces });
1458
1499
  if (mission.review?.requestFingerprint === requestFingerprint && mission.review.verdict !== "pending") {
1459
1500
  return JSON.stringify({ ...missionPacket(mission, run), status: "review-recorded",
@@ -1494,7 +1535,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1494
1535
  async function assertMissionReview(root, mission) {
1495
1536
  const run = await operators.required(root), review = mission.review;
1496
1537
  if (!review || review.runID !== run.runID || !missionReviewAccepted(review) ||
1497
- review.source !== (await missionReviewSource(input.directory, run, review.evidence)).fingerprint)
1538
+ review.source !== (await missionReviewSource(input.directory, run, review.evidence, mission.reviewBaseline)).fingerprint)
1498
1539
  throw new Error("mission-review-required-or-stale");
1499
1540
  }
1500
1541
  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.",
@@ -1746,6 +1787,11 @@ export function createProfiledPlugin(profile, assetVersion) {
1746
1787
  const mapped = translate(output, false);
1747
1788
  await core["chat.message"]?.(translate(chat, false), mapped);
1748
1789
  Object.assign(output, translate(mapped, true));
1790
+ if (role === "dog-worker")
1791
+ for (const part of output.parts) {
1792
+ if (record(part) && part.type === "text" && typeof part.text === "string")
1793
+ part.text = workerControlPrompt(part.text, true);
1794
+ }
1749
1795
  if (role === "dog-worker" && explicitWorkerSelection) {
1750
1796
  if (explicitWorkerSelection.model) {
1751
1797
  const split = explicitWorkerSelection.model.indexOf("/");
@@ -1795,9 +1841,9 @@ export function createProfiledPlugin(profile, assetVersion) {
1795
1841
  if (repairAccess !== null) {
1796
1842
  const bind = profileTool(profile, "sortie_bind_write_gate"), release = profileTool(profile, "sortie_release_write_gate");
1797
1843
  const allowedRead = request.tool.toLowerCase() === "read" && typeof args.filePath === "string" &&
1798
- resolve(args.filePath) === resolve(repairAccess.handoff_path);
1844
+ resolve(input.directory, args.filePath) === resolve(repairAccess.handoff_path);
1799
1845
  const allowedBind = request.tool === bind && args.project_root === input.directory &&
1800
- resolve(String(args.manifest_path ?? "")) === resolve(repairAccess.manifest_path);
1846
+ resolve(input.directory, String(args.manifest_path ?? "")) === resolve(repairAccess.manifest_path);
1801
1847
  const allowedRelease = request.tool === release;
1802
1848
  const allowedValidation = ["bash", "shell"].includes(request.tool.toLowerCase()) && typeof args.command === "string" &&
1803
1849
  repairAccess.expected_command !== null && normalizeCommand(args.command) === repairAccess.expected_command;
@@ -56,6 +56,12 @@ export interface Debrief {
56
56
  readonly cacheReadTokens: number;
57
57
  readonly cacheWriteTokens: number;
58
58
  };
59
+ readonly estimatedCost?: {
60
+ readonly usd: number;
61
+ readonly pricedRequests: number;
62
+ readonly unpricedRequests: number;
63
+ readonly complete: boolean;
64
+ };
59
65
  readonly hostCostZero?: boolean;
60
66
  }[] | null;
61
67
  /** Shares use observed tokens; incomplete history must not erase known model usage. */
@@ -293,6 +293,9 @@ export function buildDebrief(receipt, contract, observation, records) {
293
293
  return { model, tokens, percent: tokens / total * 100,
294
294
  ...(usage?.complete ? { inputCache: { uncachedInputTokens: usage.uncachedInputTokens,
295
295
  cacheReadTokens: usage.cacheReadTokens, cacheWriteTokens: usage.cacheWriteTokens } } : {}),
296
+ ...(usage ? { estimatedCost: { usd: usage.estimatedCost, pricedRequests: usage.pricedRequests,
297
+ unpricedRequests: usage.unpricedRequests,
298
+ complete: usage.complete && usage.pricingComplete && usage.unpricedRequests === 0 && usage.pricedRequests > 0 } } : {}),
296
299
  ...(usage?.complete && usage.costAvailable && tokens > 0 && usage.cost === 0 ? { hostCostZero: true } : {}) };
297
300
  }) : null,
298
301
  mixCoverage: { complete: observation.complete && usageComplete, observedTokens: total },
@@ -310,6 +313,7 @@ export function buildDebrief(receipt, contract, observation, records) {
310
313
  const label = (text) => text.replace(/[\r\n\t]/gu, " ").replace(/[\\`*_{}\[\]()<>!|]/gu, "").slice(0, 120);
311
314
  const milliseconds = (value) => value === null ? "未記録"
312
315
  : value < 1000 ? `${value}ms` : `${(value / 1000).toFixed(2)}s`;
316
+ const dollars = (usd) => `$${usd.toFixed(usd > 0 && usd < 0.00005 ? 6 : 4)}`;
313
317
  const gauge = (percent) => {
314
318
  const eighths = Math.max(0, Math.min(80, Math.round(percent * 0.8)));
315
319
  const whole = Math.floor(eighths / 8), remainder = eighths % 8;
@@ -324,13 +328,21 @@ export function renderDebrief(debrief) {
324
328
  const packVisible = pack?.slice(0, 4) ?? [];
325
329
  if (pack !== null && pack.length > 4)
326
330
  packVisible.push({ model: "その他", count: pack.slice(4).reduce((sum, entry) => sum + entry.count, 0) });
327
- const mix = debrief?.mix == null ? null : [...debrief.mix].sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
331
+ const mix = debrief?.mix == null ? null : [...debrief.mix].sort((a, b) => (b.estimatedCost?.pricedRequests ? 1 : 0) - (a.estimatedCost?.pricedRequests ? 1 : 0) ||
332
+ (b.estimatedCost?.usd ?? 0) - (a.estimatedCost?.usd ?? 0) || b.tokens - a.tokens || a.model.localeCompare(b.model));
333
+ const pricedUsd = mix?.reduce((sum, entry) => sum + (entry.estimatedCost?.usd ?? 0), 0) ?? 0;
334
+ const hasPriced = mix?.some(entry => (entry.estimatedCost?.pricedRequests ?? 0) > 0);
335
+ const partiallyPriced = debrief?.mixCoverage?.complete === false || mix?.some(entry => !entry.estimatedCost?.complete);
328
336
  const visible = mix?.slice(0, 4) ?? [];
329
337
  if (mix !== null && mix.length > 4) {
330
338
  const tail = mix.slice(4);
331
339
  const cacheKnown = tail.every((entry) => entry.inputCache !== undefined);
332
340
  visible.push({ model: "その他", tokens: tail.reduce((sum, entry) => sum + entry.tokens, 0),
333
341
  percent: tail.reduce((sum, entry) => sum + entry.percent, 0),
342
+ estimatedCost: { usd: tail.reduce((sum, entry) => sum + (entry.estimatedCost?.usd ?? 0), 0),
343
+ pricedRequests: tail.reduce((sum, entry) => sum + (entry.estimatedCost?.pricedRequests ?? 0), 0),
344
+ unpricedRequests: tail.reduce((sum, entry) => sum + (entry.estimatedCost?.unpricedRequests ?? 0), 0),
345
+ complete: tail.every(entry => entry.estimatedCost?.complete) },
334
346
  ...(cacheKnown ? { inputCache: {
335
347
  uncachedInputTokens: tail.reduce((sum, entry) => sum + entry.inputCache.uncachedInputTokens, 0),
336
348
  cacheReadTokens: tail.reduce((sum, entry) => sum + entry.inputCache.cacheReadTokens, 0),
@@ -342,14 +354,22 @@ export function renderDebrief(debrief) {
342
354
  : value === "WAIVED" ? "免除" : "未記録";
343
355
  const counts = new Map(packVisible.map((entry) => [entry.model, entry.count]));
344
356
  return [
345
- `モデル内訳 ${mix === null ? "usage未取得" : debrief?.mixCoverage?.complete === false
346
- ? "観測済みtoken比率(一部未取得)" : "token比率"}`,
357
+ `モデル内訳 ${mix === null ? "usage未取得" : !hasPriced
358
+ ? "予測費用未換算(モデル別token数)" : pricedUsd === 0
359
+ ? "予測費用比率(合計$0・算出不可)" : partiallyPriced
360
+ ? "換算済み予測費用の比率(一部未換算)" : "予測費用比率"}`,
347
361
  ...visible.map((entry) => {
348
362
  const count = counts.get(entry.model);
349
363
  const input = entry.inputCache === undefined ? 0 : entry.inputCache.uncachedInputTokens +
350
364
  entry.inputCache.cacheReadTokens + entry.inputCache.cacheWriteTokens;
351
365
  const cache = entry.inputCache === undefined || input === 0 ? "未取得" : `${(entry.inputCache.cacheReadTokens / input * 100).toFixed(0)}%`;
352
- return `🐕 ${label(entry.model)} ${gauge(entry.percent)} ${entry.percent.toFixed(1)}% ${entry.tokens.toLocaleString("ja-JP")} tokens${count === undefined ? "" : ` ×${count}`} ↺${cache}${entry.hostCostZero ? "†" : ""}`;
366
+ const cost = entry.estimatedCost;
367
+ const share = cost?.pricedRequests
368
+ ? pricedUsd > 0
369
+ ? `${gauge(cost.usd / pricedUsd * 100)} ${(cost.usd / pricedUsd * 100).toFixed(1)}% · ${dollars(cost.usd)}${cost.complete ? "" : "(一部未換算)"}`
370
+ : `比率算出不可 · ${dollars(cost.usd)}`
371
+ : "費用未換算";
372
+ return `🐕 ${label(entry.model)} ${share} · ${entry.tokens.toLocaleString("ja-JP")} tokens${count === undefined ? "" : ` ×${count}`} ↺${cache}${entry.hostCostZero ? "†" : ""}`;
353
373
  }),
354
374
  ...(visible.some((entry) => entry.hostCostZero) ? [" †host費用0計上あり(無料・全体価格の評価ではありません)"] : []),
355
375
  `⚡ 実行重複率 ${debrief?.overlap !== undefined && debrief.overlap.wallMilliseconds > 0
@@ -124,6 +124,8 @@ ${controls(profile, ["plan_units", "operator_next", "operator_status", "expand_u
124
124
  You are Coordinator. Own most of the practical work and dispatch within the saved original request.
125
125
  Read/search and confirmation shell commands are available; source edits belong to Worker. Do not edit
126
126
  through shell. There is no proposal/approval/contract-repair round trip in this route.
127
+ Use project-relative paths for repository read/search/shell; the native working directory is project_root.
128
+ Do not reconstruct or prepend the absolute workspace path. Preserve explicitly requested external paths.
127
129
 
128
130
  For release/benchmark work, retain the user's selected package/environment and record its receipt/hash.
129
131
  A local repack and a published tarball can have different hashes; that alone does not prohibit a requested
@@ -215,6 +217,10 @@ mode: subagent
215
217
  Implement the assigned unit promptly. Use the user's language in its handoff. Read the exact handoff_path
216
218
  once, then call ${profile.toolPrefix}bind_write_gate with project_root and operation_manifest before writes.
217
219
  The host generates these documents; do not rewrite or reconstruct them. Copy opaque paths exactly.
220
+ Use handoff_path and operation_manifest as supplied: new local references are relative to the native
221
+ project working directory; a retained older absolute reference can be used without rewriting it.
222
+ Keep repository read/search/shell paths relative; do not prepend project_root or copy an absolute root from
223
+ tool output. project_root is the exact binding identity, not a prefix to rebuild. Retain explicit external paths.
218
224
  The binding remains valid throughout this Task until return or a control/source authorization change.
219
225
  Use ${profile.toolPrefix}operator_status when the task needs native runtime identity, mission state or
220
226
  remaining budget. It is a read-only observation available to Worker; do not request a separate unit or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.12.16",
3
+ "version": "0.12.17",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",