sortie-dogs 0.9.5 → 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.5](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.5)
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
@@ -113,6 +113,7 @@ export type GoalFlightEvent = (GoalEventBase & {
113
113
  readonly delivery: GoalDeliveryMode;
114
114
  readonly budget: GoalBudget;
115
115
  readonly acceptance_contract: GoalAcceptanceContract | null;
116
+ readonly reset_no_progress?: boolean;
116
117
  }) | (GoalEventBase & {
117
118
  readonly kind: "ticket.issued";
118
119
  readonly ticket_id: string;
@@ -106,6 +106,8 @@ function addUnique(values, value) {
106
106
  }
107
107
  export function reduceGoalFlight(records) {
108
108
  let state = initial();
109
+ let replanRevision = null;
110
+ let legacyResetRevision = false;
109
111
  let previous = null;
110
112
  for (const [index, record] of records.entries()) {
111
113
  requireState(record.sequence === index + 1 && record.previous_hash === previous && HASH.test(record.event_hash), "invalid", "Goal ledger chain is malformed.");
@@ -126,6 +128,8 @@ export function reduceGoalFlight(records) {
126
128
  latest_user_message_id: event.origin_user_message_id, selected_agent: event.selected_agent,
127
129
  delivery: event.delivery, budget: event.budget, acceptance_contract: event.acceptance_contract, phase: "active",
128
130
  session_ids: [event.origin_session_id] };
131
+ replanRevision = null;
132
+ legacyResetRevision = false;
129
133
  continue;
130
134
  }
131
135
  requireState(state.goal_id !== null && event.goal_id === state.goal_id, "transition", "Goal identity mismatch.");
@@ -139,7 +143,8 @@ export function reduceGoalFlight(records) {
139
143
  requireState(state.phase !== "terminal" && event.revision === state.revision + 1 && event.scope_epoch === state.scope_epoch + 1 &&
140
144
  HASH.test(event.acceptance_fingerprint) && event.budget.max_units >= state.consumed_units &&
141
145
  event.budget.max_units >= state.validation_budget.consumed &&
142
- validAcceptanceContract(event.acceptance_contract), "transition", "Scope revision is stale or resets consumed budget.");
146
+ validAcceptanceContract(event.acceptance_contract) &&
147
+ (event.reset_no_progress === undefined || typeof event.reset_no_progress === "boolean"), "transition", "Scope revision is stale or resets consumed budget.");
143
148
  state = { ...state, revision: event.revision, scope_epoch: event.scope_epoch,
144
149
  acceptance_fingerprint: event.acceptance_fingerprint, latest_user_message_id: event.origin_user_message_id,
145
150
  selected_agent: event.selected_agent, delivery: event.delivery, budget: event.budget,
@@ -147,7 +152,12 @@ export function reduceGoalFlight(records) {
147
152
  limit: state.validation_budget.limit === null ? null : event.budget.max_units },
148
153
  acceptance_contract: event.acceptance_contract,
149
154
  session_ids: addUnique(state.session_ids, event.session_id), phase: "active", stop_reason: null,
150
- receipt: null, tickets: [] };
155
+ receipt: null, tickets: [], ...(event.reset_no_progress === true
156
+ ? { no_progress_results: 0, replan_required: false, replan_used: false }
157
+ : {}) };
158
+ if (event.reset_no_progress === true)
159
+ replanRevision = null;
160
+ legacyResetRevision = event.reset_no_progress === undefined;
151
161
  }
152
162
  else if (event.kind === "ticket.issued") {
153
163
  requireState(state.phase === "active" && event.revision === state.revision && event.scope_epoch === state.scope_epoch &&
@@ -165,6 +175,13 @@ export function reduceGoalFlight(records) {
165
175
  session_ids: addUnique(state.session_ids, event.session_id) };
166
176
  }
167
177
  else if (event.kind === "dispatch.reserved") {
178
+ // A short-lived pre-marker runtime reset no-progress on revision but persisted no flag.
179
+ // A later accepted dispatch proves that reset; older revisions followed by an explicit
180
+ // replan retain their original semantics.
181
+ if (legacyResetRevision && state.replan_required && state.replan_used && replanRevision !== null && state.revision > replanRevision) {
182
+ state = { ...state, no_progress_results: 0, replan_required: false, replan_used: false };
183
+ replanRevision = null;
184
+ }
168
185
  requireState(state.phase === "active" && !state.replan_required, "transition", "Dispatch requires terminal handling or one bounded replan.");
169
186
  requireState(state.budget !== null && state.consumed_units + state.outstanding_reservations.length < state.budget.max_units, "budget", "Goal unit budget exhausted.");
170
187
  requireState(state.budget.time_ms === null || (state.consumed_time_ms !== null && state.consumed_time_ms < state.budget.time_ms), "budget", "Goal time budget is exhausted or usage is unknown.");
@@ -222,6 +239,7 @@ export function reduceGoalFlight(records) {
222
239
  else if (event.kind === "goal.replanned") {
223
240
  requireState(state.phase === "active" && state.replan_required && !state.replan_used && event.revision === state.revision, "transition", "Bounded replan is unavailable.");
224
241
  state = { ...state, replan_used: true, replan_required: false, no_progress_results: 0 };
242
+ replanRevision = event.revision;
225
243
  }
226
244
  else if (event.kind === "goal.terminal") {
227
245
  const allAccepted = state.acceptance_contract !== null && state.acceptance_contract.criteria.every(({ criterion_id }) => state.satisfied_criteria.includes(criterion_id));
@@ -472,13 +472,16 @@ export class ScopeLeaseRegistry {
472
472
  await rmdir(this.lockPath).catch(() => undefined);
473
473
  return;
474
474
  }
475
- if (names.length !== 1)
475
+ const owners = names.filter((name) => OWNER_FILE.test(name));
476
+ if (owners.length !== 1 || names.length > MAX_TEMP_CLEANUP + 1 ||
477
+ names.some((name) => name !== owners[0] && !TEMP_FILE.test(name)))
476
478
  return;
477
- const match = OWNER_FILE.exec(names[0]);
479
+ const ownerName = owners[0];
480
+ const match = OWNER_FILE.exec(ownerName);
478
481
  if (match === null || !UUID.test(match[1]))
479
482
  return;
480
483
  const owner = match[1];
481
- const ownerPath = join(this.lockPath, names[0]);
484
+ const ownerPath = join(this.lockPath, ownerName);
482
485
  const before = await this.mutexSnapshot(ownerPath);
483
486
  if (before === undefined || Date.now() - before.mtimeMs <= this.options.mutexStaleMs || before.content !== owner)
484
487
  return;
@@ -490,7 +493,7 @@ export class ScopeLeaseRegistry {
490
493
  catch {
491
494
  return;
492
495
  }
493
- const movedOwnerPath = join(quarantine, names[0]);
496
+ const movedOwnerPath = join(quarantine, ownerName);
494
497
  const after = await this.mutexSnapshot(movedOwnerPath);
495
498
  if (after === undefined || !this.sameMutexSnapshot(before, after)) {
496
499
  // The whole-directory rename already fenced this owner. Keep its quarantine until a later
@@ -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);
@@ -69,5 +69,5 @@ export declare function canonicalManifestWriteScopes(project: ProjectPaths, mani
69
69
  export declare function canonicalManifestReadScopes(project: ProjectPaths, manifest: OperationManifest): Promise<readonly string[]>;
70
70
  export declare function writeScopesOverlap(left: readonly string[], right: readonly string[]): boolean;
71
71
  export declare function createProjectPaths(rootCandidate: string): Promise<ProjectPaths>;
72
- export declare function createWriteGate(project: ProjectPaths, value: unknown): Promise<WriteGate>;
72
+ export declare function createWriteGate(project: ProjectPaths, value: unknown, toolDirectory?: string): Promise<WriteGate>;
73
73
  export {};
@@ -964,7 +964,7 @@ export async function createProjectPaths(rootCandidate) {
964
964
  },
965
965
  };
966
966
  }
967
- export async function createWriteGate(project, value) {
967
+ export async function createWriteGate(project, value, toolDirectory = project.root) {
968
968
  const validated = validateOperationManifestSchema(value);
969
969
  if (!validated.ok)
970
970
  throw new WriteDeniedError("manifest-unavailable", "<unknown>");
@@ -1189,8 +1189,20 @@ export async function createWriteGate(project, value) {
1189
1189
  }
1190
1190
  throw new WriteDeniedError("path-required", "<missing-path>");
1191
1191
  }
1192
- for (const path of extracted.paths)
1193
- await checkPath(path);
1192
+ // OpenCode resolves native file-tool paths against its instance directory, not the
1193
+ // manifest root. Check the same destination without rewriting the tool arguments.
1194
+ const nativeFileTool = /^(?:write|edit)(?:$|[_-])/iu.test(_input.tool) || /patch/iu.test(_input.tool);
1195
+ for (const path of extracted.paths) {
1196
+ if (nativeFileTool) {
1197
+ try {
1198
+ normalizeManifestPath(path);
1199
+ }
1200
+ catch (error) {
1201
+ throw new WriteDeniedError("project-boundary", path, { cause: error });
1202
+ }
1203
+ }
1204
+ await checkPath(nativeFileTool ? resolve(toolDirectory, path) : path);
1205
+ }
1194
1206
  if (extracted.gitCommit)
1195
1207
  await checkCachedSet();
1196
1208
  },
@@ -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)
@@ -1813,7 +1876,7 @@ export const SortieDogsPlugin = async (input, options) => {
1813
1876
  delivery: declaration.delivery, budget: { max_units: maxUnits,
1814
1877
  time_ms: timeBudget, cost_usd: costBudget,
1815
1878
  source: Number.isSafeInteger(units) ? "accepted-plan" : state.budget?.source ?? "policy-default" },
1816
- acceptance_contract: declaration.contract });
1879
+ acceptance_contract: declaration.contract, reset_no_progress: true });
1817
1880
  goalDeclarationAuthority.delete(sessionID);
1818
1881
  return state;
1819
1882
  }
