sortie-dogs 0.9.0 → 0.9.1

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.1](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.1)
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.76-goal-control-report-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.76-goal-control-report-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,
@@ -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;
@@ -1520,9 +1522,20 @@ export const SortieDogsPlugin = async (input, options) => {
1520
1522
  }
1521
1523
  let goal = await currentGoal(sessionID).catch(() => undefined);
1522
1524
  let receipt = goal?.receipt ?? undefined;
1523
- if (receipt === undefined && outcome === "DONE") {
1525
+ const proved = goal?.acceptance_contract !== null && goal?.acceptance_contract !== undefined &&
1526
+ goal.acceptance_contract.criteria.every(({ criterion_id }) => goal.satisfied_criteria.includes(criterion_id));
1527
+ if (receipt === undefined && proved) {
1528
+ receipt = await terminalGoal(sessionID, "completed", "succeeded").catch(() => undefined);
1529
+ }
1530
+ else if (receipt === undefined && outcome === "DONE") {
1524
1531
  receipt = await terminalGoal(sessionID, "completed", "succeeded").catch(() => undefined);
1525
1532
  }
1533
+ else if (receipt === undefined && outcome === "INTERRUPTED") {
1534
+ receipt = await terminalGoal(sessionID, "stopped", "stopped").catch(() => undefined);
1535
+ }
1536
+ else if (receipt === undefined && outcome === "NEED_DECISION") {
1537
+ receipt = await terminalGoal(sessionID, "awaiting_user", "stopped").catch(() => undefined);
1538
+ }
1526
1539
  else if (receipt === undefined && outcome === "BLOCKED") {
1527
1540
  const reason = /(^|\n)TRUE_BLOCKER\s*:\s*user-decision\s*:/iu.test(text)
1528
1541
  ? "awaiting_user"
@@ -1535,93 +1548,167 @@ export const SortieDogsPlugin = async (input, options) => {
1535
1548
  rootAcceptanceContinuity.delete(sessionID);
1536
1549
  return { outcome, goal, receipt };
1537
1550
  }
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) {
1551
+ function goalDeclarationContract(prompt) {
1551
1552
  const lines = prompt.split(/\r?\n/u);
1552
1553
  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;
1554
+ if (criterionStarts.length === 0) {
1555
+ return { defects: [contractDefect("contract", "/goal_acceptance/criteria", "goal_criteria_missing")] };
1562
1556
  }
1563
- const entries = handoffEntries(prompt);
1557
+ const validation = handoffValue(handoffEntries(prompt), ["validation"]);
1564
1558
  const required = ["goal_criterion_id", "goal_target", "goal_entrypoint", "goal_workload",
1565
1559
  "goal_oracle_coverage", "goal_build_boundary", "goal_source", "goal_candidate", "goal_fixture",
1566
1560
  "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;
1561
+ const criteria = [];
1562
+ const defects = [];
1563
+ for (const [criterionIndex, start] of criterionStarts.entries()) {
1564
+ const block = lines.slice(start, criterionStarts[criterionIndex + 1]).join("\n") +
1565
+ (validation === undefined ? "" : `\nvalidation: ${validation}`);
1566
+ const entries = handoffEntries(block);
1567
+ const values = Object.fromEntries(required.map((key) => [key, handoffValue(entries, [key])]));
1568
+ const pointer = (field) => `/goal_acceptance/criteria/${criterionIndex}/${field}`;
1569
+ for (const key of required) {
1570
+ if (key !== "goal_oracle_coverage" && values[key] === undefined) {
1571
+ defects.push(contractDefect("contract", pointer(key), "goal_field_missing"));
1572
+ }
1573
+ }
1574
+ const inlineOracle = values.goal_oracle_coverage;
1575
+ let oracleCoverage;
1576
+ try {
1577
+ const parsed = inlineOracle === undefined
1578
+ ? taskAcceptanceCriteria(block, "goal_oracle_coverage")
1579
+ : JSON.parse(inlineOracle);
1580
+ if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((value) => typeof value === "string" && value.length > 0 && value.length <= 512) &&
1581
+ new Set(parsed).size === parsed.length)
1582
+ oracleCoverage = parsed;
1583
+ }
1584
+ catch { /* concrete defect below */ }
1585
+ if (oracleCoverage === undefined) {
1586
+ defects.push(contractDefect("contract", pointer("goal_oracle_coverage"), inlineOracle === undefined ? "goal_field_missing_or_malformed" : "goal_oracle_coverage_invalid"));
1587
+ }
1588
+ const buildBoundary = values.goal_build_boundary;
1589
+ const proofScope = values.goal_proof_scope;
1590
+ const expectedOutcome = values.goal_expected_outcome;
1591
+ const sourceBinding = handoffValue(entries, ["goal_source_binding"]);
1592
+ const candidateBinding = handoffValue(entries, ["goal_candidate_binding"]);
1593
+ if (buildBoundary !== undefined && buildBoundary !== "included" && buildBoundary !== "excluded" && buildBoundary !== "not-applicable") {
1594
+ defects.push(contractDefect("contract", pointer("goal_build_boundary"), "goal_build_boundary_invalid"));
1595
+ }
1596
+ if (proofScope !== undefined && proofScope !== "requested-full" && proofScope !== "document-deliverable" && proofScope !== "expected-negative") {
1597
+ defects.push(contractDefect("contract", pointer("goal_proof_scope"), "goal_proof_scope_invalid"));
1598
+ }
1599
+ if (expectedOutcome !== undefined && expectedOutcome !== "pass" && expectedOutcome !== "fail") {
1600
+ defects.push(contractDefect("contract", pointer("goal_expected_outcome"), "goal_expected_outcome_invalid"));
1601
+ }
1602
+ if (sourceBinding !== undefined && sourceBinding !== "declared" && sourceBinding !== "current-protected") {
1603
+ defects.push(contractDefect("contract", pointer("goal_source_binding"), "goal_source_binding_invalid"));
1604
+ }
1605
+ if (candidateBinding !== undefined && candidateBinding !== "declared" && candidateBinding !== "current-protected") {
1606
+ defects.push(contractDefect("contract", pointer("goal_candidate_binding"), "goal_candidate_binding_invalid"));
1607
+ }
1608
+ const declaredValidation = handoffValue(entries, ["validation"]);
1609
+ const structuredCommands = declaredValidation?.startsWith("{") && declaredValidation.endsWith("}")
1610
+ ? [...declaredValidation.matchAll(/(?:\{|,)\s*command\s*:\s*([^,}]+)/gu)] : [];
1611
+ const validationCommand = handoffValue(entries, ["goal_validation_command"]) ??
1612
+ (structuredCommands.length === 1 ? unquoteValue(structuredCommands[0][1].trim()) : undefined);
1613
+ if (validationCommand === undefined || normalizeCommand(validationCommand).length === 0) {
1614
+ defects.push(contractDefect("contract", pointer("goal_validation_command"), "goal_validation_command_missing"));
1615
+ }
1616
+ if (defects.some((defect) => defect.includes(`/criteria/${criterionIndex}/`)))
1617
+ continue;
1618
+ criteria.push({ criterion_id: values.goal_criterion_id, target: values.goal_target,
1619
+ entrypoint: values.goal_entrypoint, workload: values.goal_workload, oracle_coverage: oracleCoverage,
1620
+ build_boundary: buildBoundary,
1621
+ source: values.goal_source, candidate: values.goal_candidate,
1622
+ source_binding: (sourceBinding ?? "declared"),
1623
+ candidate_binding: (candidateBinding ?? "declared"),
1624
+ validation_command: normalizeCommand(validationCommand), fixture: values.goal_fixture,
1625
+ proof_scope: proofScope,
1626
+ expected_outcome: expectedOutcome });
1627
+ }
1628
+ const duplicateIDs = criteria.map(({ criterion_id }) => criterion_id)
1629
+ .filter((id, index, all) => all.indexOf(id) !== index);
1630
+ if (duplicateIDs.length > 0) {
1631
+ defects.push(contractDefect("contract", "/goal_acceptance/criteria", "goal_criterion_id_duplicate"));
1632
+ }
1633
+ return defects.length === 0 ? { contract: { criteria }, defects } : { defects };
1634
+ }
1635
+ function validateGoalDeclaration(prompt) {
1636
+ const entries = handoffEntries(prompt);
1637
+ const defects = [];
1638
+ const fingerprint = handoffValue(entries, ["goal_acceptance_fingerprint"]);
1639
+ if (fingerprint === undefined)
1640
+ defects.push(contractDefect("contract", "/goal_acceptance_fingerprint", "goal_fingerprint_missing"));
1641
+ else if (!/^sha256:[a-f0-9]{64}$/u.test(fingerprint)) {
1642
+ defects.push(contractDefect("contract", "/goal_acceptance_fingerprint", "goal_fingerprint_format"));
1577
1643
  }
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) {
1644
+ const intent = handoffValue(entries, ["delivery_intent"]);
1645
+ const intents = ["design", "registration", "implementation", "repair", "controlled-change"];
1646
+ if (intent === undefined)
1647
+ defects.push(contractDefect("contract", "/delivery_intent", "delivery_intent_missing"));
1648
+ else if (!intents.includes(intent)) {
1649
+ defects.push(contractDefect("contract", "/delivery_intent", "delivery_intent_invalid"));
1650
+ }
1651
+ const mode = handoffValue(entries, ["delivery_mode"]);
1652
+ const modes = ["planning-only", "mvp-first", "repair-first", "controlled-change"];
1653
+ if (mode !== undefined && !modes.includes(mode)) {
1654
+ defects.push(contractDefect("contract", "/delivery_mode", "delivery_mode_invalid"));
1655
+ }
1656
+ const usable = handoffValue(entries, ["usable_path_established"]);
1657
+ const controlled = handoffValue(entries, ["controlled_change"]);
1658
+ for (const [field, value] of [["usable_path_established", usable], ["controlled_change", controlled]]) {
1659
+ if (value === undefined)
1660
+ defects.push(contractDefect("contract", `/${field}`, "goal_boolean_missing"));
1661
+ else if (value !== "true" && value !== "false")
1662
+ defects.push(contractDefect("contract", `/${field}`, "goal_boolean_invalid"));
1663
+ }
1664
+ const contract = goalDeclarationContract(prompt);
1665
+ defects.push(...contract.defects);
1666
+ if (defects.length > 0 || fingerprint === undefined || intent === undefined || contract.contract === undefined)
1667
+ return { defects };
1668
+ const delivery = mode ?? selectGoalDelivery({
1669
+ declared_intent: intent, requested_usable_path_established: usable === "true",
1670
+ irreversible_or_major_scope: controlled === "true",
1671
+ });
1672
+ return { declaration: { fingerprint, delivery, contract: contract.contract }, defects };
1673
+ }
1674
+ async function bindGoalDeclaration(sessionID, prompt) {
1609
1675
  const ledger = await goalLedger(sessionID);
1610
1676
  let state = (await ledger.readGoal()).state;
1611
1677
  if (state.goal_id === null || state.origin_user_message_id === null)
1612
1678
  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
1679
  const entries = handoffEntries(prompt);
1618
- const explicitFingerprint = handoffValue(entries, ["goal_acceptance_fingerprint"]);
1619
- if (state.acceptance_contract !== null && explicitFingerprint === undefined)
1680
+ 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));
1681
+ // Accepted goals may retain their existing declaration on later units, but the first worker
1682
+ // handoff and every handoff containing declaration fields must be complete.
1683
+ if (!typedDeclarationPresent) {
1684
+ if (state.acceptance_contract === null && goalDeclarationAuthority.get(sessionID) === state.latest_user_message_id) {
1685
+ throw new HandoffDeniedError("contract-invalid", "<goal-declaration>", {
1686
+ defects: validateGoalDeclaration(prompt).defects,
1687
+ });
1688
+ }
1689
+ if (goalDeclarationAuthority.get(sessionID) === state.latest_user_message_id)
1690
+ goalDeclarationAuthority.delete(sessionID);
1691
+ return state;
1692
+ }
1693
+ const validated = validateGoalDeclaration(prompt);
1694
+ if (validated.declaration === undefined) {
1695
+ throw new HandoffDeniedError("contract-invalid", "<goal-declaration>", { defects: validated.defects });
1696
+ }
1697
+ const declaration = validated.declaration;
1698
+ const authority = goalDeclarationAuthority.get(sessionID);
1699
+ if (authority === undefined || authority !== state.latest_user_message_id) {
1700
+ if (declaration.fingerprint !== state.acceptance_fingerprint) {
1701
+ throw new HandoffDeniedError("contract-invalid", "<goal-declaration>", { defects: [
1702
+ contractDefect("contract", "/goal_acceptance_fingerprint", "goal_revision_unauthorized"),
1703
+ ] });
1704
+ }
1620
1705
  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)
1706
+ }
1707
+ if (declaration.fingerprint === state.acceptance_fingerprint &&
1708
+ goalFingerprint(declaration.contract) === goalFingerprint(state.acceptance_contract)) {
1709
+ goalDeclarationAuthority.delete(sessionID);
1624
1710
  return state;
1711
+ }
1625
1712
  const units = Number(handoffValue(entries, ["goal_budget_units"]));
