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 +7 -2
- 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/continuation.js +3 -2
- package/dist/plugin/gate.d.ts +1 -1
- package/dist/plugin/gate.js +15 -3
- package/dist/plugin/index.js +83 -7
- package/package.json +59 -59
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.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
|
-
##
|
|
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;
|
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
|
|
@@ -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
|
-
|
|
449
|
-
return
|
|
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);
|
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
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
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
|
+
}
|