sortie-dogs 0.9.1 → 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.1](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.1)
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.76-goal-control-report-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.76-goal-control-report-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,
@@ -21,7 +21,7 @@ export type ParallelDispatchPrepareResult = {
21
21
  readonly reason: "scope-overlap" | "dependency-ambiguous";
22
22
  };
23
23
  /** Runtime capacity and mapping limits that the pure admission policy cannot observe. */
24
- export type FabricDispatchSolReason = LunaFabricSolReason | "unit-count-exceeds-capacity" | "concurrent-scope-overlap" | "contract-unmappable";
24
+ export type FabricDispatchSolReason = LunaFabricSolReason | "unit-count-exceeds-capacity" | "concurrent-scope-overlap" | "contract-unmappable" | "target-checked-out";
25
25
  export type ParallelDispatchFabricPrepareResult = {
26
26
  readonly status: "prepared";
27
27
  readonly snapshot: ParallelDispatchSnapshot;
@@ -154,7 +154,7 @@ export declare class ParallelDispatchCoordinator {
154
154
  private acquire;
155
155
  private git;
156
156
  private readRef;
157
- private targetCheckedOut;
157
+ targetCheckedOut(targetRef: string): Promise<boolean>;
158
158
  private deleteFabricSourceRefs;
159
159
  private ensureCandidateRef;
160
160
  private withPrepareAuthority;
@@ -2705,7 +2705,9 @@ export class ParallelDispatchCoordinator {
2705
2705
  await rm(temporary, { force: true }).catch(() => undefined);
2706
2706
  }
2707
2707
  }
2708
- async acquire(scope = STATE_SCOPE, ttlMs = 10 * 60_000) {
2708
+ // Heartbeats retain live ownership. A crashed CLI must not leave a lease whose expiry exceeds
2709
+ // the next CLI's acquisition budget; keep room for mutex acquisition and filesystem latency.
2710
+ async acquire(scope = STATE_SCOPE, ttlMs = Math.floor(LOCK_TIMEOUT_MS / 2)) {
2709
2711
  const deadline = Date.now() + LOCK_TIMEOUT_MS;
2710
2712
  while (true) {
2711
2713
  try {
@@ -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")
@@ -1518,13 +1559,45 @@ export const SortieDogsPlugin = async (input, options) => {
1518
1559
  async function terminalGoalFromHostText(sessionID, text) {
1519
1560
  const outcome = terminalRunOutcome(text);
1520
1561
  if (outcome === undefined || !isCoordinatorSession(sessionID)) {
1521
- return { outcome, goal: undefined, receipt: undefined };
1562
+ return { outcome, goal: undefined, receipt: undefined, delivery: "ready" };
1563
+ }
1564
+ let delivery = "ready";
1565
+ const coordinator = await getParallelCoordinator().catch(() => undefined);
1566
+ let parallel = await coordinator?.snapshot(sessionID).catch(() => undefined);
1567
+ if (parallel !== undefined && !parallel.archived) {
1568
+ await restoreChildLifecycles(sessionID, parallel).catch(() => undefined);
1569
+ const knownCalls = coordinatorTaskCalls.get(sessionID) ?? new Set();
1570
+ const running = parallel.tasks.filter(({ phase }) => phase === "running");
1571
+ if (running.length > 0 && running.every(({ call_id }) => call_id !== null && !knownCalls.has(call_id))) {
1572
+ const status = input.client?.session?.status;
1573
+ const response = status === undefined ? undefined : await status.call(input.client.session, {
1574
+ query: { directory: input.directory },
1575
+ }).catch(() => undefined);
1576
+ const payload = isRecord(response) && "data" in response ? response.data : response;
1577
+ const statuses = isRecord(payload) ? payload : undefined;
1578
+ const settled = statuses !== undefined && running.every(({ child_session_id }) => {
1579
+ const observed = child_session_id === null ? undefined : statuses[child_session_id];
1580
+ return isRecord(observed) && observed.type === "idle";
1581
+ });
1582
+ if (settled)
1583
+ parallel = await coordinator.reconcile(sessionID, knownCalls, parallel.run_id).catch(() => parallel);
1584
+ }
1585
+ if (parallel !== undefined && !parallel.archived)
1586
+ delivery = "running";
1522
1587
  }
1588
+ if (parallel?.archived === true && parallel.terminal_reason !== "completed")
1589
+ delivery = "failed";
1523
1590
  let goal = await currentGoal(sessionID).catch(() => undefined);
1524
1591
  let receipt = goal?.receipt ?? undefined;
1525
1592
  const proved = goal?.acceptance_contract !== null && goal?.acceptance_contract !== undefined &&
1526
1593
  goal.acceptance_contract.criteria.every(({ criterion_id }) => goal.satisfied_criteria.includes(criterion_id));
1527
- if (receipt === undefined && proved) {
1594
+ if (delivery === "running") {
1595
+ return { outcome, goal, receipt, delivery };
1596
+ }
1597
+ if (receipt === undefined && delivery === "failed") {
1598
+ receipt = await terminalGoal(sessionID, "stopped", "stopped").catch(() => undefined);
1599
+ }
1600
+ else if (receipt === undefined && proved) {
1528
1601
  receipt = await terminalGoal(sessionID, "completed", "succeeded").catch(() => undefined);
1529
1602
  }
1530
1603
  else if (receipt === undefined && outcome === "DONE") {
@@ -1546,7 +1619,7 @@ export const SortieDogsPlugin = async (input, options) => {
1546
1619
  goal = await currentGoal(sessionID).catch(() => goal);
1547
1620
  if (receipt !== undefined)
1548
1621
  rootAcceptanceContinuity.delete(sessionID);
1549
- return { outcome, goal, receipt };
1622
+ return { outcome, goal, receipt, delivery };
1550
1623
  }
1551
1624
  function goalDeclarationContract(prompt) {
1552
1625
  const lines = prompt.split(/\r?\n/u);
@@ -1642,14 +1715,14 @@ export const SortieDogsPlugin = async (input, options) => {
1642
1715
  defects.push(contractDefect("contract", "/goal_acceptance_fingerprint", "goal_fingerprint_format"));
1643
1716
  }
1644
1717
  const intent = handoffValue(entries, ["delivery_intent"]);
1645
- const intents = ["design", "registration", "implementation", "repair", "controlled-change"];
1718
+ const intents = GOAL_DELIVERY_INTENTS;
1646
1719
  if (intent === undefined)
1647
1720
  defects.push(contractDefect("contract", "/delivery_intent", "delivery_intent_missing"));
1648
1721
  else if (!intents.includes(intent)) {
1649
1722
  defects.push(contractDefect("contract", "/delivery_intent", "delivery_intent_invalid"));
1650
1723
  }
1651
1724
  const mode = handoffValue(entries, ["delivery_mode"]);
1652
- const modes = ["planning-only", "mvp-first", "repair-first", "controlled-change"];
1725
+ const modes = GOAL_DELIVERY_MODES;
1653
1726
  if (mode !== undefined && !modes.includes(mode)) {
1654
1727
  defects.push(contractDefect("contract", "/delivery_mode", "delivery_mode_invalid"));
1655
1728
  }
@@ -3169,6 +3242,9 @@ export const SortieDogsPlugin = async (input, options) => {
3169
3242
  contract_fingerprint: structuralAdmission.contract_fingerprint, experience: experience.trace });
3170
3243
  }
3171
3244
  const coordinator = await getParallelCoordinator();
3245
+ if (await coordinator.targetCheckedOut(`refs/heads/${structuralAdmission.contract.provenance.target_branch}`)) {
3246
+ return JSON.stringify({ status: "sol-serial", reason: "target-checked-out", experience: experience.trace });
3247
+ }
3172
3248
  const result = await coordinator.prepareFabric(contract, ownerRoot, executionPlanPath === undefined ? undefined : await readJson(resolve(executionPlanPath), INPUT_LIMITS.parallel));
3173
3249
  if (result.status === "sol-serial")
3174
3250
  return JSON.stringify({ ...result, experience: experience.trace });
@@ -5257,13 +5333,18 @@ export const SortieDogsPlugin = async (input, options) => {
5257
5333
  const terminal = runOutcome === undefined || !isCoordinatorSession(textInput.sessionID)
5258
5334
  ? undefined
5259
5335
  : await terminalGoalFromHostText(textInput.sessionID, textOutput.text);
5260
- if (runOutcome === "DONE" && terminal?.goal !== undefined &&
5261
- terminal.goal.acceptance_contract !== null && terminal.receipt === undefined) {
5262
- textOutput.text = replaceDoneTerminalStatus(textOutput.text, "status: IN_PROGRESS\ngoal_control: accepted criteria remain unproved");
5336
+ if (runOutcome === "DONE" && terminal !== undefined && (terminal.delivery === "running" ||
5337
+ (terminal.receipt === undefined && terminal.goal !== undefined && terminal.goal.goal_id !== null))) {
5338
+ textOutput.text = replaceDoneTerminalStatus(textOutput.text, terminal.delivery === "running"
5339
+ ? "status: IN_PROGRESS — durable delivery active; same sessionでjoinまたはstale reconcileが必要"
5340
+ : "status: IN_PROGRESS\ngoal_control: accepted criteria remain unproved");
5263
5341
  }
5264
- if (runOutcome !== "DONE" && terminal?.receipt?.status === "succeeded") {
5342
+ if (runOutcome !== "DONE" && terminal?.delivery === "ready" && terminal.receipt?.status === "succeeded") {
5265
5343
  textOutput.text = replaceTerminalStatus(textOutput.text, "status: DONE");
5266
5344
  }
5345
+ if (runOutcome === "DONE" && terminal?.delivery === "failed") {
5346
+ textOutput.text = replaceTerminalStatus(textOutput.text, "status: INTERRUPTED — durable delivery failed");
5347
+ }
5267
5348
  if (runOutcome !== undefined && isCoordinatorSession(textInput.sessionID)) {
5268
5349
  textOutput.text = sanitizeTerminalReport(textOutput.text);
5269
5350
  }
@@ -5815,6 +5896,13 @@ export const SortieDogsPlugin = async (input, options) => {
5815
5896
  diagnosis.tools.add(toolInput.callID);
5816
5897
  return;
5817
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
+ }
5818
5906
  await recordHostGoalStart(toolInput, output);
5819
5907
  const coordinatorRoot = isCoordinatorSession(toolInput.sessionID) || await recoverCoordinatorRoot(toolInput.sessionID);
5820
5908
  const readonlyDiagnosis = coordinatorRoot && toolInput.tool === "task" && isRecord(output.args)
@@ -6033,21 +6121,31 @@ export const SortieDogsPlugin = async (input, options) => {
6033
6121
  });
6034
6122
  }
6035
6123
  if (assetVersionStatus === "current") {
6036
- const ledger = identity.acceptanceContinuity;
6037
- if (ledger === undefined) {
6124
+ const inspectedLedger = identity.acceptanceContinuity;
6125
+ if (inspectedLedger === undefined) {
6038
6126
  throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
6039
6127
  defects: [contractDefect("handoff", "/ext/sortie-dogs~1acceptance-continuity", `acceptance_continuity_${identity.acceptanceContinuityError ?? "missing"}`)],
6040
6128
  });
6041
6129
  }
6130
+ let ledger = inspectedLedger;
6042
6131
  const criteria = taskAcceptanceCriteria(contractPrompt);
6043
6132
  if (taskIDs.length !== 1 || taskIDs[0] !== identity.handoffID ||
6044
- ledger.task_id !== identity.handoffID || criteria === undefined ||
6045
- criteria.length !== ledger.criteria.length ||
6046
- criteria.some((criterion, index) => criterion !== ledger.criteria[index])) {
6133
+ ledger.task_id !== identity.handoffID) {
6047
6134
  throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
6048
6135
  defects: [contractDefect("contract", "/acceptance", "acceptance_continuity_mismatch")],
6049
6136
  });
6050
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
+ }
6051
6149
  const previous = rootAcceptanceContinuity.get(toolInput.sessionID);
6052
6150
  if (previous === undefined) {
6053
6151
  if (ledger.parent_fingerprint !== "none" &&
@@ -6069,6 +6167,24 @@ export const SortieDogsPlugin = async (input, options) => {
6069
6167
  previous.criteria.every((criterion, index) => criterion === ledger.criteria[index]);
6070
6168
  const strictAppend = ledger.criteria.length > previous.criteria.length &&
6071
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
+ }
6072
6188
  if (ledger.parent_fingerprint !== previous.fingerprint || (!exactCarryForward && !strictAppend)) {
6073
6189
  throw new HandoffDeniedError("contract-invalid", handoffPaths[0], {
6074
6190
  defects: [contractDefect("handoff", "/ext/sortie-dogs~1acceptance-continuity", "acceptance_parent_continuity_mismatch")],
@@ -6215,6 +6331,10 @@ export const SortieDogsPlugin = async (input, options) => {
6215
6331
  // boundary; unrelated commands continue through the ordinary write gate.
6216
6332
  if (activeState?.parallel === "valid" && command !== undefined && authorization?.validationCommands.has(command) === true)
6217
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;
6218
6338
  if (activeState?.parallel === "valid") {
6219
6339
  const extracted = extractWritePaths(toolInput.tool, output.args);
6220
6340
  const relativeWrite = extracted.paths.find((path) => !isAbsolute(path));