1626
1713
  const maxUnits = Number.isSafeInteger(units) && units >= state.consumed_units && units > 0
1627
1714
  ? units : state.budget?.max_units ?? 32;
@@ -1631,21 +1718,25 @@ export const SortieDogsPlugin = async (input, options) => {
1631
1718
  const costBudget = Number.isFinite(declaredCost) && declaredCost > 0 ? declaredCost : state.budget?.cost_usd ?? null;
1632
1719
  state = await ledger.appendGoal({ kind: "goal.revised", at: new Date().toISOString(), goal_id: state.goal_id,
1633
1720
  revision: state.revision + 1, scope_epoch: state.scope_epoch + 1,
1634
- acceptance_fingerprint: declaredFingerprint, origin_user_message_id: state.latest_user_message_id,
1721
+ acceptance_fingerprint: declaration.fingerprint, origin_user_message_id: state.latest_user_message_id,
1635
1722
  session_id: sessionID, selected_agent: state.selected_agent ?? COORDINATOR_AGENT,
1636
- delivery: declaredDelivery(prompt), budget: { max_units: maxUnits,
1723
+ delivery: declaration.delivery, budget: { max_units: maxUnits,
1637
1724
  time_ms: timeBudget, cost_usd: costBudget,
1638
1725
  source: Number.isSafeInteger(units) ? "accepted-plan" : state.budget?.source ?? "policy-default" },
1639
- acceptance_contract: declaredGoalContract(prompt) });
1726
+ acceptance_contract: declaration.contract });
1727
+ goalDeclarationAuthority.delete(sessionID);
1640
1728
  return state;
