sortie-dogs 0.9.6 → 0.9.7

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.6](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.6)
27
+ Release: [v0.9.7](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.7)
28
28
 
29
29
  ## Quick start
30
30
 
@@ -417,7 +417,12 @@ older runtime files, and records the installed version in
417
417
  untouched and initialization stops safely. User-owned configuration—including
418
418
  `.opencode/sortie-dogs.json`—and standard OpenCode files are preserved.
419
419
 
420
- ## Safe manual removal
420
+ ## Maintainer releases
421
+
422
+ The [release batch guide](docs/release-batch.md) covers fixed-tarball CLI verification,
423
+ global application, resumable GitHub publication, and manual npm publication checks.
424
+
425
+ ## Safe manual removal
421
426
 
422
427
  There is no supported Sortie-dogs uninstall command. Remove the npm dependency
423
428
  separately, then follow the [safe manual removal guide](docs/uninstall.md) to
@@ -12,6 +12,7 @@
12
12
  * identity fails closed into "no automatic continuation" rather than into a guess.
13
13
  */
14
14
  import { openCodeModel } from "./model-routing-hook.js";
15
+ import { terminalRunOutcome } from "./run-metrics.js";
15
16
  /** Plugin tool name the coordinator asset names as the direct continuation capability. */
16
17
  export const CONTINUATION_CAPABILITY = "sortie_compact_and_continue";
17
18
  /** Fallback marker, used only when the direct capability is unavailable. */
@@ -445,8 +446,8 @@ export function createContinuationHooks(client, directory, policySource, timings
445
446
  return firstCheckpoint(text)?.status;
446
447
  }
447
448
  function terminalCheckpoint(text) {
448
- const status = checkpointStatus(text);
449
- return status === "DONE" || status === "NEED_DECISION" || (status === "BLOCKED" && trueBlockerReport(text));
449
+ // Use the same terminal vocabulary as the receipt/debrief path, including INTERRUPTED.
450
+ return terminalRunOutcome(text) !== undefined;
450
451
  }
451
452
  function trueBlockerReport(text) {
452
453
  const checkpoint = firstCheckpoint(text);
@@ -1130,6 +1130,7 @@ export const SortieDogsPlugin = async (input, options) => {
1130
1130
  const goalLedgerFiles = new Map();
1131
1131
  const goalLedgerDirectories = new Set();
1132
1132
  const goalReservations = new Map();
1133
+ const goalReservationRecoveries = new Map();
1133
1134
  const hostGoalExecutions = new Map();
1134
1135
  const goalValidationDefects = new Set();
1135
1136
  const goalDeclarationAuthority = new Map();
@@ -1435,7 +1436,69 @@ export const SortieDogsPlugin = async (input, options) => {
1435
1436
  startedAt: observedStartedAt, endedAt, exitCode: exitCode ?? null,
1436
1437
  ...(outcome === undefined ? {} : { outcome }), ...(immutableRef === undefined ? {} : { immutableRef }), fresh });
1437
1438
  }
