sortie-dogs 0.12.23 → 0.12.25

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.23-mission-budget-recovery-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.12.25-mission-continuity-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.23-mission-budget-recovery-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.12.25-mission-continuity-v1";
@@ -174,7 +174,7 @@ export declare class OperatorMissionRuntime {
174
174
  constructor(projectRoot: string, profile: RuntimeProfile);
175
175
  private file;
176
176
  private load;
177
- /** Recover the predecessor link for missions started before this fix, using one exact archived mission. */
177
+ /** Recover non-replacement continuations only; an explicit replacement must link the current cancelled run. */
178
178
  private loadMission;
179
179
  /** Find exactly one archived cancelled mission that owned this run; ambiguity never grants recovery. */
180
180
  archivedRun(root: string, runID: string): Promise<OperatorMission | undefined>;
@@ -113,10 +113,10 @@ export class OperatorMissionRuntime {
113
113
  throw error;
114
114
  }
115
115
  }
116
- /** Recover the predecessor link for missions started before this fix, using one exact archived mission. */
116
+ /** Recover non-replacement continuations only; an explicit replacement must link the current cancelled run. */
117
117
  async loadMission(root) {
118
118
  const state = await this.load(this.file(root));
119
- if (!state || state.supersededRunID !== undefined || state.runID !== null ||
119
+ if (!state || state.supersededRunID !== undefined || state.runID !== null || state.requirementsReplaced ||
120
120
  !["open", "running", "submitted", "cancelled"].includes(state.phase) || state.requests.length === 0)
121
121
  return state;
122
122
  const directory = join(this.projectRoot, this.profile.stateDirectory, "missions");
@@ -241,8 +241,9 @@ export class OperatorMissionRuntime {
241
241
  }
242
242
  if (previous)
243
243
  await this.save(this.file(root, `.${previous.id}`), previous);
244
- // A cancelled standalone/legacy run has no mission-owned runID. Only an explicit
245
- // replacement may link it; normal continuation must not silently inherit its goal.
244
+ // Explicit replacement links the latest cancelled run even when a user narrows the
245
+ // request in the same turn. Ordinary same-turn continuation still retains acceptance.
246
+ // A cancelled standalone/legacy run has no mission-owned runID; only replacement links it.
246
247
  const predecessor = previous?.runID ?? previous?.supersededRunID ??
247
248
  (replaceRequirements ? options.cancelledRunID : undefined);
248
249
  const state = { version: "0.12", id: `mission-${randomUUID()}`, root, requests: [request],
@@ -252,7 +253,7 @@ export class OperatorMissionRuntime {
252
253
  ...(replaceRequirements ? { requirementsReplaced: true } : {}),
253
254
  coordinator: null, callID: null, dispatchOpen: false, runID: null, plans: 0, progress: [], submission: null,
254
255
  ...((previous === undefined || previous.phase === "cancelled") &&
255
- (previous?.runID === null || previous === undefined || request.id !== previous.requests[0]?.id) && predecessor
256
+ (replaceRequirements || previous?.runID === null || previous === undefined || request.id !== previous.requests[0]?.id) && predecessor
256
257
  ? { supersededRunID: predecessor } : {}) };
257
258
  await this.save(this.file(root), state);
258
259
  return state;
@@ -1,8 +1,11 @@
1
1
  /** Explicit external artifacts are filesystem inputs, not Git pathspecs. Stream complete bytes;
2
2
  * previews are bounded independently. Follow package links only inside a declared physical root.
3
3
  */
4
- export declare function declaredArtifacts(paths: readonly string[], previewLimit?: number): Promise<{
4
+ export declare function declaredArtifacts(paths: readonly string[], previewLimit?: number, options?: {
5
+ reportUnreadableDirectories?: boolean;
6
+ }): Promise<{
5
7
  entries: (readonly [string, string, (string | undefined)?])[];
6
8
  excerpt: string;
7
9
  truncated: boolean;
10
+ unreadable: string[];
8
11
  }>;
@@ -9,7 +9,7 @@ const within = (root, path) => {
9
9
  /** Explicit external artifacts are filesystem inputs, not Git pathspecs. Stream complete bytes;
10
10
  * previews are bounded independently. Follow package links only inside a declared physical root.
11
11
  */
12
- export async function declaredArtifacts(paths, previewLimit = 0) {
12
+ export async function declaredArtifacts(paths, previewLimit = 0, options = {}) {
13
13
  const roots = [...new Set(paths.map(path => resolve(path)))].sort();
14
14
  const physical = await Promise.all(roots.map(path => realpath(path).catch((error) => {
15
15
  if (error.code === "ENOENT")
@@ -18,6 +18,7 @@ export async function declaredArtifacts(paths, previewLimit = 0) {
18
18
  })));
19
19
  const entries = [];
20
20
  const excerpts = [];
21
+ const unreadable = [];
21
22
  let remaining = previewLimit, truncated = false;
22
23
  const visited = new Set();
23
24
  const visit = async (path, ancestors) => {
@@ -47,7 +48,26 @@ export async function declaredArtifacts(paths, previewLimit = 0) {
47
48
  if (metadata.isDirectory()) {
48
49
  entries.push([label, "directory", canonical]);
49
50
  const next = new Set([...ancestors, canonical]);
50
- for (const child of (await readdir(path)).sort())
51
+ let children;
52
+ try {
53
+ children = await readdir(path);
54
+ }
55
+ catch (error) {
56
+ if (!options.reportUnreadableDirectories || !["EACCES", "EPERM"].includes(error.code ?? ""))
57
+ throw error;
58
+ entries.push([label, `unreadable:${error.code}`, canonical]);
59
+ unreadable.push(label);
60
+ const notice = `\n--- external directory: ${label} ---\n[not inspected: permission denied; select specific result files as review evidence]\n`;
61
+ const bytes = Buffer.from(notice), bounded = bytes.subarray(0, remaining);
62
+ if (bounded.length) {
63
+ excerpts.push(bounded.toString("utf8"));
64
+ remaining -= bounded.length;
65
+ }
66
+ if (bounded.length < bytes.length)
67
+ truncated = true;
68
+ return;
69
+ }
70
+ for (const child of children.sort())
51
71
  await visit(join(path, child), next);
52
72
  return;
53
73
  }
@@ -76,5 +96,5 @@ export async function declaredArtifacts(paths, previewLimit = 0) {
76
96
  };
77
97
  for (const path of roots)
78
98
  await visit(path, new Set());
79
- return { entries, excerpt: excerpts.join(""), truncated };
99
+ return { entries, excerpt: excerpts.join(""), truncated, unreadable };
80
100
  }
@@ -13,4 +13,5 @@ export declare function completedMissionReviewPrompts(mission: OperatorMission |
13
13
  export declare function missionReviewSource(directory: string, run: OperatorState, evidence?: readonly MissionEvidenceExcerpt[], baseline?: string, priorScope?: MissionReviewScope): Promise<{
14
14
  fingerprint: string;
15
15
  excerpt: string;
16
+ truncatedEvidence: string[];
16
17
  }>;
@@ -214,9 +214,11 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
214
214
  if (needsNotice[index])
215
215
  selected += truncated(item.entry);
216
216
  }
217
+ const truncatedEvidence = focused.flatMap(({ entry }, index) => needsNotice[index] ? [`${entry.path}:${entry.offset}`] : []);
217
218
  if (writes.length === 0)
218
219
  return { fingerprint: `sha256:${hash.digest("hex")}`,
219
- excerpt: selected + "[Read-only units: no declared output files. Review the supplied observations, traces and validation evidence.]" };
220
+ excerpt: selected + "[Read-only units: no declared output files. Review the supplied observations, traces and validation evidence.]",
221
+ truncatedEvidence };
220
222
  // The shared dependency environment is local tooling, never reviewed or pinned source.
221
223
  const external = [], local = [];
222
224
  for (const path of writes) {
@@ -230,12 +232,15 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
230
232
  const git = async (args) => (await exec("git", args, { cwd: directory, maxBuffer: 8 * 1024 * 1024 })).stdout;
231
233
  const names = local.length ? await git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...scopes]) : "";
232
234
  const untracked = new Set((local.length ? await git(["ls-files", "-z", "--others", "--exclude-standard", "--", ...scopes]) : "").split("\0").filter(Boolean));
233
- // Explicitly declared outputs are review evidence even when gitignored (for example a probe
234
- // JSON in _testenv). Their bytes must participate in staleness checks as well as the excerpt.
235
- const ignored = local.length ? await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", ...scopes]) : "";
235
+ // Go's ignored in-project caches may be writable during validation but are not candidate
236
+ // output. Keep every other ignored declared output visible and fingerprinted, including
237
+ // directories of generated artifacts; focused references can still pin a cache file.
238
+ const ignored = local.length ? await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--",
239
+ ...scopes, ...[".gocache", ".gomodcache", ".gopath"].map(path => `:(exclude)${path}`)]) : "";
236
240
  for (const path of ignored.split("\0").filter(Boolean))
237
241
  untracked.add(path);
238
242
  const omitted = [];
243
+ const unreadable = [];
239
244
  let diff = "";
240
245
  if (local.length && baseline) {
241
246
  // HEAD-only diffs are empty once the Worker commits. Show bounded changes from the
@@ -305,13 +310,19 @@ export async function missionReviewSource(directory, run, evidence = [], baselin
305
310
  }
306
311
  }
307
312
  if (external.length) {
308
- const artifacts = await declaredArtifacts(external, Math.max(1, 24_000 - Buffer.byteLength(excerpt)));
313
+ // A declared write scope can also be a host-managed runtime directory (e.g. Docker's data root).
314
+ // Do not make review depend on listing it; record the missing coverage instead. Protected
315
+ // validation still requires readable outputs and does not use this review-only option.
316
+ const artifacts = await declaredArtifacts(external, Math.max(1, 24_000 - Buffer.byteLength(excerpt)), { reportUnreadableDirectories: true });
309
317
  hash.update(JSON.stringify(artifacts.entries));
310
318
  excerpt += artifacts.excerpt;
311
319
  if (artifacts.truncated)
312
320
  omitted.push("external artifacts");
321
+ unreadable.push(...artifacts.unreadable);
313
322
  }
314
323
  const bytes = Buffer.from(excerpt);
315
324
  return { fingerprint: `sha256:${hash.digest("hex")}`, excerpt: (bytes.length > 24_000 ? bytes.subarray(0, 24_000).toString("utf8") : excerpt) +
316
- (bytes.length > 24_000 || omitted.length ? `\n[EXCERPT TRUNCATED: ${omitted.slice(0, 20).join(", ")}; supply focused traces for missing sections, not another implementation unit]` : "") };
325
+ (bytes.length > 24_000 || omitted.length ? `\n[EXCERPT TRUNCATED: ${omitted.slice(0, 20).join(", ")}; supply focused traces for missing sections, not another implementation unit]` : "") +
326
+ (unreadable.length ? `\n[UNINSPECTED EXTERNAL DIRECTORIES: ${unreadable.slice(0, 20).join(", ")}; select specific result files as review evidence]` : ""),
327
+ truncatedEvidence };
317
328
  }
@@ -17,6 +17,7 @@ import { MISSION_EVIDENCE_GAP_REVIEW_LIMIT, OperatorMissionRuntime, missionPacke
17
17
  import { publishMissionProgress } from "./mission-progress.js";
18
18
  import { completedMissionReviewPrompts, initialMissionReviewPrompt, missionReviewBaseline, missionReviewSource } from "./mission-review.js";
19
19
  import { missionLocations, missionLocationPacket } from "./mission-location.js";
20
+ import { prepareValidationScratch } from "./validation-scratch.js";
20
21
  import { SOURCE_REVIEW_RISK_TAGS } from "../core/consultation.js";
21
22
  import { createHash, randomUUID } from "node:crypto";
22
23
  import { proposeTerminalRescue } from "../core/terminal-rescue-policy.js";
@@ -580,10 +581,12 @@ export function createProfiledPlugin(profile, assetVersion) {
580
581
  } : {}) };
581
582
  }
582
583
  function sourceReconciliationRequired(mission, previous) {
583
- return previous?.phase === "cancelled" && !["completed", "cancelled"].includes(mission.phase) &&
584
- mission.runID === null && mission.supersededRunID === undefined &&
585
- (previous.sourceRefs[0] !== `user:${mission.requests[0]?.id}` ||
586
- !previous.sourceRefs.every(ref => mission.requests.some(request => ref === `user:${request.id}`)));
584
+ if (previous?.phase !== "cancelled" || ["completed", "cancelled"].includes(mission.phase) || mission.runID !== null)
585
+ return false;
586
+ if (mission.supersededRunID !== undefined)
587
+ return mission.supersededRunID !== previous.runID;
588
+ return mission.requirementsReplaced === true || previous.sourceRefs[0] !== `user:${mission.requests[0]?.id}` ||
589
+ !previous.sourceRefs.every(ref => mission.requests.some(request => ref === `user:${request.id}`));
587
590
  }
588
591
  async function missionAuthority(id) {
589
592
  const root = await rootFor(id);
@@ -666,7 +669,7 @@ export function createProfiledPlugin(profile, assetVersion) {
666
669
  if (mission.phase === "submitted" && mission.submission?.status === "blocked" &&
667
670
  run?.units.some(unit => unit.status === "running")) {
668
671
  return { ...packet, next_action: `If the native Worker Task is still active, wait for it; do not start a duplicate. ` +
669
- `If its parent Task was interrupted and the user chose to stop/replan, Operator root (not Coordinator): ` +
672
+ `If the parent Coordinator Task has finished or was interrupted, and the user chose to stop/replan, Operator root (not Coordinator): ` +
670
673
  `call ${profile.toolPrefix}cancel_operator with reason=plain to stop owned children, then ` +
671
674
  `${profile.toolPrefix}start_mission with intent=replace and the saved requirements. ` +
672
675
  `Use the returned Coordinator Task; the previous Mission is archived and cumulative spend is retained.` };
@@ -674,7 +677,7 @@ export function createProfiledPlugin(profile, assetVersion) {
674
677
  if (sourceReconciliationRequired(mission, run))
675
678
  return { ...packet, status: "mission-source-reconciliation-required",
676
679
  next_action: mission.dispatchOpen ? "The Coordinator Task is still active. Do not redispatch; wait for its native completion and reconcile via operator_status."
677
- : `The cancelled run's source_refs cannot prove this mission is the same goal. Do not dispatch or repeat plan_units. ` +
680
+ : `The mission is not linked to the current cancelled run. Do not dispatch or repeat plan_units. ` +
678
681
  `If these saved requirements reflect the user's changed or narrowed scope, Operator: call ${profile.toolPrefix}start_mission ` +
679
682
  `with intent=replace and the exact requirements array shown here. The host repairs this mission in place, retains spend, ` +
680
683
  `and verifies old children before preparing a Worker. Otherwise obtain the user's scope decision.` };
@@ -1696,7 +1699,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1696
1699
  return JSON.stringify({ ...missionDispatchPacket(mission, previous),
1697
1700
  next_action: `Coordinator: do not repeat plan_units. Call ${submitMission} with status=blocked and report the saved ` +
1698
1701
  `requirements to Operator. Operator can relink this mission in place via ${startMission} intent=replace ` +
1699
- `when they match the user's changed scope; no Worker can start before that decision.` });
1702
+ `with those exact requirements when they match the user's changed scope; no Worker can start before that decision.` });
1700
1703
  mission = await retainCancelledMissionAcceptance(root, mission, previous);
1701
1704
  let operation = mission.execution;
1702
1705
  // Models can serialize an optional operation field as an empty object for a normal edit.
@@ -1780,7 +1783,15 @@ export function createProfiledPlugin(profile, assetVersion) {
1780
1783
  item.execution = operation;
1781
1784
  }
1782
1785
  });
1783
- return JSON.stringify(await operators.next(root, actor));
1786
+ const next = await operators.next(root, actor);
1787
+ if (!record(next) || !record(next.task))
1788
+ return JSON.stringify(next);
1789
+ const setup = await prepareValidationScratch(input.directory, plan.units.flatMap(unit => unit.validation));
1790
+ return JSON.stringify(setup.prepared_directories.length || setup.unprepared_directories.length
1791
+ ? { ...next, validation_setup: setup, ...(setup.unprepared_directories.length ? {
1792
+ next_action: "Before dispatch, correct or prepare the listed in-project TMPDIR directories. Keep this plan and its budget; no new approval or validation run is needed."
1793
+ } : {}) }
1794
+ : next);
1784
1795
  }
