sortie-dogs 0.14.1 → 0.14.3
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 +6 -6
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/cli/codex-mission.js +3 -0
- package/dist/codex/mission-session.d.ts +2 -1
- package/dist/codex/mission-session.js +57 -5
- package/dist/core/goal-bound.d.ts +8 -0
- package/dist/core/goal-bound.js +7 -2
- package/dist/core/operator-mission.d.ts +3 -0
- package/dist/plugin/docker-validation.d.ts +14 -0
- package/dist/plugin/docker-validation.js +153 -0
- package/dist/plugin/frontierharness-agent-tool-registry.json +61 -0
- package/dist/plugin/index.js +61 -16
- package/dist/plugin/mission-review.d.ts +8 -0
- package/dist/plugin/mission-review.js +125 -25
- package/dist/plugin/operator-status-output.d.ts +2 -3
- package/dist/plugin/operator-status-output.js +42 -7
- package/dist/plugin/profiled.js +15 -6
- package/dist/plugin/protected-snapshot.d.ts +10 -0
- package/dist/plugin/protected-snapshot.js +28 -0
- package/dist/plugin/v2.d.ts +73 -0
- package/dist/plugin/v2.js +41 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,8 +19,8 @@ evidence-backed result, not more agents for their own sake.
|
|
|
19
19
|
[](https://www.npmjs.com/package/sortie-dogs)
|
|
20
20
|
[](LICENSE)
|
|
21
21
|
|
|
22
|
-
**Current release: [v0.14.
|
|
23
|
-
· [Release notes](docs/release-v0.14.
|
|
22
|
+
**Current release: [v0.14.3](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.14.3)**
|
|
23
|
+
· [Release notes](docs/release-v0.14.3.md) · [Execution vision](docs/v0.14.0-execution-vision.md)
|
|
24
24
|
|
|
25
25
|
[Quick start](#quick-start) · [Workflow](#mission-workflow) · [Models](#default-models) ·
|
|
26
26
|
[Measured results](#measured-results) · [Configuration](#configuration) · [Documentation](#documentation)
|
|
@@ -57,7 +57,7 @@ Use an existing Codex CLI signed in with **ChatGPT**. On Windows, PowerShell **7
|
|
|
57
57
|
available; POSIX uses bash. Sortie does not install Codex, start login or use a metered API fallback.
|
|
58
58
|
|
|
59
59
|
```sh
|
|
60
|
-
npm install --save-dev sortie-dogs@0.14.
|
|
60
|
+
npm install --save-dev sortie-dogs@0.14.3
|
|
61
61
|
npx --no-install sortie-dogs codex init .
|
|
62
62
|
```
|
|
63
63
|
|
|
@@ -86,7 +86,7 @@ bridge. [Codex guide: SDK, permissions, progress, resume and manifest route](doc
|
|
|
86
86
|
Use **OpenCode V2**. In the target project:
|
|
87
87
|
|
|
88
88
|
```sh
|
|
89
|
-
npm install --save-dev sortie-dogs@0.14.
|
|
89
|
+
npm install --save-dev sortie-dogs@0.14.3
|
|
90
90
|
npx --no-install sortie-dogs init .
|
|
91
91
|
```
|
|
92
92
|
|
|
@@ -242,7 +242,7 @@ Install the desired package version and rerun **your host's** initializer:
|
|
|
242
242
|
|
|
243
243
|
An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
|
|
244
244
|
update it. [Global/update instructions](docs/configuration.md#global-availability).
|
|
245
|
-
The current Mission asset marker is `0.14.
|
|
245
|
+
The current Mission asset marker is `0.14.3-workspace-proof-v1`; the unchanged Codex skill marker is
|
|
246
246
|
`0.13.11-codex-skill-v2`. Installed markers alone do not prove a running host reloaded the new version.
|
|
247
247
|
|
|
248
248
|
There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
|
|
@@ -258,7 +258,7 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
|
|
|
258
258
|
[SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
|
|
259
259
|
- **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
|
|
260
260
|
[Windows tests](docs/windows-tests.md) · [Release routine](docs/release-batch.md).
|
|
261
|
-
- **Releases:** [v0.14.1](docs/release-v0.14.1.md) · [v0.14.0](docs/release-v0.14.0.md) · [v0.13.22](docs/release-v0.13.22.md) · [v0.13.21](docs/release-v0.13.21.md) · [v0.13.20](docs/release-v0.13.20.md) · [v0.13.19](docs/release-v0.13.19.md) · [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
|
|
261
|
+
- **Releases:** [v0.14.3](docs/release-v0.14.3.md) · [v0.14.2](docs/release-v0.14.2.md) · [v0.14.1](docs/release-v0.14.1.md) · [v0.14.0](docs/release-v0.14.0.md) · [v0.13.22](docs/release-v0.13.22.md) · [v0.13.21](docs/release-v0.13.21.md) · [v0.13.20](docs/release-v0.13.20.md) · [v0.13.19](docs/release-v0.13.19.md) · [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
|
|
262
262
|
[v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
|
|
263
263
|
|
|
264
264
|
## Community
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export declare const V010_RUNTIME_ASSET_VERSION = "0.14.
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.14.3-workspace-proof-v1";
|
|
7
7
|
export declare const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
8
8
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION | typeof CODEX_SKILL_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export const V010_RUNTIME_ASSET_VERSION = "0.14.
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.14.3-workspace-proof-v1";
|
|
7
7
|
export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
@@ -71,6 +71,9 @@ export function codexMissionProgress(event) {
|
|
|
71
71
|
native_status: event.params.turn?.status };
|
|
72
72
|
if (event.method === "sortie/continuation")
|
|
73
73
|
return { thread_id: event.threadId, turn_id: event.params.turnId, phase: event.params.phase, calls: event.params.calls };
|
|
74
|
+
if (event.method === "sortie/coordinatorRestored")
|
|
75
|
+
return { thread_id: event.threadId, phase: "coordinator-restored", mission_id: event.params.missionId,
|
|
76
|
+
coordinator_id: event.params.coordinatorId, call_id: event.params.callId, native_outcome: event.params.outcome, next_action: event.params.action };
|
|
74
77
|
if (event.method === "sortie/modelRoute")
|
|
75
78
|
return { thread_id: event.threadId, phase: "model-route", agent: event.params.agent, model: event.params.model,
|
|
76
79
|
effort: event.params.effort, service_tier: event.params.serviceTier };
|
|
@@ -79,8 +79,9 @@ export declare class CodexMissionSession {
|
|
|
79
79
|
close(): Promise<void>;
|
|
80
80
|
private claimOwner;
|
|
81
81
|
private rootOf;
|
|
82
|
+
private restoreCoordinator;
|
|
82
83
|
private recoverNotStarted;
|
|
83
|
-
private
|
|
84
|
+
private bindTaskDispatch;
|
|
84
85
|
private recoverCompletedWorker;
|
|
85
86
|
private required;
|
|
86
87
|
private executeHostCommand;
|
|
@@ -62,6 +62,7 @@ export class CodexMissionSession {
|
|
|
62
62
|
this.observations = missions.find(item => item.root === resume)?.codexUsage ?? [];
|
|
63
63
|
await this.restoreSession(resume, "dog-operator");
|
|
64
64
|
this.root = resume;
|
|
65
|
+
await this.restoreCoordinator(resume);
|
|
65
66
|
await this.claimOwner(resume);
|
|
66
67
|
await this.hooks.tool?.sortie_v010_operator_status?.execute({ view: "full" }, { sessionID: resume, agent: "dog-operator" });
|
|
67
68
|
}
|
|
@@ -118,6 +119,45 @@ export class CodexMissionSession {
|
|
|
118
119
|
current = this.required(current.parentID);
|
|
119
120
|
return current.id;
|
|
120
121
|
}
|
|
122
|
+
async restoreCoordinator(root) {
|
|
123
|
+
const missions = new OperatorMissionRuntime(this.directory, profile);
|
|
124
|
+
const mission = await missions.required(root);
|
|
125
|
+
if (!mission.coordinator || ["completed", "cancelled"].includes(mission.phase))
|
|
126
|
+
return;
|
|
127
|
+
if (!await codexOwnerGone(mission.codexOwner))
|
|
128
|
+
throw new Error("codex-mission-owner-active:no-resend");
|
|
129
|
+
// Codex can persist claim() and the child turn but omit the interrupted parent
|
|
130
|
+
// Task item. The existing Mission binding still owns this same Coordinator.
|
|
131
|
+
const coordinator = await this.restoreSession(mission.coordinator, "dogs-coordinator", root);
|
|
132
|
+
if (coordinator.parentID !== root || coordinator.agent !== "dogs-coordinator")
|
|
133
|
+
throw new Error("Retained Coordinator lineage mismatch.");
|
|
134
|
+
if (mission.dispatchOpen && mission.callID) {
|
|
135
|
+
// A retained terminal parent receipt already has the shared reconciliation path.
|
|
136
|
+
// The new binding is required only when that exact receipt is missing.
|
|
137
|
+
const parentReceipts = this.required(root).history.flatMap(message => Array.isArray(message.parts) ? message.parts.filter(object) : [])
|
|
138
|
+
.filter(part => part.type === "tool" && part.tool === "task" && part.callID === mission.callID && object(part.state) &&
|
|
139
|
+
["completed", "error"].includes(String(part.state.status)) && object(part.state.input) &&
|
|
140
|
+
part.state.input.subagent_type === coordinator.agent &&
|
|
141
|
+
(part.state.input.task_id === undefined || part.state.input.task_id === coordinator.id));
|
|
142
|
+
if (parentReceipts.length === 1)
|
|
143
|
+
return;
|
|
144
|
+
const binding = mission.codexCoordinatorDispatch;
|
|
145
|
+
if (!binding || binding.missionID !== mission.id || binding.threadID !== root || binding.callID !== mission.callID ||
|
|
146
|
+
binding.childThreadID !== coordinator.id || binding.ownerGeneration !== mission.codexOwner?.generation ||
|
|
147
|
+
coordinator.history.filter(message => object(message.info) && message.info.role === "user" &&
|
|
148
|
+
message.info.id === binding.clientUserMessageID).length !== 1)
|
|
149
|
+
throw new Error("Retained Coordinator has unproven execution for the current dispatch; no resend. Reconcile its exact native turn before resuming.");
|
|
150
|
+
await missions.codexRecovery(root, async (current) => {
|
|
151
|
+
if (current.codexOwner?.generation !== binding.ownerGeneration || !await codexOwnerGone(current.codexOwner) ||
|
|
152
|
+
JSON.stringify(current.codexCoordinatorDispatch) !== JSON.stringify(binding))
|
|
153
|
+
throw new Error("codex-mission-recovery-generation-stale");
|
|
154
|
+
await missions.reconcileFinishedDispatch(root, mission.id, mission.callID);
|
|
155
|
+
});
|
|
156
|
+
await this.options.onEvent?.({ method: "sortie/coordinatorRestored", threadId: root,
|
|
157
|
+
params: { missionId: mission.id, coordinatorId: coordinator.id, callId: mission.callID,
|
|
158
|
+
outcome: coordinator.outcome, action: "resume-existing-task" } });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
121
161
|
async recoverNotStarted(threadID, turn, item) {
|
|
122
162
|
if (item.tool !== "task" || !object(item.arguments) || item.arguments.task_id || typeof item.id !== "string" || typeof turn.id !== "string" ||
|
|
123
163
|
!["completed", "interrupted", "failed"].includes(String(turn.status)))
|
|
@@ -156,7 +196,18 @@ export class CodexMissionSession {
|
|
|
156
196
|
}
|
|
157
197
|
return false;
|
|
158
198
|
}
|
|
159
|
-
async
|
|
199
|
+
async bindTaskDispatch(parent, child, call, messageID) {
|
|
200
|
+
if (child.agent === "dogs-coordinator") {
|
|
201
|
+
await new OperatorMissionRuntime(this.directory, profile).update(this.rootOf(parent), state => {
|
|
202
|
+
if (state.root !== call.threadId || state.coordinator !== child.id || state.callID !== call.callId || !state.dispatchOpen ||
|
|
203
|
+
state.codexOwner?.generation !== this.owner.generation || state.codexOwner.closed)
|
|
204
|
+
throw new Error("codex-mission-dispatch-owner-stale");
|
|
205
|
+
state.codexCoordinatorDispatch = { missionID: state.id, threadID: call.threadId, turnID: call.turnId, callID: call.callId,
|
|
206
|
+
inputHash: createHash("sha256").update(JSON.stringify(call.arguments)).digest("hex"),
|
|
207
|
+
childThreadID: child.id, clientUserMessageID: messageID, ownerGeneration: this.owner.generation };
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
160
211
|
if (child.agent !== "dog-worker-v010" || !object(call.arguments) || call.arguments.task_id)
|
|
161
212
|
return;
|
|
162
213
|
const root = this.rootOf(parent), missions = new OperatorMissionRuntime(this.directory, profile);
|
|
@@ -365,7 +416,7 @@ export class CodexMissionSession {
|
|
|
365
416
|
throw new Error(`Codex thread ${id} has no matching repository history.`);
|
|
366
417
|
const turns = thread.turns.filter(object);
|
|
367
418
|
for (const turn of turns) {
|
|
368
|
-
if (!Array.isArray(turn.items) ||
|
|
419
|
+
if (!Array.isArray(turn.items) || !["completed", "failed", "interrupted", "cancelled"].includes(String(turn.status)) || turn.itemsView !== "full")
|
|
369
420
|
throw new Error(`Codex thread ${id} still has unproven execution; no resend. Reconcile its native execution before resuming.`);
|
|
370
421
|
for (const item of turn.items.filter(object)) {
|
|
371
422
|
// A terminal parent turn does not prove that its native executions or children stopped.
|
|
@@ -373,7 +424,8 @@ export class CodexMissionSession {
|
|
|
373
424
|
["commandExecution", "fileChange", "mcpToolCall"].includes(String(item.type)) &&
|
|
374
425
|
!["completed", "failed", ...(item.type === "mcpToolCall" ? [] : ["declined"])].includes(String(item.status)))
|
|
375
426
|
throw new Error(`Codex thread ${id} has an unresolved native execution; no resend. Reconcile its native execution before resuming.`);
|
|
376
|
-
if (item.type !== "dynamicToolCall" ||
|
|
427
|
+
if (item.type !== "dynamicToolCall" || ["completed", "failed"].includes(String(item.status)) &&
|
|
428
|
+
(turn.status === "completed" || item.success === true))
|
|
377
429
|
continue;
|
|
378
430
|
const unique = turns.flatMap(entry => Array.isArray(entry.items) ? entry.items.filter(object) : []).filter(entry => entry.id === item.id).length === 1;
|
|
379
431
|
if (!unique)
|
|
@@ -475,7 +527,7 @@ export class CodexMissionSession {
|
|
|
475
527
|
result = await this.promptTurn(session, "The previous native response ended while existing host tools were still running. Those exact calls have now returned:\n" +
|
|
476
528
|
JSON.stringify(result.deferredToolResults) +
|
|
477
529
|
"\nContinue this same Mission from these results through the remaining validation, delivery, review and root acceptance. " +
|
|
478
|
-
"Do not resend the original request or redispatch the completed Tasks. Preserve failures, ownership and cumulative budget; use existing Mission status for the next action.", undefined, true);
|
|
530
|
+
"Do not resend the original request or redispatch the completed Tasks. Preserve failures, ownership and cumulative budget; use existing Mission status for the next action.", session.agent === "dogs-coordinator" ? onDispatch : undefined, true);
|
|
479
531
|
}
|
|
480
532
|
return result;
|
|
481
533
|
}
|
|
@@ -577,7 +629,7 @@ export class CodexMissionSession {
|
|
|
577
629
|
});
|
|
578
630
|
await this.hooks["tool.execute.after"]?.({ tool: call.tool, sessionID: session.id, callID: call.callId, args }, { output: "", metadata: { sessionId: child.id, status: "running" } });
|
|
579
631
|
const result = await this.prompt(child, String(args.prompt), async (messageID) => {
|
|
580
|
-
await this.
|
|
632
|
+
await this.bindTaskDispatch(session, child, call, messageID);
|
|
581
633
|
dispatched = true;
|
|
582
634
|
});
|
|
583
635
|
text = result.finalResponse ?? "";
|
|
@@ -45,6 +45,14 @@ export interface GoalEvidence {
|
|
|
45
45
|
/** Non-generating native validation pins inputs/concrete outputs, not write-only directory bookkeeping.
|
|
46
46
|
* Missing on old evidence: retain its full source/output recipe. */
|
|
47
47
|
readonly validation_policy?: "inputs-and-concrete-outputs-v1";
|
|
48
|
+
/** Snapshot and recheck the exact Git workspace inside one validated Docker task container. */
|
|
49
|
+
readonly docker_workspace?: {
|
|
50
|
+
readonly container_id: string;
|
|
51
|
+
readonly cwd: string;
|
|
52
|
+
readonly repository_root: string;
|
|
53
|
+
readonly source: string;
|
|
54
|
+
readonly candidate: string;
|
|
55
|
+
};
|
|
48
56
|
/** Fixed when validation starts. Full manifest_hash still identifies the historical execution contract. */
|
|
49
57
|
readonly freshness?: {
|
|
50
58
|
readonly contract_hash: string;
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -59,6 +59,7 @@ export function validGoalEvidence(value, state) {
|
|
|
59
59
|
return false;
|
|
60
60
|
const criteria = state.acceptance_contract?.criteria.filter(({ criterion_id }) => value.measurement.criterion_ids.includes(criterion_id)) ?? [];
|
|
61
61
|
const protectedBinding = value.protected_binding;
|
|
62
|
+
const dockerWorkspace = protectedBinding?.docker_workspace;
|
|
62
63
|
const protectedBindingValid = protectedBinding !== undefined && HASH.test(protectedBinding.manifest_hash) &&
|
|
63
64
|
text(protectedBinding.project_root) && text(protectedBinding.manifest_path) && Array.isArray(protectedBinding.source_paths) &&
|
|
64
65
|
protectedBinding.source_paths.every(text) && Array.isArray(protectedBinding.candidate_paths) &&
|
|
@@ -67,6 +68,10 @@ export function validGoalEvidence(value, state) {
|
|
|
67
68
|
protectedBinding.source_policy === "declared-paths-v1") &&
|
|
68
69
|
(protectedBinding.candidate_policy === undefined || protectedBinding.candidate_policy === "project-root-artifacts-v1") &&
|
|
69
70
|
(protectedBinding.validation_policy === undefined || protectedBinding.validation_policy === "inputs-and-concrete-outputs-v1") &&
|
|
71
|
+
(dockerWorkspace === undefined || (typeof dockerWorkspace.container_id === "string" && /^[a-f0-9]{12,64}$/u.test(dockerWorkspace.container_id) &&
|
|
72
|
+
typeof dockerWorkspace.cwd === "string" && dockerWorkspace.cwd.startsWith("/") &&
|
|
73
|
+
typeof dockerWorkspace.repository_root === "string" && dockerWorkspace.repository_root.startsWith("/") &&
|
|
74
|
+
HASH.test(dockerWorkspace.source) && HASH.test(dockerWorkspace.candidate))) &&
|
|
70
75
|
(protectedBinding.freshness === undefined || (protectedBinding.freshness !== null && typeof protectedBinding.freshness === "object" &&
|
|
71
76
|
HASH.test(protectedBinding.freshness.contract_hash) &&
|
|
72
77
|
Array.isArray(protectedBinding.freshness.scratch_paths) && protectedBinding.freshness.scratch_paths.every(text) &&
|
|
@@ -89,10 +94,10 @@ export function validGoalEvidence(value, state) {
|
|
|
89
94
|
sameStrings(criterion.oracle_coverage, value.measurement.oracle_coverage) &&
|
|
90
95
|
criterion.build_boundary === value.measurement.build_boundary &&
|
|
91
96
|
(criterion.source_binding === "current-protected"
|
|
92
|
-
? protectedBindingValid && HASH.test(value.identity.source)
|
|
97
|
+
? protectedBindingValid && HASH.test(value.identity.source) && (dockerWorkspace === undefined || dockerWorkspace.source === value.identity.source)
|
|
93
98
|
: criterion.source === value.identity.source) &&
|
|
94
99
|
(criterion.candidate_binding === "current-protected"
|
|
95
|
-
? protectedBindingValid && HASH.test(value.identity.candidate)
|
|
100
|
+
? protectedBindingValid && HASH.test(value.identity.candidate) && (dockerWorkspace === undefined || dockerWorkspace.candidate === value.identity.candidate)
|
|
96
101
|
: criterion.candidate === value.identity.candidate) &&
|
|
97
102
|
(criterion.validation_command === undefined ||
|
|
98
103
|
(value.execution.command.length === 1 && value.execution.command[0] === criterion.validation_command)) &&
|
|
@@ -49,6 +49,7 @@ export interface CodexWorkerDispatch {
|
|
|
49
49
|
clientUserMessageID: string;
|
|
50
50
|
ownerGeneration: string;
|
|
51
51
|
}
|
|
52
|
+
export type CodexCoordinatorDispatch = Omit<CodexWorkerDispatch, "runID" | "unitID">;
|
|
52
53
|
export interface CodexCompletedTaskProof {
|
|
53
54
|
dispatch: CodexWorkerDispatch;
|
|
54
55
|
childTurnID: string;
|
|
@@ -209,6 +210,8 @@ export interface OperatorMission {
|
|
|
209
210
|
codexNotStarted?: CodexNotStartedProof[];
|
|
210
211
|
/** Missing parent receipts reconciled from exact completed native Worker turns. */
|
|
211
212
|
codexCompletedTasks?: CodexCompletedTaskProof[];
|
|
213
|
+
/** Current Coordinator dispatch bound before its exact native turn is sent. */
|
|
214
|
+
codexCoordinatorDispatch?: CodexCoordinatorDispatch;
|
|
212
215
|
version: "0.12";
|
|
213
216
|
id: string;
|
|
214
217
|
root: string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface DockerWorkspaceSnapshot {
|
|
2
|
+
readonly container_id: string;
|
|
3
|
+
readonly cwd: string;
|
|
4
|
+
readonly repository_root: string;
|
|
5
|
+
readonly source: string;
|
|
6
|
+
readonly candidate: string;
|
|
7
|
+
}
|
|
8
|
+
/** Hash the fixed container's actual workspace, including non-Git and ignored inputs. */
|
|
9
|
+
export declare function captureDockerWorkspaceSnapshot(containerID: string, cwd: string): Promise<DockerWorkspaceSnapshot | undefined>;
|
|
10
|
+
export declare function isDockerContainerID(value: unknown): value is string;
|
|
11
|
+
export declare function isDockerWorkspacePath(value: unknown): value is string;
|
|
12
|
+
/** Inspect bytes, never execute caller-supplied code. The Review envelope's
|
|
13
|
+
* immutable Docker candidate must match both before and after the read. */
|
|
14
|
+
export declare function readDockerWorkspaceFile(containerID: string, cwd: string, candidate: string, path: string, offset?: number, limit?: number): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { posix } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const exec = promisify(execFile);
|
|
6
|
+
const CONTAINER_ID = /^[a-f0-9]{12,64}$/u;
|
|
7
|
+
const SNAPSHOT_MARKER = "SORTIE_DOCKER_WORKSPACE_V1";
|
|
8
|
+
// Terminal tasks need not contain Git at all. Capture actual bytes and file
|
|
9
|
+
// identity, including ignored inputs; timestamps alone are not candidate edits.
|
|
10
|
+
const FILE_SNAPSHOT = String.raw `
|
|
11
|
+
import hashlib, json, os, stat, subprocess
|
|
12
|
+
cwd = os.path.realpath(os.getcwd())
|
|
13
|
+
try:
|
|
14
|
+
root = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], stderr=subprocess.DEVNULL, text=True).strip()
|
|
15
|
+
head = subprocess.check_output(["git", "rev-parse", "--verify", "HEAD^{tree}"], stderr=subprocess.DEVNULL, text=True).strip()
|
|
16
|
+
except (OSError, subprocess.CalledProcessError):
|
|
17
|
+
root, head = cwd, "no-git"
|
|
18
|
+
root = os.path.realpath(root)
|
|
19
|
+
rows = []
|
|
20
|
+
def digest(path):
|
|
21
|
+
h = hashlib.sha256()
|
|
22
|
+
with open(path, "rb") as stream:
|
|
23
|
+
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
|
24
|
+
h.update(block)
|
|
25
|
+
return h.hexdigest()
|
|
26
|
+
for directory, dirs, files in os.walk(root, followlinks=False, onerror=lambda error: (_ for _ in ()).throw(error)):
|
|
27
|
+
if directory == root:
|
|
28
|
+
dirs[:] = [name for name in dirs if name != ".git"]
|
|
29
|
+
files = [name for name in files if name != ".git"]
|
|
30
|
+
for name in [".", *dirs, *files]:
|
|
31
|
+
path = directory if name == "." else os.path.join(directory, name)
|
|
32
|
+
relative = os.path.relpath(path, root)
|
|
33
|
+
info = os.lstat(path)
|
|
34
|
+
mode = stat.S_IMODE(info.st_mode)
|
|
35
|
+
if stat.S_ISREG(info.st_mode):
|
|
36
|
+
value = [relative, "file", mode, digest(path)]
|
|
37
|
+
elif stat.S_ISLNK(info.st_mode):
|
|
38
|
+
target = os.path.realpath(path)
|
|
39
|
+
# Internal directory targets are captured at their own lexical path.
|
|
40
|
+
# External directory trees are not silently omitted from a proof.
|
|
41
|
+
if os.path.isdir(path) and os.path.commonpath([root, target]) != root:
|
|
42
|
+
raise RuntimeError("external-directory-link-snapshot-unavailable")
|
|
43
|
+
value = [relative, "link", mode, os.readlink(path), digest(path) if os.path.isfile(path) else None]
|
|
44
|
+
elif stat.S_ISDIR(info.st_mode):
|
|
45
|
+
value = [relative, "directory", mode]
|
|
46
|
+
else:
|
|
47
|
+
raise RuntimeError("unsupported-workspace-file-type")
|
|
48
|
+
rows.append(value)
|
|
49
|
+
tree = hashlib.sha256(json.dumps(sorted(rows), ensure_ascii=True, separators=(",", ":")).encode()).hexdigest()
|
|
50
|
+
print("SORTIE_DOCKER_WORKSPACE_V1")
|
|
51
|
+
print(cwd)
|
|
52
|
+
print(root)
|
|
53
|
+
print(tree)
|
|
54
|
+
print(hashlib.sha256(head.encode()).hexdigest())
|
|
55
|
+
print("0" * 64)
|
|
56
|
+
`;
|
|
57
|
+
const SNAPSHOT_COMMAND = [
|
|
58
|
+
"set -eu",
|
|
59
|
+
"if command -v python3 >/dev/null 2>&1; then",
|
|
60
|
+
"python3 -I -S - <<'SORTIE_WORKSPACE_PY'",
|
|
61
|
+
FILE_SNAPSHOT,
|
|
62
|
+
"SORTIE_WORKSPACE_PY",
|
|
63
|
+
"else",
|
|
64
|
+
"actual_cwd=$(pwd -P)",
|
|
65
|
+
"root=$(git rev-parse --show-toplevel)",
|
|
66
|
+
"cd \"$root\"",
|
|
67
|
+
"tree=$(git rev-parse --verify 'HEAD^{tree}')",
|
|
68
|
+
"tracked=$(git diff --binary HEAD -- | sha256sum | cut -d' ' -f1)",
|
|
69
|
+
"untracked=$(git ls-files -z --others | sort -z | xargs -0 -r sha256sum -- | sha256sum | cut -d' ' -f1)",
|
|
70
|
+
"printf '%s\\n%s\\n%s\\n%s\\n%s\\n%s\\n' 'SORTIE_DOCKER_WORKSPACE_V1' \"$actual_cwd\" \"$root\" \"$tree\" \"$tracked\" \"$untracked\"",
|
|
71
|
+
"fi",
|
|
72
|
+
].join("\n");
|
|
73
|
+
/** Hash the fixed container's actual workspace, including non-Git and ignored inputs. */
|
|
74
|
+
export async function captureDockerWorkspaceSnapshot(containerID, cwd) {
|
|
75
|
+
if (!CONTAINER_ID.test(containerID) || !posix.isAbsolute(cwd) || posix.normalize(cwd) !== cwd || cwd.includes("\0"))
|
|
76
|
+
return undefined;
|
|
77
|
+
try {
|
|
78
|
+
const result = await exec("docker", ["exec", "--workdir", cwd, containerID, "sh", "-lc", SNAPSHOT_COMMAND], {
|
|
79
|
+
encoding: "buffer", timeout: 30_000, maxBuffer: 64 * 1024 * 1024,
|
|
80
|
+
});
|
|
81
|
+
const output = result.stdout;
|
|
82
|
+
let offset = 0;
|
|
83
|
+
const line = () => {
|
|
84
|
+
const end = output.indexOf(0x0a, offset);
|
|
85
|
+
if (end < 0)
|
|
86
|
+
return undefined;
|
|
87
|
+
const value = output.subarray(offset, end).toString("utf8");
|
|
88
|
+
offset = end + 1;
|
|
89
|
+
return value;
|
|
90
|
+
};
|
|
91
|
+
if (line() !== SNAPSHOT_MARKER)
|
|
92
|
+
return undefined;
|
|
93
|
+
const actualCwd = line(), repositoryRoot = line(), tree = line(), tracked = line(), untracked = line();
|
|
94
|
+
if (actualCwd !== cwd || !repositoryRoot || !posix.isAbsolute(repositoryRoot) || !tree || !/^[a-f0-9]{40,64}$/u.test(tree) ||
|
|
95
|
+
!tracked || !/^[a-f0-9]{64}$/u.test(tracked) || !untracked || !/^[a-f0-9]{64}$/u.test(untracked) || offset !== output.length)
|
|
96
|
+
return undefined;
|
|
97
|
+
const source = `sha256:${createHash("sha256").update(`git-tree\0${repositoryRoot}\0${tree}`).digest("hex")}`;
|
|
98
|
+
const candidate = `sha256:${createHash("sha256").update(JSON.stringify([containerID, cwd, repositoryRoot, tree, tracked, untracked])).digest("hex")}`;
|
|
99
|
+
return { container_id: containerID, cwd, repository_root: repositoryRoot, source, candidate };
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function isDockerContainerID(value) {
|
|
106
|
+
return typeof value === "string" && CONTAINER_ID.test(value);
|
|
107
|
+
}
|
|
108
|
+
export function isDockerWorkspacePath(value) {
|
|
109
|
+
return typeof value === "string" && value.length > 0 && posix.isAbsolute(value) &&
|
|
110
|
+
posix.normalize(value) === value && !value.includes("\0");
|
|
111
|
+
}
|
|
112
|
+
const READ_WORKSPACE = String.raw `
|
|
113
|
+
import hashlib, json, os, stat, sys
|
|
114
|
+
root, requested, offset, limit = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4])
|
|
115
|
+
path = os.path.realpath(os.path.join(root, requested))
|
|
116
|
+
if os.path.commonpath([root, path]) != root:
|
|
117
|
+
raise ValueError("file-outside-candidate-workspace")
|
|
118
|
+
if os.path.isdir(path):
|
|
119
|
+
entries = sorted(os.listdir(path))
|
|
120
|
+
result = {"kind": "directory", "entries": entries[offset - 1:offset - 1 + limit], "total": len(entries)}
|
|
121
|
+
elif stat.S_ISREG(os.stat(path).st_mode):
|
|
122
|
+
h = hashlib.sha256()
|
|
123
|
+
lines, total, size = [], 0, 0
|
|
124
|
+
with open(path, "rb") as stream:
|
|
125
|
+
for line in stream:
|
|
126
|
+
h.update(line)
|
|
127
|
+
total += 1
|
|
128
|
+
if offset <= total < offset + limit and size < 64000:
|
|
129
|
+
text = line[:64000-size].decode("utf-8", errors="replace")
|
|
130
|
+
lines.append([total, text])
|
|
131
|
+
size += len(line[:64000-size])
|
|
132
|
+
result = {"kind": "file", "lines": lines, "total_lines": total, "sha256": h.hexdigest()}
|
|
133
|
+
else:
|
|
134
|
+
raise ValueError("candidate-file-not-readable")
|
|
135
|
+
print(json.dumps({"path": path, **result}, ensure_ascii=True))
|
|
136
|
+
`;
|
|
137
|
+
/** Inspect bytes, never execute caller-supplied code. The Review envelope's
|
|
138
|
+
* immutable Docker candidate must match both before and after the read. */
|
|
139
|
+
export async function readDockerWorkspaceFile(containerID, cwd, candidate, path, offset = 1, limit = 200) {
|
|
140
|
+
if (!/^sha256:[a-f0-9]{64}$/u.test(candidate) || typeof path !== "string" || path.includes("\0") ||
|
|
141
|
+
!Number.isSafeInteger(offset) || offset < 1 || !Number.isSafeInteger(limit) || limit < 1 || limit > 2000) {
|
|
142
|
+
throw new Error("docker-candidate-read-invalid");
|
|
143
|
+
}
|
|
144
|
+
const before = await captureDockerWorkspaceSnapshot(containerID, cwd);
|
|
145
|
+
if (!before || before.candidate !== candidate)
|
|
146
|
+
throw new Error("docker-review-candidate-stale-or-unavailable");
|
|
147
|
+
const { stdout } = await exec("docker", ["exec", "--workdir", cwd, containerID, "python3", "-I", "-S", "-c", READ_WORKSPACE,
|
|
148
|
+
before.repository_root, path, String(offset), String(limit)], { encoding: "utf8", timeout: 30_000, maxBuffer: 256 * 1024 });
|
|
149
|
+
const after = await captureDockerWorkspaceSnapshot(containerID, cwd);
|
|
150
|
+
if (!after || after.candidate !== candidate)
|
|
151
|
+
throw new Error("docker-review-candidate-changed-during-read");
|
|
152
|
+
return { workspace: before, ...JSON.parse(stdout) };
|
|
153
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"tool_prefix": "sortie_v010_",
|
|
4
|
+
"core_tools": ["execute", "subagent", "read"],
|
|
5
|
+
"runtime_agents": {
|
|
6
|
+
"dog-coordinator": "dog-operator",
|
|
7
|
+
"dog-worker": "dog-worker-v010",
|
|
8
|
+
"dog-luna-worker": "dog-luna-worker-v010",
|
|
9
|
+
"dog-scout": "dog-scout-v010",
|
|
10
|
+
"dog-reviewer": "dog-reviewer-v010",
|
|
11
|
+
"dog-advisor": "dog-advisor-v010",
|
|
12
|
+
"dog-operator": "dogs-coordinator"
|
|
13
|
+
},
|
|
14
|
+
"roles": {
|
|
15
|
+
"dog-coordinator": {
|
|
16
|
+
"tool_families": {
|
|
17
|
+
"mission_lifecycle": ["start_mission", "operator_next", "operator_status", "extend_mission_budget", "complete_mission", "cancel_operator", "reflection"],
|
|
18
|
+
"unit_execution": ["plan_units", "start_direct_unit", "finish_direct_unit", "retry_mission_unit", "expand_unit"],
|
|
19
|
+
"review": ["review_mission", "repair_review"]
|
|
20
|
+
},
|
|
21
|
+
"children": ["dog-operator", "dog-worker", "dog-reviewer"]
|
|
22
|
+
},
|
|
23
|
+
"dog-worker": {
|
|
24
|
+
"tool_families": {
|
|
25
|
+
"workspace": ["bind_write_gate", "release_write_gate", "operator_status", "expand_unit"]
|
|
26
|
+
},
|
|
27
|
+
"native_tool_families": {
|
|
28
|
+
"docker_task": ["fh_docker_exec"]
|
|
29
|
+
},
|
|
30
|
+
"core_tools": ["execute", "read"],
|
|
31
|
+
"children": []
|
|
32
|
+
},
|
|
33
|
+
"dog-luna-worker": {
|
|
34
|
+
"tool_families": {
|
|
35
|
+
"workspace": ["bind_write_gate", "release_write_gate", "operator_status", "expand_unit"]
|
|
36
|
+
},
|
|
37
|
+
"core_tools": ["execute", "read"],
|
|
38
|
+
"children": []
|
|
39
|
+
},
|
|
40
|
+
"dog-scout": { "tool_families": {}, "children": [] },
|
|
41
|
+
"dog-reviewer": {
|
|
42
|
+
"tool_families": { "review": ["repair_review"] },
|
|
43
|
+
"native_tool_families": { "docker_review": ["fh_docker_read"] },
|
|
44
|
+
"correction_native_tool_families": { "docker_task": ["fh_docker_exec"] },
|
|
45
|
+
"correction_tool_families": {
|
|
46
|
+
"workspace": ["bind_write_gate", "release_write_gate", "operator_status", "expand_unit"],
|
|
47
|
+
"correction": ["repair_review", "finish_direct_unit"]
|
|
48
|
+
},
|
|
49
|
+
"children": []
|
|
50
|
+
},
|
|
51
|
+
"dog-advisor": { "tool_families": {}, "children": [] },
|
|
52
|
+
"dog-operator": {
|
|
53
|
+
"tool_families": {
|
|
54
|
+
"mission_lifecycle": ["operator_next", "operator_status", "submit_mission", "skip_mission_consultation"],
|
|
55
|
+
"unit_execution": ["plan_units", "start_direct_unit", "finish_direct_unit", "retry_mission_unit", "rescue_mission_unit", "expand_unit"],
|
|
56
|
+
"review": ["review_mission", "repair_review"]
|
|
57
|
+
},
|
|
58
|
+
"children": ["dog-worker", "dog-reviewer"]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|