1439
+ async function recoverCompletedGoalReservations(sessionID) {
1440
+ const root = goalRoot(sessionID);
1441
+ const active = goalReservationRecoveries.get(root);
1442
+ if (active !== undefined)
1443
+ return active;
1444
+ const recovery = (async () => {
1445
+ const ledger = await goalLedger(sessionID);
1446
+ const state = (await ledger.readGoal()).state;
1447
+ const pending = state.outstanding_reservations.filter(reservation => ![...goalReservations.values()].some(live => live.reservationID === reservation.reservation_id));
1448
+ if (pending.length === 0 || state.goal_id === null || input.client?.session?.messages === undefined)
1449
+ return;
1450
+ const messages = input.client.session.messages;
1451
+ const response = await messages.call(input.client.session, { path: { id: root },
1452
+ query: { directory: input.directory, limit: 1000 } }).catch(() => undefined);
1453
+ const data = isRecord(response) ? response.data : undefined;
1454
+ if (!Array.isArray(data))
1455
+ return;
1456
+ for (const reservation of pending) {
1457
+ const matches = [];
1458
+ for (const message of data.slice(-1000)) {
1459
+ if (!isRecord(message) || !isRecord(message.info) || message.info.role !== "assistant" ||
1460
+ message.info.sessionID !== root || !Array.isArray(message.parts))
1461
+ continue;
1462
+ for (const part of message.parts) {
1463
+ if (!isRecord(part) || part.type !== "tool" || part.tool !== "task" || typeof part.callID !== "string" ||
1464
+ !isRecord(part.state) || !["completed", "error"].includes(String(part.state.status)) ||
1465
+ !isRecord(part.state.input) || typeof part.state.input.prompt !== "string" ||
1466
+ !["dog-worker", "dog-luna-worker"].includes(String(part.state.input.subagent_type)))
1467
+ continue;
1468
+ const unitID = handoffValue(handoffEntries(part.state.input.prompt), ["task_id"]) ?? part.callID;
1469
+ if (unitID !== reservation.unit_id || goalFingerprint({ goal_id: state.goal_id, unit_id: unitID,
1470
+ call_id: part.callID }) !== reservation.reservation_id)
1471
+ continue;
1472
+ const time = isRecord(part.state.time) ? part.state.time : undefined;
1473
+ const elapsed = typeof time?.start === "number" && typeof time.end === "number" &&
1474
+ Number.isFinite(time.start) && Number.isFinite(time.end) && time.end >= time.start ? time.end - time.start : null;
1475
+ matches.push({ callID: part.callID, status: String(part.state.status), elapsed });
1476
+ }
1477
+ }
1478
+ if (matches.length !== 1)
1479
+ continue;
1480
+ const match = matches[0];
1481
+ // Reconcile lifecycle accounting only. Lost in-memory validation bindings cannot be
1482
+ // reconstructed from worker prose and must not manufacture acceptance evidence.
1483
+ await ledger.appendGoal({ kind: "unit.settled", at: new Date().toISOString(),
1484
+ reservation_id: reservation.reservation_id,
1485
+ receipt_id: goalFingerprint({ recovered_host_task: match.callID, reservation: reservation.reservation_id }),
1486
+ goal_id: state.goal_id, unit_id: reservation.unit_id,
1487
+ disposition: match.status === "completed" ? "succeeded" : "cancelled", result_class: "process-defect",
1488
+ progress_fingerprint: null, evidence: [], elapsed_ms: match.elapsed, cost_usd: null });
1489
+ appLogInfo("goal.reservation-recovered", root, { unitID: reservation.unit_id, hostStatus: match.status });
1490
+ }
1491
+ })();
1492
+ goalReservationRecoveries.set(root, recovery);
1493
+ try {
1494
+ await recovery;
1495
+ }
1496
+ finally {
1497
+ goalReservationRecoveries.delete(root);
1498
+ }
1499
+ }
1438
1500
  async function acceptRealGoalTurn(sessionID, messageID, selectedAgent, parts) {
1501
+ await recoverCompletedGoalReservations(sessionID);
1439
1502
  const ledger = await goalLedger(sessionID);
1440
1503
  const state = (await ledger.readGoal()).state;
1441
1504
  if (state.latest_user_message_id === messageID)
@@ -1824,6 +1887,17 @@ export const SortieDogsPlugin = async (input, options) => {
1824
1887
  let state = await bindGoalDeclaration(sessionID, prompt) ?? (await ledger.readGoal()).state;
1825
1888
  if (state.goal_id === null)
1826
1889
  return; // Legacy already-authorized roots may settle without inventing authority.
1890
+ const budgetDenied = (dimension) => new Error("SORTIE_GOAL_CONTROL_DENIED: stop_budget\n" + JSON.stringify({
1891
+ goal_id: state.goal_id, revision: state.revision, exhausted_dimension: dimension,
1892
+ budget: state.budget, consumed_units: state.consumed_units,
1893
+ reserved_units: state.outstanding_reservations.length,
1894
+ remaining_units: state.budget === null ? null : Math.max(0, state.budget.max_units - state.consumed_units - state.outstanding_reservations.length),
1895
+ consumed_time_ms: state.consumed_time_ms, consumed_cost_usd: state.consumed_cost_usd,
1896
+ validation_consumed: state.validation_budget.consumed, validation_limit: state.validation_budget.limit,
1897
+ remedy: "goal_budget_units is the cumulative limit across this goal's revisions, not an added allowance. " +
1898
+ "Use these host counters when requesting an explicit budget revision. Renaming task_id or contracts does not create a new goal. " +
1899
+ "If the user holds or stops, report status: INTERRUPTED and do not redispatch."
1900
+ }));
1827
1901
  if (state.phase === "terminal" || state.receipt !== null) {
1828
1902
  throw new Error("SORTIE_GOAL_CONTROL_DENIED: terminal");
1829
1903
  }
@@ -1837,17 +1911,17 @@ export const SortieDogsPlugin = async (input, options) => {
1837
1911
  }
1838
1912
  if (state.budget !== null && state.consumed_units + state.outstanding_reservations.length >= state.budget.max_units) {
1839
1913
  await terminalGoal(sessionID, "stop_budget", "stopped");
1840
- throw new Error("SORTIE_GOAL_CONTROL_DENIED: stop_budget");
1914
+ throw budgetDenied("units");
1841
1915
  }
1842
1916
  if (state.budget !== null && state.budget.time_ms !== null &&
1843
1917
  (state.consumed_time_ms === null || state.consumed_time_ms >= state.budget.time_ms)) {
1844
1918
  await terminalGoal(sessionID, "stop_budget", "stopped");
1845
- throw new Error("SORTIE_GOAL_CONTROL_DENIED: stop_budget");
1919
+ throw budgetDenied("time_ms");
1846
1920
  }
1847
1921
  if (state.budget !== null && state.budget.cost_usd !== null &&
1848
1922
  (state.consumed_cost_usd === null || state.consumed_cost_usd >= state.budget.cost_usd)) {
1849
1923
  await terminalGoal(sessionID, "stop_budget", "stopped");
1850
- throw new Error("SORTIE_GOAL_CONTROL_DENIED: stop_budget");
1924
+ throw budgetDenied("cost_usd");
1851
1925
  }
1852
1926
  if (state.goal_id === null)
1853
1927
  return;
package/package.json CHANGED
@@ -1,62 +1,62 @@
1
- {
2
- "name": "sortie-dogs",
3
- "version": "0.9.6",
1
+ {
2
+ "name": "sortie-dogs",
3
+ "version": "0.9.7",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
- "keywords": [
6
- "opencode",
7
- "opencode-plugin",
8
- "ai-orchestration",
5
+ "keywords": [
6
+ "opencode",
7
+ "opencode-plugin",
8
+ "ai-orchestration",
9
9
  "multi-agent",
10
10
  "agent-harness",
11
11
  "harness-engineering",
12
12
  "coding-agent",
13
13
  "agentic-coding"
14
- ],
15
- "license": "MIT",
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/zufall-upon/Sortie-dogs.git"
19
- },
20
- "bugs": {
21
- "url": "https://github.com/zufall-upon/Sortie-dogs/issues"
22
- },
23
- "homepage": "https://github.com/zufall-upon/Sortie-dogs#readme",
24
- "publishConfig": {
25
- "access": "public"
26
- },
27
- "type": "module",
28
- "engines": {
29
- "node": ">=22.6.0"
30
- },
31
- "files": [
32
- "dist"
33
- ],
34
- "exports": {
35
- ".": {
36
- "types": "./dist/index.d.ts",
37
- "import": "./dist/index.js"
38
- },
39
- "./plugin": {
40
- "types": "./dist/plugin/opencode.d.ts",
41
- "import": "./dist/plugin/opencode.js"
42
- },
43
- "./server": {
44
- "types": "./dist/plugin/opencode.d.ts",
45
- "import": "./dist/plugin/opencode.js"
46
- },
47
- "./assets": {
48
- "types": "./dist/runtime-assets.d.ts",
49
- "import": "./dist/runtime-assets.js"
50
- }
51
- },
52
- "bin": {
53
- "sortie-dogs": "dist/cli/main.js"
14
+ ],
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/zufall-upon/Sortie-dogs.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/zufall-upon/Sortie-dogs/issues"
22
+ },
23
+ "homepage": "https://github.com/zufall-upon/Sortie-dogs#readme",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "engines": {
29
+ "node": ">=22.6.0"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js"
38
+ },
39
+ "./plugin": {
40
+ "types": "./dist/plugin/opencode.d.ts",
41
+ "import": "./dist/plugin/opencode.js"
42
+ },
43
+ "./server": {
44
+ "types": "./dist/plugin/opencode.d.ts",
45
+ "import": "./dist/plugin/opencode.js"
46
+ },
47
+ "./assets": {
48
+ "types": "./dist/runtime-assets.d.ts",
49
+ "import": "./dist/runtime-assets.js"
50
+ }
51
+ },
52
+ "bin": {
53
+ "sortie-dogs": "dist/cli/main.js"
54
54
  },
55
55
  "scripts": {
56
56
  "prebuild": "node --input-type=module --eval \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true });\"",
57
57
  "build": "tsc -p tsconfig.json",
58
- "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);\"",
59
- "prepack": "npm run build",
58
+ "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);\"",
59
+ "prepack": "npm run build",
60
60
  "pretest": "npm run build",
61
61
  "test": "node --experimental-strip-types --import ./test/setup.ts --test \"test/plugin.test.ts\" \"test/continuation.test.ts\" \"test/fast-lane.test.ts\"",
62
62
  "pretest:dispatch": "npm run build",
@@ -65,13 +65,13 @@
65
65
  "test:integration": "node --experimental-strip-types --import ./test/setup.ts --test --test-timeout=1200000 --test-reporter=spec \"test/integration/worktree-parallel-dispatch.test.ts\"",
66
66
  "pretest:full": "npm run build",
67
67
  "test:full": "node --experimental-strip-types test/helpers/full-test-runner.ts"
68
- },
69
- "dependencies": {
70
- "ajv": "^8.17.1",
71
- "ajv-formats": "^3.0.1"
72
- },
73
- "devDependencies": {
74
- "@types/node": "^22.0.0",
75
- "typescript": "^5.9.0"
76
- }
77
- }
68
+ },
69
+ "dependencies": {
70
+ "ajv": "^8.17.1",
71
+ "ajv-formats": "^3.0.1"
72
+ },
73
+ "devDependencies": {
74
+ "@types/node": "^22.0.0",
75
+ "typescript": "^5.9.0"
76
+ }
77
+ }