sortie-dogs 0.13.4 → 0.13.6

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
@@ -35,10 +35,10 @@ implementation, validation, review, and model routing.
35
35
  Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) ·
36
36
  [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md)
37
37
 
38
- **Current release: [v0.13.4](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.4)**
39
- ([release notes](docs/release-v0.13.4.md)). The default Mission runtime retains the `v010`
38
+ **Current release: [v0.13.6](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.6)**
39
+ ([release notes](docs/release-v0.13.6.md)). The default Mission runtime retains the `v010`
40
40
  profile, command and configuration names for compatibility; these names do not mean v0.10 is installed.
41
- The current asset marker is `0.13.4-reviewer-continuous-v1`.
41
+ The current asset marker is `0.13.6-acceptance-reuse-v1`.
42
42
 
43
43
  ## SWE-bench Lite: 170/300 (56.67%)
44
44
 
@@ -48,7 +48,7 @@ The fixed **Sortie-dogs v0.12.24** harness resolved **170 of 300 SWE-bench Lite
48
48
 
49
49
  The single official 300-instance report and frozen predictions are hash-bound in the report. Confirmed inference expense was **$162.99**; a separate **$34.60** of usage has unknown pricing and is held against the campaign cap, **not** counted as known expense. Leaderboard registration and maintainer acceptance are separate from this official local evaluation.
50
50
 
51
- Historical scores below belong to their fixed candidates, not v0.13.4. SWE-bench is a separate,
51
+ Historical scores below belong to their fixed candidates, not v0.13.6. SWE-bench is a separate,
52
52
  optional measurement rather than a mandatory release gate.
53
53
 
54
54
  > **Beta:** v0.13.x is still stabilizing. Runtime behavior,
@@ -93,7 +93,34 @@ existing local bridge loads enforcement and model routing. OpenCode can reload w
93
93
  but replacing an installed dependency may require a full restart. A new chat session alone does not
94
94
  prove the newly installed plugin is loaded.
95
95
 
96
- ## v0.13.4 runtime updates
96
+ ## v0.13.6 runtime updates
97
+
98
+ PR #156 references recorded formal PASS evidence in acceptance summaries instead of fetching
99
+ the same successful unit's native history again for display. Missing/failed proof retains history
100
+ diagnostics; historical evidence is not current freshness or acceptance. Identical external artifact
101
+ inventories are shared only inside one snapshot refresh, never across later calls. Operator guidance
102
+ proceeds to acceptance when current evidence covers the request and no concrete gap remains.
103
+ Existing freshness/review guards, provider/model selection and cache-prefix machinery remain unchanged.
104
+ No end-to-end speedup or live token/cache-hit improvement was measured. Release receipts:
105
+ `_testenv/releases/0.13.6/`; see [release notes](docs/release-v0.13.6.md).
106
+
107
+ ## v0.13.5 runtime updates (retained)
108
+
109
+ PR #154 preserves stable model instructions and appends changed host state at native history
110
+ boundaries, with complete current-state reconstruction after compaction. Genuine Reviewer tools
111
+ have deterministic read-only-first ordering; the real correction-permission transition still remains.
112
+ The final candidate restores v3 behavioral review and combined assignment/findings while retaining
113
+ exact instruction discovery, inherited Reviewer formal-command delivery, literal local shell-file
114
+ scope reconciliation and explicit repository-root read/write scope support. New whole-project
115
+ captures avoid bookkeeping-only invalidation; legacy evidence and real source/artifact freshness remain.
116
+
117
+ The final pre-release cycle 11 Anko sample passed official local score 1 and selected public probes 9/9
118
+ in 26m1.866s at estimated $1.41968316. It was faster but 15.762% costlier than the earlier v3 sample,
119
+ not combined cost-preserving optimization or a general quality/speed guarantee. Release receipts:
120
+ `_testenv/releases/0.13.5/`. See [release notes](docs/release-v0.13.5.md) and
121
+ [candidate tradeoffs/failures](docs/cache-prefix-loop-20261003.md). Native Worker startup is not completion.
122
+
123
+ ## v0.13.4 runtime updates (retained)
97
124
 
98
125
  PR #152 restores same-session Coordinator/Operator implementation and formal validation through
99
126
  `plan_units(executor="self")`, `start_direct_unit` and `finish_direct_unit`. Known single-unit work can
@@ -397,7 +424,7 @@ register stable and `v010` from the same package installation path in one host.
397
424
  Project-local installation is recommended. To expose the current Mission assets globally:
398
425
 
399
426
  ```sh
400
- npm install --global sortie-dogs@0.13.4
427
+ npm install --global sortie-dogs@0.13.6
401
428
  sortie-dogs init --global --profile v010
402
429
  ```
403
430
 
@@ -409,7 +436,7 @@ can resolve a **separate dependency** under that config root. Updating npm-globa
409
436
  it. For that layout, also install the same release at the actual config root, then rerun global init:
410
437
 
411
438
  ```sh
412
- npm install --prefix "$HOME/.config/opencode" sortie-dogs@0.13.4
439
+ npm install --prefix "$HOME/.config/opencode" sortie-dogs@0.13.6
413
440
  sortie-dogs init --global --profile v010
414
441
  ```
415
442
 
@@ -432,7 +459,7 @@ version, preserves user configuration, and stops safely on unknown ownership or
432
459
  conflicting files.
433
460
 
434
461
  Align any exact version pin or separate bridge dependency with the intended release too. An installed
435
- marker of `0.13.4-reviewer-continuous-v1` identifies the assets; it does not prove an already-running
462
+ marker of `0.13.6-acceptance-reuse-v1` identifies the assets; it does not prove an already-running
436
463
  OpenCode process has reloaded the plugin.
437
464
 
438
465
  There is no supported uninstall command. Remove the npm dependency separately,