1785
1796
  tools[planUnits] = { description: "Coordinator (or single-unit Fast-lane Operator): declare useful units, then dispatch the returned Worker immediately. Host generates IDs, handoff, manifest and proof mapping. Keep every original requirement covered. Use write: [] for read-only verification; use dir/** for directory outputs including not-yet-created trees. Native absolute paths support global installs and external outputs under host permissions; include their actual paths in read/write for evidence. Final validation command in each unit proves that unit; read-only diagnostic commands need no registration. Recalling with reason replaces settled work within unchanged requirements and cumulative budget; include required scope extensions here. Rejected budget, contract or control-storage preparation preserves the existing run so you can correct the plan directly.",
1786
1797
  args: { units: { type: "array", minItems: 1, maxItems: 32, items: { type: "object", additionalProperties: false,
@@ -1825,7 +1836,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1825
1836
  return declareMissionUnits(root, context.sessionID, mission, units, args.reason);
1826
1837
  });
1827
1838
  } };
1828
- 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.",
1839
+ 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; if truncated_evidence is returned, narrow those ranges before dispatching the Reviewer. Keep candidate lineage across corrections. A low-risk skip is recorded, never inferred from a missing review.",
1829
1840
  args: { risk_tags: { type: "array", items: { type: "string", enum: SOURCE_REVIEW_RISK_TAGS } },
1830
1841
  evidence: { type: "array", maxItems: 6, items: { type: "object", additionalProperties: false,
1831
1842
  properties: { path: { type: "string" }, offset: { type: "integer", minimum: 1 }, limit: { type: "integer", minimum: 1, maximum: 200 } },
@@ -1881,7 +1892,10 @@ export function createProfiledPlugin(profile, assetVersion) {
1881
1892
  ...(initialPrompt ? { initialPrompt } : {}),
1882
1893
  ...(mission.review?.evidenceGapReviews ? { evidenceGapReviews: mission.review.evidenceGapReviews } : {}) };
1883
1894
  });
1884
- return JSON.stringify(task ? { status: "review-required", task: missionReviewTask(reviewed) } : { status: "skipped-low-risk" });
1895
+ return JSON.stringify(task ? { status: "review-required", task: missionReviewTask(reviewed),
1896
+ ...(source.truncatedEvidence.length ? { truncated_evidence: source.truncatedEvidence,
1897
+ next_action: "Narrow these focused evidence ranges with review_mission before dispatching the Reviewer; no Worker or new validation is needed." } : {}) }
1898
+ : { status: "skipped-low-risk" });
1885
1899
  } };
