sortie-dogs 0.10.18 → 0.10.19

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
@@ -28,8 +28,6 @@ implementation, validation, review, and model routing.
28
28
  Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) ·
29
29
  [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md)
30
30
 
31
- Release: [v0.10.6](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.10.6)
32
-
33
31
  > **Beta:** v0.10.x is under active stabilization. Runtime behavior,
34
32
  > configuration, and generated assets may still change before 1.0.
35
33
 
@@ -85,78 +83,23 @@ The v0.10 profile is serial by design. The stable profile's Luna fabric and
85
83
  parallel integration path are not exposed in this profile. More agents are not a
86
84
  goal; preserving quality while reducing unnecessary expensive work is.
87
85
 
88
- ### SWE-bench policy from v0.10.6
89
-
90
- Starting with v0.10.6, development proceeds alongside recurring SWE-bench
91
- measurement. This is a measurement policy, not a claim that an unrun suite passed.
92
-
93
- - Freeze the task input, package/source snapshot, model routes, budgets, tools,
94
- and stop endpoint before comparing runs.
95
- - Freeze the candidate only after all writers stop, then grade a separate copy
96
- with one pinned official verifier execution.
97
- - Record benchmark completion, official task correctness, and harness terminal
98
- state independently. A Sortie `DONE` or review `PASS` is not an official
99
- verifier reward.
100
- - Record agent-to-freeze and verifier time separately, plus root and descendant
101
- tokens, model steps, children, cache behavior, estimated cost, and coverage.
102
- - Keep infrastructure failure distinct from scored failure. Small or unmatched
103
- samples remain case studies, not leaderboard or general success-rate claims.
104
-
105
- The pre-v0.10.6 local benchmark condition is closed historical evidence. New
106
- development decisions use SWE-bench measurements collected under the frozen
107
- contract in [Coding benchmark completion and correctness](docs/benchmark-completion-contract.md).
108
-
109
- ### v0.10.14 SWE-bench Lite dev23 baseline
110
-
111
- The first complete v0.10.14 baseline was run against all 23 `dev` instances and
112
- graded with the pinned official harness. It is a fixed candidate baseline, not a
113
- leaderboard or general success-rate claim.
114
-
115
- - Candidate: `sortie-dogs@0.10.14`, package SHA-256
116
- `f2caf67268a5f69b67a252a0d84bfc5ad09db82684aa8c1e0f5c49f8d2986993`.
117
- - Dataset: `princeton-nlp/SWE-bench_Lite`, revision
118
- `6ec7bb89b9342f664a54a6e0a6ea6501d3437cc2`, split `dev`.
119
- - Conditions: pass@1, `attempts_per_instance=1`, `retry_count=0`, per-instance
120
- inference limit `$1.50`, four independent inference slots, and official
121
- scoring with `max_workers=1`.
122
- - Official score: **6/23 (26.1%)**. All 23 rows received an official result.
123
- - Resolved instances: `marshmallow-code__marshmallow-1343`,
124
- `marshmallow-code__marshmallow-1359`, `pydicom__pydicom-1256`,
125
- `pydicom__pydicom-1694`, `pylint-dev__astroid-1333`, and
126
- `sqlfluff__sqlfluff-1733`.
127
- - Inference spend: `$15.7542788` under the separate `$50` v0.10.14 campaign.
128
- - Environment deviations retained in the result: five `pvlib` rows hit the
129
- NumPy 2.0 removal of `np.Inf`, and `pyvista__pyvista-4315` lacked
130
- `libGL.so.1`. These rows remain in the denominator and were not silently
131
- removed.
132
- - Four non-successful inference terminals (`timeout` or `patch-index-failed`)
133
- produced empty patches and are counted as unresolved official results.
134
-
135
- This baseline is the comparison point for any later v0.10.15 candidate. A
136
- later candidate must use its own package digest and preserve the same dataset,
137
- official scoring boundary, and declared budget conditions before its score is
138
- compared with this value.
139
-
140
- ## Historical local case study
141
-
142
- These are completion-filtered local references on one frozen DeepSWE task,
143
- `datacurve/anko-typed-variable-bindings`, collected from 2026-09-14 through
144
- 2026-09-18. They are not matched pairs or a leaderboard result.
145
-
146
- - Bare OpenCode: Verified PASS `0/3`, F2P `3/27`, median agent wall `24.5 min`,
147
- median completed-run API-equivalent cost `$3.53`.
148
- - Sortie v0.9.12: Verified PASS `0/3`, F2P `23/27`, median wall `25.7 min`,
149
- median completed-run cost `$2.85`; two additional interrupted attempts cost
150
- an estimated `$6.20`.
151
- - Sortie v0.10.3 one-shot: Verified PASS `0/1`, F2P `7/9`, wall `22.7 min`,
152
- estimated cost `$3.79`.
153
- - Sortie v0.10.5 one-shot: Verified PASS `1/1`, F2P `9/9`, P2P `94/94`, wall
154
- `43.8 min`, estimated cost `$2.58`.
155
-
156
- The v0.10.5 result is one verified success, not a success rate. Historical rate
157
- schedules and endpoints differ, and the host-reported zero cost is not treated as
158
- a bill. See [definitions, frozen inputs, and limitations](docs/benchmark-reference.md)
159
- and [machine-readable values](docs/benchmarks/provisional-reference.json).
86
+ ### SWE-bench evaluation
87
+
88
+ A frozen Sortie-dogs v0.10.14 build was evaluated on 23 fixed tasks.
89
+
90
+ - Official verifier PASS: 6 / 23
91
+ - Scored FAIL: 17 / 23
92
+ - Infrastructure blocked: 0 / 23
93
+ - Total estimated model cost: $15.75
94
+ - Median agent runtime: 15.2 min
95
+
96
+ The same Sortie-dogs candidate, model routes, budgets, tools, and verification
97
+ procedure were held fixed for all tasks. This is one frozen evaluation, not a
98
+ general success-rate claim.
99
+
100
+ Full methodology and per-task results: [benchmark details](docs/benchmark-v0.10.14-dev23.md)
101
+
102
+ Historical qualification references remain in [benchmark reference](docs/benchmark-reference.md).
160
103
 
161
104
  ## How v0.10.6 works
162
105
 