@@ -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;
@@ -2130,7 +2204,7 @@ export const SortieDogsPlugin = async (input, options) => {
2130
2204
  throw new WriteDeniedError("manifest-unavailable", "<unknown>");
2131
2205
  loaded.manifest = validation.value;
2132
2206
  loaded.manifestFingerprint = inspectionFingerprint(validation.value, undefined);
2133
- loaded.gate = await createWriteGate(project, validation.value);
2207
+ loaded.gate = await createWriteGate(project, validation.value, input.directory);
2134
2208
  bootstrapRequired = false;
2135
2209
  bootstrapCompleted = true;
2136
2210
  loadFailure = undefined;
@@ -4057,7 +4131,7 @@ export const SortieDogsPlugin = async (input, options) => {
4057
4131
  });
4058
4132
  }
4059
4133
  manifest = manifestValidation.value;
4060
- authorizationGate = await createWriteGate(candidateProject, manifest);
4134
+ authorizationGate = await createWriteGate(candidateProject, manifest, input.directory);
4061
4135
  inspectedProjectRoot = candidateProject.root;
4062
4136
  }
4063
4137
  catch (error) {
@@ -4352,7 +4426,7 @@ export const SortieDogsPlugin = async (input, options) => {
4352
4426
  idempotent: true,
4353
4427
  });
4354
4428
  }
4355
- const gate = await createWriteGate(candidate, validation.value);
4429
+ const gate = await createWriteGate(candidate, validation.value, input.directory);
4356
4430
  const readScopes = await canonicalManifestReadScopes(candidate, validation.value);
4357
4431
  const writeScopes = await canonicalManifestWriteScopes(candidate, validation.value);
4358
4432
  // Keep conflict detection and registration in one JavaScript turn so competing binds fail closed.
@@ -6407,6 +6481,8 @@ export const SortieDogsPlugin = async (input, options) => {
6407
6481
  }
6408
6482
  catch (error) {
6409
6483
  activeState?.inFlightCalls.delete(toolInput.callID);
6484
+ if (error instanceof WriteDeniedError)
6485
+ goalValidationDefects.add(toolInput.sessionID);
6410
6486
  if (!(error instanceof WriteDeniedError) || error.reason === "repeated-denial")
6411
6487
  throw error;
6412
6488
  const signature = denialSignature(toolInput, output, error.reason);
package/package.json CHANGED
@@ -1,62 +1,62 @@
1
- {
2
- "name": "sortie-dogs",
3
- "version": "0.9.5",
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
+ }