1886
1900
  async function assertMissionReview(root, mission) {
1887
1901
  const run = await operators.required(root), review = mission.review;
@@ -0,0 +1,5 @@
1
+ /** Prepare only an explicitly declared, in-project TMPDIR before a Worker starts validation. */
2
+ export declare function prepareValidationScratch(directory: string, commands: readonly string[]): Promise<{
3
+ prepared_directories: string[];
4
+ unprepared_directories: string[];
5
+ }>;
@@ -0,0 +1,51 @@
1
+ import { lstat, mkdir } from "node:fs/promises";
2
+ import { isAbsolute, relative, resolve, win32 } from "node:path";
3
+ import { createProjectPaths } from "./gate.js";
4
+ import { RUNTIME_PROFILES } from "../core/runtime-profile.js";
5
+ const controlDirectories = new Set([".git", ".opencode", ...Object.values(RUNTIME_PROFILES).map(profile => profile.stateDirectory)]);
6
+ /** Prepare only an explicitly declared, in-project TMPDIR before a Worker starts validation. */
7
+ export async function prepareValidationScratch(directory, commands) {
8
+ const prepared_directories = [], unprepared_directories = [];
9
+ if (!commands.some(command => /(?:^|\s)TMPDIR=/u.test(command)))
10
+ return { prepared_directories, unprepared_directories };
11
+ const project = await createProjectPaths(directory);
12
+ const seen = new Set();
13
+ for (const command of commands) {
14
+ for (const match of command.matchAll(/(?:^|\s)TMPDIR=(?:"([^"]+)"|'([^']+)'|([^\s"']+))/gu)) {
15
+ const declared = match[1] ?? match[2] ?? match[3];
16
+ // WSL's default drive mount and a native Windows project refer to the same directory.
17
+ // Other mounts are not inferred from command text.
18
+ const wsl = process.platform === "win32" ? /^\/mnt\/([a-z])\/(.+)$/iu.exec(declared) : null;
19
+ const target = wsl ? win32.resolve(`${wsl[1].toUpperCase()}:\\`, wsl[2].replaceAll("/", "\\"))
20
+ : isAbsolute(declared) ? resolve(declared) : null;
21
+ if (!target)
22
+ continue;
23
+ const path = relative(project.root, target).replaceAll("\\", "/");
24
+ if (!path || path === ".." || path.startsWith("../") || seen.has(path))
25
+ continue;
26
+ seen.add(path);
27
+ if (controlDirectories.has(path.split("/")[0]) || !await project.contains(target).catch(() => false)) {
28
+ unprepared_directories.push(path);
29
+ continue;
30
+ }
31
+ try {
32
+ const stat = await lstat(target).catch(error => {
33
+ if (error.code === "ENOENT")
34
+ return null;
35
+ throw error;
36
+ });
37
+ if (stat?.isDirectory())
38
+ continue;
39
+ if (stat)
40
+ throw new Error("path is not a directory");
41
+ await mkdir(target, { recursive: true });
42
+ prepared_directories.push(path);
43
+ }
44
+ catch {
45
+ // Existing Worker dispatch remains available; the Coordinator sees the missing setup.
46
+ unprepared_directories.push(path);
47
+ }
48
+ }
49
+ }
50
+ return { prepared_directories, unprepared_directories };
51
+ }
@@ -173,6 +173,13 @@ Do not reconstruct or prepend the absolute workspace path. Preserve explicitly r
173
173
  For release/benchmark work, retain the user's selected package/environment and record its receipt/hash.
174
174
  A local repack and a published tarball can have different hashes; that alone does not prohibit a requested
175
175
  local run. Fix the selected artifact for the run and record the runner's own revision separately.
176
+ Distinguish an outer benchmark attempt from the Worker dispatches inside it. If the user asked for no
177
+ automatic retries, do not start another Worker or replacement run for an unchanged failed task. Report
178
+ the host's actual attempt history and model observations, not only the outer runner's retry counter.
179
+ If a requested branch base is called 'main' but the snapshot has only 'master', inspect the intended
180
+ base commit. When 'master' names that same commit and the ref spelling itself is not required, use
181
+ the existing ref as start_ref in a lifecycle plan and disclose the substitution. Do not ask for
182
+ approval solely over a name; a genuinely different or unknown base needs a decision.
176
183
  Before repairing infrastructure on an old branch,
177
184
  check current main for an existing fix; preserve local edits and use a current-main worktree when needed.
178
185
  Report setup/route failures as such, with observed inference count, instead of calling runner exits a score.
@@ -215,6 +222,11 @@ ${profileAgent(profile, "dog-worker")} task verbatim, in foreground. V2 maps sub
215
222
  task_id to sessionID. Do not insert model overrides unless the user explicitly selected them.
216
223
 
217
224
  After each Worker returns, use its actual report and host evidence. Continue pending units with operator_next.
225
+ If a Worker returns process-defect with no formal validation evidence, inspect the specific missing
226
+ execution/proof route before any new dispatch. A command run through a custom container tool is not
227
+ native shell validation merely because its own output says PASS. If that route is unchanged, report
228
+ the blocker to Operator instead of creating another run or Worker with the same defect. Correct a
229
+ recoverable route within the current request and budget; this is not a new approval requirement.
218
230
  If plan_units returns mission-source-reconciliation-required, do not retry the same plan. The
219
231
  root-only Operator must reconcile the prior cancelled run. Submit status=blocked with the saved
220
232
  requirements and exact host diagnostic, then return; do not declare a user-only decision when
@@ -304,6 +316,12 @@ Use ${profile.toolPrefix}operator_status when the task needs native runtime iden
304
316
  remaining budget. It is a read-only observation available to Worker; do not request a separate unit or
305
317
  Coordinator transcription just to obtain it. It does not grant plan, dispatch or completion authority.
306
318
 
319
+ If the requested new branch is from 'main' but this source snapshot has only the checked-out
320
+ default 'master' at the intended base commit, use that existing ref as the branch base when the
321
+ ref spelling itself is not required. Report the substitution, not a user-only decision. If the
322
+ base commit differs or cannot be identified, report the ambiguity. Do not bypass a host Git
323
+ lifecycle, rewrite history or alter an existing branch.
324
+
307
325
  Read/search and read-only investigation commands are unrestricted. Use targeted reproduction/diagnosis
308
326
  without registering every exploratory command. All writes, generated/transient files and cleanup stay
309
327
  inside the unit's file/directory scopes, except the tool environment below. Do not write outside them through
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.12.23",
3
+ "version": "0.12.25",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",