@@ -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.10.0-v0912-language4-cost-rpt10-compaction-ref2-proposal1-review-remediation1-surface4-proposal-recovery2-quality1-terminal2-route1-path1-permission2-prerequisite1-v2-bridge1-intent1-reflection1-cancel1-proposal-budget1-goal-recovery1-delegate-execute1-v2-depth1-process-replacement1-unstarted-v2-1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.10.0-v0912-language4-cost-rpt10-compaction-ref2-proposal1-review-remediation1-surface4-proposal-recovery2-quality1-terminal2-route1-path1-permission2-prerequisite1-v2-bridge1-intent1-reflection1-cancel1-proposal-budget1-goal-recovery1-delegate-execute1-v2-depth1-process-replacement1-unstarted-v2-1-cold-turn1-return-report-v2-2";
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.10.0-v0912-language4-cost-rpt10-compaction-ref2-proposal1-review-remediation1-surface4-proposal-recovery2-quality1-terminal2-route1-path1-permission2-prerequisite1-v2-bridge1-intent1-reflection1-cancel1-proposal-budget1-goal-recovery1-delegate-execute1-v2-depth1-process-replacement1-unstarted-v2-1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.10.0-v0912-language4-cost-rpt10-compaction-ref2-proposal1-review-remediation1-surface4-proposal-recovery2-quality1-terminal2-route1-path1-permission2-prerequisite1-v2-bridge1-intent1-reflection1-cancel1-proposal-budget1-goal-recovery1-delegate-execute1-v2-depth1-process-replacement1-unstarted-v2-1-cold-turn1-return-report-v2-2";
@@ -1,10 +1,23 @@
1
1
  /** Shared planner guidance and admission enums; no inferred acceptance or dispatch authority. */
2
2
  export declare const GOAL_DELIVERY_INTENTS: readonly ["design", "registration", "implementation", "repair", "controlled-change"];
3
3
  export declare const GOAL_DELIVERY_MODES: readonly ["planning-only", "mvp-first", "repair-first", "controlled-change"];
4
+ export declare const GOAL_CRITERION_ENUMS: {
5
+ readonly build_boundary: readonly ["included", "excluded", "not-applicable"];
6
+ readonly proof_scope: readonly ["requested-full", "document-deliverable", "expected-negative"];
7
+ readonly expected_outcome: readonly ["pass", "fail"];
8
+ readonly source_binding: readonly ["declared", "current-protected"];
9
+ readonly candidate_binding: readonly ["declared", "current-protected"];
10
+ };
4
11
  export declare function hasGoalCommandAliasConflict(values: Record<string, unknown>): boolean;
5
12
  /** Shared root fields are defaults too. Explicit defaults and then per-criterion
6
13
  * fields take precedence; no commands, identifiers or criteria are rewritten. */
7
14
  export declare function goalDeclarationDefaults(object: Record<string, unknown>): Record<string, unknown>;
15
+ export interface GoalDeclarationFieldDiagnostic {
16
+ readonly pointer: string;
17
+ readonly expected: string;
18
+ }
19
+ /** Validate structured planner fields before immutable controls exist; expansion itself remains lossless. */
20
+ export declare function goalDeclarationFieldDiagnostics(object: Record<string, unknown>): readonly GoalDeclarationFieldDiagnostic[];
8
21
  export declare const GOAL_DECLARATION_FORMAT: string;
9
22
  /** Normalize a shared declaration without guessing acceptance, coverage, delivery, or validation. */
10
23
  export declare function expandGoalDeclaration(value: unknown): string;
@@ -2,6 +2,13 @@ import { goalFingerprint } from "./goal-bound.js";
2
2
  /** Shared planner guidance and admission enums; no inferred acceptance or dispatch authority. */
3
3
  export const GOAL_DELIVERY_INTENTS = ["design", "registration", "implementation", "repair", "controlled-change"];
4
4
  export const GOAL_DELIVERY_MODES = ["planning-only", "mvp-first", "repair-first", "controlled-change"];
5
+ export const GOAL_CRITERION_ENUMS = {
6
+ build_boundary: ["included", "excluded", "not-applicable"],
7
+ proof_scope: ["requested-full", "document-deliverable", "expected-negative"],
8
+ expected_outcome: ["pass", "fail"],
9
+ source_binding: ["declared", "current-protected"],
10
+ candidate_binding: ["declared", "current-protected"],
11
+ };
5
12
  const CRITERION_FIELDS = ["criterion_id", "target", "entrypoint", "workload", "oracle_coverage", "build_boundary", "source",
6
13
  "candidate", "source_binding", "candidate_binding", "fixture", "proof_scope", "expected_outcome", "validation_command"];
