sortie-dogs 0.12.10 → 0.12.11

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.10-mission-continuity-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.12.11-operation-recovery-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.10-mission-continuity-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.12.11-operation-recovery-v1";
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { normalizeRelativePath } from "./path.js";
4
+ import { normalizeRelativeScope } from "./path.js";
5
5
  import { parseOperatorPlan } from "./operator-runtime.js";
6
6
  import { profileAgent } from "./runtime-profile.js";
7
7
  const digest = (value) => createHash("sha256").update(value).digest("hex");
@@ -286,7 +286,7 @@ export function missionPlan(mission, raw) {
286
286
  const entries = value[field] ?? [];
287
287
  if (!Array.isArray(entries) || !entries.every(item => typeof item === "string"))
288
288
  throw new Error(`mission-unit-${index + 1}: ${field} must be paths`);
289
- return [...new Set(entries.map(item => normalizeRelativePath(item)))];
289
+ return [...new Set(entries.map(item => normalizeRelativeScope(item)))];
290
290
  };
291
291
  const ids = value.requirement_ids ?? mission.requirements.map(item => item.id);
292
292
  if (!Array.isArray(ids) || ids.length === 0 || !ids.every(id => mission.requirements.some(item => item.id === id))) {
@@ -336,6 +336,12 @@ export function missionPacket(mission, run) {
336
336
  return { mission_id: mission.id, phase: mission.phase, coordinator_session_id: mission.coordinator,
337
337
  requirements: mission.requirements, original_request_refs: mission.requests.map(item => `user:${item.id}`),
338
338
  submission: mission.submission, progress: mission.progress,
339
+ execution_summary: { completed_units: run?.units.filter(unit => unit.status === "succeeded").length ?? 0,
340
+ running_units: run?.units.filter(unit => unit.status === "running").map(unit => ({ id: unit.unit.id, title: unit.unit.title, child_session_id: unit.childSessionID })) ?? [],
341
+ pending_units: run?.units.filter(unit => unit.status === "pending").length ?? 0,
342
+ failed_units: run?.units.filter(unit => unit.status === "failed").length ?? 0,
343
+ historical_failed_attempts: mission.progress.filter(unit => unit.status === "failed").length,
344
+ accepted: mission.phase === "completed" },
339
345
  review: mission.review ? { risk_tags: mission.review.risk, verdict: mission.review.verdict,
340
346
  run_id: mission.review.runID, current_run: currentReview,
341
347
  source_fingerprint: mission.review.source, reviewer_session_id: mission.review.child ?? null,
@@ -5,7 +5,7 @@ import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { acceptanceContinuityFingerprint, inspectAcceptanceContinuity, normalizeAcceptanceCriteria, ACCEPTANCE_CONTINUITY_EXTENSION, MAX_ACCEPTANCE_CONTINUITY_BYTES, MAX_ACCEPTANCE_CRITERIA } from "./acceptance-continuity.js";
7
7
  import { expandGoalDeclaration, goalDeclarationDefaults, goalDeclarationFieldDiagnostics, hasGoalCommandAliasConflict } from "./goal-declaration-format.js";
8
- import { normalizeManifestPath, normalizeRelativePath } from "./path.js";
8
+ import { normalizeManifestPath, normalizeManifestScope, normalizeRelativePath } from "./path.js";
9
9
  import { CONTRACT_TEXT_LIMITS, validateHandoffSchema, validateOperationManifestSchema } from "./validate-schema.js";
10
10
  import { validateManifest } from "./validate-manifest.js";
11
11
  import { profileAgent, RUNTIME_PROFILES } from "./runtime-profile.js";
@@ -44,7 +44,10 @@ const text = (value) => typeof value === "string" && value.trim().length > 0 &&
44
44
  export function operatorGitPathAuthorized(path, scopes, platform = process.platform) {
45
45
  const key = (value) => platform === "win32" ? value.toLowerCase() : value;
46
46
  const candidate = key(path);
47
- return scopes.some(scope => candidate === key(scope) || candidate.startsWith(`${key(scope)}/`));
47
+ return scopes.some(scope => {
48
+ const base = key(normalizeManifestScope(scope).path);
49
+ return candidate === base || candidate.startsWith(`${base}/`);
50
+ });
48
51
  }
49
52
  const contractError = (diagnostic) => { throw new OperatorContractError([diagnostic]); };
50
53
  const planError = (pointer, code, rule, repair_kind = "repair-field") => contractError({ document: "plan", pointer, code, rule, repair_kind });
@@ -21,3 +21,10 @@ export declare function normalizeRelativePath(input: string): string;
21
21
  * error instead of being resolved away.
22
22
  */
23
23
  export declare function normalizeManifestPath(input: string): ManifestPath;
24
+ /** A trailing /** (or slash) declares a directory, including one not created yet.
25
+ * Tool destinations still use normalizeManifestPath: this notation belongs to declarations only.
26
+ */
27
+ export declare function normalizeManifestScope(input: string): ManifestPath & {
28
+ readonly directory: boolean;
29
+ };
30
+ export declare function normalizeRelativeScope(input: string): string;
package/dist/core/path.js CHANGED
@@ -52,3 +52,18 @@ export function normalizeManifestPath(input) {
52
52
  }
53
53
  return { kind: "relative", path: normalizeRelativePath(input) };
54
54
  }
55
+ /** A trailing /** (or slash) declares a directory, including one not created yet.
56
+ * Tool destinations still use normalizeManifestPath: this notation belongs to declarations only.
57
+ */
58
+ export function normalizeManifestScope(input) {
59
+ const unified = input.replaceAll("\\", "/");
60
+ const directory = unified.endsWith("/**") || unified.endsWith("/");
61
+ const path = unified.endsWith("/**") ? unified.slice(0, -3) : unified;
62
+ return { ...normalizeManifestPath(path), directory };
63
+ }
64
+ export function normalizeRelativeScope(input) {
65
+ const scope = normalizeManifestScope(input);
66
+ if (scope.kind !== "relative")
67
+ throw new RelativePathError("absolute");
68
+ return scope.path + (scope.directory ? "/**" : "");
69
+ }
@@ -31,7 +31,7 @@ function collectManifestPaths(paths, pointer, diagnostics) {
31
31
  paths.forEach((path, index) => {
32
32
  let normalized;
33
33
  try {
34
- normalized = pathUtils.normalizeManifestPath(path);
34
+ normalized = pathUtils.normalizeManifestScope(path);
35
35
  }
36
36
  catch (error) {
37
37
  if (!(error instanceof pathUtils.RelativePathError))
@@ -48,7 +48,7 @@ function collectManifestPaths(paths, pointer, diagnostics) {
48
48
  else if (normalized.kind === "relative") {
49
49
  // Absolute manifest targets authorize tools only. Handoff scope/source and Git paths remain
50
50
  // repository-relative and therefore never compare equal to an external target.
51
- normalizedPaths.add(normalized.path);
51
+ normalizedPaths.add(normalized.path + (normalized.directory ? "/**" : ""));
52
52
  }
53
53
  });
54
54
  return normalizedPaths;
@@ -79,7 +79,7 @@ function addPathDiagnostics(diagnostics, paths, allowed, code, pointer) {
79
79
  const reported = new Set();
80
80
  paths.forEach((path, index) => {
81
81
  const normalized = normalizePath(path);
82
- if (normalized !== undefined && (allowed.has(normalized) || reported.has(normalized)))
82
+ if (normalized !== undefined && (scopeAllows(allowed, normalized) || reported.has(normalized)))
83
83
  return;
84
84
  if (normalized !== undefined)
85
85
  reported.add(normalized);
@@ -91,6 +91,10 @@ function addPathDiagnostics(diagnostics, paths, allowed, code, pointer) {
91
91
  });
92
92
  });
93
93
  }
94
+ function scopeAllows(scopes, path) {
95
+ return scopes.has(path) || [...scopes].some(scope => scope.endsWith("/**") &&
96
+ (path === scope.slice(0, -3) || path.startsWith(scope.slice(0, -2))));
97
+ }
94
98
  /** Compare one schema-valid handoff with one schema-valid operation manifest. */
95
99
  export function validateManifest(handoff, manifest, changedPaths, changedPathsProvided, options = {}) {
96
100
  const diagnostics = [];
@@ -105,7 +109,7 @@ export function validateManifest(handoff, manifest, changedPaths, changedPathsPr
105
109
  handoff.sources?.forEach((source, index) => {
106
110
  const normalized = normalizePath(source.path);
107
111
  if (normalized !== undefined &&
108
- (readableOrWritable.has(normalized) || reportedSources.has(normalized)))
112
+ (scopeAllows(readableOrWritable, normalized) || reportedSources.has(normalized)))
109
113
  return;
110
114
  if (normalized !== undefined)
111
115
  reportedSources.add(normalized);
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { lstat, realpath } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, relative, resolve, win32 } from "node:path";
4
4
  import { promisify } from "node:util";
5
- import { RelativePathError, normalizeManifestPath, normalizeRelativePath } from "../core/path.js";
5
+ import { RelativePathError, normalizeManifestPath, normalizeManifestScope, normalizeRelativePath } from "../core/path.js";
6
6
  import { validateOperationManifestSchema } from "../core/validate-schema.js";
7
7
  export class WriteDeniedError extends Error {
8
8
  reason;
@@ -144,6 +144,13 @@ function curlOutput(tokens) {
144
144
  return undefined;
145
145
  continue;
146
146
  }
147
+ if (token === "-w" || token === "--write-out" || token.startsWith("--write-out=")) {
148
+ const format = token.startsWith("--write-out=") ? token.slice(12) : tokens[++index];
149
+ // curl write-out can open additional files or read a format file. Only stdout metadata is pathless.
150
+ if (format === undefined || format.startsWith("@") || /%output\{/u.test(format))
151
+ return undefined;
152
+ continue;
153
+ }
147
154
  if (token === "-o" || token === "--output") {
148
155
  const candidate = tokens[++index];
149
156
  if (candidate === undefined || candidate.startsWith("-") || output !== undefined)
@@ -259,8 +266,40 @@ function isRemoteGitHubMutation(tokens) {
259
266
  function isReadOnlyGitCommand(tokens) {
260
267
  const command = tokens[1]?.toLowerCase() ?? "";
261
268
  return READ_ONLY_GIT_COMMANDS.has(command) ||
269
+ (command === "tag" && tokens.length >= 3 && ["--points-at", "--contains", "--list", "-l"].includes(tokens[2]) &&
270
+ tokens.slice(3).every(token => !token.startsWith("-"))) ||
262
271
  (command === "branch" && tokens.length === 3 && tokens[2] === "--show-current");
263
272
  }
273
+ function gitArchiveOutput(tokens) {
274
+ let output, tree = false, paths = false;
275
+ for (let index = 2; index < tokens.length; index++) {
276
+ const token = tokens[index];
277
+ if (token === "--" && tree) {
278
+ paths = true;
279
+ continue;
280
+ }
281
+ if (paths || !token.startsWith("-")) {
282
+ tree = true;
283
+ continue;
284
+ }
285
+ if (token === "-o" || token === "--output") {
286
+ const value = tokens[++index];
287
+ if (!value || value.startsWith("-") || output !== undefined)
288
+ return undefined;
289
+ output = value;
290
+ }
291
+ else if (token.startsWith("--output=")) {
292
+ if (output !== undefined || !token.slice(9))
293
+ return undefined;
294
+ output = token.slice(9);
295
+ }
296
+ else if (/^--(?:format|prefix)=.+$/u.test(token) || /^-[0-9]$/u.test(token))
297
+ continue;
298
+ else
299
+ return undefined;
300
+ }
301
+ return tree ? { output } : undefined;
302
+ }
264
303
  function exactGitAddPaths(tokens) {
265
304
  if (tokens.length < 4 || tokens[2] !== "--")
266
305
  return undefined;
@@ -632,6 +671,18 @@ function shellPaths(command, powershell, depth = 0) {
632
671
  else
633
672
  ambiguous = true;
634
673
  }
674
+ else if (executable === "git" && tokens[1] === "archive") {
675
+ const archive = gitArchiveOutput(tokens);
676
+ if (archive === undefined) {
677
+ applies = true;
678
+ ambiguous = true;
679
+ issue ??= commandIssue(source, "unsupported-git-archive-form", "use git archive --format=tar --output=<scoped-file> <local-ref>");
680
+ }
681
+ else if (archive.output !== undefined) {
682
+ applies = true;
683
+ paths.push(archive.output);
684
+ }
685
+ }
635
686
  else if (executable === "git" && isReadOnlyGitCommand(tokens)) {
636
687
  // Explicitly read-only git subcommands.
637
688
  }
@@ -677,8 +728,10 @@ function shellPaths(command, powershell, depth = 0) {
677
728
  ...(issue ? { issue } : {}),
678
729
  };
679
730
  }
680
- function issuePath(issue) {
681
- return `segment=${issue.segment}; cause=${issue.cause}; hint=${issue.hint}; retry=false; action=return-denial-to-coordinator`;
731
+ function issuePath(issue, formatCorrection = false) {
732
+ return `segment=${issue.segment}; cause=${issue.cause}; hint=${issue.hint}; ` + (formatCorrection
733
+ ? "retry=after-format-correction; action=correct-format-within-current-manifest; preserve operation and destinations; otherwise return exact required correction to Coordinator"
734
+ : "retry=false; action=return-denial-to-coordinator");
682
735
  }
683
736
  export function describeUnclassifiedCommand(tool, args) {
684
737
  const extracted = extractWritePaths(tool, args);
@@ -925,7 +978,7 @@ function isWithin(root, target) {
925
978
  }
926
979
  async function canonicalManifestScopes(project, entries) {
927
980
  const scopes = await Promise.all(entries.map(async (entry) => {
928
- const normalized = normalizeManifestPath(entry);
981
+ const normalized = normalizeManifestScope(entry);
929
982
  const absolute = normalized.kind === "relative"
930
983
  ? project.absolute(normalized.path)
931
984
  : resolve(normalized.path);
@@ -987,10 +1040,13 @@ export async function createWriteGate(project, value, toolDirectory = project.ro
987
1040
  }
988
1041
  }
989
1042
  const writable = new Set();
1043
+ const explicitDirectories = new Set();
990
1044
  const externalEntries = [];
991
1045
  try {
992
1046
  for (const entry of manifest.write) {
993
- const normalized = normalizeManifestPath(entry);
1047
+ const normalized = normalizeManifestScope(entry);
1048
+ if (normalized.directory)
1049
+ explicitDirectories.add(normalized.path);
994
1050
  if (normalized.kind === "relative")
995
1051
  writable.add(normalized.path);
996
1052
  else {
@@ -1015,7 +1071,7 @@ export async function createWriteGate(project, value, toolDirectory = project.ro
1015
1071
  }
1016
1072
  }
1017
1073
  catch (error) {
1018
- if ((declaredDirectories.has(path) || inferredDirectories.has(path)) &&
1074
+ if ((explicitDirectories.has(path) || declaredDirectories.has(path) || inferredDirectories.has(path)) &&
1019
1075
  isRecord(error) && error.code === "ENOENT") {
1020
1076
  writableDirectories.push({ path, realPath: await nearestExistingRealPath(project.absolute(path)) });
1021
1077
  }
@@ -1059,7 +1115,11 @@ export async function createWriteGate(project, value, toolDirectory = project.ro
1059
1115
  if (!isRecord(error) || error.code !== "ENOENT") {
1060
1116
  throw new WriteDeniedError("manifest-unavailable", "<unknown>", { cause: error });
1061
1117
  }
1062
- externalFiles.push({ path, anchorRealPath: await nearestExistingRealPath(resolve(path, "..")) });
1118
+ if (explicitDirectories.has(path.replaceAll("\\", "/"))) {
1119
+ externalDirectories.push({ path, realPath: await nearestExistingRealPath(path) });
1120
+ }
1121
+ else
1122
+ externalFiles.push({ path, anchorRealPath: await nearestExistingRealPath(resolve(path, "..")) });
1063
1123
  }
1064
1124
  }
1065
1125
  const matchingExternalScopes = (absolute) => ({
@@ -1192,7 +1252,9 @@ export async function createWriteGate(project, value, toolDirectory = project.ro
1192
1252
  return;
1193
1253
  if (extracted.ambiguous || (extracted.paths.length === 0 && !extracted.gitCommit)) {
1194
1254
  if (extracted.issue !== undefined) {
1195
- throw new WriteDeniedError("unclassified-command", issuePath(extracted.issue));
1255
+ const formatCorrection = options?.investigativeShell === true &&
1256
+ ["unsupported-curl-form", "unsupported-git-archive-form", "unsupported-webrequest-form"].includes(extracted.issue.cause);
1257
+ throw new WriteDeniedError("unclassified-command", issuePath(extracted.issue, formatCorrection));
1196
1258
  }
1197
1259
  throw new WriteDeniedError("path-required", "<missing-path>");
1198
1260
  }
@@ -1208,7 +1270,15 @@ export async function createWriteGate(project, value, toolDirectory = project.ro
1208
1270
  throw new WriteDeniedError("project-boundary", path, { cause: error });
1209
1271
  }
1210
1272
  }
1211
- await checkPath(nativeFileTool ? resolve(toolDirectory, path) : path);
1273
+ try {
1274
+ await checkPath(nativeFileTool ? resolve(toolDirectory, path) : path);
1275
+ }
1276
+ catch (error) {
1277
+ if (options?.investigativeShell && error instanceof WriteDeniedError && error.reason === "manifest-scope") {
1278
+ error.message += " Coordinator: expand_unit with the exact missing output directory as dir/**, preserving settled units and cumulative spend; then dispatch the returned Task. No user approval is needed for an in-request correction.";
1279
+ }
1280
+ throw error;
1281
+ }
1212
1282
  }
1213
1283
  if (extracted.gitCommit)
1214
1284
  await checkCachedSet();
@@ -7760,7 +7760,10 @@ export const SortieDogsPlugin = async (input, options) => {
7760
7760
  return null;
7761
7761
  const reserved = state.outstanding_reservations.length;
7762
7762
  return { max_units: state.budget.max_units, consumed_units: state.consumed_units,
7763
- reserved_units: reserved, remaining_units: Math.max(0, state.budget.max_units - state.consumed_units - reserved) };
7763
+ reserved_units: reserved, remaining_units: Math.max(0, state.budget.max_units - state.consumed_units - reserved),
7764
+ settled_cost_usd: state.consumed_cost_usd, cost_limit_usd: state.budget.cost_usd,
7765
+ cost_status: state.consumed_cost_usd === null ? "unknown-usage" : reserved > 0 ? "in-flight-not-final" : "settled",
7766
+ cost_note: "Goal Worker ledger only; excludes unfinalized native requests, orchestration/review and external campaign spend. Zero settled cost does not mean free execution." };
7764
7767
  },
7765
7768
  registerGoalDeclaration: async (root, prompt, missionRevision) => {
7766
7769
  if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
@@ -8,6 +8,7 @@ import { TOOL_ENVIRONMENT } from "../runtime-mission-assets.js";
8
8
  import { MISSION_REVIEW_REFERENCE } from "../core/operator-mission.js";
9
9
  import { canonicalAgent } from "../core/runtime-profile.js";
10
10
  import { taskChildSessionID } from "./task-result-repair.js";
11
+ import { normalizeManifestScope } from "../core/path.js";
11
12
  const exec = promisify(execFile);
12
13
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
13
14
  /** Recover from native completion records, never from the pending review's inherited child ID. */
@@ -75,7 +76,7 @@ export async function completedMissionReviewPrompts(mission, profile, root, requ
75
76
  /** Pin all scoped tracked/untracked source bytes, including deletions; display a bounded excerpt only. */
76
77
  export async function missionReviewSource(directory, run) {
77
78
  const hash = createHash("sha256").update(JSON.stringify(run.units.map(unit => ({ unit: unit.unit, hashes: unit.hashes }))));
78
- const writes = [...new Set(run.units.flatMap(unit => unit.unit.write))];
79
+ const writes = [...new Set(run.units.flatMap(unit => unit.unit.write).map(path => path === "." ? path : normalizeManifestScope(path).path))];
79
80
  if (writes.length === 0)
80
81
  return { fingerprint: `sha256:${hash.digest("hex")}`,
81
82
  excerpt: "[Read-only units: no declared output files. Review the supplied observations, traces and validation evidence.]" };
@@ -1277,7 +1277,7 @@ export function createProfiledPlugin(profile, assetVersion) {
1277
1277
  });
1278
1278
  return JSON.stringify(await operators.next(root, actor));
1279
1279
  }
1280
- 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; do not invent a write path. 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.",
1280
+ 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. 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.",
1281
1281
  args: { units: { type: "array", minItems: 1, maxItems: 32, items: { type: "object", additionalProperties: false,
1282
1282
  properties: { title: { type: "string" }, objective: { type: "string" }, read: stringList, write: stringList,
1283
1283
  validation: stringList, requirement_ids: stringList }, required: ["title", "objective", "write", "validation"] } },
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { lstat, readFile, readdir, readlink, realpath, stat } from "node:fs/promises";
3
3
  import { isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { goalFingerprint } from "../core/goal-bound.js";
5
- import { normalizeManifestPath } from "../core/path.js";
5
+ import { normalizeManifestScope } from "../core/path.js";
6
6
  import { RUNTIME_PROFILES } from "../core/runtime-profile.js";
7
7
  const controlRoots = new Set([".git", ...Object.values(RUNTIME_PROFILES).map(profile => profile.stateDirectory)]);
8
8
  /** Live orchestration/Git bookkeeping is readable context, not the source being validated. */
@@ -67,7 +67,7 @@ export async function protectedSnapshot(authorization) {
67
67
  const relativePath = relative(authorization.projectRoot, authorization.manifestPath).replaceAll("\\", "/");
68
68
  const manifest = JSON.parse(manifestSource.toString("utf8"));
69
69
  const actualPaths = (entries) => entries.map((entry) => {
70
- const path = normalizeManifestPath(entry);
70
+ const path = normalizeManifestScope(entry);
71
71
  return path.kind === "relative" ? resolve(authorization.projectRoot, path.path) : resolve(path.path);
72
72
  });
73
73
  const candidatePaths = actualPaths(manifest.write);
@@ -3,6 +3,17 @@ import { STRATEGY_TRIGGERS, SOURCE_REVIEW_RISK_TAGS } from "./core/consultation.
3
3
  import { SCOUT_EVIDENCE_CODES } from "./core/scout-contract.js";
4
4
  /** Repository-local dependency environment shared by all units; excluded from reviewed and captured source. */
5
5
  export const TOOL_ENVIRONMENT = ".sortie-env";
6
+ const OPERATION_GUIDE = `## Practical operation guide
7
+
8
+ Use file paths for exact outputs and dir/** for a directory tree, including a directory that does not
9
+ exist yet. The host interprets dir/** consistently for writes, validation fingerprints and review.
10
+ Create the directory with mkdir -p dir; do not add parent directories merely to make child writes work.
11
+ For local source snapshots use git archive --format=tar --output=dir/source.tar <local-ref>.
12
+ For tag observation use git tag --points-at <commit>. For HTTPS downloads use curl -fLsS -o dir/file <url>;
13
+ --write-out '%{http_code} %{size_download}\\n' may print metadata to stdout. Declare all actual outputs.
14
+ Reuse pinned artifacts and successful checks when the request permits and the inputs are unchanged.
15
+ Do not repeat candidate discovery, dependency setup or validation merely because a Worker changed.
16
+ `;
6
17
  function controls(profile, names) {
7
18
  return names.map(name => ` ${profile.toolPrefix}${name}: true`).join("\n");
8
19
  }
@@ -62,6 +73,10 @@ Ordinary defects return to Coordinator, not the user. Ask through question only
62
73
  an extension beyond the original requirements, or a cumulative budget increase. Resume the same work after
63
74
  the answer. Do not reset spend, silently shrink acceptance, or create substitute goals.
64
75
 
76
+ When bounded process reflections are injected, pass the relevant prevention in Coordinator feedback while
77
+ preserving the original requirements. After a resolved repeated process failure, record its verified cause/prevention
78
+ through reflection if enabled; retain concrete evidence and prefer a durable fix for recurring causes.
79
+
65
80
  Retain 🐾 Sortie presentation and measured return panels. Append complete_mission's return_report verbatim
66
81
  once. Do not invent scores, medals, costs, savings, models or successful checks. Release/publish requires the
67
82
  existing user authorization and project gates; npm publication remains manual.
@@ -114,10 +129,14 @@ check or reviewer finding, call plan_units with the corrected units and a short
114
129
  preserves every requirement, failed-check history and cumulative budget. Never replace an active Worker.
115
130
  Rejected budget/contract checks or control-storage preparation leave the old run available. Correct the
116
131
  reported cause and call plan_units again; a local plan repair needs no cancellation or user approval.
132
+ On a returned denial, correct the exact command form or output scope before redispatch; reuse established
133
+ candidate identity, setup and evidence in the next objective instead of requesting the entire investigation again.
117
134
  Workers freely investigate within their unit and execute exact formal checks for host recording. Do not
118
135
  require them to predeclare exploratory commands. Require meaningful evidence, not extra testing for its
119
136
  own sake. Do not repeat passed checks unless source changes or unresolved concerns justify it.
120
137
 
138
+ ${OPERATION_GUIDE}
139
+
121
140
  Scout is optional for one precise missing fact. Its prompt includes missing_evidence_code:
122
141
  ${SCOUT_EVIDENCE_CODES.join(" | ")}, an exact project_root and at most four known_paths.
123
142
  Advisor is optional for one material decision; include strategy_trigger: ${STRATEGY_TRIGGERS.join(" | ")}.
@@ -183,7 +202,13 @@ are complete. Never fabricate logs, costs or exits. Do not stage outside declare
183
202
  publish, or take over Coordinator decisions. The parent releases your write binding after return.
184
203
 
185
204
  A local tool/permission/handoff defect returns PROCESS_DEFECT: local: <condition> and its exact diagnostic
186
- once to Coordinator. Do not repeat the same refused operation. Only a proven external dependency or
205
+ once to Coordinator when its remedy requires a changed contract. If the host explicitly returns
206
+ action=correct-format-within-current-manifest, correct the supported command form in this same Task,
207
+ preserving the operation, inputs and destinations, then continue. An unchanged denied request is not a correction.
208
+ If correcting the form changes the intended operation or requires another output, return the exact required
209
+ correction to Coordinator. Do not repeat the same refused operation. Only a proven external dependency or
187
210
  user-only choice uses TRUE_BLOCKER: external: <condition> or TRUE_BLOCKER: user-decision: <condition>.
211
+
212
+ ${OPERATION_GUIDE}
188
213
  `;
189
214
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.12.10",
3
+ "version": "0.12.11",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",