sortie-dogs 0.9.2 → 0.9.3

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
@@ -24,7 +24,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
24
24
 
25
25
  Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
26
26
 
27
- Release: [v0.9.2](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.2)
27
+ Release: [v0.9.3](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.3)
28
28
 
29
29
  ## Quick start
30
30
 
@@ -2,5 +2,5 @@
2
2
  * Version of the installable runtime assets. Kept in its own module so the plugin can compare an
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
- export declare const RUNTIME_ASSET_VERSION = "0.3.77-terminal-delivery-v1";
5
+ export declare const RUNTIME_ASSET_VERSION = "0.3.80-review-evidence-v1";
6
6
  export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION;
@@ -2,4 +2,4 @@
2
2
  * Version of the installable runtime assets. Kept in its own module so the plugin can compare an
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
- export const RUNTIME_ASSET_VERSION = "0.3.77-terminal-delivery-v1";
5
+ export const RUNTIME_ASSET_VERSION = "0.3.80-review-evidence-v1";
package/dist/cli/main.js CHANGED
@@ -1,12 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
3
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
4
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
5
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
6
+ });
7
+ }
8
+ return path;
9
+ };
2
10
  import { open, realpath } from "node:fs/promises";
3
11
  import { resolve } from "node:path";
4
12
  import { pathToFileURL } from "node:url";