@@ -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.13.4-reviewer-continuous-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.13.6-acceptance-reuse-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.13.4-reviewer-continuous-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.13.6-acceptance-reuse-v1";
@@ -39,6 +39,9 @@ export interface GoalEvidence {
39
39
  readonly candidate_paths: readonly string[];
40
40
  /** Missing on legacy evidence: retain its original all-paths snapshot recipe. */
41
41
  readonly source_policy?: "project-files-v1" | "declared-paths-v1";
42
+ /** New captures distinguish a whole-project output grant from explicitly named control-like artifacts.
43
+ * Missing on old evidence: keep its original candidate recipe, including bookkeeping bytes. */
44
+ readonly candidate_policy?: "project-root-artifacts-v1";
42
45
  /** Fixed when validation starts. Full manifest_hash still identifies the historical execution contract. */
43
46
  readonly freshness?: {
44
47
  readonly contract_hash: string;
@@ -65,6 +65,7 @@ export function validGoalEvidence(value, state) {
65
65
  protectedBinding.candidate_paths.every(text) &&
66
66
  (protectedBinding.source_policy === undefined || protectedBinding.source_policy === "project-files-v1" ||
67
67
  protectedBinding.source_policy === "declared-paths-v1") &&
68
+ (protectedBinding.candidate_policy === undefined || protectedBinding.candidate_policy === "project-root-artifacts-v1") &&
68
69
  (protectedBinding.freshness === undefined || (protectedBinding.freshness !== null && typeof protectedBinding.freshness === "object" &&
69
70
  HASH.test(protectedBinding.freshness.contract_hash) &&
70
71
  Array.isArray(protectedBinding.freshness.scratch_paths) && protectedBinding.freshness.scratch_paths.every(text) &&
@@ -443,10 +443,11 @@ export function missionPlan(mission, raw, projectRoot) {
443
443
  if (!Array.isArray(entries) || !entries.every(item => typeof item === "string"))
444
444
  throw new Error(`mission-unit-${index + 1}: ${field} must be paths`);
445
445
  return [...new Set(entries.map(item => {
446
- // A model's repository-root read means the current project, not an invalid empty path.
447
- // Resolve only this read shorthand at the host boundary; saved plans and write scopes stay exact.
448
- const rootRead = field === "read" && [".", "./", ".\\", "./**", ".\\**"].includes(item);
449
- return normalizeExecutionScope(rootRead && projectRoot ? `${resolve(projectRoot).replaceAll("\\", "/")}/**` : item);
446
+ // An explicit repository-root scope means this project for both reads and writes.
447
+ // Resolve this shorthand only at the host boundary; saved plans, prohibitions and
448
+ // native permissions still use the exact directory scope, never an inferred grant.
449
+ const rootScope = [".", "./", ".\\", "./**", ".\\**"].includes(item);
450
+ return normalizeExecutionScope(rootScope && projectRoot ? `${resolve(projectRoot).replaceAll("\\", "/")}/**` : item);
450
451
  }))];
451
452
  };
452
453
  // A sole unit owns the whole request. This schedules work; it does not prove acceptance.
@@ -533,6 +534,15 @@ export async function missionAcceptanceSummary(mission, run, operators, observe)
533
534
  const provenance = { run_id: item.state.runID, unit_id: unit.unit.id, worker_session_id: unit.childSessionID,
534
535
  state_archive_path: item.path, handoff_path: unit.handoffPath, historical: item.historical,
535
536
  authority: "native declared-command observations; not additional formal evidence or current freshness" };
537
+ // Settled proof already records these commands. Do not fetch the same native
538
+ // history again merely to render acceptance; freshness is checked separately.
539
+ const proofs = unit.evidence ?? [];
540
+ if (unit.status === "succeeded" && unit.unit.validation.every(command => proofs.some(proof => proof.execution.exit_code === 0 && proof.execution.outcome === "pass" &&
541
+ proof.execution.command.includes(command)))) {
542
+ return { ...provenance, status: "recorded-formal-evidence",
543
+ evidence_ids: proofs.map(proof => proof.evidence_id),
544
+ details_ref: { field: "formal_validation", run_id: item.state.runID, unit_id: unit.unit.id } };
545
+ }
536
546
  try {
537
547
  if (!observe || !unit.childSessionID)
538
548
  throw new Error("native-worker-history-unavailable");
@@ -7487,7 +7487,8 @@ export const SortieDogsPlugin = async (input, options) => {
7487
7487
  if (extracted.paths.length)
7488
7488
  await input.runtimeBridge?.assertMissionWrite?.(toolInput.sessionID, extracted.paths);
7489
7489
  const nativeFile = /^(?:write|edit)(?:$|[_-])/iu.test(toolInput.tool) || /patch/iu.test(toolInput.tool);
7490
- if (nativeFile && !extracted.ambiguous && extracted.paths.length) {
7490
+ const nativeShell = ["bash", "shell"].includes(toolInput.tool) && authorization.writeScopes.length > 0;
7491
+ if ((nativeFile && !extracted.ambiguous || nativeShell && !extracted.gitAmbiguous) && extracted.paths.length) {
7491
7492
  const missing = [];
7492
7493
  for (const path of extracted.paths) {
7493
7494
  const actual = nativeFile ? resolve(input.directory, path) : path;
@@ -7498,6 +7499,17 @@ export const SortieDogsPlugin = async (input, options) => {
7498
7499
  if (!(error instanceof WriteDeniedError) || !["manifest-scope", "project-boundary"].includes(error.reason))
7499
7500
  throw error;
7500
7501
  const local = relative(authorization.projectRoot, resolve(input.directory, actual)).replaceAll("\\", "/");
7502
+ if (nativeShell) {
7503
+ // Reconcile known local file destinations, not expansions, directory trees or
7504
+ // opaque program internals. Unknown executables may accompany a literal rm/redirect.
7505
+ if (/[$*?\[\]{}()~`]/u.test(path) || local === ".." || local.startsWith("../") || isAbsolute(local))
7506
+ continue;
7507
+ if (extracted.createdDirectories?.length || extracted.requiredDirectories?.some(directory => resolve(input.directory, isRecord(output.args) && typeof output.args.workdir === "string" ? output.args.workdir : ".", directory) ===
7508
+ resolve(input.directory, actual)))
7509
+ continue;
7510
+ if (await stat(resolve(input.directory, actual)).then(value => value.isDirectory()).catch(() => false))
7511
+ continue;
7512
+ }
7501
7513
  missing.push(local === ".." || local.startsWith("../") || isAbsolute(local) ? resolve(input.directory, actual) : local);
7502
7514
  }
7503
7515
  }
@@ -0,0 +1,15 @@
1
+ export interface ModelLiveStateHistory {
2
+ version: 1;
3
+ native: string[];
4
+ blocks: Record<string, string[]>;
5
+ updates: {
6
+ after: number;
7
+ text: string[];
8
+ }[];
9
+ }
10
+ /** Reinsert already sent state at native boundaries; append changed blocks, never persist fake native messages. */
11
+ export declare function modelLiveState(system: readonly string[], messages?: readonly unknown[], saved?: unknown): {
12
+ system: string[];
13
+ messages?: unknown[];
14
+ history?: ModelLiveStateHistory;
15
+ };
@@ -0,0 +1,65 @@
1
+ import { createHash } from "node:crypto";
2
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3
+ // Only Sortie's own generated elements are classified here, not the native system
4
+ // prompt or retrieved/user/tool text. Keep policy and role instructions in front;
5
+ // counters, findings and lifecycle changes should not invalidate all prior reads.
6
+ const headings = new Set([
7
+ "SORTIE_GOAL_BOUND_STATE", "SORTIE_ACCEPTANCE_CONTINUITY_STATE", "SORTIE_PARALLEL_DISPATCH_STATE",
8
+ // Recognize the former split findings block so restored histories can explicitly retire it.
9
+ "SORTIE_REVIEWER_CONTINUOUS_CONTEXT", "SORTIE_REVIEWER_RETAINED_FINDINGS", "SORTIE_WORKER_CONTEXT", "SORTIE_FAST_LANE_TERMINAL",
10
+ ]);
11
+ const prefixes = ["Native tools actually available in this request: ",
12
+ "Confirmed launch conditions (fixed caps, not remaining Worker/campaign budget): "];
13
+ const blockName = (text) => {
14
+ const heading = text.split("\n", 1)[0];
15
+ return headings.has(heading) ? heading : prefixes.find(prefix => text.startsWith(prefix));
16
+ };
17
+ const projected = new WeakSet();
18
+ function retained(value) {
19
+ return object(value) && value.version === 1 && Array.isArray(value.native) && value.native.every(hash => typeof hash === "string") &&
20
+ object(value.blocks) && Object.values(value.blocks).every(texts => Array.isArray(texts) && texts.every(text => typeof text === "string")) &&
21
+ Array.isArray(value.updates) && value.updates.every((update, index, updates) => object(update) &&
22
+ Number.isSafeInteger(update.after) && Number(update.after) >= 0 && Number(update.after) <= value.native.length &&
23
+ (index === 0 || Number(update.after) >= Number(updates[index - 1].after)) &&
24
+ Array.isArray(update.text) && update.text.every(text => typeof text === "string"));
25
+ }
26
+ /** Reinsert already sent state at native boundaries; append changed blocks, never persist fake native messages. */
27
+ export function modelLiveState(system, messages, saved) {
28
+ // Compatibility callers with no transcript still receive every state field.
29
+ if (messages === undefined)
30
+ return { system: [...system] };
31
+ const stable = [], blocks = {};
32
+ for (const text of system) {
33
+ const name = blockName(text);
34
+ if (name === undefined)
35
+ stable.push(text);
36
+ else
37
+ (blocks[name] ??= []).push(text);
38
+ }
39
+ const native = messages.filter(message => !object(message) || !projected.has(message));
40
+ const hashes = native.map(message => createHash("sha256").update(JSON.stringify(message) ?? "undefined").digest("hex"));
41
+ // Compaction, edited native history or a missing/malformed ledger starts from
42
+ // complete CURRENT durable host state. Old projection is not review/test proof.
43
+ const prior = retained(saved) && saved.native.length <= hashes.length &&
44
+ saved.native.every((hash, index) => hashes[index] === hash) ? saved : { version: 1, native: [], blocks: {}, updates: [] };
45
+ const changed = Object.entries(blocks).filter(([name, text]) => JSON.stringify(prior.blocks[name]) !== JSON.stringify(text)).flatMap(([, text]) => text);
46
+ const withdrawn = Object.keys(prior.blocks).filter(name => !Object.hasOwn(blocks, name));
47
+ if (withdrawn.length)
48
+ changed.push(`SORTIE_LIVE_STATE_WITHDRAWN\n${JSON.stringify(withdrawn)}`);
49
+ const history = { version: 1, native: hashes, blocks,
50
+ updates: [...prior.updates, ...(changed.length ? [{ after: native.length, text: changed }] : [])] };
51
+ const output = [];
52
+ let cursor = 0;
53
+ for (let index = 0; index <= native.length; index++) {
54
+ while (history.updates[cursor]?.after === index) {
55
+ // V2 lowers role=system to chronological developer input, not absolute instructions.
56
+ const update = { role: "system", content: history.updates[cursor++].text.map(text => ({ type: "text", text })),
57
+ metadata: { "sortie-dogs/live-state": "request-only" } };
58
+ projected.add(update);
59
+ output.push(update);
60
+ }
61
+ if (index < native.length)
62
+ output.push(native[index]);
63
+ }
64
+ return { system: stable, messages: output, history };
65
+ }
@@ -1,5 +1,5 @@
1
1
  import { V010_RUNTIME_ASSET_VERSION } from "../asset-version.js";
2
- import { MISSION_BEHAVIOR_REVIEW, MISSION_GIT_SCOPE } from "../runtime-mission-assets.js";
2
+ import { MISSION_BEHAVIOR_REVIEW, MISSION_GIT_SCOPE, REVIEWER_VALIDATION_WORKFLOW } from "../runtime-mission-assets.js";
3
3
  import { cancelledMissionRetainsAcceptance, operatorGitPathAuthorized, OperatorContractError, OperatorRuntime } from "../core/operator-runtime.js";
4
4
  import { DEFAULT_OPERATOR_PROPOSAL_BUDGET, OPERATOR_APPROVAL_CONTRACT, OPERATOR_PROPOSAL_BUDGET_CAPS, OPERATOR_PROPOSAL_REVISION_CONTRACT, OperatorProposalBudgetError, OperatorProposalRuntime } from "../core/operator-proposal.js";
5
5
  import { CANONICAL_AGENT_ROLES, canonicalAgent, profileAgent, profileTool, V010_RUNTIME_PROFILE } from "../core/runtime-profile.js";
@@ -2450,7 +2450,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2450
2450
  await activateDirect(author);
2451
2451
  return JSON.stringify({ status: "correction-running", execution: "same-native-task",
2452
2452
  findings: correction.findings, validation: unit.unit.validation, write: unit.unit.write,
2453
- next_action: `Correct all retained defects here using your existing context and public test harness; no handoff read or new Task. Run inherited formal commands in order, retain requested commit/clean delivery, then ${finishDirectUnit} and your explicit SELF_RECHECKED native terminal. Additional investigation may widen when useful; no reduced effort or acceptance of unresolved Medium.` });
2453
+ next_action: `Correct all retained defects here using your existing context and public test harness; no handoff read or new Task. ${REVIEWER_VALIDATION_WORKFLOW} Retain requested commit/clean delivery, then ${finishDirectUnit} and your explicit SELF_RECHECKED native terminal. Additional investigation may widen when useful; no reduced effort or acceptance of unresolved Medium.` });
2454
2454
  }
2455
2455
  tools[repairReview] = { description: "Reviewer: record your concrete Major/Medium findings and continue correction, inherited formal checks, commit and self-recheck HERE without ending this native Task. Controller: recover the ORIGINAL correction owner after a terminated review/correction. Host prepares/binds the existing authorized write scope and validation; no findings transcription, new approval or Worker rediscovery. Author self-recheck is not independent PASS. Operation and read-only reviews do not use this route.",
2456
2456
  args: { findings: optionalStringSchema }, execute: async (args, context) => {
@@ -2526,7 +2526,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2526
2526
  throw new Error("mission-review-correction-budget-unavailable");
2527
2527
  const reviewIdentity = missionReviewIdentity(mission);
2528
2528
  const plan = missionPlan(mission, [{ title: `Correct ${run.units[0].unit.title}`,
2529
- objective: 'Correct all retained concrete Major/Medium findings in this SAME native conversation; preserve every original requirement. Exact findings are at correction_context.findings in the handoff; retained_findings_ref is optional lineage, not an instruction to reread/print the entire Mission or previous review prompt. Relevant search and focused diagnostics may widen normally; do not rediscover unchanged work. Run all unchanged declared formal validation in order and retain the requested commit/clean boundary. THEN explicitly self-recheck the original requirements, ALL retained findings, correction and relevant impact in this Task. Correct known defects and revalidate affected checks before finishing. First line SELF_RECHECKED. Next line self_recheck: {"candidate":"current-validated","unresolved_findings":[],"residual_major":null}. The HOST binds current-validated to actual current source and fresh successful native checks after this exact prompt; never copy a pre-edit hash. Explain the actual comparison below. List unresolved concrete Major/Medium defects in unresolved_findings (no acceptance). Only concrete reachable residual Major risk uses residual_major:{"reachable_path":"...","consequence":"serious consequence"} and requires a different Reviewer. Tags/hashes/Medium/prose gaps alone never trigger it. This is author self-recheck, NOT independent PASS. Legacy CORRECTION_READY without self-recheck only permits a same-author read-only fallback Task, never acceptance.',
2529
+ objective: `Correct all retained concrete Major/Medium findings in this SAME native conversation; preserve every original requirement. Exact findings are at correction_context.findings in the handoff; retained_findings_ref is optional lineage, not an instruction to reread/print the entire Mission or previous review prompt. Relevant search and focused diagnostics may widen normally; do not rediscover unchanged work. ${REVIEWER_VALIDATION_WORKFLOW} Retain the requested commit/clean boundary. THEN explicitly self-recheck the original requirements, ALL retained findings, correction and relevant impact in this Task. Correct known defects and revalidate affected checks before finishing. First line SELF_RECHECKED. Next line self_recheck: {"candidate":"current-validated","unresolved_findings":[],"residual_major":null}. The HOST binds current-validated to actual current source and fresh successful native checks after this exact prompt; never copy a pre-edit hash. Explain the actual comparison below. List unresolved concrete Major/Medium defects in unresolved_findings (no acceptance). Only concrete reachable residual Major risk uses residual_major:{"reachable_path":"...","consequence":"serious consequence"} and requires a different Reviewer. Tags/hashes/Medium/prose gaps alone never trigger it. This is author self-recheck, NOT independent PASS. Legacy CORRECTION_READY without self-recheck only permits a same-author read-only fallback Task, never acceptance.`,
2530
2530
  read: [...new Set([...(mission.reviewScope?.read ?? []), ...run.units.flatMap(unit => unit.unit.read)])],
2531
2531
  write: [...new Set([...(mission.reviewScope?.write ?? []), ...run.units.flatMap(unit => unit.unit.write)])],
2532
2532
  validation: run.units.flatMap(unit => unit.unit.validation) }], input.directory);
@@ -3783,6 +3783,10 @@ export function createProfiledPlugin(profile, assetVersion) {
3783
3783
  (output.system ??= []).push(`SORTIE_RUNTIME_PROFILE ${profile.id}; marker ${assetVersion}. ` +
3784
3784
  `Shared MkII protocol role names are logical: ${protocolMap}. Use only ${profile.toolPrefix} tools for this profile. ` +
3785
3785
  "Never rewrite user acceptance or evidence to rename protocol roles. Final acceptance belongs only to the root coordinator.");
3786
+ output.system.push("SORTIE_LIVE_STATE_POLICY\nCurrent host state may follow the conversation as a request-only system update. " +
3787
+ "Use the latest update of each named state block for current findings, assignment, counters, acceptance continuity and receipt. " +
3788
+ "SORTIE_LIVE_STATE_WITHDRAWN retires the listed blocks; earlier scope, tool lists and receipts are historical, not current authority. " +
3789
+ "It does not replace original requirements, native tool outcomes or formal evidence; author self-recheck is not independent approval.");
3786
3790
  const mission = await missions.read(root), run = await operators.read(root);
3787
3791
  const inline = mission?.corrections?.find(item => item.runID === run?.runID && item.author === request.sessionID &&
3788
3792
  item.inlineReview && ["running", "ready"].includes(item.status) && !item.selfRecheck);
@@ -88,11 +88,11 @@ export function isRuntimeControlPath(path) {
88
88
  const first = path.replaceAll("\\", "/").split("/")[0];
89
89
  return controlRoots.has(process.platform === "win32" ? first.toLowerCase() : first);
90
90
  }
91
- async function protectedScopeDigest(projectRoot, paths, manifestHash, sourcePolicy, excluded = [], binding) {
91
+ async function protectedScopeDigest(projectRoot, paths, manifestHash, sourcePolicy, excluded = [], binding, candidatePolicy) {
92
92
  const entries = [];
93
93
  const canonicalRoot = await realpath(projectRoot);
94
94
  const scratchExcluded = binding ? snapshotScratchExclusion(binding) : () => false;
95
- const visit = async (absolute, ancestors = new Set()) => {
95
+ const visit = async (absolute, ancestors = new Set(), projectArtifacts = false) => {
96
96
  if (excluded.some(root => !outside(root, absolute)))
97
97
  return true;
98
98
  if (scratchExcluded(absolute))
@@ -100,7 +100,7 @@ async function protectedScopeDigest(projectRoot, paths, manifestHash, sourcePoli
100
100
  const scoped = relative(projectRoot, absolute).replaceAll("\\", "/");
101
101
  if (scoped === ".." || scoped.startsWith("../") || isAbsolute(scoped))
102
102
  return false;
103
- if (sourcePolicy === "project-files-v1" && isRuntimeControlPath(scoped))
103
+ if ((sourcePolicy === "project-files-v1" || projectArtifacts) && isRuntimeControlPath(scoped))
104
104
  return true;
105
105
  const metadata = await lstat(absolute).catch((error) => {
106
106
  if (error.code === "ENOENT")
@@ -126,7 +126,7 @@ async function protectedScopeDigest(projectRoot, paths, manifestHash, sourcePoli
126
126
  entries.push([scoped, `symlink:${link}`]);
127
127
  const next = new Set([...ancestors, target]);
128
128
  for (const child of (await readdir(absolute)).sort())
129
- if (!await visit(join(absolute, child), next))
129
+ if (!await visit(join(absolute, child), next, projectArtifacts))
130
130
  return false;
131
131
  return true;
132
132
  }
@@ -142,7 +142,7 @@ async function protectedScopeDigest(projectRoot, paths, manifestHash, sourcePoli
142
142
  entries.push([scoped, "directory"]);
143
143
  const next = new Set([...ancestors, real]);
144
144
  for (const child of (await readdir(absolute)).sort())
145
- if (!await visit(join(absolute, child), next))
145
+ if (!await visit(join(absolute, child), next, projectArtifacts))
146
146
  return false;
147
147
  return true;
148
148
  }
@@ -151,22 +151,32 @@ async function protectedScopeDigest(projectRoot, paths, manifestHash, sourcePoli
151
151
  entries.push([scoped, "file", createHash("sha256").update(await readFile(absolute)).digest("hex")]);
152
152
  return true;
153
153
  };
154
- for (const path of [...new Set(paths)].sort())
155
- if (!await visit(path))
154
+ for (const path of [...new Set(paths)].sort()) {
155
+ // Only a whole-project grant excludes incidental host bookkeeping. A separately
156
+ // declared control-like output still traverses and pins its exact bytes.
157
+ const projectArtifacts = candidatePolicy === "project-root-artifacts-v1" && resolve(path) === resolve(projectRoot);
158
+ if (!await visit(path, new Set(), projectArtifacts))
156
159
  return undefined;
160
+ }
157
161
  return goalFingerprint({ manifest_hash: `sha256:${manifestHash}`, entries });
158
162
  }
159
163
  function outside(projectRoot, path) {
160
164
  const scoped = relative(projectRoot, path).replaceAll("\\", "/");
161
165
  return scoped === ".." || scoped.startsWith("../") || isAbsolute(scoped);
162
166
  }
163
- async function declaredScopeDigest(projectRoot, paths, manifestHash, source, excluded = [], binding) {
167
+ async function declaredScopeDigest(projectRoot, paths, manifestHash, source, excluded = [], binding, candidatePolicy, artifactsMemo) {
164
168
  const local = paths.filter(path => !outside(projectRoot, path));
165
169
  const external = paths.filter(path => outside(projectRoot, path));
166
- const project = await protectedScopeDigest(projectRoot, local, manifestHash, source ? "project-files-v1" : undefined, excluded, binding);
170
+ const project = await protectedScopeDigest(projectRoot, local, manifestHash, source ? "project-files-v1" : undefined, excluded, binding, candidatePolicy);
167
171
  if (project === undefined)
168
172
  return undefined;
169
- const artifacts = await declaredArtifacts(external).catch(() => undefined);
173
+ const key = JSON.stringify(external);
174
+ let pending = artifactsMemo?.get(key);
175
+ if (!pending) {
176
+ pending = declaredArtifacts(external);
177
+ artifactsMemo?.set(key, pending);
178
+ }
179
+ const artifacts = await pending.catch(() => undefined);
170
180
  return artifacts && goalFingerprint({ project, external: artifacts.entries.filter(([path]) => !excluded.some(root => !outside(root, path))) });
171
181
  }
172
182
  /** An existing operation may create its declared outputs. Compare its other inputs during execution;
@@ -226,6 +236,7 @@ export async function protectedSnapshot(authorization, options = {}) {
226
236
  const tracked = await exec("git", ["ls-files", "-z"], { cwd: authorization.projectRoot, maxBuffer: 16 * 1024 * 1024 }).then(value => value.stdout.split("\0").filter(Boolean)).catch(() => []);
227
237
  const binding = { manifest_hash: `sha256:${manifestHash}`, project_root: authorization.projectRoot,
228
238
  manifest_path: relativePath, source_policy: external ? "declared-paths-v1" : "project-files-v1",
239
+ candidate_policy: "project-root-artifacts-v1",
229
240
  source_paths: sourcePaths.map(evidencePath),
230
241
  candidate_paths: candidatePaths.map(evidencePath),
231
242
  freshness: { contract_hash: validationContractHash(manifest), scratch_paths: validationScratchPaths(authorization.projectRoot, manifest.validation, process.env),
@@ -269,14 +280,17 @@ export async function refreshProtectedSnapshot(projectRoot, binding) {
269
280
  }
270
281
  }
271
282
  if (binding.source_policy === "declared-paths-v1") {
272
- const source = await declaredScopeDigest(projectRoot, sourcePaths, manifestHash, true, [], binding);
273
- const candidate = await declaredScopeDigest(projectRoot, candidatePaths, manifestHash, false, [], binding);
283
+ // Reuse identical external inventories only inside this read-only refresh.
284
+ // A later status/completion call always observes filesystem changes anew.
285
+ const artifactsMemo = new Map();
286
+ const source = await declaredScopeDigest(projectRoot, sourcePaths, manifestHash, true, [], binding, undefined, artifactsMemo);
287
+ const candidate = await declaredScopeDigest(projectRoot, candidatePaths, manifestHash, false, [], binding, binding.candidate_policy, artifactsMemo);
274
288
  return source === undefined || candidate === undefined ? undefined : { source, candidate };
275
289
  }
276
290
  // Legacy evidence retains its original recipe; a new policy cannot relabel stale proof as fresh.
277
291
  if (binding.source_policy !== undefined && binding.source_policy !== "project-files-v1")
278
292
  return undefined;
279
293
  const source = await protectedScopeDigest(projectRoot, sourcePaths, manifestHash, binding.source_policy, [], binding);
280
- const candidate = await protectedScopeDigest(projectRoot, candidatePaths, manifestHash, undefined, [], binding);
294
+ const candidate = await protectedScopeDigest(projectRoot, candidatePaths, manifestHash, undefined, [], binding, binding.candidate_policy);
281
295
  return source === undefined || candidate === undefined ? undefined : { source, candidate };
282
296
  }
package/dist/plugin/v2.js CHANGED
@@ -8,6 +8,7 @@ import { V010_RUNTIME_ASSET_VERSION } from "../asset-version.js";
8
8
  import { NativeBackgroundLifecycle } from "./native-background.js";
9
9
  import { isSessionNotFoundError } from "@opencode/client";
10
10
  import { completionReportModelMessages } from "./receipt-presentation.js";
11
+ import { modelLiveState } from "./model-live-state.js";
11
12
  // Capture once when this module evaluates. A later package replacement must not make an old
12
13
  // process report the replacement's bytes as its loaded adapter.
13
14
  const loadedAdapter = Object.freeze({ adapter_url: import.meta.url,
@@ -16,10 +17,11 @@ const loadedAdapter = Object.freeze({ adapter_url: import.meta.url,
16
17
  // Named module snapshots, not a claim to identify every transitive dependency. Retain these
17
18
  // bytes across package replacement so an unchanged adapter cannot conceal an old implementation.
18
19
  implementation_sha256: Object.freeze(Object.fromEntries([
19
- "index", "profiled", "gate", "protected-snapshot", "declared-artifacts", "native-contract-read", "native-background",
20
+ "index", "profiled", "gate", "protected-snapshot", "declared-artifacts", "native-contract-read", "native-background", "model-live-state",
20
21
  ].map(name => [`${name}.js`, createHash("sha256").update(readFileSync(new URL(`./${name}.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`, import.meta.url))).digest("hex")]))),
21
22
  loaded_at: new Date().toISOString(), pid: process.pid });
22
23
  const childSelectionKey = (id) => `v2-child-model-selection:${id}`;
24
+ const liveStateKey = (id) => `v2-model-live-state:${id}`;
23
25
  function record(value) {
24
26
  return value !== null && typeof value === "object" && !Array.isArray(value);
25
27
  }
@@ -330,6 +332,13 @@ export function createV2ReturnReportFinalizer(context, hooks) {
330
332
  };
331
333
  }
332
334
  const correctionAgent = (agent) => typeof agent === "string" && /^dog-reviewer-correction-v010-[a-f0-9]{16}$/u.test(agent);
335
+ const reviewerCorrectionOnlyTools = new Set(["edit", "write", "patch", "shell", "sortie_v010_bind_write_gate",
336
+ "sortie_v010_release_write_gate", "sortie_v010_operator_status", "sortie_v010_finish_direct_unit"]);
337
+ // Order only the genuine, already-filtered native definitions. Initial read-only
338
+ // tools remain a deterministic prefix when correction admits additional tools;
339
+ // object identities, schemas, executors and native authority are unchanged.
340
+ const reviewerModelToolOrder = (tools) => Object.fromEntries(Object.entries(tools).sort(([left], [right]) => Number(reviewerCorrectionOnlyTools.has(left)) - Number(reviewerCorrectionOnlyTools.has(right)) ||
341
+ (left < right ? -1 : left > right ? 1 : 0)));
333
342
  const correctionAgentID = (sessionID) => `dog-reviewer-correction-v010-${createHash("sha256").update(sessionID).digest("hex").slice(0, 16)}`;
334
343
  const correctionStoragePrefix = "v2-reviewer-correction-permissions:";
335
344
  // OpenCode 2.0.18 util/wildcard.ts + permission.evaluate ordering.
@@ -473,7 +482,7 @@ async function syncReviewerCorrectionPermissionsOnce(context, hooks, sessionID,
473
482
  if (info.agent !== selected)
474
483
  await context.session.switchAgent({ sessionID, agent: selected });
475
484
  }
476
- async function registerV2Hooks(context, hooks, background) {
485
+ async function registerV2Hooks(context, hooks, background, liveState) {
477
486
  const explicitlySelectedChildren = new Set();
478
487
  const selectedChildModels = new Set();
479
488
  const selectionKey = (parent, role, prompt) => `${parent}\0${role}\0${prompt}`;
@@ -736,8 +745,6 @@ async function registerV2Hooks(context, hooks, background) {
736
745
  const output = { system: [] };
737
746
  await hooks["experimental.chat.system.transform"]({ sessionID: String(event.sessionID ?? "") }, output);
738
747
  await reconcileCorrectionPermissions(String(event.sessionID));
739
- if (Array.isArray(event.system))
740
- event.system.push(...output.system.map(text => ({ type: "text", text })));
741
748
  if (record(event.tools)) {
742
749
  const visible = {
743
750
  "dog-operator": ["start_mission", "plan_units", "start_direct_unit", "finish_direct_unit", "retry_mission_unit", "operator_next", "operator_status", "expand_unit", "review_mission", "repair_review", "complete_mission", "cancel_operator", "reflection"],
@@ -759,10 +766,28 @@ async function registerV2Hooks(context, hooks, background) {
759
766
  if (!allowed || !key.startsWith("sortie_v010_") || !allowed.includes(key.slice("sortie_v010_".length)))
760
767
  delete event.tools[key];
761
768
  }
762
- if ((event.agent === "dog-reviewer-v010" || correctionAgent(event.agent)) && Array.isArray(event.system)) {
763
- event.system.push({ type: "text", text: `Native tools actually available in this request: ${Object.keys(event.tools).sort().join(", ")}. Use these tools directly; do not infer missing tools or ask the root to transcribe source.` });
769
+ if (event.agent === "dog-reviewer-v010" || correctionAgent(event.agent)) {
770
+ const tools = reviewerModelToolOrder(event.tools);
771
+ event.tools = tools;
772
+ output.system.push(`Native tools actually available in this request: ${Object.keys(tools).sort().join(", ")}. Use these tools directly; do not infer missing tools or ask the root to transcribe source.`);
764
773
  }
765
774
  }
775
+ const id = String(event.sessionID ?? "");
776
+ const previous = liveState.get(id) ?? await context.storage?.get(liveStateKey(id)).catch(() => {
777
+ console.warn("[sortie-dogs-v010] Live-state projection restore unavailable; rebuilding current host state");
778
+ return undefined;
779
+ });
780
+ const projection = modelLiveState(output.system, Array.isArray(event.messages) ? event.messages : undefined, previous);
781
+ if (projection.history) {
782
+ liveState.set(id, projection.history);
783
+ await context.storage?.set(liveStateKey(id), projection.history).catch(() => {
784
+ console.warn("[sortie-dogs-v010] Live-state projection persistence unavailable; current instance retains continuity");
785
+ });
786
+ }
787
+ if (Array.isArray(event.system))
788
+ event.system.push(...projection.system.map(text => ({ type: "text", text })));
789
+ if (projection.messages)
790
+ event.messages = projection.messages;
766
791
  });
767
792
  if (hooks["experimental.session.compacting"])
768
793
  await context.session.hook("compaction", async (event) => {
@@ -796,6 +821,7 @@ export function createSortieDogsV2Plugin(legacyFactory = SortieDogsV010Plugin) {
796
821
  id: "sortie-dogs.v010",
797
822
  async setup(context) {
798
823
  let hooks;
824
+ const liveState = new Map();
799
825
  const background = new NativeBackgroundLifecycle(context, async (dispatch, text) => {
800
826
  if (dispatch.owner)
801
827
  hooks.backgroundOwner?.(dispatch.callID, dispatch.owner);
@@ -808,7 +834,7 @@ export function createSortieDogsV2Plugin(legacyFactory = SortieDogsV010Plugin) {
808
834
  hooks = await legacyFactory({ directory: context.location.directory, client: legacyClient(context),
809
835
  nativeBackground: background, returnReportTransport: "tool-result",
810
836
  reviewerCorrectionPermissions: !!context.agent && !!context.storage && !!context.session.switchAgent }, context.options ?? {});
811
- await registerV2Hooks(context, hooks, background);
837
+ await registerV2Hooks(context, hooks, background, liveState);
812
838
  // Recreate exact durable correction profiles at plugin setup, before resumed native Steps
813
839
  // resolve their agent/tool snapshots. Storage.scan is part of Plugin 2.0.18's public domain.
814
840
  if (context.storage?.scan) {
@@ -836,6 +862,8 @@ export function createSortieDogsV2Plugin(legacyFactory = SortieDogsV010Plugin) {
836
862
  await background.event(event);
837
863
  if (event.type === "session.deleted") {
838
864
  await context.storage?.remove?.(childSelectionKey(id));
865
+ liveState.delete(id);
866
+ await context.storage?.remove?.(liveStateKey(id));
839
867
  // This native event is definite deletion, unlike a transient session.get failure.
840
868
  await disposeCorrectionPermissions(context, id);
841
869
  }
@@ -6,6 +6,8 @@ export declare const MISSION_GIT_SCOPE = "Requested git add <paths> (with option
6
6
  export declare const VALIDATION_WORKFLOW = "## Time-aware validation workflow\n\nOperator/Coordinator: put the known meaningful formal check from the user, project or task context in validation,\nnot another objective copy. A literal command in the original request is not required; empty or dummy checks do not qualify.\nKeep any user/project-required broad validation for the final integrated candidate,\nnot every implementation unit. Do not add a full suite merely as a precaution or create a testing agent.\nWorker: reproduce, batch the related edits, inspect the complete diff, then run the focused checks.\nFix failures with the smallest exercising check before starting a costly suite. Do not execute the entire\nformal validation list after each patch; run it in declared order when the unit candidate is stable.\nIf broad validation is already declared, defer it until focused checks pass and known edits are finished;\ndo not remove it, substitute a tiny check for it, or claim an unrun requirement passed.\nAfter a late fix, select checks for the affected inputs; repeat broad validation when the requested\ncontract or host evidence freshness requires it, not solely because any file or Worker changed.\nDocumentation-only changes do not automatically invalidate unrelated runtime/test results; retain the\nchecked candidate and show why reuse is valid under project and host rules. Never promote stale proof.\nBefore a costly check, state its purpose and known duration (unknown if unmeasured). In the concise return,\nreport command, scope, actual exit/elapsed time and any rerun reason; never invent savings or timings.\nReview evidence gaps need original excerpts, not another patch or full test. Keep autonomous correction,\nrequired review, accepted criteria and cumulative budget intact; this workflow adds no approval or denial.\n";
7
7
  /** Worker-only projection; planning/review authorities retain the complete shared workflow. */
8
8
  export declare const WORKER_VALIDATION_WORKFLOW = "Batch edits, reconcile requirements/diff, run focused tests, then every registered formal check exactly in order:\nseparate foreground native shell calls, no extra tee, redirect or wrapper. Diagnostics are not formal evidence.\nRerun affected checks and required broad checks when contract/freshness requires; never drop required validation or claim stale/unrun proof.\n";
9
+ /** Correction uses the inherited command identities; diagnostics do not acquire formal evidence. */
10
+ export declare const REVIEWER_VALIDATION_WORKFLOW = "Run inherited formal commands in order as exact separate foreground native shell calls.\nRun formatting and diagnostics separately; do not append undeclared shell commands, tee, redirect or wrapper.";
9
11
  export declare function missionOperatorContent(profile: RuntimeProfile, version: string): string;
10
12
  /** Shared by the installed Reviewer and its host-generated mission prompt. */
11
13
  export declare const MISSION_BEHAVIOR_REVIEW = "Establish the test oracle independently of the patch: tests added with the implementation are claims to\nreview, not established API behavior. For a changed failure path, compare a pre-existing analogous public\ntest and its shared assertion helper, including default expected outputs, with the new case. A new test\nthat asserts the implementation's current result can encode the defect rather than catch it. Preserve\nthe pre-change contract unless the request changes it; do not invent a universal failure-result convention.\nTrace a concrete rejected input through the changed code back to the public caller, checking result,\nerror and observable state together, including earlier results that can survive failure. Missing assertions\nalone are not defects; a concrete contradiction with the established contract is a finding even if tests pass.\nDuring correction, reuse the established public test harness where applicable and exercise the smallest\nregression exposing the defect before the fix and passing afterward. Do not change expected values or\nhelper defaults merely to agree with the implementation; derive them from the original contract.\nPrefer existing suites/subtests for regressions. New test entry/helper names should be distinctive\nand compose with other same-package test files, not create generic package-level collisions.\n\nFor a requested per-call mode/option, trace where the setting is chosen and where changed code uses\nretained objects or closures. Distinguish the current caller's setting from a captured creation-time\nsetting; use the requested contract to decide which governs. If actual source crosses that boundary\nand existing tests do not cover it, exercise its smallest public case, including the disabled behavior.\nDo not invent a lifecycle matrix or infer that an opt-in feature is correct from only its enabled path.\n\nFor a changed failure handler, inspect the operation it calls and the public inputs reaching it,\nincluding failures not listed in the new handler. Use the supplied source/tests and established API behavior\nto identify a concrete input that could still violate the requested contract. A passing normal input does\nnot settle a different failure outcome of that same operation. A demonstrable material defect is FINDINGS; ask for\nan excerpt or result only when a specific material outcome cannot be settled. Do not invent new behavior,\nrequire an exhaustive exception inventory, or recommend catching every exception.\n\nReport FINDINGS only for concrete major or medium defects with a material impact on the original requirements,\npublic behavior, correctness or required validation. Name the consequence and smallest necessary fix.\nDo not turn minor style, wording, optional improvements or speculative edge cases into FINDINGS or EVIDENCE_GAPS.\nUse read/search to settle missing context in the current review. If a consequential uncertainty remains,\nrecord EVIDENCE_GAPS as an advisory limitation, not a demand for another evidence packet. Missing narration,\nrequirement mappings, hashes or clipped excerpts alone never justify a blocking finding.\n\nAssess behavioral requirements against actual source, tests and observed results. A required check known to\nhave failed or not run is a concrete finding; missing documentation of a check is not proof it did not run.\nFor incidental workflow constraints such as cache settings or command-path spelling, use existing host observations;\nabsence of a separate settings dump or historical log is not itself an evidence gap. Flag observed material\ncontradictions. The Operator owns final comparison with the original request. If a missing check genuinely\naffects correctness or a requested deliverable, name that consequence and the smallest useful next check.\nOn verification, compare the correction and its affected public outcomes with the original contract, not\nonly the wording of previous findings. Reuse unchanged evidence; do not expand into optional improvements\nor an exhaustive input matrix. PASS means no material finding, not exhaustive proof of every path.";
@@ -34,6 +34,9 @@ export const WORKER_VALIDATION_WORKFLOW = `Batch edits, reconcile requirements/d
34
34
  separate foreground native shell calls, no extra tee, redirect or wrapper. Diagnostics are not formal evidence.
35
35
  Rerun affected checks and required broad checks when contract/freshness requires; never drop required validation or claim stale/unrun proof.
36
36
  `;
37
+ /** Correction uses the inherited command identities; diagnostics do not acquire formal evidence. */
38
+ export const REVIEWER_VALIDATION_WORKFLOW = `Run inherited formal commands in order as exact separate foreground native shell calls.
39
+ Run formatting and diagnostics separately; do not append undeclared shell commands, tee, redirect or wrapper.`;
37
40
  const OPERATION_GUIDE = `## Practical operation guide
38
41
 
39
42
  Use file paths for exact outputs and dir/** for a directory tree, including a directory that does not
@@ -159,8 +162,8 @@ quality threshold and explicit model/budget choice. Follow AGENTS.md and use the
159
162
  For a Coordinator ready candidate, use operator_status's acceptance_summary: verbatim original
160
163
  requests, anchored cumulative formal validation, current review disposition (author self-recheck
161
164
  is not independent approval) and recorded delivery state.
162
- Historical results are references, not current freshness PASS. Inspect source or evidence only for
163
- concrete unresolved gaps; do not routinely search run archives or reread every source/test file.
165
+ If current evidence covers the request and no concrete gap remains, proceed to acceptance.
166
+ Historical results are not current freshness PASS; read source or artifacts only to resolve a gap.
164
167
  Existing validation freshness and Review guards still apply; this summary does not accept the mission.
165
168
  One compact status check can lead directly to review_mission and its returned Reviewer Task; original
166
169
  requests, diff and checks are supplied automatically. Add traces only for concrete extra information.
@@ -310,7 +313,8 @@ binds the existing scope/checks and lets you correct HERE, without another promp
310
313
  round trip. Reuse the known project test harness and the reasoning/source already in this conversation.
311
314
  If correction exposes another concrete Major/Medium defect, retain it through ${profile.toolPrefix}repair_review
312
315
  here before fixing it; the host accumulates known findings without another Task, scope or check contract.
313
- Run inherited formal commands in order, retain the requested commit/clean boundary, then
316
+ ${REVIEWER_VALIDATION_WORKFLOW}
317
+ After current formal checks, retain the requested commit/clean boundary, then
314
318
  ${profile.toolPrefix}finish_direct_unit. Explicitly self-recheck and end this same native Task with
315
319
  SELF_RECHECKED; only its actual successful terminal binds the current validated source. This is author
316
320
  self-recheck, never independent approval of your own edits. No unresolved Medium may pass.
@@ -525,31 +529,30 @@ mode: subagent
525
529
  ---
526
530
  # ${profileAgent(profile, "dog-worker")}
527
531
 
528
- Implement, test and requested commit in this Task. Follow AGENTS.md.
529
- Read handoff_path in full first: task.objective, verbatim original_requests/unit_instruction, mission-context constraints,
530
- unit-coverage indices. Preserve user scope and ordering; prove assigned criteria, not Mission completion.
531
- Host ready: implement. Denied: reason/remedy. No routine manifest/goal/status/bind.
532
- Legacy/recovery: ${profile.toolPrefix}bind_write_gate with exact project_root and manifest_path=operation_manifest.
532
+ Implement, test and requested commit in this Task.
533
+ Reuse supplied AGENTS.md; for gaps prefer exact ancestor files/affected subtrees over parent globs.
534
+ Read handoff_path in full first: task.objective, verbatim original_requests/unit_instruction, Mission constraints,
535
+ coverage indices. Preserve user scope and ordering; prove assigned criteria, not Mission completion.
536
+ Ready: implement. Denied: reason/remedy. No routine manifest/goal/status/bind.
537
+ Recovery: ${profile.toolPrefix}bind_write_gate with exact project_root and manifest_path=operation_manifest.
533
538
  Treat cwd/project_root and paths as opaque; never shorten or normalize segments.
534
- After compaction recover handoff; inspect diff/results before repeating work.
535
-
536
- Read/search: existing permissions. Fix unit.write estimates/outputs/checks
539
+ After compaction recover handoff; inspect diff/results before repeats.
540
+ Read/search: existing permissions. Fix unit.write/outputs/checks
537
541
  here via ${profile.toolPrefix}expand_unit/contract updates and host repair diagnostics;
538
- no extra approval, restart or delegation. Keep prohibitions, host Git lifecycle and cumulative budget.
542
+ no extra approval, restart or delegation. Keep prohibitions, host Git lifecycle, cumulative budget.
539
543
  Requested add/commit needs source paths, not .git/** scope.
540
-
541
544
  Use pre-change test helpers as oracles, not new implementation/tests. Check
542
545
  result/error/state together, without a hypothetical exhaustive matrix. Keep reproduction entrypoint/input/layout; rerun or report why unverified.
543
546
  Missing tooling: one documented bounded setup in ${TOOL_ENVIRONMENT}/; reuse, never delete.
544
547
  Per-call modes: check retained creation/use for captured vs current settings.
545
548
  Tests: existing suites/subtests or distinctive regression entry names.
546
549
  ${WORKER_VALIDATION_WORKFLOW}
547
- Done: behavior, ordered checks/commit; never fabricate completion.
550
+ Done: behavior/ordered checks/commit; never fabricate completion.
548
551
  Parent: independent Review after return, not before execution; no review-before-commit gate.
549
- Do not spawn nested subagents, amend, push or publish. Return changes, command/exit/elapsed,
550
- rerun reasons, unresolved/untested behavior; no proof document.
551
- No unchanged denial retries. Only unrecoverable PROCESS_DEFECT: local: plus diagnostic or proven
552
- TRUE_BLOCKER: external: / TRUE_BLOCKER: user-decision: returns early.
553
- Use the user's latest instruction language (previous if unclear); keep protocol/code/quotes verbatim.
552
+ Do not spawn nested subagents, amend, push/publish. Return changes, command/exit/elapsed,
553
+ rerun reasons, unresolved/untested behavior; no proof doc.
554
+ No unchanged denial retry. Early: unrecoverable PROCESS_DEFECT: local: + diagnostic or proven
555
+ TRUE_BLOCKER: external: / TRUE_BLOCKER: user-decision:.
556
+ Prose: user's latest instruction language (previous if unclear); protocol/code/quotes verbatim.
554
557
  `;
555
558
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.13.4",
3
+ "version": "0.13.6",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",