sortie-dogs 0.10.12 → 0.10.14

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
@@ -50,7 +50,7 @@ two-level subagent depth to `.opencode/opencode.json`, preserving existing value
50
50
  ```json
51
51
  {
52
52
  "plugins": ["sortie-dogs"],
53
- "subagent_depth": 2
53
+ "experimental": { "subagent_depth": 2 }
54
54
  }
55
55
  ```
56
56
 
@@ -302,7 +302,7 @@ npm install --global sortie-dogs
302
302
  sortie-dogs init --global --profile v010
303
303
  ```
304
304
 
305
- Then add `sortie-dogs` and `subagent_depth: 2` to the global OpenCode config.
305
+ Then add `sortie-dogs` and `experimental.subagent_depth: 2` to the global OpenCode config.
306
306
  Global initialization installs assets only; it does not silently change the
307
307
  default agent or merge user settings.
308
308
 
@@ -3,5 +3,5 @@
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
5
  export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
6
- export declare const V010_RUNTIME_ASSET_VERSION = "0.10.0-v0912-language4-cost-rpt10-compaction-ref2-proposal1-review-remediation1-surface4-proposal-recovery2-quality1-terminal2-route1-path1-permission2-prerequisite1-v2-bridge1-intent1-reflection1-cancel1-proposal-budget1-goal-recovery1-delegate-execute1";
6
+ export declare const V010_RUNTIME_ASSET_VERSION = "0.10.0-v0912-language4-cost-rpt10-compaction-ref2-proposal1-review-remediation1-surface4-proposal-recovery2-quality1-terminal2-route1-path1-permission2-prerequisite1-v2-bridge1-intent1-reflection1-cancel1-proposal-budget1-goal-recovery1-delegate-execute1-v2-depth1-process-replacement1-unstarted-v2-1";
7
7
  export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION;
@@ -3,4 +3,4 @@
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
5
  export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
6
- export const V010_RUNTIME_ASSET_VERSION = "0.10.0-v0912-language4-cost-rpt10-compaction-ref2-proposal1-review-remediation1-surface4-proposal-recovery2-quality1-terminal2-route1-path1-permission2-prerequisite1-v2-bridge1-intent1-reflection1-cancel1-proposal-budget1-goal-recovery1-delegate-execute1";
6
+ export const V010_RUNTIME_ASSET_VERSION = "0.10.0-v0912-language4-cost-rpt10-compaction-ref2-proposal1-review-remediation1-surface4-proposal-recovery2-quality1-terminal2-route1-path1-permission2-prerequisite1-v2-bridge1-intent1-reflection1-cancel1-proposal-budget1-goal-recovery1-delegate-execute1-v2-depth1-process-replacement1-unstarted-v2-1";
@@ -303,6 +303,7 @@ export declare class OperatorRuntime {
303
303
  observeChild(root: string, callID: string, childID: string): Promise<void>;
304
304
  private observeChildOnce;
305
305
  operatorReturned(root: string): Promise<OperatorState>;
306
+ processReplacementRequired(root: string, runID: string): Promise<OperatorState>;
306
307
  private operatorReturnedOnce;
307
308
  interrupted(root: string, reason: string): Promise<readonly string[]>;
308
309
  private interruptedOnce;
@@ -29,6 +29,7 @@ export class OperatorContractError extends Error {
29
29
  const OPERATOR_TASK_REFERENCE = "SORTIE_OPERATOR_TASK_REF";
30
30
  const OPERATOR_DELEGATE_TASK_REFERENCE = "SORTIE_OPERATOR_DELEGATE_REF";
31
31
  const REPAIR_VALIDATION_STALE_PROOF_DECISION = "operator-contract-repair-validation-incomplete:operator-recovery-proof-unavailable-or-stale";
32
+ const PROCESS_REPLACEMENT_DECISION = "operator-process-remediation-replacement-required";
32
33
  const ACCEPTANCE_REMEDIATION_DECISION = "operator-acceptance-remediation-required";
33
34
  const REVIEW_REMEDIATION_DECISION = "operator-review-remediation-required";
34
35
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
@@ -739,7 +740,7 @@ export class OperatorRuntime {
739
740
  [ACCEPTANCE_CONTINUITY_EXTENSION]: { schema_version: "0.1", authority: "dispatch", task_id: taskID,
740
741
  criteria: plan.acceptance, fingerprint: acceptanceFingerprint,
741
742
  parent_fingerprint: index === 0
742
- ? remediationTaskID !== undefined && priorAcceptedUnits.length === 0 ? "none" : parent?.acceptanceFingerprint ?? "none"
743
+ ? priorAcceptedUnits.length === 0 ? "none" : parent?.acceptanceFingerprint ?? "none"
743
744
  : acceptanceFingerprint },
744
745
  "sortie-dogs/unit-coverage": { schema_version: "0.1", task_id: taskID,
745
746
  acceptance_fingerprint: acceptanceFingerprint, indices: unit.acceptance_indices },
@@ -980,7 +981,10 @@ export class OperatorRuntime {
980
981
  }
981
982
  matchesRecordedWorkerTask(state, unitID, args) {
982
983
  const unit = state.units.find(item => item.unit.id === unitID);
983
- return unit !== undefined && this.matchesWorkerTask(state, unit, args);
984
+ const normalized = record(args) && typeof args.agent === "string" && args.subagent_type === undefined
985
+ ? { ...args, subagent_type: args.agent }
986
+ : args;
987
+ return unit !== undefined && this.matchesWorkerTask(state, unit, normalized);
984
988
  }
985
989
  admitOperator(root, callID, args) {
986
990
  return this.serial(root, () => this.admitOperatorOnce(root, callID, args));
@@ -1407,6 +1411,17 @@ export class OperatorRuntime {
1407
1411
  operatorReturned(root) {
1408
1412
  return this.serial(root, () => this.operatorReturnedOnce(root));
1409
1413
  }
1414
+ processReplacementRequired(root, runID) {
1415
+ return this.serial(root, async () => {
1416
+ const state = await this.required(root);
1417
+ if (state.runID !== runID || state.phase !== "awaiting-decision" || state.decision !== "process-defect") {
1418
+ throw new Error("operator-process-remediation-replacement-not-ready");
1419
+ }
1420
+ state.decision = PROCESS_REPLACEMENT_DECISION;
1421
+ await this.save(state);
1422
+ return state;
1423
+ });
1424
+ }
1410
1425
  async operatorReturnedOnce(root) {
1411
1426
  const state = await this.required(root);
1412
1427
  if (state.phase === "running") {
@@ -1651,8 +1666,9 @@ export class OperatorRuntime {
1651
1666
  const repairValidationRetryAvailable = this.canRetryRepairValidation(state);
1652
1667
  const acceptanceRemediation = state.decision === ACCEPTANCE_REMEDIATION_DECISION;
1653
1668
  const reviewRemediation = state.decision === REVIEW_REMEDIATION_DECISION;
1669
+ const processReplacement = state.decision === PROCESS_REPLACEMENT_DECISION;
1654
1670
  const hostReconciliationRequired = state.phase === "awaiting-decision" && state.contractRepair === null &&
1655
- !repairValidationRetryAvailable && !acceptanceRemediation && !reviewRemediation;
1671
+ !repairValidationRetryAvailable && !acceptanceRemediation && !reviewRemediation && !processReplacement;
1656
1672
  const failedAcceptanceUnit = acceptanceRemediation
1657
1673
  ? state.units.find(unit => unit.status === "failed" && ["acceptance", "process-defect"].includes(unit.resultClass ?? "")) : undefined;
1658
1674
  const failedEvidence = failedAcceptanceUnit?.failure;
@@ -1725,6 +1741,9 @@ export class OperatorRuntime {
1725
1741
  ...(hostReconciliationRequired ? {
1726
1742
  next_action: `call ${this.profile.toolPrefix}resume_operator once with this run_id and acceptance_fingerprint; if it returns a Task, dispatch that exact Task in this turn`,
1727
1743
  } : {}),
1744
+ ...(processReplacement ? {
1745
+ next_action: `this immutable run is not resumable: call ${this.profile.toolPrefix}cancel_operator with reason=plain, then call ${this.profile.toolPrefix}prepare_operator for a replacement run under the same goal; copy this packet's acceptance array verbatim, retain consumed cumulative budget, and repair only the read, write, preparation, and validation contract required by that acceptance; do not call resume_operator again or claim evidence`,
1746
+ } : {}),
1728
1747
  ...(acceptanceRemediation || reviewRemediation ? { next_action: remediationNextAction } : {}),