5
- const core = await import(`../core/validate-schema.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
6
- const diagnostics = await import(`../core/diagnostics.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
7
- const manifestValidator = await import(`../core/validate-manifest.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
8
- const pathUtils = await import(`../core/path.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
9
- const initializer = await import(`../core/initialize.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
13
+ const core = await import(__rewriteRelativeImportExtension(`../core/validate-schema.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
14
+ const diagnostics = await import(__rewriteRelativeImportExtension(`../core/diagnostics.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
15
+ const manifestValidator = await import(__rewriteRelativeImportExtension(`../core/validate-manifest.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
16
+ const pathUtils = await import(__rewriteRelativeImportExtension(`../core/path.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
17
+ const initializer = await import(__rewriteRelativeImportExtension(`../core/initialize.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
10
18
  const LIMITS = {
11
19
  handoffBytes: 2 * 1024 * 1024,
12
20
  manifestBytes: 512 * 1024,
@@ -1,4 +1,12 @@
1
- const validateSemantics = await import(`./validate-semantics.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ const validateSemantics = await import(__rewriteRelativeImportExtension(`./validate-semantics.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
2
10
  const DEFAULT_SEVERITIES = {
3
11
  H001: "error",
4
12
  H002: "error",
@@ -0,0 +1,4 @@
1
+ /** Shared planner guidance and admission enums; no inferred acceptance or dispatch authority. */
2
+ export declare const GOAL_DELIVERY_INTENTS: readonly ["design", "registration", "implementation", "repair", "controlled-change"];
3
+ export declare const GOAL_DELIVERY_MODES: readonly ["planning-only", "mvp-first", "repair-first", "controlled-change"];
4
+ export declare const GOAL_DECLARATION_FORMAT: string;
@@ -0,0 +1,33 @@
1
+ /** Shared planner guidance and admission enums; no inferred acceptance or dispatch authority. */
2
+ export const GOAL_DELIVERY_INTENTS = ["design", "registration", "implementation", "repair", "controlled-change"];
3
+ export const GOAL_DELIVERY_MODES = ["planning-only", "mvp-first", "repair-first", "controlled-change"];
4
+ export const GOAL_DECLARATION_FORMAT = `Goal declaration belongs in the Task prompt, not in the handoff JSON or operation manifest.
5
+ delivery_intent must be exactly one of: ${GOAL_DELIVERY_INTENTS.join(" | ")}. Never use prose or a boolean.
6
+ delivery_mode is a separate optional enum: ${GOAL_DELIVERY_MODES.join(" | ")}.
7
+ Use flat key: value lines. Each criterion begins with its own goal_criterion_id: line.
8
+ Repeat the entire flat criterion block for multiple criteria. Do not use goal_criteria, goal_acceptance.criteria,
9
+ YAML object/list wrappers, inline { goal_criterion_id: ... }, or JSON objects for these blocks.
10
+ Replace every placeholder with the accepted task's actual value; preserve its acceptance and validation:
11
+ goal_acceptance_fingerprint: <sha256: followed by exactly 64 lowercase hexadecimal characters>
12
+ delivery_intent: implementation
13
+ delivery_mode: mvp-first
14
+ usable_path_established: false
15
+ controlled_change: false
16
+ goal_budget_units: <accepted positive integer>
17
+ goal_criterion_id: <stable criterion id>
18
+ goal_target: <requested behavior>
19
+ goal_entrypoint: <real entrypoint>
20
+ goal_workload: <requested workload>
21
+ goal_oracle_coverage: ["<actual oracle>"]
22
+ goal_build_boundary: <included | excluded | not-applicable>
23
+ goal_source: <fixed source label>
24
+ goal_candidate: <fixed candidate label>
25
+ goal_source_binding: current-protected
26
+ goal_candidate_binding: current-protected
27
+ goal_fixture: <actual fixture identity>
28
+ goal_proof_scope: requested-full
29
+ goal_expected_outcome: pass
30
+ goal_validation_command: <exact operation manifest validation command>
31
+ The example's implementation/mvp-first selections apply only to an implementation without its usable path;
32
+ select other enum values only from the accepted request. A declaration rejection launches no child.
33
+ Repair the Task prompt's named fields, not unrelated files; retry only with the corrected declaration.`;
@@ -1,9 +1,17 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
1
9
  import { createHash } from "node:crypto";
2
10
  import { constants } from "node:fs";
3
11
  import { lstat, mkdir, open, readFile, realpath, rm, rmdir, stat } from "node:fs/promises";
4
12
  import { homedir } from "node:os";
5
13
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
6
- const assets = await import(`../runtime-assets.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
14
+ const assets = await import(__rewriteRelativeImportExtension(`../runtime-assets.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
7
15
  const { runtimeAssets } = assets;
8
16
  const OPEN_CODE_DIRECTORY = ".opencode";
9
17
  const VERSION_MARKER = `${OPEN_CODE_DIRECTORY}/sortie-dogs.version`;
@@ -1,4 +1,12 @@
1
- const pathUtils = await import(`./path.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ const pathUtils = await import(__rewriteRelativeImportExtension(`./path.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
2
10
  const MESSAGES = {
3
11
  H001_PATH_RELATIVE: "Manifest path must be a valid repository-relative path.",
4
12
  H011_VALIDATION_MISSING: "A required manifest validation has not passed.",
@@ -1,4 +1,12 @@
1
- const pathUtils = await import(`./path.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`);
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ const pathUtils = await import(__rewriteRelativeImportExtension(`./path.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`));
2
10
  const HASH_LENGTHS = {
3
11
  sha256: 64,
4
12
  sha512: 128,
@@ -60,6 +60,8 @@ export declare function isRemoteMutation(tool: string, args: unknown): boolean;
60
60
  * same command written with different spacing compares equal.
61
61
  */
62
62
  export declare function normalizeCommand(command: string): string;
63
+ /** Canonicalize only an exact or unambiguous basename-shortened sequence of declared validations. */
64
+ export declare function canonicalDeclaredValidationSequence(command: string, declared: ReadonlySet<string>): string | undefined;
63
65
  /** Unbound sessions may invoke only tools whose complete input is known to be read-only. */
64
66
  export declare function isKnownReadOnlyTool(tool: string, args: unknown, additionalReadOnlyTools?: ReadonlySet<string>): boolean;
65
67
  /** Canonical scopes let sibling worker bindings reject equal or ancestor ownership. */
@@ -797,6 +797,33 @@ export function normalizeCommand(command) {
797
797
  ? normalized
798
798
  : `${callOperator[2]}${normalized.slice(callOperator[0].length)}`;
799
799
  }
800
+ function declaredCommandMatch(segment, declared) {
801
+ const normalized = normalizeCommand(segment);
802
+ const exact = declared.filter((command) => normalizeCommand(command) === normalized);
803
+ if (exact.length === 1)
804
+ return normalizeCommand(exact[0]);
805
+ const firstSpace = normalized.indexOf(" ");
806
+ const executable = firstSpace === -1 ? normalized : normalized.slice(0, firstSpace);
807
+ const suffix = firstSpace === -1 ? "" : normalized.slice(firstSpace);
808
+ if (isAbsolute(executable))
809
+ return undefined;
810
+ const equivalent = declared.map(normalizeCommand).filter((command) => {
811
+ const boundary = command.indexOf(" ");
812
+ const declaredExecutable = boundary === -1 ? command : command.slice(0, boundary);
813
+ const declaredSuffix = boundary === -1 ? "" : command.slice(boundary);
814
+ return isAbsolute(declaredExecutable) && basename(declaredExecutable) === executable && declaredSuffix === suffix;
815
+ });
816
+ return equivalent.length === 1 ? equivalent[0] : undefined;
817
+ }
818
+ /** Canonicalize only an exact or unambiguous basename-shortened sequence of declared validations. */
819
+ export function canonicalDeclaredValidationSequence(command, declared) {
820
+ const normalized = normalizeCommand(command);
821
+ const segments = shellSegments(command, "posix").map((segment) => segment.trim()).filter(Boolean);
822
+ if (segments.length === 0 || normalized !== segments.map(normalizeCommand).join(" && "))
823
+ return undefined;
824
+ const canonical = segments.map((segment) => declaredCommandMatch(segment, [...declared]));
825
+ return canonical.every((segment) => segment !== undefined) ? canonical.join(" && ") : undefined;
826
+ }
800
827
  /** Unbound sessions may invoke only tools whose complete input is known to be read-only. */
801
828
  export function isKnownReadOnlyTool(tool, args, additionalReadOnlyTools = new Set()) {
802
829
  const name = tool.toLowerCase();
@@ -1,7 +1,16 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
1
9
  import { createHash, randomUUID } from "node:crypto";
2
- import { lstat, mkdir, open, readFile, readdir, realpath, rm, stat } from "node:fs/promises";
10
+ import { lstat, mkdir, open, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
3
11
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
12
  import { RUNTIME_ASSET_VERSION } from "../asset-version.js";
13
+ import { GOAL_DECLARATION_FORMAT, GOAL_DELIVERY_INTENTS, GOAL_DELIVERY_MODES } from "../core/goal-declaration-format.js";
5
14
  import { ACCEPTANCE_CONTINUITY_AUTHORITY, ACCEPTANCE_CONTINUITY_EXTENSION, ACCEPTANCE_CONTINUITY_SCHEMA_VERSION, acceptanceContinuityFingerprint, inspectAcceptanceContinuity, normalizeAcceptanceCriteria, } from "../core/acceptance-continuity.js";
6
15
  import { resolveGlobalConfigRoot } from "../core/initialize.js";
7
16
  import { admitLunaFabric } from "../core/luna-fabric-contract.js";
@@ -19,7 +28,7 @@ import { validateManifest } from "../core/validate-manifest.js";
19
28
  import { safeSchemaPointer, validateHandoffSchema, validateOperationManifestSchema, } from "../core/validate-schema.js";
20
29
  import { DEFAULT_PLUGIN_OPTIONS, resolvePluginConfiguration, resolvePluginConfigurationSourcesWithGlobal, } from "./config.js";
21
30
  import { CONTINUATION_CAPABILITY, CONTINUATION_MARKER, ROLLOVER_MARKER, createContinuationHooks, } from "./continuation.js";
22
- import { WriteDeniedError, canonicalManifestReadScopes, canonicalManifestWriteScopes, createProjectPaths, createWriteGate, describeUnclassifiedCommand, extractWritePaths, bootstrapWritePaths, isGitMutation, isKnownReadOnlyTool, isRemoteMutation, normalizeCommand, resolveProjectRoot, safePath, writeScopesOverlap, } from "./gate.js";
31
+ import { WriteDeniedError, canonicalManifestReadScopes, canonicalManifestWriteScopes, canonicalDeclaredValidationSequence, createProjectPaths, createWriteGate, describeUnclassifiedCommand, extractWritePaths, bootstrapWritePaths, isGitMutation, isKnownReadOnlyTool, isRemoteMutation, normalizeCommand, resolveProjectRoot, safePath, writeScopesOverlap, } from "./gate.js";
23
32
  import { BACKLOG_DRAIN_CAPABILITY, FastLaneController } from "./fast-lane.js";
24
33
  import { createModelRoutingHook, } from "./model-routing-hook.js";
25
34
  import { createTaskResultRepairHook, lastAssistantText, markConsultationFallbackRetry, taskChildSessionID, } from "./task-result-repair.js";
@@ -112,7 +121,8 @@ export class HandoffDeniedError extends Error {
112
121
  const defects = normalizeDefects(options?.defects);
113
122
  super(`Handoff denied for "${safePath(path)}": handoff and operation manifest contract.` +
114
123
  (defects.length === 0 ? "" : ` Defects: ${describeDefects(defects)}.`) +
115
- " Correct the registered handoff or its operation manifest, then read the handoff again.", options);
124
+ (path === "<goal-declaration>" ? ` ${GOAL_DECLARATION_FORMAT}` :
125
+ " Correct the registered handoff or its operation manifest, then read the handoff again."), options);
116
126
  this.name = "HandoffDeniedError";
117
127
  this.reason = reason;
118
128
  this.defects = defects;
@@ -728,6 +738,30 @@ function taskAcceptanceCriteria(text, key = "acceptance") {
728
738
  }
729
739
  return values.length === 0 ? undefined : normalizeAcceptanceCriteria(values);
730
740
  }
741
+ function canonicalTaskAcceptance(text, criteria) {
742
+ const lines = text.split(/\r?\n/u);
743
+ const headers = lines.flatMap((line, index) => /^([\t ]*)acceptance[\t ]*:/iu.exec(line) === null ? [] : [index]);
744
+ if (headers.length !== 1)
745
+ return undefined;
746
+ const index = headers[0];
747
+ const indent = /^[\t ]*/u.exec(lines[index])[0];
748
+ const headerIndent = indent.replaceAll("\t", " ").length;
749
+ let end = index + 1;
750
+ if (/^\s*acceptance\s*:\s*$/iu.test(lines[index])) {
751
+ while (end < lines.length) {
752
+ const line = lines[end];
753
+ if (line.trim().length === 0) {
754
+ end += 1;
755
+ continue;
756
+ }
757
+ if (/^\s*/u.exec(line)[0].replaceAll("\t", " ").length <= headerIndent)
758
+ break;
759
+ end += 1;
760
+ }
761
+ }
762
+ lines.splice(index, end - index, `${indent}acceptance: ${JSON.stringify(criteria)}`);
763
+ return lines.join("\n");
764
+ }
731
765
  function taskContractText(text) {
732
766
  const lines = text.split(/\r?\n/u);
733
767
  const digestIndexes = lines.flatMap((line, index) => /^\s*context_digest\s*:\s*$/iu.test(line) ? [index] : []);
@@ -1058,7 +1092,7 @@ export const SortieDogsPlugin = async (input, options) => {
1058
1092
  throw new Error("invalid-child-lifecycle-check-wait");
1059
1093
  }
1060
1094
  const pluginModuleName = "@opencode-ai/plugin";
1061
- const pluginModule = await import(pluginModuleName).catch(() => undefined);
1095
+ const pluginModule = await import(__rewriteRelativeImportExtension(pluginModuleName)).catch(() => undefined);
1062
1096
  const toolCandidate = pluginModule?.tool;
1063
1097
  const toolSchema = typeof toolCandidate === "function"
1064
1098
  ? toolCandidate.schema
@@ -1328,8 +1362,11 @@ export const SortieDogsPlugin = async (input, options) => {
1328
1362
  const unitID = authorization.taskID;
1329
1363
  const unitBound = unitID !== undefined && goal.outstanding_reservations.some((reservation) => reservation.unit_id === unitID && reservation.session_id === identity.parentID);
1330
1364
  const criteria = goal.acceptance_contract?.criteria.filter((criterion) => criterion.validation_command === rawCommand && criterion.expected_outcome === "pass") ?? [];
1331
- if (!unitBound || criteria.length === 0)
1365
+ if (!unitBound)
1332
1366
  throw new Error("SORTIE_VALIDATION_BUDGET_DENIED: requirement-unbound");
1367
+ // Exact generation and formatting checks may support acceptance without proving a criterion.
1368
+ if (criteria.length === 0)
1369
+ return;
1333
1370
  const scope = criteria.every((criterion) => criterion.proof_scope === "requested-full") ? "full" : "targeted";
1334
1371
  const request = { run_id: goal.goal_id, operation_id: toolInput.callID,
1335
1372
  source_snapshot: snapshot.source, candidate: snapshot.candidate, command: [rawCommand], scope,
@@ -1481,6 +1518,10 @@ export const SortieDogsPlugin = async (input, options) => {
1481
1518
  return state.receipt ?? undefined;
1482
1519
  const records = (await ledger.readGoal()).records;
1483
1520
  const settledEvidence = records.flatMap(({ event }) => event.kind === "unit.settled" ? event.evidence : []);
1521
+ if (status === "succeeded" && (state.acceptance_contract === null ||
1522
+ state.acceptance_contract.criteria.length === 0 ||
1523
+ !state.acceptance_contract.criteria.every(({ criterion_id }) => state.satisfied_criteria.includes(criterion_id))))
1524
+ return undefined;
1484
1525
  if (status === "succeeded" && state.acceptance_contract !== null) {
1485
1526
  for (const criterion of state.acceptance_contract.criteria) {
1486
1527
  if (criterion.source_binding !== "current-protected" && criterion.candidate_binding !== "current-protected")
@@ -1674,14 +1715,14 @@ export const SortieDogsPlugin = async (input, options) => {
1674
1715
  defects.push(contractDefect("contract", "/goal_acceptance_fingerprint", "goal_fingerprint_format"));
1675
1716
  }
1676
1717
  const intent = handoffValue(entries, ["delivery_intent"]);
1677
- const intents = ["design", "registration", "implementation", "repair", "controlled-change"];
1718
+ const intents = GOAL_DELIVERY_INTENTS;
1678
1719
  if (intent === undefined)
1679
1720
  defects.push(contractDefect("contract", "/delivery_intent", "delivery_intent_missing"));
1680
1721
  else if (!intents.includes(intent)) {
1681
1722
  defects.push(contractDefect("contract", "/delivery_intent", "delivery_intent_invalid"));
1682
1723
  }
1683
1724
  const mode = handoffValue(entries, ["delivery_mode"]);
1684
- const modes = ["planning-only", "mvp-first", "repair-first", "controlled-change"];
1725
+ const modes = GOAL_DELIVERY_MODES;
1685
1726
  if (mode !== undefined && !modes.includes(mode)) {
1686
1727
  defects.push(contractDefect("contract", "/delivery_mode", "delivery_mode_invalid"));
1687
1728
  }
@@ -5293,7 +5334,7 @@ export const SortieDogsPlugin = async (input, options) => {
5293
5334
  ? undefined
5294
5335
  : await terminalGoalFromHostText(textInput.sessionID, textOutput.text);
5295
5336
  if (runOutcome === "DONE" && terminal !== undefined && (terminal.delivery === "running" ||
5296
- (terminal.receipt === undefined && terminal.goal !== undefined && terminal.goal.acceptance_contract !== null))) {
5337
+ (terminal.receipt === undefined && terminal.goal !== undefined && terminal.goal.goal_id !== null))) {
5297
5338
  textOutput.text = replaceDoneTerminalStatus(textOutput.text, terminal.delivery === "running"
5298
5339
  ? "status: IN_PROGRESS — durable delivery active; same sessionでjoinまたはstale reconcileが必要"
5299
5340
  : "status: IN_PROGRESS\ngoal_control: accepted criteria remain unproved");
@@ -5855,6 +5896,13 @@ export const SortieDogsPlugin = async (input, options) => {
5855
5896
  diagnosis.tools.add(toolInput.callID);
5856
5897
  return;
5857
5898
  }
5899
+ if (isRecord(output.args) && typeof output.args.command === "string") {
5900
+ const authorization = sessionAuthorizations.get(toolInput.sessionID);
5901
+ const canonical = authorization === undefined ? undefined
5902
+ : canonicalDeclaredValidationSequence(output.args.command, authorization.validationCommands);
5903
+ if (canonical !== undefined)
5904
+ output.args.command = canonical;
5905
+ }
5858
5906
  await recordHostGoalStart(toolInput, output);
5859
5907
  const coordinatorRoot = isCoordinatorSession(toolInput.sessionID) || await recoverCoordinatorRoot(toolInput.sessionID);
5860
5908
  const readonlyDiagnosis = coordinatorRoot && toolInput.tool === "task" && isRecord(output.args)
@@ -6073,21 +6121,31 @@ export const SortieDogsPlugin = async (input, options) => {
6073
6121
  });
6074
6122
  }
6075
6123
  if (assetVersionStatus === "current") {
6076
- const ledger = identity.acceptanceContinuity;
6077
- if (ledger === undefined) {
6124
+ const inspectedLedger = identity.acceptanceContinuity;
6125
+ if (inspectedLedger === undefined) {
6078
6126
  throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
6079
6127
  defects: [contractDefect("handoff", "/ext/sortie-dogs~1acceptance-continuity", `acceptance_continuity_${identity.acceptanceContinuityError ?? "missing"}`)],
6080
6128
  });
6081
6129
  }
6130
+ let ledger = inspectedLedger;
6082
6131
  const criteria = taskAcceptanceCriteria(contractPrompt);
6083
6132
  if (taskIDs.length !== 1 || taskIDs[0] !== identity.handoffID ||
6084
- ledger.task_id !== identity.handoffID || criteria === undefined ||
6085
- criteria.length !== ledger.criteria.length ||
6086
- criteria.some((criterion, index) => criterion !== ledger.criteria[index])) {
6133
+ ledger.task_id !== identity.handoffID) {
6087
6134
  throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
6088
6135
  defects: [contractDefect("contract", "/acceptance", "acceptance_continuity_mismatch")],
6089
6136
  });
6090
6137
  }
6138
+ if (criteria === undefined || criteria.length !== ledger.criteria.length ||
6139
+ criteria.some((criterion, index) => criterion !== ledger.criteria[index])) {
6140
+ const canonical = canonicalTaskAcceptance(prompt, ledger.criteria);
6141
+ if (canonical === undefined) {
6142
+ throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
6143
+ defects: [contractDefect("contract", "/acceptance", "acceptance_continuity_mismatch")],
6144
+ });
6145
+ }
6146
+ output.args.prompt = canonical;
6147
+ prompt = canonical;
6148
+ }
6091
6149
  const previous = rootAcceptanceContinuity.get(toolInput.sessionID);
6092
6150
  if (previous === undefined) {
6093
6151
  if (ledger.parent_fingerprint !== "none" &&
@@ -6109,6 +6167,24 @@ export const SortieDogsPlugin = async (input, options) => {
6109
6167
  previous.criteria.every((criterion, index) => criterion === ledger.criteria[index]);
6110
6168
  const strictAppend = ledger.criteria.length > previous.criteria.length &&
6111
6169
  previous.criteria.every((criterion, index) => criterion === ledger.criteria[index]);
6170
+ // Serial parent identity is controller-owned. Repair only an omitted link on
6171
+ // an otherwise exact carry-forward/append; explicit conflicting links still fail.
6172
+ // Persist it so the worker's Read and later recovery see the same contract.
6173
+ if (reservedParallelDescriptor === undefined && ledger.parent_fingerprint === "none" &&
6174
+ (exactCarryForward || strictAppend)) {
6175
+ const handoff = JSON.parse(await readFile(handoffPaths[0], "utf8"));
6176
+ const validated = validateHandoffSchema(handoff);
6177
+ const current = validated.ok ? inspectAcceptanceContinuity(validated.value).ledger : undefined;
6178
+ if (current === undefined || JSON.stringify(current) !== JSON.stringify(ledger)) {
6179
+ throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
6180
+ defects: [contractDefect("handoff", "/ext/sortie-dogs~1acceptance-continuity", "acceptance_parent_continuity_mismatch")],
6181
+ });
6182
+ }
6183
+ ledger = { ...ledger, parent_fingerprint: previous.fingerprint };
6184
+ handoff.ext[ACCEPTANCE_CONTINUITY_EXTENSION] = ledger;
6185
+ await writeFile(handoffPaths[0], `${JSON.stringify(handoff, null, 2)}\n`);
6186
+ await inspect(handoffPaths[0], undefined, { report: true });
6187
+ }
6112
6188
  if (ledger.parent_fingerprint !== previous.fingerprint || (!exactCarryForward && !strictAppend)) {
6113
6189
  throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
6114
6190
  defects: [contractDefect("handoff", "/ext/sortie-dogs~1acceptance-continuity", "acceptance_parent_continuity_mismatch")],
@@ -6255,6 +6331,10 @@ export const SortieDogsPlugin = async (input, options) => {
6255
6331
  // boundary; unrelated commands continue through the ordinary write gate.
6256
6332
  if (activeState?.parallel === "valid" && command !== undefined && authorization?.validationCommands.has(command) === true)
6257
6333
  throw new WriteDeniedError("parallel-validation", "<parallel-unit>");
6334
+ const declaredSequence = command === undefined || authorization === undefined ? undefined
6335
+ : canonicalDeclaredValidationSequence(command, authorization.validationCommands);
6336
+ if (activeState?.parallel !== "valid" && declaredSequence !== undefined)
6337
+ return;
6258
6338
  if (activeState?.parallel === "valid") {
6259
6339
  const extracted = extractWritePaths(toolInput.tool, output.args);
6260
6340
  const relativeWrite = extracted.paths.find((path) => !isAbsolute(path));