sortie-dogs 0.9.0 → 0.9.2

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.0](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.0)
27
+ Release: [v0.9.2](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.2)
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.75-sequential-acceptance-parent-v1";
5
+ export declare const RUNTIME_ASSET_VERSION = "0.3.77-terminal-delivery-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.75-sequential-acceptance-parent-v1";
5
+ export const RUNTIME_ASSET_VERSION = "0.3.77-terminal-delivery-v1";
@@ -143,6 +143,8 @@ export type GoalFlightEvent = (GoalEventBase & {
143
143
  readonly goal_id: string;
144
144
  readonly unit_id: string;
145
145
  readonly disposition: "succeeded" | "failed" | "cancelled";
146
+ /** Missing on pre-v0.9.1 ledgers and therefore interpreted as an acceptance result. */
147
+ readonly result_class?: "acceptance" | "process-defect" | "interrupted";
146
148
  readonly progress_fingerprint: string | null;
147
149
  readonly evidence: readonly GoalEvidence[];
148
150
  readonly elapsed_ms: number | null;
@@ -178,7 +178,11 @@ export function reduceGoalFlight(records) {
178
178
  .filter((criterionID) => !state.satisfied_criteria.includes(criterionID));
179
179
  const progress = newCriteria.length > 0;
180
180
  requireState(event.progress_fingerprint === (progress ? goalFingerprint(event.evidence) : null), "evidence", "Progress fingerprint does not represent newly accepted evidence.");
181
- const nextNoProgress = progress ? 0 : state.no_progress_results + 1;
181
+ const acceptanceFailure = event.disposition === "failed" &&
182
+ (event.result_class === undefined || event.result_class === "acceptance");
183
+ const nextNoProgress = progress ? 0 : acceptanceFailure
184
+ ? state.no_progress_results + 1
185
+ : state.no_progress_results;
182
186
  const elapsed = state.consumed_time_ms === null || event.elapsed_ms === null ? null : state.consumed_time_ms + event.elapsed_ms;
183
187
  const cost = state.consumed_cost_usd === null || event.cost_usd === null ? null : state.consumed_cost_usd + event.cost_usd;
184
188
  state = { ...state, consumed_units: state.consumed_units + 1, consumed_time_ms: elapsed, consumed_cost_usd: cost,
@@ -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 {
@@ -36,7 +36,7 @@ import { TerminalRescueRuntime } from "../core/terminal-rescue-runtime.js";
36
36
  import { OpenCodeTerminalRescueHost } from "./terminal-rescue-host.js";
37
37
  import { AdaptiveRemediationRuntime, AdaptiveRunFlightLineage } from "../core/adaptive-remediation-runtime.js";
38
38
  import { DEFAULT_ADAPTIVE_REMEDIATION_MODEL, GitAdaptiveRemediationHost, OpenCodeAdaptiveRemediationProvider } from "./adaptive-remediation-host.js";
39
- import { collectRunMetrics, createSortieResult, insertRunMetrics, insertSortieResult, terminalRunOutcome } from "./run-metrics.js";
39
+ import { collectRunMetrics, createSortieResult, insertRunMetrics, insertSortieResult, replaceDoneTerminalStatus, replaceTerminalStatus, sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
40
40
  const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024, parallel: 512 * 1024 };
41
41
  const INSPECTION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
42
42
  const ACTIVE_SESSION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
@@ -619,6 +619,8 @@ const LABELLED_VALUE = /^[\t ]*(?:[-*][\t ]+)?[^\r\n:=]{1,64}[\t ]*[=:][\t ]*(.*
619
619
  function roleTokenValues(text) {
620
620
  const values = [];
621
621
  for (const line of text.split(/\r?\n/u)) {
622
+ if (/^[\t ]*(?:[-*][\t ]+)?(?:delivery_intent|goal_[a-z0-9_]+)[\t ]*[=:]/iu.test(line))
623
+ continue;
622
624
  const match = LABELLED_VALUE.exec(line);
623
625
  if (match === null)
624
626
  continue;
@@ -1516,13 +1518,56 @@ export const SortieDogsPlugin = async (input, options) => {
1516
1518
  async function terminalGoalFromHostText(sessionID, text) {
1517
1519
  const outcome = terminalRunOutcome(text);
1518
1520
  if (outcome === undefined || !isCoordinatorSession(sessionID)) {
1519
- return { outcome, goal: undefined, receipt: undefined };
1521
+ return { outcome, goal: undefined, receipt: undefined, delivery: "ready" };
1522
+ }
1523
+ let delivery = "ready";
1524
+ const coordinator = await getParallelCoordinator().catch(() => undefined);
1525
+ let parallel = await coordinator?.snapshot(sessionID).catch(() => undefined);
1526
+ if (parallel !== undefined && !parallel.archived) {
1527
+ await restoreChildLifecycles(sessionID, parallel).catch(() => undefined);
1528
+ const knownCalls = coordinatorTaskCalls.get(sessionID) ?? new Set();
1529
+ const running = parallel.tasks.filter(({ phase }) => phase === "running");
1530
+ if (running.length > 0 && running.every(({ call_id }) => call_id !== null && !knownCalls.has(call_id))) {
1531
+ const status = input.client?.session?.status;
1532
+ const response = status === undefined ? undefined : await status.call(input.client.session, {
1533
+ query: { directory: input.directory },
1534
+ }).catch(() => undefined);
1535
+ const payload = isRecord(response) && "data" in response ? response.data : response;
1536
+ const statuses = isRecord(payload) ? payload : undefined;
1537
+ const settled = statuses !== undefined && running.every(({ child_session_id }) => {
1538
+ const observed = child_session_id === null ? undefined : statuses[child_session_id];
1539
+ return isRecord(observed) && observed.type === "idle";
1540
+ });
1541
+ if (settled)
1542
+ parallel = await coordinator.reconcile(sessionID, knownCalls, parallel.run_id).catch(() => parallel);
1543
+ }
1544
+ if (parallel !== undefined && !parallel.archived)
1545
+ delivery = "running";
1520
1546
  }
1547
+ if (parallel?.archived === true && parallel.terminal_reason !== "completed")
1548
+ delivery = "failed";
1521
1549
  let goal = await currentGoal(sessionID).catch(() => undefined);
1522
1550
  let receipt = goal?.receipt ?? undefined;
1523
- if (receipt === undefined && outcome === "DONE") {
1551
+ const proved = goal?.acceptance_contract !== null && goal?.acceptance_contract !== undefined &&
1552
+ goal.acceptance_contract.criteria.every(({ criterion_id }) => goal.satisfied_criteria.includes(criterion_id));
1553
+ if (delivery === "running") {
1554
+ return { outcome, goal, receipt, delivery };
1555
+ }
1556
+ if (receipt === undefined && delivery === "failed") {
1557
+ receipt = await terminalGoal(sessionID, "stopped", "stopped").catch(() => undefined);
1558
+ }
1559
+ else if (receipt === undefined && proved) {
1524
1560
  receipt = await terminalGoal(sessionID, "completed", "succeeded").catch(() => undefined);
1525
1561
  }
1562
+ else if (receipt === undefined && outcome === "DONE") {
1563
+ receipt = await terminalGoal(sessionID, "completed", "succeeded").catch(() => undefined);
1564
+ }
1565
+ else if (receipt === undefined && outcome === "INTERRUPTED") {
1566
+ receipt = await terminalGoal(sessionID, "stopped", "stopped").catch(() => undefined);
1567
+ }
1568
+ else if (receipt === undefined && outcome === "NEED_DECISION") {
1569
+ receipt = await terminalGoal(sessionID, "awaiting_user", "stopped").catch(() => undefined);
1570
+ }
1526
1571
  else if (receipt === undefined && outcome === "BLOCKED") {
1527
1572
  const reason = /(^|\n)TRUE_BLOCKER\s*:\s*user-decision\s*:/iu.test(text)
1528
1573
  ? "awaiting_user"
@@ -1533,95 +1578,169 @@ export const SortieDogsPlugin = async (input, options) => {
1533
1578
  goal = await currentGoal(sessionID).catch(() => goal);
1534
1579
  if (receipt !== undefined)
1535
1580
  rootAcceptanceContinuity.delete(sessionID);
1536
- return { outcome, goal, receipt };
1581
+ return { outcome, goal, receipt, delivery };
1537
1582
  }
1538
- function declaredDelivery(prompt) {
1539
- const entries = handoffEntries(prompt);
1540
- const explicit = handoffValue(entries, ["delivery_mode"]);
1541
- if (explicit === "planning-only" || explicit === "mvp-first" || explicit === "repair-first" || explicit === "controlled-change")
1542
- return explicit;
1543
- const intent = handoffValue(entries, ["delivery_intent"]);
1544
- const declared = intent === "design" || intent === "registration" || intent === "repair" || intent === "controlled-change"
1545
- ? intent : "implementation";
1546
- return selectGoalDelivery({ declared_intent: declared,
1547
- requested_usable_path_established: handoffValue(entries, ["usable_path_established"]) === "true",
1548
- irreversible_or_major_scope: handoffValue(entries, ["controlled_change"]) === "true" });
1549
- }
1550
- function declaredGoalContract(prompt) {
1583
+ function goalDeclarationContract(prompt) {
1551
1584
  const lines = prompt.split(/\r?\n/u);
1552
1585
  const criterionStarts = lines.flatMap((line, index) => /^\s*goal_criterion_id\s*:/u.test(line) ? [index] : []);
1553
- if (criterionStarts.length > 1) {
1554
- const validation = handoffValue(handoffEntries(prompt), ["validation"]);
1555
- const contracts = criterionStarts.map((start, index) => declaredGoalContract(lines.slice(start, criterionStarts[index + 1]).join("\n") +
1556
- (validation === undefined ? "" : `\nvalidation: ${validation}`)));
1557
- if (contracts.some((contract) => contract === null))
1558
- return null;
1559
- const criteria = contracts.flatMap((contract) => contract.criteria);
1560
- return new Set(criteria.map((criterion) => criterion.criterion_id)).size === criteria.length
1561
- ? { criteria } : null;
1586
+ if (criterionStarts.length === 0) {
1587
+ return { defects: [contractDefect("contract", "/goal_acceptance/criteria", "goal_criteria_missing")] };
1562
1588
  }
1563
- const entries = handoffEntries(prompt);
1589
+ const validation = handoffValue(handoffEntries(prompt), ["validation"]);
1564
1590
  const required = ["goal_criterion_id", "goal_target", "goal_entrypoint", "goal_workload",
1565
1591
  "goal_oracle_coverage", "goal_build_boundary", "goal_source", "goal_candidate", "goal_fixture",
1566
1592
  "goal_proof_scope", "goal_expected_outcome"];
1567
- const values = Object.fromEntries(required.map((key) => [key, handoffValue(entries, [key])]));
1568
- if (required.some((key) => key !== "goal_oracle_coverage" && values[key] === undefined))
1569
- return null;
1570
- let oracleCoverage;
1571
- try {
1572
- const parsed = values.goal_oracle_coverage === undefined
1573
- ? taskAcceptanceCriteria(prompt, "goal_oracle_coverage") : JSON.parse(values.goal_oracle_coverage);
1574
- if (!Array.isArray(parsed) || !parsed.every((value) => typeof value === "string" && value.length > 0))
1575
- return null;
1576
- oracleCoverage = parsed;
1593
+ const criteria = [];
1594
+ const defects = [];
1595
+ for (const [criterionIndex, start] of criterionStarts.entries()) {
1596
+ const block = lines.slice(start, criterionStarts[criterionIndex + 1]).join("\n") +
1597
+ (validation === undefined ? "" : `\nvalidation: ${validation}`);
1598
+ const entries = handoffEntries(block);
1599
+ const values = Object.fromEntries(required.map((key) => [key, handoffValue(entries, [key])]));
1600
+ const pointer = (field) => `/goal_acceptance/criteria/${criterionIndex}/${field}`;
1601
+ for (const key of required) {
1602
+ if (key !== "goal_oracle_coverage" && values[key] === undefined) {
1603
+ defects.push(contractDefect("contract", pointer(key), "goal_field_missing"));
1604
+ }
1605
+ }
1606
+ const inlineOracle = values.goal_oracle_coverage;
1607
+ let oracleCoverage;
1608
+ try {
1609
+ const parsed = inlineOracle === undefined
1610
+ ? taskAcceptanceCriteria(block, "goal_oracle_coverage")
1611
+ : JSON.parse(inlineOracle);
1612
+ if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((value) => typeof value === "string" && value.length > 0 && value.length <= 512) &&
1613
+ new Set(parsed).size === parsed.length)
1614
+ oracleCoverage = parsed;
1615
+ }
1616
+ catch { /* concrete defect below */ }
1617
+ if (oracleCoverage === undefined) {
1618
+ defects.push(contractDefect("contract", pointer("goal_oracle_coverage"), inlineOracle === undefined ? "goal_field_missing_or_malformed" : "goal_oracle_coverage_invalid"));
1619
+ }
1620
+ const buildBoundary = values.goal_build_boundary;
1621
+ const proofScope = values.goal_proof_scope;
1622
+ const expectedOutcome = values.goal_expected_outcome;
1623
+ const sourceBinding = handoffValue(entries, ["goal_source_binding"]);
1624
+ const candidateBinding = handoffValue(entries, ["goal_candidate_binding"]);
1625
+ if (buildBoundary !== undefined && buildBoundary !== "included" && buildBoundary !== "excluded" && buildBoundary !== "not-applicable") {
1626
+ defects.push(contractDefect("contract", pointer("goal_build_boundary"), "goal_build_boundary_invalid"));
1627
+ }
1628
+ if (proofScope !== undefined && proofScope !== "requested-full" && proofScope !== "document-deliverable" && proofScope !== "expected-negative") {
1629
+ defects.push(contractDefect("contract", pointer("goal_proof_scope"), "goal_proof_scope_invalid"));
1630
+ }
1631
+ if (expectedOutcome !== undefined && expectedOutcome !== "pass" && expectedOutcome !== "fail") {
1632
+ defects.push(contractDefect("contract", pointer("goal_expected_outcome"), "goal_expected_outcome_invalid"));
1633
+ }
1634
+ if (sourceBinding !== undefined && sourceBinding !== "declared" && sourceBinding !== "current-protected") {
1635
+ defects.push(contractDefect("contract", pointer("goal_source_binding"), "goal_source_binding_invalid"));
1636
+ }
1637
+ if (candidateBinding !== undefined && candidateBinding !== "declared" && candidateBinding !== "current-protected") {
1638
+ defects.push(contractDefect("contract", pointer("goal_candidate_binding"), "goal_candidate_binding_invalid"));
1639
+ }
1640
+ const declaredValidation = handoffValue(entries, ["validation"]);
1641
+ const structuredCommands = declaredValidation?.startsWith("{") && declaredValidation.endsWith("}")
1642
+ ? [...declaredValidation.matchAll(/(?:\{|,)\s*command\s*:\s*([^,}]+)/gu)] : [];
1643
+ const validationCommand = handoffValue(entries, ["goal_validation_command"]) ??
1644
+ (structuredCommands.length === 1 ? unquoteValue(structuredCommands[0][1].trim()) : undefined);
1645
+ if (validationCommand === undefined || normalizeCommand(validationCommand).length === 0) {
1646
+ defects.push(contractDefect("contract", pointer("goal_validation_command"), "goal_validation_command_missing"));
1647
+ }
1648
+ if (defects.some((defect) => defect.includes(`/criteria/${criterionIndex}/`)))
1649
+ continue;
1650
+ criteria.push({ criterion_id: values.goal_criterion_id, target: values.goal_target,
1651
+ entrypoint: values.goal_entrypoint, workload: values.goal_workload, oracle_coverage: oracleCoverage,
1652
+ build_boundary: buildBoundary,
1653
+ source: values.goal_source, candidate: values.goal_candidate,
1654
+ source_binding: (sourceBinding ?? "declared"),
1655
+ candidate_binding: (candidateBinding ?? "declared"),
1656
+ validation_command: normalizeCommand(validationCommand), fixture: values.goal_fixture,
1657
+ proof_scope: proofScope,
1658
+ expected_outcome: expectedOutcome });
1659
+ }
1660
+ const duplicateIDs = criteria.map(({ criterion_id }) => criterion_id)
1661
+ .filter((id, index, all) => all.indexOf(id) !== index);
1662
+ if (duplicateIDs.length > 0) {
1663
+ defects.push(contractDefect("contract", "/goal_acceptance/criteria", "goal_criterion_id_duplicate"));
1664
+ }
1665
+ return defects.length === 0 ? { contract: { criteria }, defects } : { defects };
1666
+ }
1667
+ function validateGoalDeclaration(prompt) {
1668
+ const entries = handoffEntries(prompt);
1669
+ const defects = [];
1670
+ const fingerprint = handoffValue(entries, ["goal_acceptance_fingerprint"]);
1671
+ if (fingerprint === undefined)
1672
+ defects.push(contractDefect("contract", "/goal_acceptance_fingerprint", "goal_fingerprint_missing"));
1673
+ else if (!/^sha256:[a-f0-9]{64}$/u.test(fingerprint)) {
1674
+ defects.push(contractDefect("contract", "/goal_acceptance_fingerprint", "goal_fingerprint_format"));
1577
1675
  }
1578
- catch {
1579
- return null;
1580
- }
1581
- const buildBoundary = values.goal_build_boundary;
1582
- const proofScope = values.goal_proof_scope;
1583
- const expectedOutcome = values.goal_expected_outcome;
1584
- const sourceBinding = handoffValue(entries, ["goal_source_binding"]);
1585
- const candidateBinding = handoffValue(entries, ["goal_candidate_binding"]);
1586
- const declaredValidation = handoffValue(entries, ["validation"]);
1587
- const structuredCommands = declaredValidation?.startsWith("{") && declaredValidation.endsWith("}")
1588
- ? [...declaredValidation.matchAll(/(?:\{|,)\s*command\s*:\s*([^,}]+)/gu)] : [];
1589
- const validationCommand = handoffValue(entries, ["goal_validation_command"]) ??
1590
- (structuredCommands.length === 1 ? unquoteValue(structuredCommands[0][1].trim()) : undefined);
1591
- if (buildBoundary !== "included" && buildBoundary !== "excluded" && buildBoundary !== "not-applicable")
1592
- return null;
1593
- if (proofScope !== "requested-full" && proofScope !== "document-deliverable" && proofScope !== "expected-negative")
1594
- return null;
1595
- if (expectedOutcome !== "pass" && expectedOutcome !== "fail")
1596
- return null;
1597
- if (sourceBinding !== undefined && sourceBinding !== "declared" && sourceBinding !== "current-protected")
1598
- return null;
1599
- if (candidateBinding !== undefined && candidateBinding !== "declared" && candidateBinding !== "current-protected")
1600
- return null;
1601
- return { criteria: [{ criterion_id: values.goal_criterion_id, target: values.goal_target,
1602
- entrypoint: values.goal_entrypoint, workload: values.goal_workload, oracle_coverage: oracleCoverage,
1603
- build_boundary: buildBoundary, source: values.goal_source, candidate: values.goal_candidate,
1604
- source_binding: sourceBinding ?? "declared", candidate_binding: candidateBinding ?? "declared",
1605
- ...(validationCommand === undefined ? {} : { validation_command: normalizeCommand(validationCommand) }),
1606
- fixture: values.goal_fixture, proof_scope: proofScope, expected_outcome: expectedOutcome }] };
1607
- }
1608
- async function bindGoalDeclaration(sessionID, prompt, acceptance) {
1676
+ const intent = handoffValue(entries, ["delivery_intent"]);
1677
+ const intents = ["design", "registration", "implementation", "repair", "controlled-change"];
1678
+ if (intent === undefined)
1679
+ defects.push(contractDefect("contract", "/delivery_intent", "delivery_intent_missing"));
1680
+ else if (!intents.includes(intent)) {
1681
+ defects.push(contractDefect("contract", "/delivery_intent", "delivery_intent_invalid"));
1682
+ }
1683
+ const mode = handoffValue(entries, ["delivery_mode"]);
1684
+ const modes = ["planning-only", "mvp-first", "repair-first", "controlled-change"];
1685
+ if (mode !== undefined && !modes.includes(mode)) {
1686
+ defects.push(contractDefect("contract", "/delivery_mode", "delivery_mode_invalid"));
1687
+ }
1688
+ const usable = handoffValue(entries, ["usable_path_established"]);
1689
+ const controlled = handoffValue(entries, ["controlled_change"]);
1690
+ for (const [field, value] of [["usable_path_established", usable], ["controlled_change", controlled]]) {
1691
+ if (value === undefined)
1692
+ defects.push(contractDefect("contract", `/${field}`, "goal_boolean_missing"));
1693
+ else if (value !== "true" && value !== "false")
1694
+ defects.push(contractDefect("contract", `/${field}`, "goal_boolean_invalid"));
1695
+ }
1696
+ const contract = goalDeclarationContract(prompt);
1697
+ defects.push(...contract.defects);
1698
+ if (defects.length > 0 || fingerprint === undefined || intent === undefined || contract.contract === undefined)
1699
+ return { defects };
1700
+ const delivery = mode ?? selectGoalDelivery({
1701
+ declared_intent: intent, requested_usable_path_established: usable === "true",
1702
+ irreversible_or_major_scope: controlled === "true",
1703
+ });
1704
+ return { declaration: { fingerprint, delivery, contract: contract.contract }, defects };
1705
+ }
1706
+ async function bindGoalDeclaration(sessionID, prompt) {
1609
1707
  const ledger = await goalLedger(sessionID);
1610
1708
  let state = (await ledger.readGoal()).state;
1611
1709
  if (state.goal_id === null || state.origin_user_message_id === null)
1612
1710
  return undefined;
1613
- const authority = goalDeclarationAuthority.get(sessionID);
1614
- if (authority === undefined || authority !== state.latest_user_message_id)
1615
- return state;
1616
- goalDeclarationAuthority.delete(sessionID);
1617
1711
  const entries = handoffEntries(prompt);
1618
- const explicitFingerprint = handoffValue(entries, ["goal_acceptance_fingerprint"]);
1619
- if (state.acceptance_contract !== null && explicitFingerprint === undefined)
1712
+ const typedDeclarationPresent = prompt.split(/\r?\n/u).some((line) => /^\s*(?:goal_[a-z0-9_]+|delivery_intent|delivery_mode|usable_path_established|controlled_change)\s*:/iu.test(line));
1713
+ // Accepted goals may retain their existing declaration on later units, but the first worker
1714
+ // handoff and every handoff containing declaration fields must be complete.
1715
+ if (!typedDeclarationPresent) {
1716
+ if (state.acceptance_contract === null && goalDeclarationAuthority.get(sessionID) === state.latest_user_message_id) {
1717
+ throw new HandoffDeniedError("contract-invalid", "<goal-declaration>", {
1718
+ defects: validateGoalDeclaration(prompt).defects,
1719
+ });
1720
+ }
1721
+ if (goalDeclarationAuthority.get(sessionID) === state.latest_user_message_id)
1722
+ goalDeclarationAuthority.delete(sessionID);
1620
1723
  return state;
1621
- const declaredFingerprint = explicitFingerprint ?? acceptance?.fingerprint;
1622
- if (declaredFingerprint === undefined || !/^sha256:[a-f0-9]{64}$/u.test(declaredFingerprint) ||
1623
- declaredFingerprint === state.acceptance_fingerprint)
1724
+ }
1725
+ const validated = validateGoalDeclaration(prompt);
1726
+ if (validated.declaration === undefined) {
1727
+ throw new HandoffDeniedError("contract-invalid", "<goal-declaration>", { defects: validated.defects });
1728
+ }
1729
+ const declaration = validated.declaration;
1730
+ const authority = goalDeclarationAuthority.get(sessionID);
1731
+ if (authority === undefined || authority !== state.latest_user_message_id) {
1732
+ if (declaration.fingerprint !== state.acceptance_fingerprint) {
1733
+ throw new HandoffDeniedError("contract-invalid", "<goal-declaration>", { defects: [
1734
+ contractDefect("contract", "/goal_acceptance_fingerprint", "goal_revision_unauthorized"),
1735
+ ] });
1736
+ }
1737
+ return state;
1738
+ }
1739
+ if (declaration.fingerprint === state.acceptance_fingerprint &&
1740
+ goalFingerprint(declaration.contract) === goalFingerprint(state.acceptance_contract)) {
1741
+ goalDeclarationAuthority.delete(sessionID);
1624
1742
  return state;
1743
+ }
1625
1744
  const units = Number(handoffValue(entries, ["goal_budget_units"]));
1626
1745
  const maxUnits = Number.isSafeInteger(units) && units >= state.consumed_units && units > 0
1627
1746
  ? units : state.budget?.max_units ?? 32;
@@ -1631,21 +1750,25 @@ export const SortieDogsPlugin = async (input, options) => {
1631
1750
  const costBudget = Number.isFinite(declaredCost) && declaredCost > 0 ? declaredCost : state.budget?.cost_usd ?? null;
1632
1751
  state = await ledger.appendGoal({ kind: "goal.revised", at: new Date().toISOString(), goal_id: state.goal_id,
1633
1752
  revision: state.revision + 1, scope_epoch: state.scope_epoch + 1,
1634
- acceptance_fingerprint: declaredFingerprint, origin_user_message_id: state.latest_user_message_id,
1753
+ acceptance_fingerprint: declaration.fingerprint, origin_user_message_id: state.latest_user_message_id,
1635
1754
  session_id: sessionID, selected_agent: state.selected_agent ?? COORDINATOR_AGENT,
1636
- delivery: declaredDelivery(prompt), budget: { max_units: maxUnits,
1755
+ delivery: declaration.delivery, budget: { max_units: maxUnits,
1637
1756
  time_ms: timeBudget, cost_usd: costBudget,
1638
1757
  source: Number.isSafeInteger(units) ? "accepted-plan" : state.budget?.source ?? "policy-default" },
1639
- acceptance_contract: declaredGoalContract(prompt) });
1758
+ acceptance_contract: declaration.contract });
1759
+ goalDeclarationAuthority.delete(sessionID);
1640
1760
  return state;
1641
1761
  }
1642
- async function reserveGoalDispatch(sessionID, callID, prompt, acceptance) {
1762
+ async function reserveGoalDispatch(sessionID, callID, prompt) {
1643
1763
  if (goalReservations.has(callID))
1644
1764
  return;
1645
1765
  const ledger = await goalLedger(sessionID);
1646
- let state = await bindGoalDeclaration(sessionID, prompt, acceptance) ?? (await ledger.readGoal()).state;
1766
+ let state = await bindGoalDeclaration(sessionID, prompt) ?? (await ledger.readGoal()).state;
1647
1767
  if (state.goal_id === null)
1648
1768
  return; // Legacy already-authorized roots may settle without inventing authority.
1769
+ if (state.phase === "terminal" || state.receipt !== null) {
1770
+ throw new Error("SORTIE_GOAL_CONTROL_DENIED: terminal");
1771
+ }
1649
1772
  if (state.replan_required) {
1650
1773
  if (state.replan_used) {
1651
1774
  await terminalGoal(sessionID, "stop_no_progress", "stopped");
@@ -1735,10 +1858,18 @@ export const SortieDogsPlugin = async (input, options) => {
1735
1858
  entry.execution.units.includes(reservation.unitID));
1736
1859
  const newEvidence = acceptedEvidence.filter((entry) => entry.measurement.criterion_ids.some((criterionID) => !state.satisfied_criteria.includes(criterionID)));
1737
1860
  const progress = newEvidence.length > 0;
1861
+ const metadata = isRecord(output.metadata) ? output.metadata : undefined;
1862
+ const interrupted = metadata?.status === "cancel" || metadata?.status === "cancelled" ||
1863
+ output.status === "cancel" || output.status === "cancelled";
1864
+ const hostBindingDefect = childSessionID !== undefined && [...(bindingDenials.get(reservation.root)?.values() ?? [])]
1865
+ .some((candidateDenials) => [...candidateDenials.values()].includes(childSessionID));
1866
+ const processDefect = childSessionID === undefined || hostBindingDefect;
1867
+ const resultClass = progress ? "acceptance" : interrupted ? "interrupted" : processDefect ? "process-defect" : "acceptance";
1738
1868
  await ledger.appendGoal({ kind: "unit.settled", at: new Date().toISOString(),
1739
1869
  reservation_id: reservation.reservationID, receipt_id: goalFingerprint({ call_id: callID, output: outputText.slice(0, 2048) }),
1740
1870
  goal_id: state.goal_id, unit_id: reservation.unitID,
1741
- disposition: progress ? "succeeded" : "failed", progress_fingerprint: progress ? goalFingerprint(acceptedEvidence) : null,
1871
+ disposition: progress ? "succeeded" : interrupted ? "cancelled" : "failed", result_class: resultClass,
1872
+ progress_fingerprint: progress ? goalFingerprint(acceptedEvidence) : null,
1742
1873
  evidence: acceptedEvidence, elapsed_ms: Math.max(0, Date.now() - reservation.started), cost_usd: null });
1743
1874
  if (childSessionID !== undefined) {
1744
1875
  for (const [executionCallID, execution] of hostGoalExecutions) {
@@ -3070,6 +3201,9 @@ export const SortieDogsPlugin = async (input, options) => {
3070
3201
  contract_fingerprint: structuralAdmission.contract_fingerprint, experience: experience.trace });
3071
3202
  }
3072
3203
  const coordinator = await getParallelCoordinator();
3204
+ if (await coordinator.targetCheckedOut(`refs/heads/${structuralAdmission.contract.provenance.target_branch}`)) {
3205
+ return JSON.stringify({ status: "sol-serial", reason: "target-checked-out", experience: experience.trace });
3206
+ }
3073
3207
  const result = await coordinator.prepareFabric(contract, ownerRoot, executionPlanPath === undefined ? undefined : await readJson(resolve(executionPlanPath), INPUT_LIMITS.parallel));
3074
3208
  if (result.status === "sol-serial")
3075
3209
  return JSON.stringify({ ...result, experience: experience.trace });
@@ -5158,9 +5292,20 @@ export const SortieDogsPlugin = async (input, options) => {
5158
5292
  const terminal = runOutcome === undefined || !isCoordinatorSession(textInput.sessionID)
5159
5293
  ? undefined
5160
5294
  : await terminalGoalFromHostText(textInput.sessionID, textOutput.text);
5161
- if (runOutcome === "DONE" && terminal?.goal !== undefined &&
5162
- terminal.goal.acceptance_contract !== null && terminal.receipt === undefined) {
5163
- textOutput.text = textOutput.text.replace(/(^|\n)status:\s*DONE\b/iu, "$1status: IN_PROGRESS\ngoal_control: accepted criteria remain unproved");
5295
+ if (runOutcome === "DONE" && terminal !== undefined && (terminal.delivery === "running" ||
5296
+ (terminal.receipt === undefined && terminal.goal !== undefined && terminal.goal.acceptance_contract !== null))) {
5297
+ textOutput.text = replaceDoneTerminalStatus(textOutput.text, terminal.delivery === "running"
5298
+ ? "status: IN_PROGRESS — durable delivery active; same sessionでjoinまたはstale reconcileが必要"
5299
+ : "status: IN_PROGRESS\ngoal_control: accepted criteria remain unproved");
5300
+ }
5301
+ if (runOutcome !== "DONE" && terminal?.delivery === "ready" && terminal.receipt?.status === "succeeded") {
5302
+ textOutput.text = replaceTerminalStatus(textOutput.text, "status: DONE");
5303
+ }
5304
+ if (runOutcome === "DONE" && terminal?.delivery === "failed") {
5305
+ textOutput.text = replaceTerminalStatus(textOutput.text, "status: INTERRUPTED — durable delivery failed");
5306
+ }
5307
+ if (runOutcome !== undefined && isCoordinatorSession(textInput.sessionID)) {
5308
+ textOutput.text = sanitizeTerminalReport(textOutput.text);
5164
5309
  }
5165
5310
  if ((isCoordinatorSession(textInput.sessionID) || await recoverCoordinatorRoot(textInput.sessionID)) &&
5166
5311
  runOutcome !== undefined) {
@@ -6004,6 +6149,12 @@ export const SortieDogsPlugin = async (input, options) => {
6004
6149
  fastLane.enableParallelDispatch(toolInput.sessionID, boundSnapshot.max_workers, counts.dispatched - currentContribution, counts.running - currentContribution, counts.total);
6005
6150
  parallelWorkerAuthorized = true;
6006
6151
  }
6152
+ if (toolInput.tool === "task" && taskRole !== undefined && IMPLEMENTATION_AGENTS.has(taskRole) &&
6153
+ isRecord(output.args)) {
6154
+ // Declaration admission precedes routing state and reservation. A concrete field denial can
6155
+ // therefore be repaired by a corrected Task call in this same coordinator turn.
6156
+ await bindGoalDeclaration(toolInput.sessionID, typeof output.args.prompt === "string" ? output.args.prompt : "");
6157
+ }
6007
6158
  const resumedWorkerSessionID = fastLane.beforeTool(toolInput.sessionID, toolInput.tool, output.args, {
6008
6159
  readonlyDiagnosisAuthorized: readonlyDiagnosis,
6009
6160
  consultationFallbackAuthorized,
@@ -6012,7 +6163,7 @@ export const SortieDogsPlugin = async (input, options) => {
6012
6163
  });
6013
6164
  if (toolInput.tool === "task" && taskRole !== undefined && IMPLEMENTATION_AGENTS.has(taskRole) &&
6014
6165
  isRecord(output.args)) {
6015
- await reserveGoalDispatch(toolInput.sessionID, toolInput.callID, typeof output.args.prompt === "string" ? output.args.prompt : "", validatedRootAcceptance);
6166
+ await reserveGoalDispatch(toolInput.sessionID, toolInput.callID, typeof output.args.prompt === "string" ? output.args.prompt : "");
6016
6167
  }
6017
6168
  if (validatedRootAcceptance !== undefined && reservedParallelDescriptor === undefined) {
6018
6169
  rootAcceptanceContinuity.delete(toolInput.sessionID);
@@ -73,7 +73,7 @@ export interface SortieResult {
73
73
  readonly accounting_phase: "pre-terminal";
74
74
  readonly as_of: string;
75
75
  readonly mission: {
76
- readonly status: "COMPLETED" | "FAILED" | "STOPPED";
76
+ readonly status: "COMPLETED" | "INTERRUPTED" | "EXTERNAL_BLOCKER" | "USER_DECISION";
77
77
  readonly stop_reason: GoalTerminalReceipt["stop_reason"];
78
78
  };
79
79
  readonly speed: {
@@ -103,12 +103,15 @@ export interface SortieResult {
103
103
  type SortieGoalSnapshot = Pick<GoalFlightState, "acceptance_contract" | "consumed_time_ms" | "satisfied_criteria">;
104
104
  /** Builds a pure terminal snapshot from the durable goal receipt and already-observed host metrics. */
105
105
  export declare function createSortieResult(receipt: GoalTerminalReceipt, goal: SortieGoalSnapshot, metrics: RunMetrics | undefined, asOf?: string): SortieResult;
106
- export type RunTerminalOutcome = "DONE" | "BLOCKED" | "NEED_DECISION";
106
+ export type RunTerminalOutcome = "DONE" | "INTERRUPTED" | "BLOCKED" | "NEED_DECISION";
107
107
  export declare function collectRunMetrics(client: RunMetricsClient | undefined, rootSessionID: string, directory?: string, now?: number, window?: RunMetricsWindow): Promise<RunMetrics | undefined>;
108
108
  export declare function formatSortieResult(result: SortieResult): string;
109
109
  export declare function formatRunMetrics(metrics: RunMetrics): string;
110
110
  export declare function isDoneTerminalText(text: string): boolean;
111
111
  export declare function terminalRunOutcome(text: string): RunTerminalOutcome | undefined;
112
+ export declare function replaceTerminalStatus(text: string, replacement: string): string;
113
+ export declare function replaceDoneTerminalStatus(text: string, replacement: string): string;
114
+ export declare function sanitizeTerminalReport(text: string): string;
112
115
  export declare function insertRunMetrics(text: string, metrics: RunMetrics): string;
113
116
  export declare function insertSortieResult(text: string, result: SortieResult): string;
114
117
  export {};