sortie-dogs 0.9.5 → 0.9.6

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.6](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.6)
28
28
 
29
29
  ## Quick start
30
30
 
@@ -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
@@ -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
  },
@@ -1813,7 +1813,7 @@ export const SortieDogsPlugin = async (input, options) => {
1813
1813
  delivery: declaration.delivery, budget: { max_units: maxUnits,
1814
1814
  time_ms: timeBudget, cost_usd: costBudget,
1815
1815
  source: Number.isSafeInteger(units) ? "accepted-plan" : state.budget?.source ?? "policy-default" },
1816
- acceptance_contract: declaration.contract });
1816
+ acceptance_contract: declaration.contract, reset_no_progress: true });
1817
1817
  goalDeclarationAuthority.delete(sessionID);
1818
1818
  return state;
1819
1819
  }
@@ -2130,7 +2130,7 @@ export const SortieDogsPlugin = async (input, options) => {
2130
2130
  throw new WriteDeniedError("manifest-unavailable", "<unknown>");
2131
2131
  loaded.manifest = validation.value;
2132
2132
  loaded.manifestFingerprint = inspectionFingerprint(validation.value, undefined);
2133
- loaded.gate = await createWriteGate(project, validation.value);
2133
+ loaded.gate = await createWriteGate(project, validation.value, input.directory);
2134
2134
  bootstrapRequired = false;
2135
2135
  bootstrapCompleted = true;
2136
2136
  loadFailure = undefined;
@@ -4057,7 +4057,7 @@ export const SortieDogsPlugin = async (input, options) => {
4057
4057
  });
4058
4058
  }
4059
4059
  manifest = manifestValidation.value;
4060
- authorizationGate = await createWriteGate(candidateProject, manifest);
4060
+ authorizationGate = await createWriteGate(candidateProject, manifest, input.directory);
4061
4061
  inspectedProjectRoot = candidateProject.root;
4062
4062
  }
4063
4063
  catch (error) {
@@ -4352,7 +4352,7 @@ export const SortieDogsPlugin = async (input, options) => {
4352
4352
  idempotent: true,
4353
4353
  });
4354
4354
  }
4355
- const gate = await createWriteGate(candidate, validation.value);
4355
+ const gate = await createWriteGate(candidate, validation.value, input.directory);
4356
4356
  const readScopes = await canonicalManifestReadScopes(candidate, validation.value);
4357
4357
  const writeScopes = await canonicalManifestWriteScopes(candidate, validation.value);
4358
4358
  // Keep conflict detection and registration in one JavaScript turn so competing binds fail closed.
@@ -6407,6 +6407,8 @@ export const SortieDogsPlugin = async (input, options) => {
6407
6407
  }
6408
6408
  catch (error) {
6409
6409
  activeState?.inFlightCalls.delete(toolInput.callID);
6410
+ if (error instanceof WriteDeniedError)
6411
+ goalValidationDefects.add(toolInput.sessionID);
6410
6412
  if (!(error instanceof WriteDeniedError) || error.reason === "repeated-denial")
6411
6413
  throw error;
6412
6414
  const signature = denialSignature(toolInput, output, error.reason);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",