1641
1729
  }
1642
- async function reserveGoalDispatch(sessionID, callID, prompt, acceptance) {
1730
+ async function reserveGoalDispatch(sessionID, callID, prompt) {
1643
1731
  if (goalReservations.has(callID))
1644
1732
  return;
1645
1733
  const ledger = await goalLedger(sessionID);
1646
- let state = await bindGoalDeclaration(sessionID, prompt, acceptance) ?? (await ledger.readGoal()).state;
1734
+ let state = await bindGoalDeclaration(sessionID, prompt) ?? (await ledger.readGoal()).state;
1647
1735
  if (state.goal_id === null)
1648
1736
  return; // Legacy already-authorized roots may settle without inventing authority.
1737
+ if (state.phase === "terminal" || state.receipt !== null) {
1738
+ throw new Error("SORTIE_GOAL_CONTROL_DENIED: terminal");
1739
+ }
1649
1740
  if (state.replan_required) {
1650
1741
  if (state.replan_used) {
1651
1742
  await terminalGoal(sessionID, "stop_no_progress", "stopped");
@@ -1735,10 +1826,18 @@ export const SortieDogsPlugin = async (input, options) => {
1735
1826
  entry.execution.units.includes(reservation.unitID));
1736
1827
  const newEvidence = acceptedEvidence.filter((entry) => entry.measurement.criterion_ids.some((criterionID) => !state.satisfied_criteria.includes(criterionID)));
1737
1828
  const progress = newEvidence.length > 0;
1829
+ const metadata = isRecord(output.metadata) ? output.metadata : undefined;
1830
+ const interrupted = metadata?.status === "cancel" || metadata?.status === "cancelled" ||
1831
+ output.status === "cancel" || output.status === "cancelled";
1832
+ const hostBindingDefect = childSessionID !== undefined && [...(bindingDenials.get(reservation.root)?.values() ?? [])]
1833
+ .some((candidateDenials) => [...candidateDenials.values()].includes(childSessionID));
1834
+ const processDefect = childSessionID === undefined || hostBindingDefect;
1835
+ const resultClass = progress ? "acceptance" : interrupted ? "interrupted" : processDefect ? "process-defect" : "acceptance";
1738
1836
  await ledger.appendGoal({ kind: "unit.settled", at: new Date().toISOString(),
1739
1837
  reservation_id: reservation.reservationID, receipt_id: goalFingerprint({ call_id: callID, output: outputText.slice(0, 2048) }),
1740
1838
  goal_id: state.goal_id, unit_id: reservation.unitID,
1741
- disposition: progress ? "succeeded" : "failed", progress_fingerprint: progress ? goalFingerprint(acceptedEvidence) : null,
1839
+ disposition: progress ? "succeeded" : interrupted ? "cancelled" : "failed", result_class: resultClass,
1840
+ progress_fingerprint: progress ? goalFingerprint(acceptedEvidence) : null,
1742
1841
  evidence: acceptedEvidence, elapsed_ms: Math.max(0, Date.now() - reservation.started), cost_usd: null });
1743
1842
  if (childSessionID !== undefined) {
1744
1843
  for (const [executionCallID, execution] of hostGoalExecutions) {
@@ -5160,7 +5259,13 @@ export const SortieDogsPlugin = async (input, options) => {
5160
5259
  : await terminalGoalFromHostText(textInput.sessionID, textOutput.text);
5161
5260
  if (runOutcome === "DONE" && terminal?.goal !== undefined &&
5162
5261
  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");
5262
+ textOutput.text = replaceDoneTerminalStatus(textOutput.text, "status: IN_PROGRESS\ngoal_control: accepted criteria remain unproved");
5263
+ }
5264
+ if (runOutcome !== "DONE" && terminal?.receipt?.status === "succeeded") {
5265
+ textOutput.text = replaceTerminalStatus(textOutput.text, "status: DONE");
5266
+ }
5267
+ if (runOutcome !== undefined && isCoordinatorSession(textInput.sessionID)) {
5268
+ textOutput.text = sanitizeTerminalReport(textOutput.text);
5164
5269
  }
5165
5270
  if ((isCoordinatorSession(textInput.sessionID) || await recoverCoordinatorRoot(textInput.sessionID)) &&
5166
5271
  runOutcome !== undefined) {
@@ -6004,6 +6109,12 @@ export const SortieDogsPlugin = async (input, options) => {
6004
6109
  fastLane.enableParallelDispatch(toolInput.sessionID, boundSnapshot.max_workers, counts.dispatched - currentContribution, counts.running - currentContribution, counts.total);
6005
6110
  parallelWorkerAuthorized = true;
6006
6111
  }
6112
+ if (toolInput.tool === "task" && taskRole !== undefined && IMPLEMENTATION_AGENTS.has(taskRole) &&
6113
+ isRecord(output.args)) {
6114
+ // Declaration admission precedes routing state and reservation. A concrete field denial can
6115
+ // therefore be repaired by a corrected Task call in this same coordinator turn.
6116
+ await bindGoalDeclaration(toolInput.sessionID, typeof output.args.prompt === "string" ? output.args.prompt : "");
6117
+ }
6007
6118
  const resumedWorkerSessionID = fastLane.beforeTool(toolInput.sessionID, toolInput.tool, output.args, {
6008
6119
  readonlyDiagnosisAuthorized: readonlyDiagnosis,
6009
6120
  consultationFallbackAuthorized,
@@ -6012,7 +6123,7 @@ export const SortieDogsPlugin = async (input, options) => {
6012
6123
  });
6013
6124
  if (toolInput.tool === "task" && taskRole !== undefined && IMPLEMENTATION_AGENTS.has(taskRole) &&
6014
6125
  isRecord(output.args)) {
6015
- await reserveGoalDispatch(toolInput.sessionID, toolInput.callID, typeof output.args.prompt === "string" ? output.args.prompt : "", validatedRootAcceptance);
6126
+ await reserveGoalDispatch(toolInput.sessionID, toolInput.callID, typeof output.args.prompt === "string" ? output.args.prompt : "");
6016
6127
  }
6017
6128
  if (validatedRootAcceptance !== undefined && reservedParallelDescriptor === undefined) {
6018
6129
  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 {};
@@ -18,14 +18,20 @@ export function createSortieResult(receipt, goal, metrics, asOf = receipt.ended_
18
18
  const hostMetric = (value) => value === undefined
19
19
  ? unavailable(metrics === undefined ? "host-metrics-unavailable" : "incomplete-host-coverage")
20
20
  : available(value, "host-reported");
21
- const failed = receipt.stop_reason === "external_dependency" || receipt.stop_reason === "persistence_unavailable";
21
+ const missionStatus = receipt.status === "succeeded"
22
+ ? "COMPLETED"
23
+ : receipt.stop_reason === "awaiting_user"
24
+ ? "USER_DECISION"
25
+ : receipt.stop_reason === "external_dependency" || receipt.stop_reason === "persistence_unavailable"
26
+ ? "EXTERNAL_BLOCKER"
27
+ : "INTERRUPTED";
22
28
  return {
23
29
  schema_version: "0.1",
24
30
  result_id: [receipt.goal_id, receipt.terminal_revision],
25
31
  accounting_phase: "pre-terminal",
26
32
  as_of: asOf,
27
33
  mission: {
28
- status: receipt.status === "succeeded" ? "COMPLETED" : failed ? "FAILED" : "STOPPED",
34
+ status: missionStatus,
29
35
  stop_reason: receipt.stop_reason,
30
36
  },
31
37
  speed: {
@@ -105,11 +111,11 @@ function assistantMessages(value) {
105
111
  });
106
112
  }
107
113
  function conclusionStatusAlias(line) {
108
- const match = /^([✅⛔❓])[ \t]+conclusion:\s*status:\s*(DONE|BLOCKED|NEED_DECISION)\b/iu.exec(line);
114
+ const match = /^(✅|⚠️|⛔|❓)[ \t]+conclusion:\s*status:\s*(DONE|INTERRUPTED|BLOCKED|NEED_DECISION)\b/iu.exec(line);
109
115
  const outcome = match?.[2]?.toUpperCase();
110
- if (outcome !== "DONE" && outcome !== "BLOCKED" && outcome !== "NEED_DECISION")
116
+ if (outcome !== "DONE" && outcome !== "INTERRUPTED" && outcome !== "BLOCKED" && outcome !== "NEED_DECISION")
111
117
  return undefined;
112
- const expectedIcon = outcome === "DONE" ? "✅" : outcome === "BLOCKED" ? "⛔" : "❓";
118
+ const expectedIcon = outcome === "DONE" ? "✅" : outcome === "INTERRUPTED" ? "⚠️" : outcome === "BLOCKED" ? "⛔" : "❓";
113
119
  return match?.[1] === expectedIcon ? outcome : undefined;
114
120
  }
115
121
  export async function collectRunMetrics(client, rootSessionID, directory, now = Date.now(), window) {
@@ -287,18 +293,22 @@ function duration(milliseconds) {
287
293
  return `${minutes}m ${String(seconds % 60).padStart(2, "0")}s`;
288
294
  }
289
295
  function metricText(metric, render) {
290
- return metric.availability === "available" ? render(metric.value) : `unavailable (${metric.reason})`;
296
+ return metric.availability === "available" ? render(metric.value) : "計測不可";
291
297
  }
292
298
  export function formatSortieResult(result) {
293
299
  const criteria = metricText(result.proof.criteria, (entries) => {
294
300
  const passing = entries.filter(({ status }) => status === "PASS").length;
295
- return `${passing}/${entries.length} criteria`;
301
+ return `${passing}/${entries.length}`;
296
302
  });
303
+ const achievement = result.mission.status === "COMPLETED" ? "完了"
304
+ : result.mission.status === "INTERRUPTED" ? "中断(未完了)"
305
+ : result.mission.status === "EXTERNAL_BLOCKER" ? "外部要因で未完了"
306
+ : "ユーザー判断待ち(未完了)";
297
307
  return [
298
308
  "**Sortie Result**",
299
- `**Speed:** goal ${metricText(result.speed.goal_wall_ms, duration)} · worker ${metricText(result.speed.worker_execution_ms, duration)} · compression ${metricText(result.speed.execution_compression, (value) => `${value.toFixed(2)}x`)}`,
300
- `**Cost:** ${metricText(result.cost.total_tokens, (value) => `${value.toLocaleString("en-US")} tokens`)} · ${metricText(result.cost.cost_usd, (value) => `$${value.toFixed(4)}`)} · ${result.accounting_phase}`,
301
- `**Proof:** ${result.proof.overall} · ${result.mission.status.toLowerCase()} (${result.mission.stop_reason}) · ${criteria} · ${result.proof.evidence_refs.length} evidence ref${result.proof.evidence_refs.length === 1 ? "" : "s"}`,
309
+ `**Speed:** 全体 ${metricText(result.speed.goal_wall_ms, duration)} · worker ${metricText(result.speed.worker_execution_ms, duration)}`,
310
+ `**Cost:** ${metricText(result.cost.total_tokens, (value) => `${value.toLocaleString("ja-JP")}トークン`)} · ${metricText(result.cost.cost_usd, (value) => `$${value.toFixed(4)}`)}`,
311
+ `**達成:** ${achievement} · acceptance ${criteria}`,
302
312
  ].join("\n");
303
313
  }
304
314
  export function formatRunMetrics(metrics) {
@@ -337,15 +347,16 @@ function terminalCheckpoint(text) {
337
347
  return undefined;
338
348
  const checkpoint = (() => {
339
349
  const { index, line } = first;
340
- const normalized = /^status:\s*(DONE|BLOCKED|NEED_DECISION)\b/iu.exec(line)?.[1]?.toUpperCase();
341
- const explicit = normalized === "DONE" || normalized === "BLOCKED" || normalized === "NEED_DECISION"
350
+ const normalized = /^status:\s*(DONE|INTERRUPTED|BLOCKED|NEED_DECISION)\b/iu.exec(line)?.[1]?.toUpperCase();
351
+ const explicit = normalized === "DONE" || normalized === "INTERRUPTED" || normalized === "BLOCKED" || normalized === "NEED_DECISION"
342
352
  ? normalized
343
353
  : undefined;
344
354
  const outcome = explicit ?? conclusionStatusAlias(line) ??
345
355
  (/^✅[ \t]+\*\*DONE\*\*/u.test(line) ? "DONE" :
346
- /^⛔[ \t]+\*\*BLOCKED\*\*/u.test(line) ? "BLOCKED" :
347
- /^❓[ \t]+\*\*NEED_DECISION\*\*/u.test(line) ? "NEED_DECISION" : undefined);
348
- return outcome === "DONE" || outcome === "BLOCKED" || outcome === "NEED_DECISION"
356
+ /^⚠️[ \t]+\*\*INTERRUPTED\*\*/u.test(line) ? "INTERRUPTED" :
357
+ /^⛔[ \t]+\*\*BLOCKED\*\*/u.test(line) ? "BLOCKED" :
358
+ /^❓[ \t]+\*\*NEED_DECISION\*\*/u.test(line) ? "NEED_DECISION" : undefined);
359
+ return outcome === "DONE" || outcome === "INTERRUPTED" || outcome === "BLOCKED" || outcome === "NEED_DECISION"
349
360
  ? { index, outcome }
350
361
  : undefined;
351
362
  })();
@@ -365,25 +376,49 @@ export function terminalRunOutcome(text) {
365
376
  ? "BLOCKED"
366
377
  : undefined;
367
378
  }
368
- export function insertRunMetrics(text, metrics) {
379
+ export function replaceTerminalStatus(text, replacement) {
369
380
  const checkpoint = terminalCheckpoint(text);
370
- if (checkpoint?.outcome !== "DONE")
371
- return text;
372
- if (topLevelLines(text).some(({ index, line }) => index > checkpoint.index && /^\*\*Run:\*\*/u.test(line)))
381
+ if (checkpoint === undefined)
373
382
  return text;
374
383
  const newline = text.includes("\r\n") ? "\r\n" : "\n";
375
384
  const lines = text.split(/\r?\n/u);
385
+ lines.splice(checkpoint.index, 1, ...replacement.split(/\r?\n/u));
386
+ return lines.join(newline);
387
+ }
388
+ export function replaceDoneTerminalStatus(text, replacement) {
389
+ return terminalCheckpoint(text)?.outcome === "DONE" ? replaceTerminalStatus(text, replacement) : text;
390
+ }
391
+ export function sanitizeTerminalReport(text) {
392
+ const newline = text.includes("\r\n") ? "\r\n" : "\n";
393
+ const internal = /\b(?:evidence_refs?|manifest|raw|raw_status|reason_code|goal_control|TRUE_BLOCKER)\s*:|\bEvidence\b/iu;
394
+ return text.replace(/^[ \t]*(`{3,}|~{3,})[^\r\n]*\r?\n([\s\S]*?)^[ \t]*\1[ \t]*$/gimu, (block, _fence, body) => internal.test(body) ? "" : block)
395
+ .replace(/<details\b[^>]*>[\s\S]*?<\/details>/giu, "")
396
+ .split(/\r?\n/u)
397
+ .filter((line) => !/^\s*(?:(?:#{1,6}\s*)?\**Evidence\**\s*:|(?:TRUE_BLOCKER|goal_control|evidence_refs?|reason_code|raw|raw_status|manifest)\s*:)/iu.test(line))
398
+ .join(newline)
399
+ .trimEnd();
400
+ }
401
+ export function insertRunMetrics(text, metrics) {
402
+ const visible = sanitizeTerminalReport(text);
403
+ const checkpoint = terminalCheckpoint(visible);
404
+ if (checkpoint?.outcome !== "DONE")
405
+ return visible;
406
+ if (topLevelLines(visible).some(({ index, line }) => index > checkpoint.index && /^\*\*Run:\*\*/u.test(line)))
407
+ return visible;
408
+ const newline = visible.includes("\r\n") ? "\r\n" : "\n";
409
+ const lines = visible.split(/\r?\n/u);
376
410
  lines.splice(checkpoint.index + 1, 0, "", formatRunMetrics(metrics));
377
411
  return lines.join(newline);
378
412
  }
379
413
  export function insertSortieResult(text, result) {
380
- const checkpoint = terminalCheckpoint(text);
414
+ const visible = sanitizeTerminalReport(text);
415
+ const checkpoint = terminalCheckpoint(visible);
381
416
  if (checkpoint === undefined)
382
- return text;
383
- if (topLevelLines(text).some(({ index, line }) => index > checkpoint.index && /^\*\*Sortie Result\*\*/u.test(line)))
384
- return text;
385
- const newline = text.includes("\r\n") ? "\r\n" : "\n";
386
- const lines = text.split(/\r?\n/u);
417
+ return visible;
418
+ if (topLevelLines(visible).some(({ index, line }) => index > checkpoint.index && /^\*\*Sortie Result\*\*/u.test(line)))
419
+ return visible;
420
+ const newline = visible.includes("\r\n") ? "\r\n" : "\n";
421
+ const lines = visible.split(/\r?\n/u);
387
422
  lines.splice(checkpoint.index + 1, 0, "", formatSortieResult(result));
388
423
  return lines.join(newline);
389
424
  }