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 +1 -1
- package/dist/core/goal-bound.d.ts +1 -0
- package/dist/core/goal-bound.js +20 -2
- package/dist/core/scope-lease-registry.js +7 -4
- package/dist/plugin/gate.d.ts +1 -1
- package/dist/plugin/gate.js +15 -3
- package/dist/plugin/index.js +6 -4
- package/package.json +1 -1
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.
|
|
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;
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
package/dist/plugin/gate.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/plugin/gate.js
CHANGED
|
@@ -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
|
-
|
|
1193
|
-
|
|
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
|
},
|
package/dist/plugin/index.js
CHANGED
|
@@ -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);
|