7
14
  export function hasGoalCommandAliasConflict(values) {
@@ -23,6 +30,67 @@ export function goalDeclarationDefaults(object) {
23
30
  ? object.defaults : {};
24
31
  return { ...normalizedCriterionFields(object), ...normalizedCriterionFields(defaults) };
25
32
  }
33
+ /** Validate structured planner fields before immutable controls exist; expansion itself remains lossless. */
34
+ export function goalDeclarationFieldDiagnostics(object) {
35
+ const defects = new Map();
36
+ const add = (pointer, expected) => { defects.set(pointer, { pointer, expected }); };
37
+ const scalar = (value) => typeof value === "string" ? value.trim()
38
+ : typeof value === "boolean" || typeof value === "number" ? String(value) : undefined;
39
+ const singleLine = (value) => {
40
+ const text = scalar(value);
41
+ return text !== undefined && text.length > 0 && !/[\r\n]/u.test(String(value));
42
+ };
43
+ for (const [field, allowed, required] of [
44
+ ["delivery_intent", GOAL_DELIVERY_INTENTS, true], ["delivery_mode", GOAL_DELIVERY_MODES, false],
45
+ ["usable_path_established", ["true", "false"], true], ["controlled_change", ["true", "false"], true],
46
+ ]) {
47
+ if ((required || object[field] !== undefined) && (!singleLine(object[field]) || !allowed.some(item => item === scalar(object[field])))) {
48
+ add(`/goal_declaration/${field}`, allowed.join(" | "));
49
+ }
50
+ }
51
+ if (object.goal_acceptance_fingerprint !== undefined &&
52
+ !/^sha256:[a-f0-9]{64}$/u.test(String(object.goal_acceptance_fingerprint))) {
53
+ add("/goal_declaration/goal_acceptance_fingerprint", "sha256: followed by 64 lowercase hexadecimal characters");
54
+ }
55
+ const defaults = object.defaults !== null && typeof object.defaults === "object" && !Array.isArray(object.defaults)
56
+ ? object.defaults : undefined;
57
+ const criteria = Array.isArray(object.criteria) ? object.criteria : [];
58
+ for (const [index, raw] of criteria.entries()) {
59
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
60
+ continue;
61
+ const criterion = raw;
62
+ const fieldValue = (field) => {
63
+ for (const [owner, prefix] of [[criterion, `/goal_declaration/criteria/${index}`],
64
+ [defaults, "/goal_declaration/defaults"], [object, "/goal_declaration"]]) {
65
+ if (!owner)
66
+ continue;
67
+ for (const key of [`goal_${field}`, field])
68
+ if (owner[key] !== undefined && owner[key] !== null) {
69
+ return { value: owner[key], pointer: `${prefix}/${key}` };
70
+ }
71
+ }
72
+ return { value: undefined, pointer: `/goal_declaration/${defaults ? "defaults/" : ""}${field}` };
73
+ };
74
+ for (const field of ["target", "entrypoint", "workload", "source", "candidate", "fixture"]) {
75
+ const { value, pointer } = fieldValue(field);
76
+ if (!singleLine(value))
77
+ add(pointer, "nonblank single-line scalar");
78
+ }
79
+ for (const [field, allowed] of Object.entries(GOAL_CRITERION_ENUMS)) {
80
+ const { value, pointer } = fieldValue(field);
81
+ if (value === undefined && (field === "source_binding" || field === "candidate_binding"))
82
+ continue;
83
+ if (!singleLine(value) || !allowed.some(item => item === scalar(value)))
84
+ add(pointer, allowed.join(" | "));
85
+ }
86
+ const oracle = fieldValue("oracle_coverage");
87
+ if (!Array.isArray(oracle.value) || oracle.value.length === 0 ||
88
+ !oracle.value.every(item => typeof item === "string" && item.length > 0 && item.length <= 512) ||
89
+ new Set(oracle.value).size !== oracle.value.length)
90
+ add(oracle.pointer, "nonempty unique string array; each string has 1..512 characters");
91
+ }
92
+ return [...defects.values()];
93
+ }
26
94
  export const GOAL_DECLARATION_FORMAT = `Declare a goal once, then reference it with goal_declaration_path in the Task prompt.
27
95
  The referenced JSON may contain shared defaults and a criteria array; the host expands them privately.
28
96
  An inline ext["sortie-dogs/goal-declaration"] in the registered handoff is also supported.
@@ -121,6 +121,8 @@ export declare class OperatorProposalRuntime {
121
121
  private spendFile;
122
122
  private readSpend;
123
123
  private saveSpend;
124
+ /** A released investigation still belongs to its accepted order across a host restart. No grant is revived. */
125
+ continuationCheckpoint(root: string): Promise<string | undefined>;
124
126
  read(root: string): Promise<OperatorProposalState | undefined>;
125
127
  private readUnlocked;
126
128
  private save;
@@ -324,6 +324,13 @@ export class OperatorProposalRuntime {
324
324
  !Number.isSafeInteger(spend.reads) || spend.reads < 0 || !Number.isSafeInteger(spend.submissions) || spend.submissions < 0) {
325
325
  throw new Error("operator-proposal-spend-invalid");
326
326
  }
327
+ if (spend.retry !== undefined && (!record(spend.retry) || !/^[a-f0-9]{64}$/u.test(spend.retry.intent_hash) ||
328
+ !record(spend.retry.goal_binding) || !text(spend.retry.goal_binding.goal_id) ||
329
+ !Number.isSafeInteger(spend.retry.goal_binding.revision) || spend.retry.goal_binding.revision < 1 ||
330
+ !Number.isSafeInteger(spend.retry.goal_binding.scope_epoch) || spend.retry.goal_binding.scope_epoch < 1 ||
331
+ !/^sha256:[a-f0-9]{64}$/u.test(spend.retry.goal_binding.acceptance_fingerprint))) {
332
+ throw new Error("operator-proposal-spend-invalid");
333
+ }
327
334
  return spend;
328
335
  }
329
336
  async saveSpend(spend) {
@@ -338,6 +345,19 @@ export class OperatorProposalRuntime {
338
345
  await rm(temporary, { force: true }).catch(() => undefined);
339
346
  }
340
347
  }
348
+ /** A released investigation still belongs to its accepted order across a host restart. No grant is revived. */
349
+ async continuationCheckpoint(root) {
350
+ return this.serial(root, async () => {
351
+ const state = await this.readUnlocked(root);
352
+ if (state?.phase === "approved")
353
+ return undefined;
354
+ const retry = state?.goal_binding ? { intent_hash: state.intent_hash, goal_binding: state.goal_binding }
355
+ : state ? undefined : (await this.readSpend(root)).retry;
356
+ return retry === undefined ? undefined : JSON.stringify({ authority: "durable-proposal-continuity",
357
+ root_session_id: root, ...retry, status: state?.phase ?? "cancelled",
358
+ next_action: "Preserve the accepted goal and cumulative spend. Read operator_status; a cancelled proposal requires an explicit root retry decision and a new exact proposal Task, never revival of its old child." });
359
+ });
360
+ }
341
361
  async read(root) {
342
362
  return this.serial(root, () => this.readUnlocked(root));
343
363
  }
@@ -489,7 +509,8 @@ export class OperatorProposalRuntime {
489
509
  const spend = await this.readSpend(root);
490
510
  const state = { schema_version: "0.1", profile: this.profile.id, root_session_id: root,
491
511
  intent_id: `intent-${intentHash.slice(0, 24)}`, intent_hash: intentHash, intent, created_at: new Date().toISOString(), phase: "investigating",
492
- goal_binding: null, proposal_call_id: null, proposal_session_id: null, read_count: spend.reads, read_paths: [], submission_count: spend.submissions, proposal_id: null,
512
+ goal_binding: spend.retry?.intent_hash === intentHash ? structuredClone(spend.retry.goal_binding) : null,
513
+ proposal_call_id: null, proposal_session_id: null, read_count: spend.reads, read_paths: [], submission_count: spend.submissions, proposal_id: null,
493
514
  ...(spend.reads > 0 || spend.submissions > 0 ? { prior_spend: { reads: spend.reads, submissions: spend.submissions } } : {}),
494
515
  proposal_revision: null, proposal_hash: null, proposal: null, approval_rationale: null };
495
516
  this.assertBudgetAvailable(state);
@@ -647,7 +668,8 @@ export class OperatorProposalRuntime {
647
668
  if (!state || state.phase === "approved")
648
669
  return undefined;
649
670
  await this.saveSpend({ schema_version: "0.1", profile: this.profile.id, root_session_id: root,
650
- reads: state.read_count, submissions: state.submission_count });
671
+ reads: state.read_count, submissions: state.submission_count,
672
+ ...(state.goal_binding === null ? {} : { retry: { intent_hash: state.intent_hash, goal_binding: state.goal_binding } }) });
651
673
  await rm(this.file(root), { force: true });
652
674
  this.states.delete(root);
653
675
  return state;
@@ -4,7 +4,7 @@ import { lstat, mkdir, open, readFile, realpath, rename, rm, unlink, writeFile }
4
4
  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
- import { expandGoalDeclaration, goalDeclarationDefaults, hasGoalCommandAliasConflict } from "./goal-declaration-format.js";
7
+ import { expandGoalDeclaration, goalDeclarationDefaults, goalDeclarationFieldDiagnostics, hasGoalCommandAliasConflict } from "./goal-declaration-format.js";
8
8
  import { normalizeManifestPath, normalizeRelativePath } from "./path.js";
9
9
  import { CONTRACT_TEXT_LIMITS, validateHandoffSchema, validateOperationManifestSchema } from "./validate-schema.js";
10
10
  import { validateManifest } from "./validate-manifest.js";
@@ -235,6 +235,12 @@ export function parseOperatorPlan(value) {
235
235
  return planError("/git_lifecycle/post_commit_validation", "operator-git-post-commit-validation-invalid", "canonical-declared-goal-validations-must-be-exclusive-contiguous-final-unit-suffix", "repair-proof-mapping");
236
236
  }
237
237
  }
238
+ const declarationDefects = goalDeclarationFieldDiagnostics(value.goal_declaration);
239
+ if (declarationDefects.length)
240
+ throw new OperatorContractError(declarationDefects.map(defect => ({
241
+ document: "plan", pointer: defect.pointer, code: "operator-goal-field-invalid", rule: "declared-goal-field",
242
+ repair_kind: "repair-field", repair_paths: [defect.pointer], expected: defect.expected,
243
+ })));
238
244
  expandGoalDeclaration(value.goal_declaration);
239
245
  return { ...value, acceptance: normalizeAcceptanceCriteria(value.acceptance) };
240
246
  }
@@ -574,6 +580,21 @@ export class OperatorRuntime {
574
580
  if (!record(patch) || !exactKeys(patch, ["op", "path", "value"]) || !["replace", "add"].includes(String(patch.op)) || typeof patch.path !== "string" || !Object.hasOwn(patch, "value")) {
575
581
  throw new Error("operator-repair-invalid");
576
582
  }
583
+ const goalFieldPath = /^\/goal_declaration\/(delivery_intent|delivery_mode|usable_path_established|controlled_change|goal_acceptance_fingerprint)$/.test(patch.path) ||
584
+ /^\/goal_declaration\/(?:defaults\/|criteria\/(?:0|[1-9]\d*)\/)?(?:goal_)?(?:target|entrypoint|workload|source|candidate|fixture|oracle_coverage|build_boundary|proof_scope|expected_outcome|source_binding|candidate_binding)$/.test(patch.path);
585
+ if (goalFieldPath && Array.isArray(draft.diagnostics) && draft.diagnostics.some((item) => item.document === "plan" && item.pointer === patch.path && item.code === "operator-goal-field-invalid")) {
586
+ // Only the exact invalid or missing field is editable; acceptance, valid declaration fields,
587
+ // budgets and scope never become mutable just because another field was diagnosed.
588
+ const keys = patch.path.slice(1).split("/");
589
+ let owner = draft.plan;
590
+ for (const key of keys.slice(0, -1))
591
+ owner = owner?.[key];
592
+ const key = keys.at(-1);
593
+ if (!record(owner) || (patch.op === "replace" && !Object.hasOwn(owner, key)))
594
+ throw new Error("operator-repair-path-forbidden");
595
+ owner[key] = patch.value;
596
+ continue;
597
+ }
577
598
  const readMatch = /^\/units\/(0|[1-9]\d*)\/read\/(0|[1-9]\d*)$/.exec(patch.path);
578
599
  if (readMatch) {
579
600
  const unitIndex = Number(readMatch[1]), pathIndex = Number(readMatch[2]);
@@ -13,6 +13,8 @@ export interface OpenCodePluginInput {
13
13
  childLifecycleCheckWaitMs?: number;
14
14
  /** Installed-profile adapter only; never supplied by a model or project configuration. */
15
15
  runtimeBridge?: RuntimeBridge;
16
+ /** V2 has no assistant-part update API; return the canonical panel with explicit acceptance. */
17
+ returnReportTransport?: "tool-result";
16
18
  /** The host SDK client. Absent in hosts that construct the plugin without one. */
17
19
  client?: SessionMessageReader & RunMetricsClient & ContinuationClient & OpenCodeModelAvailabilityClient & {
18
20
  app?: {
@@ -10,7 +10,7 @@ import { createHash, randomUUID } from "node:crypto";
10
10
  import { lstat, mkdir, open, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
11
11
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
12
12
  import { RUNTIME_ASSET_VERSION } from "../asset-version.js";
13
- import { GOAL_DECLARATION_FORMAT, GOAL_DELIVERY_INTENTS, GOAL_DELIVERY_MODES, expandGoalDeclaration } from "../core/goal-declaration-format.js";
13
+ import { GOAL_DECLARATION_FORMAT, GOAL_DELIVERY_INTENTS, GOAL_DELIVERY_MODES, GOAL_CRITERION_ENUMS, expandGoalDeclaration } from "../core/goal-declaration-format.js";
14
14
  import { ACCEPTANCE_CONTINUITY_AUTHORITY, ACCEPTANCE_CONTINUITY_EXTENSION, ACCEPTANCE_CONTINUITY_SCHEMA_VERSION, acceptanceContinuityFingerprint, inspectAcceptanceContinuity, normalizeAcceptanceCriteria, } from "../core/acceptance-continuity.js";
15
15
  import { resolveGlobalConfigRoot } from "../core/initialize.js";
16
16
  import { admitLunaFabric } from "../core/luna-fabric-contract.js";
@@ -1770,11 +1770,14 @@ export const SortieDogsPlugin = async (input, options) => {
1770
1770
  origin_user_message_id: ticket.origin_user_message_id,
1771
1771
  } } };
1772
1772
  }
1773
- async function terminalGoal(sessionID, stopReason, status) {
1773
+ async function terminalGoal(sessionID, stopReason, status, explicitAcceptance = false) {
1774
1774
  const ledger = await goalLedger(sessionID);
1775
1775
  const state = (await ledger.readGoal()).state;
1776
1776
  if (state.goal_id === null || state.receipt !== null || state.outstanding_reservations.length > 0 || state.acceptance_fingerprint === null)
1777
1777
  return state.receipt ?? undefined;
1778
+ if (status === "succeeded" && !explicitAcceptance &&
1779
+ await input.runtimeBridge?.requiresExplicitAcceptance?.(sessionID) === true)
1780
+ return undefined;
1778
1781
  const records = (await ledger.readGoal()).records;
1779
1782
  const settledEvidence = records.flatMap(({ event }) => event.kind === "unit.settled" || event.kind === "unit.evidence-reconciled" ? event.evidence : []);
1780
1783
  if (status === "succeeded" && (state.acceptance_contract === null ||
@@ -1857,7 +1860,8 @@ export const SortieDogsPlugin = async (input, options) => {
1857
1860
  if (receipt === undefined && delivery === "failed") {
1858
1861
  receipt = await terminalGoal(sessionID, "stopped", "stopped").catch(() => undefined);
1859
1862
  }
1860
- else if (receipt === undefined && proved) {
1863
+ else if (receipt === undefined && proved &&
1864
+ await input.runtimeBridge?.requiresExplicitAcceptance?.(sessionID) !== true) {
1861
1865
  receipt = await terminalGoal(sessionID, "completed", "succeeded").catch(() => undefined);
1862
1866
  }
1863
1867
  else if (receipt === undefined && outcome === "INTERRUPTED") {
@@ -1934,19 +1938,19 @@ export const SortieDogsPlugin = async (input, options) => {
1934
1938
  const expectedOutcome = values.goal_expected_outcome;
1935
1939
  const sourceBinding = handoffValue(entries, ["goal_source_binding"]);
1936
1940
  const candidateBinding = handoffValue(entries, ["goal_candidate_binding"]);
1937
- if (buildBoundary !== undefined && buildBoundary !== "included" && buildBoundary !== "excluded" && buildBoundary !== "not-applicable") {
1941
+ if (buildBoundary !== undefined && !GOAL_CRITERION_ENUMS.build_boundary.some(value => value === buildBoundary)) {
1938
1942
  defects.push(contractDefect("contract", pointer("goal_build_boundary"), "goal_build_boundary_invalid"));
1939
1943
  }
1940
- if (proofScope !== undefined && proofScope !== "requested-full" && proofScope !== "document-deliverable" && proofScope !== "expected-negative") {
1944
+ if (proofScope !== undefined && !GOAL_CRITERION_ENUMS.proof_scope.some(value => value === proofScope)) {
1941
1945
  defects.push(contractDefect("contract", pointer("goal_proof_scope"), "goal_proof_scope_invalid"));
1942
1946
  }
1943
- if (expectedOutcome !== undefined && expectedOutcome !== "pass" && expectedOutcome !== "fail") {
1947
+ if (expectedOutcome !== undefined && !GOAL_CRITERION_ENUMS.expected_outcome.some(value => value === expectedOutcome)) {
1944
1948
  defects.push(contractDefect("contract", pointer("goal_expected_outcome"), "goal_expected_outcome_invalid"));
1945
1949
  }
1946
- if (sourceBinding !== undefined && sourceBinding !== "declared" && sourceBinding !== "current-protected") {
1950
+ if (sourceBinding !== undefined && !GOAL_CRITERION_ENUMS.source_binding.some(value => value === sourceBinding)) {
1947
1951
  defects.push(contractDefect("contract", pointer("goal_source_binding"), "goal_source_binding_invalid"));
1948
1952
  }
1949
- if (candidateBinding !== undefined && candidateBinding !== "declared" && candidateBinding !== "current-protected") {
1953
+ if (candidateBinding !== undefined && !GOAL_CRITERION_ENUMS.candidate_binding.some(value => value === candidateBinding)) {
1950
1954
  defects.push(contractDefect("contract", pointer("goal_candidate_binding"), "goal_candidate_binding_invalid"));
1951
1955
  }
1952
1956
  const declaredValidation = handoffValue(entries, ["validation"]);
@@ -7721,7 +7725,7 @@ export const SortieDogsPlugin = async (input, options) => {
7721
7725
  throw new Error("operator-goal-stopped");
7722
7726
  // Same proof/freshness/reservation gate used by terminal text, requested
7723
7727
  // explicitly by the root instead of inferred from a model's wording.
7724
- const receipt = goal.receipt ?? await terminalGoal(sessionID, "completed", "succeeded");
7728
+ const receipt = goal.receipt ?? await terminalGoal(sessionID, "completed", "succeeded", true);
7725
7729
  if (receipt?.status !== "succeeded")
7726
7730
  return { status: "awaiting-evidence" };
7727
7731
  // Acceptance must return its receipt to the active tool call. Revoking
@@ -8,7 +8,7 @@ import { relative, resolve, sep } from "node:path";
8
8
  import { readFile, realpath } from "node:fs/promises";
9
9
  import { BUILT_IN_MODEL_CATALOG } from "./model-routing.js";
10
10
  import { goalFingerprint } from "../core/goal-bound.js";
11
- import { decoratePreviewHeadings } from "./receipt-presentation.js";
11
+ import { decoratePreviewHeadings, returnReportPanel } from "./receipt-presentation.js";
12
12
  import { sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
13
13
  import { normalizeCommand } from "./gate.js";
14
14
  import { normalizeRelativePath } from "../core/path.js";
@@ -266,7 +266,11 @@ export function createProfiledPlugin(profile, assetVersion) {
266
266
  });
267
267
  const runtimeBridge = {
268
268
  profile, assetVersion,
269
- continuationCheckpoint: root => operators.continuationCheckpoint(root),
269
+ continuationCheckpoint: async (root) => await operators.continuationCheckpoint(root) ?? await proposals.continuationCheckpoint(root),
270
+ requiresExplicitAcceptance: async (root) => {
271
+ const state = await operators.read(root);
272
+ return state !== undefined && state.phase !== "completed" && operatorTurnLifecycle.get(root) !== "historical";
273
+ },
270
274
  ownsCanonicalValidation: async (root, taskID, child, command) => {
271
275
  const state = await operators.read(root);
272
276
  if (state === undefined || state.phase === "cancelled" || state.phase === "completed")
@@ -486,7 +490,7 @@ export function createProfiledPlugin(profile, assetVersion) {
486
490
  await registerPreparedGoal(context.sessionID, state);
487
491
  return preparedTask(state);
488
492
  } };
489
- tools[repair] = { description: "Repair a root-owned draft with named fields, git_lifecycle branch/start/message fields, or an already-declared criterion command. A diagnosed /units/i/read/j scope error may be replaced with a normalized repository-relative spelling of the same resource only: absolute inputs require an existing relative alias whose realpath is identical; read expansion, redirection, whole-array replacement and write-scope repair are forbidden. Empty patches revalidate the saved draft without resending it. Acceptance, unit count and write scope remain fixed.",
493
+ tools[repair] = { description: "Repair a root-owned draft with named fields, git_lifecycle branch/start/message fields, or an already-declared criterion command. A diagnosed operator-goal-field-invalid may repair only its exact invalid or missing goal declaration field using the reported expected values; valid fields, budgets and whole declaration replacement remain forbidden. A diagnosed /units/i/read/j scope error may be replaced with a normalized repository-relative spelling of the same resource only: absolute inputs require an existing relative alias whose realpath is identical; read expansion, redirection, whole-array replacement and write-scope repair are forbidden. Empty patches revalidate the saved draft without resending it. Acceptance, unit count and write scope remain fixed.",
490
494
  args: { draft_id: stringSchema, patches_json: stringSchema }, execute: async (args, context) => {
491
495
  await requireRoot(context.sessionID);
492
496
  let patches;
@@ -654,16 +658,27 @@ export function createProfiledPlugin(profile, assetVersion) {
654
658
  if (!["awaiting-acceptance", "completed"].includes(state.phase) || state.units.some(unit => unit.status !== "succeeded")) {
655
659
  return JSON.stringify({ status: "not-ready", packet: operators.packet(state) });
656
660
  }
657
- const goalFingerprint = await operators.completionGoalFingerprint(state);
661
+ const declarationFingerprint = await operators.completionGoalFingerprint(state);
658
662
  if (state.phase === "awaiting-acceptance") {
659
- await relinkRegisteredGoal(context.sessionID, state, goalFingerprint);
660
- await control.assertActiveGoal(context.sessionID, goalFingerprint);
663
+ await relinkRegisteredGoal(context.sessionID, state, declarationFingerprint);
664
+ await control.assertActiveGoal(context.sessionID, declarationFingerprint);
661
665
  }
662
- const result = await control.completeRoot(context.sessionID, goalFingerprint);
666
+ const result = await control.completeRoot(context.sessionID, declarationFingerprint);
663
667
  if (result.receipt)
664
668
  await operators.terminal(context.sessionID, result.receipt);
669
+ let panel;
670
+ if (input.returnReportTransport === "tool-result" && result.receipt?.status === "succeeded") {
671
+ const text = `✅ **DONE** \`${state.runID}\` — ${state.acceptance.length} acceptance requirements verified.\n\n` +
672
+ `**変更点:** ${state.units.map(unit => unit.unit.title).join("; ")}\n\n` +
673
+ `**確認結果:** PASS — ${[...new Set(state.units.flatMap(unit => unit.unit.validation))].join("; ")}\n\n**次:** なし`;
674
+ const rendered = await control.renderReturnReport(context.sessionID, text, goalFingerprint(result.receipt)).catch(() => undefined);
675
+ if (rendered)
676
+ panel = returnReportPanel(rendered);
677
+ }
665
678
  return JSON.stringify({ status: result.status, run_id: state.runID,
666
- acceptance_fingerprint: state.acceptanceFingerprint, receipt: result.receipt ?? null });
679
+ acceptance_fingerprint: state.acceptanceFingerprint, receipt: result.receipt ?? null,
680
+ ...(panel ? { return_report: panel,
681
+ return_report_instruction: "Append return_report verbatim exactly once to your final answer as Markdown, outside any code fence. It is a host-authored receipt display, not a new task or input. Do not summarize, recalculate or request another model turn for it." } : {}) });
667
682
  } };
