sortie-dogs 0.13.12 → 0.13.13

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
@@ -19,8 +19,8 @@ evidence-backed result, not more agents for their own sake.
19
19
  [![Node.js](https://img.shields.io/node/v/sortie-dogs)](https://www.npmjs.com/package/sortie-dogs)
20
20
  [![MIT License](https://img.shields.io/npm/l/sortie-dogs)](LICENSE)
21
21
 
22
- **Current release: [v0.13.12](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.12)**
23
- · [Release notes](docs/release-v0.13.12.md)
22
+ **Current release: [v0.13.13](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.13)**
23
+ · [Release notes](docs/release-v0.13.13.md)
24
24
 
25
25
  [Quick start](#quick-start) · [Workflow](#mission-workflow) · [Models](#default-models) ·
26
26
  [Measured results](#measured-results) · [Configuration](#configuration) · [Documentation](#documentation)
@@ -57,7 +57,7 @@ Use an existing Codex CLI signed in with **ChatGPT**. On Windows, PowerShell **7
57
57
  available; POSIX uses bash. Sortie does not install Codex, start login or use a metered API fallback.
58
58
 
59
59
  ```sh
60
- npm install --save-dev sortie-dogs@0.13.12
60
+ npm install --save-dev sortie-dogs@0.13.13
61
61
  npx --no-install sortie-dogs codex init .
62
62
  ```
63
63
 
@@ -86,7 +86,7 @@ bridge. [Codex guide: SDK, permissions, progress, resume and manifest route](doc
86
86
  Use **OpenCode V2**. In the target project:
87
87
 
88
88
  ```sh
89
- npm install --save-dev sortie-dogs@0.13.12
89
+ npm install --save-dev sortie-dogs@0.13.13
90
90
  npx --no-install sortie-dogs init .
91
91
  ```
92
92
 
@@ -188,7 +188,7 @@ known expense. Official local evaluation and leaderboard acceptance are separate
188
188
  **8/23 (34.8%)**, versus v0.12.25's 7/23; all seven prior resolutions retained, no empty patches or
189
189
  official evaluation errors. [Fixed conditions, stopped-run accounting and costs](docs/benchmarks/swebench-lite-history.md#v0131-dev23-2026-09-30).
190
190
 
191
- All scores belong to their fixed candidates, **not v0.13.12**. SWE-bench remains an optional
191
+ All scores belong to their fixed candidates, **not v0.13.13**. SWE-bench remains an optional
192
192
  measurement, not a release gate. Different budgets, models and conditions are not controlled comparisons.
193
193
 
194
194
  ## Configuration
@@ -211,7 +211,7 @@ Install the desired package version and rerun **your host's** initializer:
211
211
 
212
212
  An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
213
213
  update it. [Global/update instructions](docs/configuration.md#global-availability).
214
- The current Mission asset marker is `0.13.12-validation-paths-v1`; the unchanged Codex skill marker is
214
+ The current Mission asset marker is `0.13.13-mission-context-v1`; the unchanged Codex skill marker is
215
215
  `0.13.11-codex-skill-v2`. Installed markers alone do not prove a running host reloaded the new version.
216
216
 
217
217
  There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
@@ -227,7 +227,7 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
227
227
  [SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
228
228
  - **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
229
229
  [Windows tests](docs/windows-tests.md) · [Release routine](docs/release-batch.md).
230
- - **Releases:** [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
230
+ - **Releases:** [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
231
231
  [v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
232
232
 
233
233
  ## Community
@@ -3,6 +3,6 @@
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.12-validation-paths-v1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.13.13-mission-context-v1";
7
7
  export declare const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
8
8
  export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION | typeof CODEX_SKILL_ASSET_VERSION;
@@ -3,5 +3,5 @@
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.12-validation-paths-v1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.13.13-mission-context-v1";
7
7
  export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
@@ -0,0 +1,73 @@
1
+ export interface PublicMissionContext {
2
+ id: string;
3
+ role: "user" | "assistant";
4
+ text: string;
5
+ tools: {
6
+ call_id?: string;
7
+ tool: string;
8
+ status?: string;
9
+ input?: unknown;
10
+ output?: string;
11
+ error?: string;
12
+ exit?: number;
13
+ time?: unknown;
14
+ }[];
15
+ }
16
+ export interface MissionContextSource {
17
+ session_id: string;
18
+ path: string;
19
+ sha256: string;
20
+ entries: number;
21
+ observed_at: string;
22
+ history_available: boolean;
23
+ }
24
+ export interface MissionContinuity {
25
+ sources: MissionContextSource[];
26
+ notes?: {
27
+ text: string;
28
+ session_id: string;
29
+ recorded_at: string;
30
+ }[];
31
+ }
32
+ /** Public task data only. Keep exact text/results in a lazy snapshot, not private reasoning. */
33
+ export declare function publicMissionContext(messages: readonly Record<string, unknown>[]): PublicMissionContext[];
34
+ /** Preserve earlier available entries when the host later returns a compacted/partial history. */
35
+ export declare function retainMissionContext(directory: string, sessionID: string, messages: readonly Record<string, unknown>[], previous?: MissionContextSource): Promise<{
36
+ source: MissionContextSource;
37
+ entries: PublicMissionContext[];
38
+ }>;
39
+ /** Small inline view, with explicit pointers to every omitted byte in the exact snapshot. */
40
+ export declare function missionContextView(source: MissionContextSource, entries: readonly PublicMissionContext[], suppliedRequestIDs?: readonly string[]): {
41
+ omitted_text_entries: number;
42
+ conversation: {
43
+ truncated?: boolean | undefined;
44
+ original_characters?: number | undefined;
45
+ id: string;
46
+ role: "user" | "assistant";
47
+ text: string;
48
+ }[];
49
+ omitted_tool_entries: number;
50
+ recent_tools: {
51
+ input: string | undefined;
52
+ output: string | undefined;
53
+ error: string | undefined;
54
+ truncated: boolean;
55
+ detail_ref: {
56
+ path: string;
57
+ message_id: string;
58
+ call_id: string | undefined;
59
+ };
60
+ call_id?: string;
61
+ tool: string;
62
+ status?: string;
63
+ exit?: number;
64
+ time?: unknown;
65
+ message_id: string;
66
+ }[];
67
+ session_id: string;
68
+ path: string;
69
+ sha256: string;
70
+ entries: number;
71
+ observed_at: string;
72
+ history_available: boolean;
73
+ };
@@ -0,0 +1,66 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
5
+ /** Public task data only. Keep exact text/results in a lazy snapshot, not private reasoning. */
6
+ export function publicMissionContext(messages) {
7
+ return messages.flatMap(message => {
8
+ const info = record(message.info) ? message.info : message;
9
+ if ((info.role !== "user" && info.role !== "assistant") || typeof info.id !== "string" || !Array.isArray(message.parts))
10
+ return [];
11
+ const parts = message.parts.filter(record);
12
+ const text = parts.filter(part => part.type === "text" && part.synthetic !== true && typeof part.text === "string")
13
+ .map(part => part.text).join("\n");
14
+ const tools = parts.flatMap(part => {
15
+ if (part.type !== "tool" || typeof part.tool !== "string" || !record(part.state))
16
+ return [];
17
+ const state = part.state, metadata = record(state.metadata) ? state.metadata : {};
18
+ return [{ tool: part.tool, ...(typeof part.callID === "string" ? { call_id: part.callID } : {}),
19
+ ...(typeof state.status === "string" ? { status: state.status } : {}),
20
+ ...(state.input !== undefined ? { input: state.input } : {}),
21
+ ...(typeof state.output === "string" ? { output: state.output } : {}),
22
+ ...(typeof state.error === "string" ? { error: state.error } : {}),
23
+ ...(typeof metadata.exit === "number" ? { exit: metadata.exit } : {}),
24
+ ...(part.time !== undefined ? { time: part.time } : {}) }];
25
+ });
26
+ return text.trim() || tools.length ? [{ id: info.id, role: info.role, text, tools }] : [];
27
+ });
28
+ }
29
+ /** Preserve earlier available entries when the host later returns a compacted/partial history. */
30
+ export async function retainMissionContext(directory, sessionID, messages, previous) {
31
+ const current = publicMissionContext(messages);
32
+ const retained = previous ? await readFile(previous.path, "utf8")
33
+ .then(content => JSON.parse(content).entries).catch(() => undefined) : undefined;
34
+ const merged = new Map((retained ?? []).map(entry => [entry.id, entry]));
35
+ for (const entry of current)
36
+ merged.set(entry.id, entry);
37
+ const entries = [...merged.values()];
38
+ const content = JSON.stringify({ session_id: sessionID, entries }, null, 2) + "\n";
39
+ const sha256 = createHash("sha256").update(content).digest("hex");
40
+ const path = join(directory, `context-${sha256}.json`);
41
+ if (!retained || previous?.sha256 !== sha256) {
42
+ await mkdir(directory, { recursive: true });
43
+ await writeFile(path, content);
44
+ }
45
+ return { source: { session_id: sessionID, path, sha256, entries: entries.length,
46
+ observed_at: new Date().toISOString(), history_available: current.length > 0 }, entries };
47
+ }
48
+ /** Small inline view, with explicit pointers to every omitted byte in the exact snapshot. */
49
+ export function missionContextView(source, entries, suppliedRequestIDs = []) {
50
+ const textEntries = entries.filter(entry => entry.text.trim() && !suppliedRequestIDs.includes(entry.id));
51
+ const selected = textEntries.slice(-8);
52
+ const tools = entries.flatMap(entry => entry.tools.map(tool => ({ message_id: entry.id, ...tool })));
53
+ const recentTools = tools.filter(tool => !tool.tool.startsWith("sortie_") && ["completed", "error"].includes(tool.status ?? "")).slice(-8);
54
+ return { ...source, omitted_text_entries: textEntries.length - selected.length,
55
+ conversation: selected.map(entry => ({ id: entry.id, role: entry.role, text: entry.text.slice(0, 4000),
56
+ ...(entry.text.length > 4000 ? { truncated: true, original_characters: entry.text.length } : {}) })),
57
+ omitted_tool_entries: tools.length - recentTools.length,
58
+ recent_tools: recentTools
59
+ .map(tool => {
60
+ const input = tool.input === undefined ? undefined : JSON.stringify(tool.input);
61
+ return { ...tool, input: input?.slice(0, 1000), output: tool.output?.slice(0, 1000), error: tool.error?.slice(0, 1000),
62
+ truncated: [input, tool.output, tool.error].some(value => value !== undefined && value.length > 1000),
63
+ detail_ref: { path: source.path, message_id: tool.message_id, call_id: tool.call_id } };
64
+ }),
65
+ };
66
+ }
@@ -1,5 +1,6 @@
1
1
  import { type OperatorPlan, type OperatorState, type OperatorTask, type OperatorRuntime } from "./operator-runtime.js";
2
2
  import { type RuntimeProfile } from "./runtime-profile.js";
3
+ import { type MissionContinuity } from "./mission-context.js";
3
4
  export { missionValidationCommand } from "./mission-validation-command.js";
4
5
  export declare const MISSION_REFERENCE = "SORTIE_MISSION_REF ";
5
6
  export declare const MISSION_REVIEW_REFERENCE = "SORTIE_MISSION_REVIEW_REF ";
@@ -192,6 +193,8 @@ export interface OperatorMission {
192
193
  requests: MissionRequest[];
193
194
  /** Prior public conversation context, not additional immutable requirements. */
194
195
  context?: MissionContext[];
196
+ /** Shared public context/working notes; observations, never extra requirements or acceptance. */
197
+ continuity?: MissionContinuity;
195
198
  /** Explicit continue deliveries, keyed by the original real turn. */
196
199
  steering?: {
197
200
  requestID: string;
@@ -321,6 +324,10 @@ export declare class OperatorMissionRuntime {
321
324
  constructor(projectRoot: string, profile: RuntimeProfile);
322
325
  private file;
323
326
  correctionReference(root: string, reviewIdentity: string): string;
327
+ /** Stage context for a proposed handoff; publish Mission metadata only after plan preparation succeeds. */
328
+ contextSnapshot(state: OperatorMission, sessionID: string, messages: readonly Record<string, unknown>[], notes?: string): Promise<OperatorMission>;
329
+ retainContext(root: string, sessionID: string, messages: readonly Record<string, unknown>[], notes?: string): Promise<OperatorMission>;
330
+ handoffContext(state: OperatorMission): Promise<Record<string, unknown>>;
324
331
  private load;
325
332
  /** Recover non-replacement continuations only; an explicit replacement must link the current cancelled run. */
326
333
  private loadMission;
@@ -6,6 +6,7 @@ import { parseOperatorPlan } from "./operator-runtime.js";
6
6
  import { profileAgent } from "./runtime-profile.js";
7
7
  import { normalizeCommand } from "../plugin/gate.js";
8
8
  import { missionValidationCommand } from "./mission-validation-command.js";
9
+ import { retainMissionContext, missionContextView } from "./mission-context.js";
9
10
  export { missionValidationCommand } from "./mission-validation-command.js";
10
11
  const digest = (value) => createHash("sha256").update(value).digest("hex");
11
12
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
@@ -184,6 +185,47 @@ export class OperatorMissionRuntime {
184
185
  correctionReference(root, reviewIdentity) {
185
186
  return JSON.stringify({ path: this.file(root), field: "corrections[]", review_identity: reviewIdentity });
186
187
  }
188
+ /** Stage context for a proposed handoff; publish Mission metadata only after plan preparation succeeds. */
189
+ async contextSnapshot(state, sessionID, messages, notes) {
190
+ const next = structuredClone(state);
191
+ const previous = next.continuity?.sources.find(source => source.session_id === sessionID);
192
+ const snapshot = await retainMissionContext(join(this.projectRoot, this.profile.stateDirectory, "missions"), sessionID, messages, previous);
193
+ next.continuity ??= { sources: [] };
194
+ next.continuity.sources = [...next.continuity.sources.filter(source => source.session_id !== sessionID), snapshot.source];
195
+ if (typeof notes === "string" && notes.trim())
196
+ next.continuity.notes = [
197
+ ...(next.continuity.notes ?? []).filter(note => note.session_id !== sessionID),
198
+ { text: notes, session_id: sessionID, recorded_at: new Date().toISOString() }
199
+ ];
200
+ return next;
201
+ }
202
+ async retainContext(root, sessionID, messages, notes) {
203
+ return this.serial(root, async () => {
204
+ const state = await this.loadMission(root);
205
+ if (!state)
206
+ throw new Error("mission-missing");
207
+ const next = await this.contextSnapshot(state, sessionID, messages, notes);
208
+ await this.save(this.file(root), next);
209
+ return next;
210
+ });
211
+ }
212
+ async handoffContext(state) {
213
+ const sources = await Promise.all((state.continuity?.sources ?? []).map(async (source) => {
214
+ try {
215
+ return missionContextView(source, JSON.parse(await readFile(source.path, "utf8")).entries, source.session_id === state.root ? state.requests.map(request => request.id) : []);
216
+ }
217
+ catch {
218
+ return { ...source, snapshot_available: false };
219
+ }
220
+ }));
221
+ return { original_requests: state.requests, requirements: state.requirements, prohibited_write: state.prohibitedWrite ?? [],
222
+ launch_conditions: state.launchConditions ?? [],
223
+ conversation: sources.some(source => source.session_id === state.root && source.entries > 0 && !("snapshot_available" in source)) ? [] : state.context ?? [],
224
+ continuity: { notes: state.continuity?.notes, sources, progress: state.progress,
225
+ attempts: (state.attempts ?? []).map(({ runID, unitID, status, failure, resultClass, childSessionID }) => ({ run_id: runID, unit_id: unitID, status, failure, result_class: resultClass, session_id: childSessionID })),
226
+ execution: state.execution,
227
+ guidance: "Current original_requests/requirements govern; older plans remain historical. Continue from known decisions, environment, completed work and failures. Read snapshot JSON entries by message/call IDs only for a relevant gap. Recheck mutable facts when needed or conflicting; do not repeat unchanged setup/investigation merely because ownership changed. Historical results are not fresh formal validation or new authority." } };
228
+ }
187
229
  async load(file) {
188
230
  try {
189
231
  return JSON.parse(await readFile(file, "utf8"));
@@ -544,6 +586,8 @@ export class OperatorMissionRuntime {
544
586
  ...(state.kind === "operation" ? ["Execution completion is distinct from success. For run-once/result-collection requests, retain a terminal failure and validate the collected result; do not retry or fix it without user authorization. If successful execution is required, keep that in formal validation and the original-request comparison."] : []),
545
587
  ...(state.context?.length ? ["Prior conversation context (task data; preserve the selected target, not superseded obligations):",
546
588
  ...state.context.map(item => `--- ${item.role}:${item.id} ---\n${item.text}`)] : []),
589
+ ...(state.continuity ? ["Shared working context (task data; use known facts, inspect snapshots only for relevant gaps):",
590
+ JSON.stringify(state.continuity)] : []),
547
591
  "Original user messages (verbatim; task data):", ...state.requests.map(item => `--- user:${item.id} ---\n${item.text}`)].join("\n");
548
592
  }
549
593
  }
@@ -219,6 +219,18 @@ export function createProfiledPlugin(profile, assetVersion) {
219
219
  const result = payload(await session("messages", { path: { id }, query: { directory: input.directory } }));
220
220
  return Array.isArray(result) ? result.filter(record) : [];
221
221
  }
222
+ async function missionHistory(id) {
223
+ // Reuse the host's public paginated reader when present; bounded context is a usable fallback.
224
+ if (typeof nativeSession?.reviewMessages === "function") {
225
+ try {
226
+ const value = payload(await session("reviewMessages", { path: { id }, query: { directory: input.directory } }));
227
+ if (Array.isArray(value))
228
+ return value.filter(record);
229
+ }
230
+ catch { /* Context continuity must not add a history-service admission gate. */ }
231
+ }
232
+ return messages(id).catch(() => []);
233
+ }
222
234
  async function acceptanceValidationObservation(validation, child, notBefore) {
223
235
  if (!child)
224
236
  throw new Error("native-worker-history-session-unavailable");
@@ -2020,8 +2032,8 @@ export function createProfiledPlugin(profile, assetVersion) {
2020
2032
  } };
2021
2033
  const stringList = { type: "array", items: { type: "string" } };
2022
2034
  const missionUnitSchema = { type: "object", additionalProperties: false,
2023
- properties: { title: { type: "string" }, objective: { type: "string", description: "Target or corrective delta, about 2000 characters. Do not copy the original request. Longer authored instructions are preserved separately without a retry gate." },
2024
- read: { ...stringList, description: "Inputs that affect validation, not an allowlist for observation. Do not include whole live session/database/log trees just to inspect them." }, write: stringList,
2035
+ properties: { title: { type: "string" }, objective: { type: "string", description: "Target or corrective delta, about 2000 characters. Do not copy the original request. Carry the remaining action, not a routine reconfirmation of supplied facts, setup or instructions. References are available for relevant gaps, not mandatory rereads. Longer authored instructions are preserved separately without a retry gate." },
2036
+ read: { ...stringList, description: "Known inputs that affect validation, not an allowlist or a checklist to reread. Avoid guessed AGENTS.md paths and whole live session/database/log trees just to inspect them; reuse already supplied instructions and known context." }, write: stringList,
2025
2037
  validation: { ...stringList, description: "Meaningful commands using observed repository test paths and runner conventions, not guessed filenames. Reuse already discovered recipes; when unknown, locate the relevant test or repository script with ordinary read/search before declaring it. This is planning guidance, not new approval or proof of execution; native checks still establish acceptance." },
2026
2038
  validation_cwd: { type: "object", additionalProperties: { type: "string" }, description: "Exact cwd per declared command; omitted commands use project_root. Registration-only correction of your active direct unit keeps the same admission and budget and requires new native checks, not historical proof." },
2027
2039
  requirement_ids: { ...stringList, description: "Related requirement IDs. A single unit inherits all requirements when omitted; specify coverage when splitting work across units." } },
@@ -2043,10 +2055,13 @@ export function createProfiledPlugin(profile, assetVersion) {
2043
2055
  const missionExecutionSchema = { type: "object", properties: { commands: stringList, directory: { type: "string" } },
2044
2056
  required: ["commands", "directory"], additionalProperties: false,
2045
2057
  description: "For an operation: the actual run/grade commands, not a preflight or NO_START check. Host observes their native shell completion separately from successful exits; no handwritten proof file is needed. For run-once/result collection, validate the collected result without rerunning the operation.", "x-sortie-optional": true };
2058
+ const handoffContextSchema = { type: "string", "x-sortie-optional": true,
2059
+ description: "Optional current working notes: intent/rationale, known environment and paths, completed setup/checks with results, failed approaches and remaining work. Reuse known facts with source references; no required form or new approval. Public history and Mission observations are also supplied automatically. Omit when already sufficient; do not recopy the original request." };
2046
2060
  tools[startMission] = { description: `Operator: save current requirements; the host retains the original request verbatim. With a meaningful formal check known from user, project or task context and one useful unit, include unit NOW (and execution for an operation) and dispatch its Worker directly; plan_units remains available after start. A literal command in the original request is not required. Worker owns investigation/edit/check/requested commit before independent Review; no routine preparation or commit-only handoff. ${MISSION_GIT_SCOPE} Unknown checks or real unit decomposition use Coordinator. intent=replace follows an actual user requirement change, retaining unchanged constraints and cumulative spend; for mission-source-reconciliation-required use the saved requirements when they reflect that change. intent=new is separate work in another location.`,
2047
2061
  args: { requirements: { ...stringList, minItems: 1, maxItems: 64 },
2048
2062
  unit: { ...missionUnitSchema, description: "When the meaningful check and single-unit scope are already known, include this unit now. Returns its configured Worker directly, combining start_mission and plan_units without another model round trip.", "x-sortie-optional": true },
2049
2063
  execution: missionExecutionSchema,
2064
+ handoff_context: handoffContextSchema,
2050
2065
  confirmed_conditions: conditionsSchema,
2051
2066
  prohibited_write: { ...stringList, description: "Only explicit path prohibitions from the user or applicable instructions. Never infer a parent glob from project/repository names, semantic 'do not modify the product', or the complement of estimated unit.write. Preserve the authorized clone and exact prohibited paths; semantic constraints stay in requirements.", "x-sortie-optional": true },
2052
2067
  kind: { type: "string", enum: ["implementation", "operation"], description: "Use operation for running an existing benchmark, command or procedure. The host records its actual execution separately from setup and checks.", "x-sortie-optional": true },
@@ -2092,11 +2107,13 @@ export function createProfiledPlugin(profile, assetVersion) {
2092
2107
  return JSON.stringify({ ...relocated, budget: await control.currentBudget(context.sessionID) });
2093
2108
  }
2094
2109
  const cancelled = await operators.read(context.sessionID);
2110
+ const history = await missionHistory(context.sessionID);
2095
2111
  let mission = await missions.start(context.sessionID, requirements, args.intent === "replace" || args.intent === "new", {
2096
2112
  kind: args.kind === "operation" ? "operation" : "implementation", executionHost: input.executionHost,
2097
- context: missionConversationContext(await messages(context.sessionID)),
2113
+ context: missionConversationContext(history),
2098
2114
  ...(args.intent === "replace" && cancelled?.phase === "cancelled" ? { cancelledRunID: cancelled.runID } : {}),
2099
2115
  });
2116
+ mission = await missions.retainContext(context.sessionID, context.sessionID, history, args.handoff_context);
2100
2117
  const prohibited = args.prohibited_write;
2101
2118
  if (prohibited !== undefined) {
2102
2119
  if (!Array.isArray(prohibited) || !prohibited.every(path => typeof path === "string"))
@@ -2298,7 +2315,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2298
2315
  const commands = [...new Set(execution.commands.map(missionValidationCommand).map(normalizeCommand))];
2299
2316
  return { commands, directory: resolve(input.directory, execution.directory), observations: operation?.observations ?? [] };
2300
2317
  }
2301
- async function declareMissionUnits(root, actor, mission, raw, reason, execution, planningCallID) {
2318
+ async function declareMissionUnits(root, actor, mission, raw, reason, execution, planningCallID, handoffNotes) {
2302
2319
  await control.currentBudget(root);
2303
2320
  mission = await missions.required(root);
2304
2321
  let previous = await operators.read(root);
@@ -2452,13 +2469,16 @@ export function createProfiledPlugin(profile, assetVersion) {
2452
2469
  },
2453
2470
  }) : [];
2454
2471
  const dispatcher = actor === root ? undefined : { sessionID: actor, callID: mission.callID };
2455
- const context = { original_requests: mission.requests, requirements: mission.requirements, prohibited_write: mission.prohibitedWrite ?? [],
2456
- launch_conditions: mission.launchConditions ?? [] };
2472
+ const histories = await Promise.all([...new Set([root, actor])].map(async (id) => ({ id, messages: await missionHistory(id) })));
2473
+ for (const history of histories)
2474
+ mission = await missions.contextSnapshot(mission, history.id, history.messages, history.id === actor ? handoffNotes : undefined);
2475
+ const context = await missions.handoffContext(mission);
2457
2476
  const state = replanning && previous ? await operators.replanMission(root, previous.runID, plan, dispatcher, context)
2458
2477
  : await operators.prepareMission(root, plan, dispatcher, mission.supersededRunID, terminalChildren, mission.requirementsReplaced, context);
2459
2478
  await control.registerGoalDeclaration(root, state.units[0].task.prompt, true);
2460
2479
  control.enableUnits(root, state.units.filter(unit => unit.status === "pending").length);
2461
2480
  await missions.update(root, item => {
2481
+ item.continuity = mission.continuity;
2462
2482
  item.reviewScope = missionReviewScope(item.reviewScope, ...(previous && item.runID === previous.runID ? [previous] : []), state);
2463
2483
  if (item.runID !== state.runID)
2464
2484
  item.plans++;
@@ -2484,6 +2504,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2484
2504
  tools[planUnits] = { description: `Coordinator or single-unit Fast-lane Operator: declare useful work with estimated read/write scope and meaningful formal checks. executor=self starts implementation/correction and formal validation HERE without a Worker or handoff read; otherwise dispatch the returned configured Worker promptly. Keep investigation/edit/check/requested commit with the same author before independent Review, not a commit-only handoff. ${MISSION_GIT_SCOPE} Native scope reconciliation/expand_unit retain host permissions and explicit path prohibitions. Objective is the target or corrective delta: aim for 2000 characters; the full original request/public reproduction is supplied separately, not copied here. Oversized unit instructions are retained verbatim in handoff Mission context, not rejected for another planning round. Keep every requirement covered. Final validation proves the unit; empty or dummy checks do not qualify. Diagnostics need no registration. write: [] is read-only; dir/** and native absolute paths support actual outputs. reason replans settled work, or ends your own quiescent direct unit without acceptance to correct its declaration, in the same requirements/budget; same-scope failed validation uses retry_mission_unit. Reviewer FINDINGS stay with the same Reviewer for correction, formal validation and self-recheck.`,
2485
2505
  args: { units: { type: "array", minItems: 1, maxItems: 32, items: missionUnitSchema },
2486
2506
  execution: missionExecutionSchema,
2507
+ handoff_context: handoffContextSchema,
2487
2508
  reason: { type: "string", "x-sortie-optional": true },
2488
2509
  executor: { type: "string", enum: ["worker", "self"], description: "Use self to implement/correct and formally validate here without a Worker handoff. Default worker retains configured Worker routing.", "x-sortie-optional": true } }, execute: async (args, context) => {
2489
2510
  const { root, mission } = await missionAuthority(context.sessionID);
@@ -2491,7 +2512,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2491
2512
  throw new Error("mission-executor-invalid");
2492
2513
  try {
2493
2514
  return await serializeDispatchTransition(root, async () => {
2494
- const planned = await declareMissionUnits(root, context.sessionID, mission, args.units, args.reason, args.execution, context.callID);
2515
+ const planned = await declareMissionUnits(root, context.sessionID, mission, args.units, args.reason, args.execution, context.callID, args.handoff_context);
2495
2516
  return args.executor === "self" && record(JSON.parse(planned).task) ? startDirect(root, context.sessionID) : planned;
2496
2517
  });
2497
2518
  }
@@ -2695,7 +2716,7 @@ export function createProfiledPlugin(profile, assetVersion) {
2695
2716
  const baseline = await missionReviewBaseline(input.directory);
2696
2717
  const dispatcher = mission.coordinator ? { sessionID: mission.coordinator, callID: mission.callID } : undefined;
2697
2718
  const prepared = await operators.prepareReviewerCorrection(root, run.runID, correctedPlan, authorID, reviewIdentity, retainedPlan.units[0].write, dispatcher, {
2698
- original_requests: mission.requests, requirements: mission.requirements, prohibited_write: mission.prohibitedWrite ?? [],
2719
+ ...await missions.handoffContext(mission),
2699
2720
  correction_context: { findings: recovery ? existing.findings : review.result,
2700
2721
  retained_findings_ref: JSON.parse(missions.correctionReference(root, reviewIdentity)),
2701
2722
  mission_id: mission.id, prior_run_id: run.runID, review_identity: reviewIdentity,
@@ -429,6 +429,12 @@ run/grade commands and working directory. Setup, launch and result collection no
429
429
  do not forbid execution while assigning that Worker the requirement to execute.
430
430
  Write objective as a target or corrective delta: aim for 2000 characters.
431
431
  The Worker reads the original requests verbatim from the handoff once; do not copy them into objective.
432
+ Public conversation, recent tool results and Mission progress are supplied automatically in continuity.
433
+ When useful, include handoff_context in start_mission/plan_units: current intent and rationale, known environment,
434
+ completed setup/checks, failed approaches and remaining work with existing references. It is optional working
435
+ context, not a form or approval. Preserve useful knowledge instead of assigning unchanged discovery again.
436
+ Assign the remaining action. Reference paths support relevant gaps, not a mandatory reread checklist; avoid
437
+ routine environment/instruction reconfirmation and guessed AGENTS.md paths when that context is already supplied.
432
438
  Keep investigation, edits, formal checks and any requested commit in the same Worker before independent Review;
433
439
  do not invent a review-before-commit gate or a commit-only handoff. Preserve explicit user ordering.
434
440
  ${MISSION_GIT_SCOPE}
@@ -549,8 +555,8 @@ mode: subagent
549
555
 
550
556
  Implement, test and requested commit in this Task.
551
557
  Reuse supplied AGENTS.md; for gaps prefer exact ancestor files/affected subtrees over parent globs.
552
- Read handoff_path in full first: task.objective, verbatim original_requests/unit_instruction, Mission constraints,
553
- coverage indices. Preserve user scope and ordering; prove assigned criteria, not Mission completion.
558
+ Read handoff_path in full first: task.objective, verbatim original_requests/unit_instruction, constraints/continuity,
559
+ coverage. Preserve user scope and ordering; prove assigned criteria, not Mission completion.
554
560
  Denied: reason/remedy. No routine manifest/goal/status/bind.
555
561
  Recovery: ${profile.toolPrefix}bind_write_gate with exact project_root and manifest_path=operation_manifest.
556
562
  Treat cwd/project_root and paths as opaque; never shorten or normalize segments.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.13.12",
3
+ "version": "0.13.13",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",