1729
1748
  ...(state.repairResidualPaths.length === 0 ? {} : { repair_residual_paths: state.repairResidualPaths }),
1730
1749
  ...(state.phase === "cancelled" && state.decision === "operator-contract-repair-unavailable-after-cancel" &&
@@ -4,6 +4,9 @@ import { type CatalogModel } from "./model-routing.js";
4
4
  export declare function processRemediationReplacementPacket(code: string, packet: unknown, cancelTool: string, prepareTool: string): {
5
5
  status: string;
6
6
  code: string;
7
+ same_run_resumable: boolean;
8
+ replacement_preserves_goal: boolean;
9
+ cumulative_spend_resets: boolean;
7
10
  packet: unknown;
8
11
  next_action: string;
9
12
  };
@@ -39,10 +39,13 @@ const PREVIEW_ROUTES = Object.freeze([
39
39
  PREVIEW_REVIEW_ROUTE,
40
40
  ]);
41
41
  export function processRemediationReplacementPacket(code, packet, cancelTool, prepareTool) {
42
- return { status: "operator-process-remediation-replacement-required", code, packet,
43
- next_action: `Call ${cancelTool} with reason=plain, then call ${prepareTool} with the exact same ordered acceptance, ` +
44
- "the same unit and write scope, and the remaining cumulative budget. Do not call resume_operator again, " +
45
- "claim evidence, reset spend, or widen scope." };
42
+ const durable = record(packet) ? { ...packet, resume_requires_host_reconciliation: false,
43
+ next_action: `This immutable run is not resumable. Call ${cancelTool} with reason=plain, then call ${prepareTool} for a replacement run under the same goal.` } : packet;
44
+ return { status: "operator-process-remediation-replacement-required", code, same_run_resumable: false,
45
+ replacement_preserves_goal: true, cumulative_spend_resets: false, packet: durable,
46
+ next_action: `Call ${cancelTool} with reason=plain, then call ${prepareTool} for a replacement run under the same goal. ` +
47
+ "Copy the packet acceptance array byte-for-byte and retain cumulative spend. Repair only the read, write, preparation, and validation contract required by that acceptance. " +
48
+ "Do not call resume_operator again, claim evidence, or treat the replacement as a new goal." };
46
49
  }
47
50
  /**
48
51
  * Declare every preview route in the catalog, adding the variant to a listed model and the whole model
@@ -570,10 +573,11 @@ export function createProfiledPlugin(profile, assetVersion) {
570
573
  if (requestedReason !== undefined && requestedReason !== "review-blocking" && requestedReason !== "acceptance-remediation")
571
574
  throw new Error("operator-cancel-reason-invalid");
572
575
  const pending = await operators.read(context.sessionID);
573
- if (!pending) {
576
+ const pendingProposal = await proposals.read(context.sessionID);
577
+ if (!pending || (["cancelled", "completed"].includes(pending.phase) && pendingProposal?.phase !== "approved" && pendingProposal !== undefined)) {
574
578
  // A pre-approval proposal owns no execution lane. Its admitted child can never be rebound, so without
575
579
  // this release the root can neither resume, redispatch, nor start any replacement investigation.
576
- const proposal = await proposals.read(context.sessionID);
580
+ const proposal = pendingProposal;
577
581
  if (!proposal || proposal.phase === "approved")
578
582
  throw new Error("operator-run-missing");
579
583
  if (requestedReason !== undefined)
@@ -704,7 +708,7 @@ export function createProfiledPlugin(profile, assetVersion) {
704
708
  if (unit.resultClass === "process-defect" && unit.childSessionID === null && unit.callID && state.decision === "dispatch-admission-rejected") {
705
709
  const history = await messages(state.units.length > 1 ? delegate : context.sessionID);
706
710
  const matching = history.flatMap(message => Array.isArray(message.parts) ? message.parts : []).filter(part => record(part) &&
707
- part.type === "tool" && part.tool === "task" && part.callID === unit.callID && record(part.state) && part.state.status === "error" &&
711
+ part.type === "tool" && ["task", "subagent"].includes(String(part.tool)) && part.callID === unit.callID && record(part.state) && part.state.status === "error" &&
708
712
  record(part.state.input) && operators.matchesRecordedWorkerTask(state, unit.unit.id, part.state.input) &&
709
713
  taskChildSessionID({ metadata: part.state.metadata, output: typeof part.state.output === "string" ? part.state.output : "" }) === undefined);
710
714
  if (matching.length !== 1 || !await control.hasNoGoalReservation(context.sessionID, taskID, unit.callID))
@@ -736,7 +740,8 @@ export function createProfiledPlugin(profile, assetVersion) {
736
740
  // A process defect without an authorized Git lifecycle has no committed remediation
737
741
  // baseline. Preserve the failed run and return an explicit replacement route instead
738
742
  // of turning this local routing limitation into a terminal tool exception.
739
- return JSON.stringify(processRemediationReplacementPacket(remediationError.message, operators.packet(state), cancel, prepare));
743
+ const replacement = await operators.processReplacementRequired(context.sessionID, state.runID);
744
+ return JSON.stringify(processRemediationReplacementPacket(remediationError.message, operators.packet(replacement), cancel, prepare));
740
745
  }
741
746
  }
742
747
  const retried = await operators.resumeRepairValidation(context.sessionID, state.runID, source => control.authorizeOperatorContractRepairValidationRetry(context.sessionID, { ...source, declarationFingerprint: goalFingerprint }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.10.12",
3
+ "version": "0.10.14",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",
@@ -62,14 +62,13 @@
62
62
  "build": "tsc -p tsconfig.json",
63
63
  "postbuild": "node --input-type=module --eval \"import fs from 'node:fs'; const path = 'dist/cli/main.js'; const source = fs.readFileSync(path, 'utf8'); if (!source.startsWith('#!')) fs.writeFileSync(path, '#!/usr/bin/env node\\n' + source); if (process.platform !== 'win32') fs.chmodSync(path, 0o755);\"",
64
64
  "prepack": "npm run build",
65
- "pretest": "npm run build",
66
- "test": "node --experimental-strip-types --import ./test/setup.ts --test \"test/plugin.test.ts\" \"test/continuation.test.ts\" \"test/fast-lane.test.ts\"",
65
+ "test": "node scripts/test-router.mjs quick",
66
+ "test:windows": "node scripts/test-router.mjs windows",
67
67
  "pretest:dispatch": "npm run build",
68
68
  "test:dispatch": "node --experimental-strip-types --import ./test/setup.ts --test --test-timeout=100000 --test-reporter=spec \"test/execution-plan.test.ts\" \"test/worktree-parallel-dispatch.test.ts\" \"test/acceptance-compiler.test.ts\" \"test/run-flight-ledger.test.ts\" \"test/evidence-capsule.test.ts\"",
69
69
  "pretest:integration": "npm run build",
70
70
  "test:integration": "node --experimental-strip-types --import ./test/setup.ts --test --test-timeout=1200000 --test-reporter=spec \"test/integration/worktree-parallel-dispatch.test.ts\"",
71
- "pretest:full": "npm run build",
72
- "test:full": "node --experimental-strip-types test/helpers/full-test-runner.ts"
71
+ "test:full": "node scripts/test-router.mjs full"
73
72
  },
74
73
  "dependencies": {
75
74
  "ajv": "^8.17.1",