668
683
  tools[resume] = { description: "Reconcile a finished process-defect unit from native host validation records and unchanged source. For the exact first validation-only contract-repair continuation process defect, unchanged durable controls/candidate and available validation budget permit one same-worker validation-only retry; the result includes the exact Task to invoke. Never reimplements a unit or resets implementation/goal spend.",
669
684
  args: { run_id: stringSchema, acceptance_fingerprint: stringSchema }, execute: async (args, context) => {
@@ -978,8 +993,9 @@ export function createProfiledPlugin(profile, assetVersion) {
978
993
  output.parts.some(part => record(part) && part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0);
979
994
  if (realTurn) {
980
995
  const state = await operators.read(chat.sessionID);
981
- if ((state !== undefined && (state.phase === "cancelled" || state.phase === "completed")) ||
982
- operatorTurnLifecycle.get(chat.sessionID) === "cancelled") {
996
+ if (((state !== undefined && (state.phase === "cancelled" || state.phase === "completed")) ||
997
+ operatorTurnLifecycle.get(chat.sessionID) === "cancelled") &&
998
+ await runtimeBridge.continuationCheckpoint(chat.sessionID) === undefined) {
983
999
  operatorTurnLifecycle.set(chat.sessionID, "historical");
984
1000
  const messageID = typeof chat.messageID === "string" && chat.messageID.length > 0
985
1001
  ? chat.messageID
@@ -1,4 +1,6 @@
1
1
  import type { GoalTerminalReceipt } from "../core/goal-bound.js";
2
+ /** Extract only the host card, excluding the terminal prose that follows it. */
3
+ export declare function returnReportPanel(rendered: string): string | undefined;
2
4
  /** Cosmetic headings only; no acceptance or execution state is inferred. */
3
5
  export declare function decoratePreviewHeadings(text: string): string;
4
6
  /** Only a host-verified current receipt may supply a missing success heading. */
@@ -1,4 +1,13 @@
1
1
  import { terminalRunOutcome } from "./run-metrics.js";
2
+ /** Extract only the host card, excluding the terminal prose that follows it. */
3
+ export function returnReportPanel(rendered) {
4
+ const summary = rendered.indexOf("<summary><strong>🐾 SORTIE DOGS — 帰還報告");
5
+ if (summary < 0)
6
+ return undefined;
7
+ const start = rendered.lastIndexOf("<details", summary);
8
+ const end = rendered.indexOf("\n</details>", summary);
9
+ return start < 0 || end < 0 ? undefined : rendered.slice(start, end + "\n</details>".length).trim();
10
+ }
2
11
  /** Cosmetic headings only; no acceptance or execution state is inferred. */
3
12
  export function decoratePreviewHeadings(text) {
4
13
  const icons = { "変更点": "🔧", "確認結果": "🔍", "次": "➡️", changes: "🔧", validation: "🔍", next: "➡️" };
@@ -25,6 +25,7 @@ export interface RuntimeBridge {
25
25
  readonly defaultModelCatalog?: import("./model-routing.js").ModelCatalog;
26
26
  transformConfiguration?(value: unknown): unknown;
27
27
  continuationCheckpoint?(rootSessionID: string): Promise<string | undefined>;
28
+ requiresExplicitAcceptance?(rootSessionID: string): Promise<boolean>;
28
29
  ownsCanonicalValidation?(rootSessionID: string, unitID: string, childSessionID: string, command: string): Promise<boolean>;
29
30
  onSerialSettlement?(settlement: SerialDispatchSettlement): Promise<void>;
30
31
  onRootTerminal?(rootSessionID: string, receipt: GoalTerminalReceipt): Promise<void>;
@@ -1,5 +1,4 @@
1
1
  import type { OpenCodeHooks, OpenCodePlugin } from "./index.js";
2
- export declare const V2_RETURN_REPORT_METADATA_KEY = "sortie-dogs/return-report";
3
2
  type JsonObject = Record<string, unknown>;
4
3
  type Registration = {
5
4
  dispose(): Promise<void> | void;
@@ -33,6 +32,7 @@ export interface OpenCodeV2Context {
33
32
  hook(name: "execute.before" | "execute.after", callback: (event: JsonObject) => Promise<void> | void): Promise<Registration>;
34
33
  };
35
34
  readonly session: {
35
+ list?(input: JsonObject): Promise<unknown>;
36
36
  get(input: {
37
37
  sessionID: string;
38
38
  }): Promise<unknown>;
@@ -46,6 +46,9 @@ export interface OpenCodeV2Context {
46
46
  switchModel(input: JsonObject): Promise<unknown>;
47
47
  hook(name: "prompt" | "context" | "compaction", callback: (event: JsonObject) => Promise<void> | void): Promise<Registration>;
48
48
  };
49
+ readonly message?: {
50
+ list(input: JsonObject): Promise<unknown>;
51
+ };
49
52
  readonly permission: {
50
53
  hook(name: "evaluate", callback: (event: JsonObject) => Promise<void> | void): Promise<Registration>;
51
54
  };
@@ -60,8 +63,8 @@ export interface OpenCodeV2Plugin {
60
63
  readonly id: string;
61
64
  setup(context: OpenCodeV2Context): Promise<(() => void) | void>;
62
65
  }
63
- /** V2 has no public assistant-part update API; publish one durable, non-resuming synthetic report card instead. */
64
- export declare function createV2ReturnReportPublisher(context: OpenCodeV2Context, hooks: OpenCodeHooks): (session: string) => Promise<void>;
66
+ /** Observe terminal text for accounting only. Synthetic input is queued work, even with resume=false. */
67
+ export declare function createV2ReturnReportFinalizer(context: OpenCodeV2Context, hooks: OpenCodeHooks): (session: string) => Promise<void>;
65
68
  export declare function createSortieDogsV2Plugin(legacyFactory?: OpenCodePlugin): OpenCodeV2Plugin;
66
69
  export declare const SortieDogsV2Plugin: OpenCodeV2Plugin;
67
70
  export default SortieDogsV2Plugin;
package/dist/plugin/v2.js CHANGED
@@ -1,7 +1,4 @@
1
- import { createHash } from "node:crypto";
2
1
  import { SortieDogsV010Plugin } from "./profiled.js";
3
- const RETURN_REPORT_MARKER = "<summary><strong>🐾 SORTIE DOGS — 帰還報告";
4
- export const V2_RETURN_REPORT_METADATA_KEY = "sortie-dogs/return-report";
5
2
  function record(value) {
6
3
  return value !== null && typeof value === "object" && !Array.isArray(value);
7
4
  }
@@ -49,8 +46,17 @@ function legacyPart(value, session, message) {
49
46
  return { ...value, sessionID: session, messageID: message };
50
47
  const state = record(value.state) ? value.state : {};
51
48
  const output = toolContentText(state.content);
52
- return { ...value, tool: string(value.name) ?? string(value.tool), callID: string(value.id), sessionID: session, messageID: message,
53
- state: { ...state, ...(output === undefined ? {} : { output }) } };
49
+ const name = string(value.name) ?? string(value.tool);
50
+ const tool = name === "subagent" ? "task" : name === "shell" ? "bash" : name === "patch" ? "apply_patch" : name;
51
+ const time = record(value.time) ? value.time : undefined;
52
+ const metadata = record(state.metadata) ? { ...state.metadata } : undefined;
53
+ if (metadata && Array.isArray(metadata.files))
54
+ metadata.files = metadata.files.map(file => record(file) && typeof file.patch === "string"
55
+ ? { ...file, diff: file.patch } : file);
56
+ return { ...value, tool, callID: string(value.id), sessionID: session, messageID: message,
57
+ state: { ...state, input: legacyToolInput(name, state.input), ...(metadata ? { metadata } : {}),
58
+ ...(time ? { time: { start: time.ran ?? time.created, end: time.completed } } : {}),
59
+ ...(output === undefined ? {} : { output }) } };
54
60
  }
55
61
  function legacyMessage(value, session, agent) {
56
62
  if (!record(value) || typeof value.id !== "string" || typeof value.type !== "string")
@@ -62,13 +68,15 @@ function legacyMessage(value, session, agent) {
62
68
  }
63
69
  if (value.type !== "assistant") {
64
70
  if (value.type === "synthetic")
65
- return { info: { id, sessionID: session, role: "system", time: value.time },
71
+ return { info: { id, sessionID: session, role: "user", agent, time: value.time },
66
72
  parts: [{ id: `${id}-text`, sessionID: session, messageID: id, type: "text", text: String(value.text ?? ""), synthetic: true }] };
67
73
  return undefined;
68
74
  }
69
75
  const model = modelReference(value.model);
70
76
  return { info: { id, sessionID: session, role: "assistant", agent: string(value.agent) ?? agent,
71
- ...(model === undefined ? {} : { model }), finish: value.finish, time: value.time, tokens: value.tokens, cost: value.cost },
77
+ ...(model === undefined ? {} : { model, providerID: model.providerID, modelID: model.modelID }),
78
+ ...(record(value.providerState) && typeof value.providerState.serviceTier === "string" ? { serviceTier: value.providerState.serviceTier } : {}),
79
+ ...(value.error === undefined ? {} : { error: value.error }), finish: value.finish, time: value.time, tokens: value.tokens, cost: value.cost },
72
80
  parts: array(value.content).flatMap(part => {
73
81
  const converted = legacyPart(part, session, id);
74
82
  return converted === undefined ? [] : [converted];
@@ -77,7 +85,8 @@ function legacyMessage(value, session, agent) {
77
85
  async function legacyMessages(context, id) {
78
86
  const [info, history] = await Promise.all([
79
87
  context.session.get({ sessionID: id }).catch(() => undefined),
80
- context.session.context({ sessionID: id }).catch(() => []),
88
+ context.message ? nativePages(cursor => context.message.list({ sessionID: id, limit: 100, ...(cursor ? { cursor } : { order: "asc" }) }))
89
+ : context.session.context({ sessionID: id }),
81
90
  ]);
82
91
  const agent = record(info) ? string(info.agent) : undefined;
83
92
  return array(history).flatMap(value => {
@@ -85,6 +94,24 @@ async function legacyMessages(context, id) {
85
94
  return converted === undefined ? [] : [converted];
86
95
  });
87
96
  }
97
+ async function nativePages(fetch) {
98
+ const items = [], seen = new Set();
99
+ let cursor;
100
+ for (let page = 0; page < 100; page += 1) {
101
+ const result = await fetch(cursor);
102
+ if (!record(result) || !Array.isArray(result.data) || !record(result.cursor))
103
+ throw new Error("v2-history-page-invalid");
104
+ items.push(...result.data);
105
+ const next = string(result.cursor.next);
106
+ if (!next)
107
+ return items;
108
+ if (seen.has(next))
109
+ throw new Error("v2-history-cursor-repeated");
110
+ seen.add(next);
111
+ cursor = next;
112
+ }
113
+ throw new Error("v2-history-page-limit");
114
+ }
88
115
  function legacyClient(context) {
89
116
  const directory = context.location.directory;
90
117
  const session = {
@@ -101,7 +128,13 @@ function legacyClient(context) {
101
128
  return { data: id === undefined || wanted === undefined ? undefined
102
129
  : (await legacyMessages(context, id)).find(message => record(message.info) && message.info.id === wanted) };
103
130
  },
104
- children: async () => ({ data: [] }),
131
+ ...(context.session.list ? { children: async (request) => {
132
+ const id = sessionID(request);
133
+ if (id === undefined)
134
+ throw new Error("v2-history-parent-missing");
135
+ return { data: await nativePages(cursor => context.session.list({ parentID: id, limit: 100,
136
+ ...(cursor ? { cursor } : { order: "asc" }) })) };
137
+ } } : {}),
105
138
  abort: async (request) => {
106
139
  const id = sessionID(request);
107
140
  return id === undefined ? undefined : await context.session.interrupt({ sessionID: id, resume: false });
@@ -185,24 +218,8 @@ function assistantText(message) {
185
218
  return record(text) && typeof text.text === "string" ? text.text.trim() : undefined;
186
219
  }
187
220
  const V2_SUBAGENT_PROMPT_PREFIX = "You are a subagent spawned by another session.\n";
188
- function reportPanel(rendered) {
189
- const summary = rendered.indexOf(RETURN_REPORT_MARKER);
190
- if (summary < 0)
191
- return undefined;
192
- const details = rendered.lastIndexOf("<details", summary);
193
- return rendered.slice(details < 0 ? summary : details).trim();
194
- }
195
- function reportIdentity(session, sourceMessageID, panel) {
196
- return createHash("sha256").update(`${session}\0${sourceMessageID}\0${panel}`).digest("hex");
197
- }
198
- function reportMetadata(message) {
199
- if (!record(message) || message.type !== "synthetic" || !record(message.metadata))
200
- return undefined;
201
- const value = message.metadata[V2_RETURN_REPORT_METADATA_KEY];
202
- return record(value) ? value : undefined;
203
- }
204
- /** V2 has no public assistant-part update API; publish one durable, non-resuming synthetic report card instead. */
205
- export function createV2ReturnReportPublisher(context, hooks) {
221
+ /** Observe terminal text for accounting only. Synthetic input is queued work, even with resume=false. */
222
+ export function createV2ReturnReportFinalizer(context, hooks) {
206
223
  const active = new Map();
207
224
  return async (session) => {
208
225
  const previous = active.get(session) ?? Promise.resolve();
@@ -217,15 +234,6 @@ export function createV2ReturnReportPublisher(context, hooks) {
217
234
  const base = assistantText(source);
218
235
  const output = { text: base };
219
236
  await hooks["experimental.text.complete"]?.({ sessionID: session, messageID: source.id }, output);
220
- const panel = reportPanel(output.text);
221
- if (panel === undefined)
222
- return;
223
- const identity = reportIdentity(session, source.id, panel);
224
- if (history.some(message => reportMetadata(message)?.identity === identity))
225
- return;
226
- await context.session.synthetic({ sessionID: session, id: `sortie-report-${identity}`, text: panel,
227
- description: "Sortie Dogs return report", delivery: "queue", resume: false,
228
- metadata: { [V2_RETURN_REPORT_METADATA_KEY]: { schema_version: "0.1", identity, source_message_id: source.id } } });
229
237
  return;
230
238
  }
231
239
  }).finally(() => { if (active.get(session) === operation)
@@ -311,9 +319,10 @@ export function createSortieDogsV2Plugin(legacyFactory = SortieDogsV010Plugin) {
311
319
  return {
312
320
  id: "sortie-dogs.v010",
313
321
  async setup(context) {
314
- const hooks = await legacyFactory({ directory: context.location.directory, client: legacyClient(context) }, context.options ?? {});
322
+ const hooks = await legacyFactory({ directory: context.location.directory, client: legacyClient(context),
323
+ returnReportTransport: "tool-result" }, context.options ?? {});
315
324
  await registerV2Hooks(context, hooks);
316
- const publish = createV2ReturnReportPublisher(context, hooks);
325
+ const finalize = createV2ReturnReportFinalizer(context, hooks);
317
326
  const controller = new AbortController();
318
327
  void (async () => {
319
328
  for await (const event of context.event.subscribe({ signal: controller.signal })) {
@@ -327,7 +336,7 @@ export function createSortieDogsV2Plugin(legacyFactory = SortieDogsV010Plugin) {
327
336
  if (id === undefined)
328
337
  continue;
329
338
  if (event.type === "session.execution.succeeded")
330
- await publish(id);
339
+ await finalize(id);
331
340
  if (event.type === "session.created" || event.type === "session.deleted" || event.type === "session.idle") {
332
341
  const info = event.type === "session.created" ? await context.session.get({ sessionID: id }).catch(() => ({ id })) : { id };
333
342
  await hooks.event?.({ event: { type: String(event.type), properties: { sessionID: id, info } } });
@@ -23,6 +23,8 @@ Retain the canonical product's game-like user guidance and icons. Role separatio
23
23
  Use the user's language for prose, concise icon-led plan/progress/evidence blocks, and the canonical terminal heading
24
24
  after the host accepts the result. The host supplies the 🐾 return report, mission/proof, cost/pack and career panels;
25
25
  do not fabricate scores, counts, medals or success, and do not suppress these panels as redundant decoration.
26
+ When complete_operator returns return_report, append that host-authored Markdown verbatim exactly once to the existing
27
+ final answer, outside any code fence. Do not calculate its values or turn the panel into another task or model call.
26
28
 
27
29
  ${canonicalFixture("READABLE_OUTPUT_FIXTURE")}
28
30
  `;
@@ -219,7 +221,9 @@ directly (fast path); a larger plan returns ${operator}. Do not launch another i
219
221
  Do not manually reconstruct, translate, shorten, or append to that returned prompt. Do not add task_id to resume a
220
222
  fresh dispatch. If the exact Task is no longer in context, call operator_next once to retrieve it rather than guess.
221
223
  If prepare reports an invalid plan, repair only the reported JSON pointer using its code and repair_kind; preserve the
222
- original objective, acceptance, and unit count. Do not regenerate the full plan or send placeholder/probe plans. If it reports
224
+ original objective, acceptance, and unit count. For operator-goal-field-invalid, use the exact expected enum or required
225
+ field value at the diagnosed pointer; add a missing field without replacing defaults, criteria, budgets or the whole declaration.
226
+ Do not regenerate the full plan or send placeholder/probe plans. If it reports
223
227
  an immutable active contract, read operator_status and reuse the existing next Task. Cancel only when an actual scope
224
228
  change or explicit stop requires it; do not cancel/recreate an unchanged plan to work around an admission error.
225
229
  Cancellation does not close the accepted user order. A replacement repair plan must keep the exact previous ordered
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.10.18",
3
+ "version": "0.10.19",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",