sortie-dogs 0.9.10 → 0.9.12
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 +56 -2
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/worktree-commit-artifact.js +6 -6
- package/dist/core/worktree-parallel-dispatch.js +68 -46
- package/dist/plugin/continuation.d.ts +1 -1
- package/dist/plugin/continuation.js +19 -4
- package/dist/plugin/index.d.ts +5 -0
- package/dist/plugin/index.js +168 -92
- package/dist/plugin/run-metrics.d.ts +9 -1
- package/dist/plugin/run-metrics.js +66 -12
- package/dist/plugin/sortie-career.js +18 -14
- package/dist/plugin/sortie-debrief.d.ts +4 -0
- package/dist/plugin/sortie-debrief.js +22 -13
- package/dist/runtime-assets.d.ts +14 -11
- package/dist/runtime-assets.js +14 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,9 +20,42 @@ bounded implementation, canonical validation, and evidence-backed completion.
|
|
|
20
20
|
|
|
21
21
|
Requirements: Node.js 22.6 or newer, npm, and OpenCode.
|
|
22
22
|
|
|
23
|
-
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
|
|
23
|
+
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [テスト実行](docs/testing.md) · [CLI testing](docs/cli-testing.md)
|
|
24
24
|
|
|
25
|
-
Release: [v0.9.
|
|
25
|
+
Release: [v0.9.12](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.12)
|
|
26
|
+
|
|
27
|
+
## Provisional quality–cost position
|
|
28
|
+
|
|
29
|
+
**Reference values, not a successful benchmark claim.** Quality and end-to-end completion
|
|
30
|
+
problems remain. The latest completed qualification attempt ended at `IN_PROGRESS`, so its official
|
|
31
|
+
verifier was not run. Further benchmarks are frozen while completion defects are repaired.
|
|
32
|
+
|
|
33
|
+
The last complete measured **Bare OpenCode vs Sortie** pair below used one frozen task,
|
|
34
|
+
`datacurve/anko-typed-variable-bindings`, on 2026-09-10. It used Sortie **v0.9.5**, not the
|
|
35
|
+
current release. Both candidates failed the official verifier.
|
|
36
|
+
|
|
37
|
+
| Metric · one task, one trial per arm | Bare OpenCode | Sortie v0.9.5 |
|
|
38
|
+
| --- | ---: | ---: |
|
|
39
|
+
| Verified PASS | 0/1 | 0/1 |
|
|
40
|
+
| Task-check completion · F2P | 55.6% · 5/9 | 88.9% · 8/9 |
|
|
41
|
+
| Retained checks · P2P | 94/94 | 93/94 |
|
|
42
|
+
| Estimated API-equivalent total cost | $5.42 | $1.46 |
|
|
43
|
+
| Median agent wall · n=1 | 28.7 min | 10.0 min |
|
|
44
|
+
| Premium-model token share · Sol | 100% | 20.1% |
|
|
45
|
+
|
|
46
|
+
Later Sortie-only evidence is weaker: the v0.9.9 recovery candidate passed **5/9** task
|
|
47
|
+
checks with **0/1 Verified PASS**; `0.9.11-bench.2` did not reach a gradeable completion.
|
|
48
|
+
Those attempts are not pooled into the historical pair above.
|
|
49
|
+
|
|
50
|
+

|
|
51
|
+
|
|
52
|
+
The goal is **higher OpenCode task success with selective use of premium models**.
|
|
53
|
+
These reference observations do not yet establish that success-rate claim: Sortie missed
|
|
54
|
+
one task check and regressed one retained check. Codex, Pi, and Oh My OpenCode belong to
|
|
55
|
+
separate methodologies and are not assigned comparable positions on this chart.
|
|
56
|
+
|
|
57
|
+
[Definitions, frozen inputs, current failure status, and limitations](docs/benchmark-reference.md)
|
|
58
|
+
· [Machine-readable reference values](docs/benchmarks/provisional-reference.json)
|
|
26
59
|
|
|
27
60
|
## Why Sortie-dogs?
|
|
28
61
|
|
|
@@ -41,6 +74,15 @@ models are reserved for implementation, escalation, and independent review.
|
|
|
41
74
|
Writes stay scoped, and completion requires validation evidence. Every completed
|
|
42
75
|
run can return a concise Speed / Cost / Proof debrief.
|
|
43
76
|
|
|
77
|
+
### Use only as much harness as the task needs
|
|
78
|
+
|
|
79
|
+
Small changes can skip Scout and independent review when one worker and targeted
|
|
80
|
+
validation are sufficient. Larger work can be decomposed into multiple units;
|
|
81
|
+
units that are safely independent can use a Luna fabric DAG for bounded parallel
|
|
82
|
+
execution. Higher-risk candidates add independent review, while full-suite and
|
|
83
|
+
package verification are reserved for release work. Not every task pays the
|
|
84
|
+
cost of the heaviest workflow.
|
|
85
|
+
|
|
44
86
|
## Designed to coexist with OpenCode
|
|
45
87
|
|
|
46
88
|
Sortie-dogs adds a workflow to your existing setup rather than replacing it.
|
|
@@ -266,6 +308,18 @@ dog-coordinator: completion evidence accepted
|
|
|
266
308
|
progress; repeated batches remain bounded rather than becoming endless
|
|
267
309
|
delegation.
|
|
268
310
|
|
|
311
|
+
## Built to work on itself
|
|
312
|
+
|
|
313
|
+
Self-improvement keeps the same scoped manifests, worker ownership, validation,
|
|
314
|
+
and review gates as other work. A loaded plugin is not treated as hot-reloadable:
|
|
315
|
+
source changes are validated first, then packaged into an isolated `_testenv`
|
|
316
|
+
fixture and exercised through the real OpenCode CLI. Continuation and compaction
|
|
317
|
+
changes must demonstrate same-session recovery and terminal completion there.
|
|
318
|
+
|
|
319
|
+
`npm run test:full` is reserved for explicit release validation; ordinary
|
|
320
|
+
changes run targeted tests and `npm test`. The control plane coordinating a run
|
|
321
|
+
is not replaced while that run is in flight.
|
|
322
|
+
|
|
269
323
|
## A visual walkthrough
|
|
270
324
|
|
|
271
325
|
### Control complexity
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export declare const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
6
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
@@ -978,11 +978,11 @@ async function stagedPaths(context) {
|
|
|
978
978
|
return new Set(fields);
|
|
979
979
|
}
|
|
980
980
|
async function assertNoSubmodules(context, entries) {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
981
|
+
if (entries.length === 0)
|
|
982
|
+
return;
|
|
983
|
+
const index = await context.git(["ls-files", "--stage", "-z", "--", ...entries.map(({ path }) => path)]);
|
|
984
|
+
if (index.toString("utf8").split("\0").some((entry) => entry.startsWith("160000 "))) {
|
|
985
|
+
throw new WorktreeCommitArtifactError("invalid-state", "Submodule changes are forbidden.");
|
|
986
986
|
}
|
|
987
987
|
}
|
|
988
988
|
function decodeGitOutput(source, message) {
|
|
@@ -1261,7 +1261,7 @@ export async function produceWorktreeCommitArtifact(request, control) {
|
|
|
1261
1261
|
throw new WorktreeCommitArtifactError("validation-failed", "Validation was cancelled.");
|
|
1262
1262
|
control?.enterProtectedPhase();
|
|
1263
1263
|
await context.git(["add", "--", ...before.map(({ path }) => path)]);
|
|
1264
|
-
|
|
1264
|
+
// Keep one fresh index check after staging, immediately before the protected commit.
|
|
1265
1265
|
await assertValidatedIndex(context, before, beforeFingerprint);
|
|
1266
1266
|
await context.git([
|
|
1267
1267
|
"-c", "user.name=Sortie Fabric",
|
|
@@ -2138,57 +2138,79 @@ export class ParallelDispatchCoordinator {
|
|
|
2138
2138
|
};
|
|
2139
2139
|
}
|
|
2140
2140
|
async buildFabricCandidate(runID, base, tasks) {
|
|
2141
|
+
if (tasks.length === 0)
|
|
2142
|
+
return base;
|
|
2141
2143
|
let head = base;
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
const
|
|
2147
|
-
const
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
]);
|
|
2155
|
-
if (patch.byteLength > 8 * 1024 * 1024) {
|
|
2156
|
-
throw new ParallelDispatchError("wave-integration-failed", "Fabric artifact patch exceeds the practical bound.");
|
|
2157
|
-
}
|
|
2158
|
-
await writeFile(patchPath, patch, { flag: "wx", mode: 0o600 });
|
|
2159
|
-
await this.gitBuffer(["apply", "--cached", "--3way", "--binary", patchPath], indexEnvironment);
|
|
2160
|
-
const tree = (await this.gitBuffer(["write-tree"], indexEnvironment)).toString("utf8").trim();
|
|
2161
|
-
const timestamp = (await this.gitBuffer(["show", "-s", "--format=%ct", artifact.commit_sha])).toString("utf8").trim();
|
|
2162
|
-
if (!SHA.test(tree) || !/^\d{1,12}$/u.test(timestamp))
|
|
2144
|
+
const indexPath = join(this.stateRoot, `.fabric-index-${randomUUID()}`);
|
|
2145
|
+
const indexEnvironment = this.cleanGitEnvironment({ GIT_INDEX_FILE: indexPath });
|
|
2146
|
+
try {
|
|
2147
|
+
// Commit objects are immutable; collect their timestamps once for this build only.
|
|
2148
|
+
const commits = [...new Set(tasks.map((task) => task.artifact.commit_sha))];
|
|
2149
|
+
const metadata = (await this.gitBuffer([
|
|
2150
|
+
"show", "-s", "--no-walk=unsorted", "--format=%H%x00%ct", ...commits, "--",
|
|
2151
|
+
])).toString("utf8").trimEnd();
|
|
2152
|
+
const timestamps = new Map();
|
|
2153
|
+
for (const line of metadata.split(/\r?\n/u)) {
|
|
2154
|
+
const fields = line.split("\0");
|
|
2155
|
+
if (fields.length !== 2 || !SHA.test(fields[0]) || !/^\d{1,12}$/u.test(fields[1]) || timestamps.has(fields[0])) {
|
|
2163
2156
|
throw new Error("identity");
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
`Sortie-Artifact: ${artifact.commit_sha}\nSortie-Base: ${artifact.base_sha}\n`;
|
|
2167
|
-
await writeFile(messagePath, message, { flag: "wx", mode: 0o600 });
|
|
2168
|
-
const commitEnvironment = this.cleanGitEnvironment({
|
|
2169
|
-
GIT_AUTHOR_NAME: "Sortie Fabric",
|
|
2170
|
-
GIT_AUTHOR_EMAIL: "sortie@example.invalid",
|
|
2171
|
-
GIT_COMMITTER_NAME: "Sortie Fabric",
|
|
2172
|
-
GIT_COMMITTER_EMAIL: "sortie@example.invalid",
|
|
2173
|
-
GIT_AUTHOR_DATE: `${timestamp} +0000`,
|
|
2174
|
-
GIT_COMMITTER_DATE: `${timestamp} +0000`,
|
|
2175
|
-
});
|
|
2176
|
-
const commit = (await this.gitBuffer(["commit-tree", tree, "-p", head, "-F", messagePath], commitEnvironment))
|
|
2177
|
-
.toString("utf8").trim();
|
|
2178
|
-
if (!SHA.test(commit))
|
|
2179
|
-
throw new Error("commit");
|
|
2180
|
-
head = commit;
|
|
2181
|
-
}
|
|
2182
|
-
catch (error) {
|
|
2183
|
-
if (error instanceof ParallelDispatchError)
|
|
2184
|
-
throw error;
|
|
2185
|
-
throw new ParallelDispatchError("wave-integration-failed", "Fabric artifact could not be applied to the hidden candidate.");
|
|
2157
|
+
}
|
|
2158
|
+
timestamps.set(fields[0], fields[1]);
|
|
2186
2159
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2160
|
+
if (timestamps.size !== commits.length || commits.some((commit) => !timestamps.has(commit)))
|
|
2161
|
+
throw new Error("identity");
|
|
2162
|
+
await this.gitBuffer(["read-tree", base], indexEnvironment);
|
|
2163
|
+
for (const task of tasks) {
|
|
2164
|
+
const artifact = task.artifact;
|
|
2165
|
+
const nonce = randomUUID();
|
|
2166
|
+
const patchPath = join(this.stateRoot, `.fabric-patch-${nonce}`);
|
|
2167
|
+
const messagePath = join(this.stateRoot, `.fabric-message-${nonce}`);
|
|
2168
|
+
try {
|
|
2169
|
+
const patch = await this.gitBuffer([
|
|
2170
|
+
"diff-tree", "-p", "--binary", "--full-index", "--no-ext-diff", "--no-renames",
|
|
2171
|
+
artifact.base_sha, artifact.commit_sha, "--",
|
|
2172
|
+
]);
|
|
2173
|
+
if (patch.byteLength > 8 * 1024 * 1024) {
|
|
2174
|
+
throw new ParallelDispatchError("wave-integration-failed", "Fabric artifact patch exceeds the practical bound.");
|
|
2175
|
+
}
|
|
2176
|
+
await writeFile(patchPath, patch, { flag: "wx", mode: 0o600 });
|
|
2177
|
+
await this.gitBuffer(["apply", "--cached", "--3way", "--binary", patchPath], indexEnvironment);
|
|
2178
|
+
const tree = (await this.gitBuffer(["write-tree"], indexEnvironment)).toString("utf8").trim();
|
|
2179
|
+
const timestamp = timestamps.get(artifact.commit_sha);
|
|
2180
|
+
if (!SHA.test(tree) || timestamp === undefined)
|
|
2181
|
+
throw new Error("identity");
|
|
2182
|
+
const message = `Sortie fabric integration: ${task.descriptor.task_id}\n\n` +
|
|
2183
|
+
`Sortie-Run: ${runID}\nSortie-Task: ${task.descriptor.task_id}\n` +
|
|
2184
|
+
`Sortie-Artifact: ${artifact.commit_sha}\nSortie-Base: ${artifact.base_sha}\n`;
|
|
2185
|
+
await writeFile(messagePath, message, { flag: "wx", mode: 0o600 });
|
|
2186
|
+
const commitEnvironment = this.cleanGitEnvironment({
|
|
2187
|
+
GIT_AUTHOR_NAME: "Sortie Fabric",
|
|
2188
|
+
GIT_AUTHOR_EMAIL: "sortie@example.invalid",
|
|
2189
|
+
GIT_COMMITTER_NAME: "Sortie Fabric",
|
|
2190
|
+
GIT_COMMITTER_EMAIL: "sortie@example.invalid",
|
|
2191
|
+
GIT_AUTHOR_DATE: `${timestamp} +0000`,
|
|
2192
|
+
GIT_COMMITTER_DATE: `${timestamp} +0000`,
|
|
2193
|
+
});
|
|
2194
|
+
const commit = (await this.gitBuffer(["commit-tree", tree, "-p", head, "-F", messagePath], commitEnvironment))
|
|
2195
|
+
.toString("utf8").trim();
|
|
2196
|
+
if (!SHA.test(commit))
|
|
2197
|
+
throw new Error("commit");
|
|
2198
|
+
head = commit;
|
|
2199
|
+
}
|
|
2200
|
+
finally {
|
|
2201
|
+
await Promise.all([patchPath, messagePath].map((path) => rm(path, { force: true }).catch(() => undefined)));
|
|
2202
|
+
}
|
|
2189
2203
|
}
|
|
2204
|
+
return head;
|
|
2205
|
+
}
|
|
2206
|
+
catch (error) {
|
|
2207
|
+
if (error instanceof ParallelDispatchError)
|
|
2208
|
+
throw error;
|
|
2209
|
+
throw new ParallelDispatchError("wave-integration-failed", "Fabric artifact could not be applied to the hidden candidate.");
|
|
2210
|
+
}
|
|
2211
|
+
finally {
|
|
2212
|
+
await Promise.all([indexPath, `${indexPath}.lock`].map((path) => rm(path, { force: true }).catch(() => undefined)));
|
|
2190
2213
|
}
|
|
2191
|
-
return head;
|
|
2192
2214
|
}
|
|
2193
2215
|
cleanGitEnvironment(extra = {}) {
|
|
2194
2216
|
const environment = { ...process.env };
|
|
@@ -199,7 +199,7 @@ export interface ContinuationHooks {
|
|
|
199
199
|
toolStarted(sessionID: string, tool: string): void;
|
|
200
200
|
blocksTool(sessionID: string): boolean;
|
|
201
201
|
sessionIdle(sessionID: string): Promise<void>;
|
|
202
|
-
stopAutomaticRecovery(sessionID: string, abortSession?: boolean): Promise<void>;
|
|
202
|
+
stopAutomaticRecovery(sessionID: string, abortSession?: boolean, resumeOnRealUserTurn?: boolean): Promise<void>;
|
|
203
203
|
recoverStalledTask(sessionID: string, callIDs: readonly string[]): Promise<"recovered" | "identity-rejected" | "capability-unavailable" | "request-rejected">;
|
|
204
204
|
forgetSession(sessionID: string): void;
|
|
205
205
|
}
|
|
@@ -225,6 +225,7 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
225
225
|
const sessions = new Map();
|
|
226
226
|
const warned = new Set();
|
|
227
227
|
const stoppedSessions = new Set();
|
|
228
|
+
const realTurnResumableStops = new Set();
|
|
228
229
|
function observeTransition(type, sessionID, state, reason) {
|
|
229
230
|
try {
|
|
230
231
|
transitionObserver?.({
|
|
@@ -811,8 +812,9 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
811
812
|
}
|
|
812
813
|
sessions.delete(sessionID);
|
|
813
814
|
stoppedSessions.delete(sessionID);
|
|
815
|
+
realTurnResumableStops.delete(sessionID);
|
|
814
816
|
}
|
|
815
|
-
async function stopAutomaticRecovery(sessionID, abortSession = true) {
|
|
817
|
+
async function stopAutomaticRecovery(sessionID, abortSession = true, resumeOnRealUserTurn = false) {
|
|
816
818
|
const state = sessions.get(sessionID);
|
|
817
819
|
if (state !== undefined) {
|
|
818
820
|
clearTimer(state.cooldownTimer);
|
|
@@ -830,8 +832,14 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
830
832
|
}
|
|
831
833
|
stoppedSessions.delete(sessionID);
|
|
832
834
|
stoppedSessions.add(sessionID);
|
|
835
|
+
if (resumeOnRealUserTurn)
|
|
836
|
+
realTurnResumableStops.add(sessionID);
|
|
837
|
+
else
|
|
838
|
+
realTurnResumableStops.delete(sessionID);
|
|
833
839
|
while (stoppedSessions.size > MAX_TRACKED_SESSIONS) {
|
|
834
|
-
stoppedSessions.
|
|
840
|
+
const oldest = stoppedSessions.values().next().value;
|
|
841
|
+
stoppedSessions.delete(oldest);
|
|
842
|
+
realTurnResumableStops.delete(oldest);
|
|
835
843
|
}
|
|
836
844
|
const abort = abortSession ? client?.session?.abort : undefined;
|
|
837
845
|
if (abort !== undefined) {
|
|
@@ -842,6 +850,8 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
842
850
|
}
|
|
843
851
|
}
|
|
844
852
|
async function recoverStalledTask(sessionID, callIDs) {
|
|
853
|
+
if (stoppedSessions.has(sessionID))
|
|
854
|
+
return "request-rejected";
|
|
845
855
|
const active = policy();
|
|
846
856
|
const resolution = resolveContinuation({
|
|
847
857
|
identity: await readIdentity(sessionID),
|
|
@@ -863,6 +873,8 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
863
873
|
// Reserve durable authority before mutating the host session. A configured authority that
|
|
864
874
|
// cannot issue a ticket must not cause an abort/retry loop or an unproven synthetic send.
|
|
865
875
|
const metadata = await ticketMetadata(sessionID, `watchdog:${callIDs.join(",")}`);
|
|
876
|
+
if (stoppedSessions.has(sessionID))
|
|
877
|
+
return "request-rejected";
|
|
866
878
|
await abort.call(client.session, {
|
|
867
879
|
path: { id: sessionID },
|
|
868
880
|
query: { directory },
|
|
@@ -1164,8 +1176,11 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
1164
1176
|
output.enabled = false;
|
|
1165
1177
|
},
|
|
1166
1178
|
observeModel(sessionID, model, synthetic = false) {
|
|
1167
|
-
if (stoppedSessions.has(sessionID))
|
|
1168
|
-
|
|
1179
|
+
if (stoppedSessions.has(sessionID)) {
|
|
1180
|
+
if (synthetic || !realTurnResumableStops.delete(sessionID))
|
|
1181
|
+
return;
|
|
1182
|
+
stoppedSessions.delete(sessionID);
|
|
1183
|
+
}
|
|
1169
1184
|
if (!nonEmpty(model.providerID) || !nonEmpty(model.modelID))
|
|
1170
1185
|
return;
|
|
1171
1186
|
const state = stateFor(sessionID);
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -91,6 +91,11 @@ export type HandoffDenialReason = "configuration-unavailable" | "path-invalid" |
|
|
|
91
91
|
export type FreshSessionReason = "child-lineage" | "asset-contract-skew";
|
|
92
92
|
export type FreshSessionAction = "open-fresh-root" | "install-assets-then-open-fresh-root" | "restart-host-after-install";
|
|
93
93
|
export type FreshSessionResult = Readonly<{
|
|
94
|
+
status: "redispatch-queued";
|
|
95
|
+
reason: FreshSessionReason;
|
|
96
|
+
source_session_id: string;
|
|
97
|
+
retry_same_session: false;
|
|
98
|
+
}> | Readonly<{
|
|
94
99
|
status: "redispatched";
|
|
95
100
|
reason: FreshSessionReason;
|
|
96
101
|
source_session_id: string;
|
package/dist/plugin/index.js
CHANGED
|
@@ -483,15 +483,58 @@ function sameRelativePath(left, right) {
|
|
|
483
483
|
function textPart(part) {
|
|
484
484
|
return isRecord(part) && typeof part.text === "string" ? part.text : undefined;
|
|
485
485
|
}
|
|
486
|
+
const FILE_PART_KEYS = new Set(["id", "sessionID", "messageID", "type", "mime", "filename", "url", "source"]);
|
|
487
|
+
function safeFilePart(part) {
|
|
488
|
+
if (Object.keys(part).some((key) => !FILE_PART_KEYS.has(key)) ||
|
|
489
|
+
typeof part.mime !== "string" || part.mime.length === 0 ||
|
|
490
|
+
typeof part.url !== "string" || part.url.length === 0 ||
|
|
491
|
+
(part.filename !== undefined && typeof part.filename !== "string") ||
|
|
492
|
+
(part.id !== undefined && typeof part.id !== "string") ||
|
|
493
|
+
(part.sessionID !== undefined && typeof part.sessionID !== "string") ||
|
|
494
|
+
(part.messageID !== undefined && typeof part.messageID !== "string") ||
|
|
495
|
+
(part.source !== undefined && !isRecord(part.source)))
|
|
496
|
+
return undefined;
|
|
497
|
+
return {
|
|
498
|
+
type: "file",
|
|
499
|
+
mime: part.mime,
|
|
500
|
+
...(part.filename === undefined ? {} : { filename: part.filename }),
|
|
501
|
+
url: part.url,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
/** OpenCode expands text attachments into synthetic explanatory text alongside the real file.
|
|
505
|
+
* Those derived text parts carry no user authority and are never copied into a fresh prompt.
|
|
506
|
+
* A ticket-bearing synthetic turn (or a turn without real user text and a safe file) stays synthetic.
|
|
507
|
+
*/
|
|
508
|
+
function realAttachmentParts(parts) {
|
|
509
|
+
const hasUserText = parts.some((part) => isRecord(part) && part.type === "text" &&
|
|
510
|
+
part.synthetic !== true && typeof part.text === "string" && part.text.trim().length > 0);
|
|
511
|
+
const hasFile = parts.some((part) => isRecord(part) && part.type === "file" && part.synthetic !== true && safeFilePart(part) !== undefined);
|
|
512
|
+
if (!hasUserText || !hasFile || parts.some((part) => isRecord(part) && part.synthetic === true &&
|
|
513
|
+
(part.type !== "text" || typeof part.text !== "string" || part.metadata !== undefined)))
|
|
514
|
+
return parts;
|
|
515
|
+
return parts.filter((part) => !isRecord(part) || part.synthetic !== true);
|
|
516
|
+
}
|
|
517
|
+
function syntheticPrompt(parts) {
|
|
518
|
+
return realAttachmentParts(parts).some((part) => isRecord(part) && part.synthetic === true);
|
|
519
|
+
}
|
|
486
520
|
function freshSessionPrompt(parts) {
|
|
487
521
|
const prompt = [];
|
|
488
|
-
for (const part of parts) {
|
|
489
|
-
if (!isRecord(part) || part.
|
|
522
|
+
for (const part of realAttachmentParts(parts)) {
|
|
523
|
+
if (!isRecord(part) || part.synthetic === true)
|
|
490
524
|
return undefined;
|
|
525
|
+
if (part.type === "text" && typeof part.text === "string")
|
|
526
|
+
prompt.push({ type: "text", text: part.text });
|
|
527
|
+
else if (part.type === "file") {
|
|
528
|
+
const attachment = safeFilePart(part);
|
|
529
|
+
if (attachment === undefined)
|
|
530
|
+
return undefined;
|
|
531
|
+
prompt.push(attachment);
|
|
491
532
|
}
|
|
492
|
-
|
|
533
|
+
else
|
|
534
|
+
return undefined;
|
|
493
535
|
}
|
|
494
|
-
return prompt.length > 0 && prompt.some((
|
|
536
|
+
return prompt.length > 0 && prompt.some((part) => part.type === "text" && part.text.trim().length > 0)
|
|
537
|
+
? prompt : undefined;
|
|
495
538
|
}
|
|
496
539
|
/**
|
|
497
540
|
* One handoff entry per line. The coordinator asset emits inline digests as `key: value`, often
|
|
@@ -1282,10 +1325,6 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1282
1325
|
const messages = input.client?.session?.messages;
|
|
1283
1326
|
if (messages === undefined)
|
|
1284
1327
|
return undefined;
|
|
1285
|
-
const currentText = currentParts.filter(isRecord).filter((part) => part.type === "text" && part.synthetic !== true)
|
|
1286
|
-
.map((part) => part.text).filter((text) => typeof text === "string");
|
|
1287
|
-
if (currentText.length === 0)
|
|
1288
|
-
return undefined;
|
|
1289
1328
|
for (const delay of [0, 10, 50]) {
|
|
1290
1329
|
if (delay > 0)
|
|
1291
1330
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
@@ -1303,11 +1342,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1303
1342
|
continue;
|
|
1304
1343
|
const info = isRecord(message.info) ? message.info : undefined;
|
|
1305
1344
|
if ((info?.role ?? message.role) !== "user" || (info?.agent ?? message.agent) !== selectedAgent ||
|
|
1306
|
-
!Array.isArray(message.parts) || message.parts
|
|
1345
|
+
!Array.isArray(message.parts) || syntheticPrompt(message.parts))
|
|
1307
1346
|
continue;
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
if (JSON.stringify(persistedText) !== JSON.stringify(currentText))
|
|
1347
|
+
const persistedParts = freshSessionPrompt(message.parts);
|
|
1348
|
+
if (persistedParts === undefined || JSON.stringify(persistedParts) !== JSON.stringify(currentParts))
|
|
1311
1349
|
continue;
|
|
1312
1350
|
const messageID = info?.id ?? message.id;
|
|
1313
1351
|
if (typeof messageID === "string" && messageID.length > 0)
|
|
@@ -1503,16 +1541,24 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1503
1541
|
}
|
|
1504
1542
|
}
|
|
1505
1543
|
async function acceptRealGoalTurn(sessionID, messageID, selectedAgent, parts) {
|
|
1544
|
+
// Root recovery can prove the persisted user message identity before its parts are available.
|
|
1545
|
+
// Preserve that existing empty projection while every observed non-empty shape stays strict.
|
|
1546
|
+
const safeParts = parts.length === 0
|
|
1547
|
+
? []
|
|
1548
|
+
: freshSessionPrompt(parts);
|
|
1549
|
+
if (safeParts === undefined)
|
|
1550
|
+
throw new Error("SORTIE_GOAL_CONTROL_DENIED: unsafe-message-parts");
|
|
1506
1551
|
await recoverCompletedGoalReservations(sessionID);
|
|
1507
1552
|
const ledger = await goalLedger(sessionID);
|
|
1508
1553
|
const state = (await ledger.readGoal()).state;
|
|
1509
1554
|
if (state.latest_user_message_id === messageID)
|
|
1510
1555
|
return;
|
|
1511
1556
|
const at = new Date().toISOString();
|
|
1512
|
-
const explicitContinuation =
|
|
1557
|
+
const explicitContinuation = safeParts.filter((part) => part.type === "text")
|
|
1558
|
+
.map((part) => part.text).join("\n")
|
|
1513
1559
|
.split(/\r?\n/u).some((line) => /^\s*(?:goal_acceptance_fingerprint|goal_budget_(?:units|time_ms|cost_usd))\s*:/iu.test(line));
|
|
1514
1560
|
if (state.goal_id === null || state.phase === "terminal" || (state.phase === "stopped" && !explicitContinuation)) {
|
|
1515
|
-
const acceptance = goalFingerprint({ message_id: messageID, parts:
|
|
1561
|
+
const acceptance = goalFingerprint({ message_id: messageID, parts: safeParts.map((part) => part.type === "text" ? part.text : part) });
|
|
1516
1562
|
await ledger.appendGoal({ kind: "goal.accepted", at,
|
|
1517
1563
|
goal_id: goalFingerprint({ root: goalRoot(sessionID), origin_user_message_id: messageID }),
|
|
1518
1564
|
revision: 1, scope_epoch: 1, acceptance_fingerprint: acceptance,
|
|
@@ -1544,7 +1590,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1544
1590
|
const persistedInfo = isRecord(message.info) ? message.info : undefined;
|
|
1545
1591
|
if ((persistedInfo?.role ?? message.role) !== "user" ||
|
|
1546
1592
|
(persistedInfo?.agent ?? message.agent) !== COORDINATOR_AGENT || !Array.isArray(message.parts) ||
|
|
1547
|
-
message.parts
|
|
1593
|
+
syntheticPrompt(message.parts))
|
|
1548
1594
|
return;
|
|
1549
1595
|
await acceptRealGoalTurn(sessionID, info.id, COORDINATOR_AGENT, message.parts);
|
|
1550
1596
|
goalDeclarationAuthority.set(sessionID, info.id);
|
|
@@ -1685,9 +1731,6 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1685
1731
|
else if (receipt === undefined && proved) {
|
|
1686
1732
|
receipt = await terminalGoal(sessionID, "completed", "succeeded").catch(() => undefined);
|
|
1687
1733
|
}
|
|
1688
|
-
else if (receipt === undefined && outcome === "DONE") {
|
|
1689
|
-
receipt = await terminalGoal(sessionID, "completed", "succeeded").catch(() => undefined);
|
|
1690
|
-
}
|
|
1691
1734
|
else if (receipt === undefined && outcome === "INTERRUPTED") {
|
|
1692
1735
|
receipt = await terminalGoal(sessionID, "stopped", "stopped").catch(() => undefined);
|
|
1693
1736
|
}
|
|
@@ -2054,6 +2097,9 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2054
2097
|
entry.execution.units.includes(reservation.unitID));
|
|
2055
2098
|
const newEvidence = acceptedEvidence.filter((entry) => entry.measurement.criterion_ids.some((criterionID) => !state.satisfied_criteria.includes(criterionID)));
|
|
2056
2099
|
const progress = newEvidence.length > 0;
|
|
2100
|
+
// Revalidation can succeed for an already-satisfied criterion after a candidate change.
|
|
2101
|
+
// New criterion coverage controls progress accounting, not the validation disposition.
|
|
2102
|
+
const validated = acceptedEvidence.length > 0;
|
|
2057
2103
|
const metadata = isRecord(output.metadata) ? output.metadata : undefined;
|
|
2058
2104
|
const interrupted = metadata?.status === "cancel" || metadata?.status === "cancelled" ||
|
|
2059
2105
|
output.status === "cancel" || output.status === "cancelled";
|
|
@@ -2063,12 +2109,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2063
2109
|
execution.endedAt !== undefined && Date.parse(execution.startedAt) >= reservation.started - 1000 &&
|
|
2064
2110
|
execution.outcome === "fail");
|
|
2065
2111
|
const processDefect = !failedAcceptanceExecution && (childSessionID === undefined || hostBindingDefect ||
|
|
2066
|
-
goalValidationDefects.has(childSessionID));
|
|
2067
|
-
const resultClass =
|
|
2112
|
+
goalValidationDefects.has(childSessionID) || !validated);
|
|
2113
|
+
const resultClass = validated ? "acceptance" : interrupted ? "interrupted" : processDefect ? "process-defect" : "acceptance";
|
|
2068
2114
|
await ledger.appendGoal({ kind: "unit.settled", at: new Date().toISOString(),
|
|
2069
2115
|
reservation_id: reservation.reservationID, receipt_id: goalFingerprint({ call_id: callID, output: outputText.slice(0, 2048) }),
|
|
2070
2116
|
goal_id: state.goal_id, unit_id: reservation.unitID,
|
|
2071
|
-
disposition:
|
|
2117
|
+
disposition: validated ? "succeeded" : interrupted ? "cancelled" : "failed", result_class: resultClass,
|
|
2072
2118
|
progress_fingerprint: progress ? goalFingerprint(acceptedEvidence) : null,
|
|
2073
2119
|
evidence: acceptedEvidence, elapsed_ms: Math.max(0, Date.now() - reservation.started), cost_usd: null });
|
|
2074
2120
|
if (childSessionID !== undefined) {
|
|
@@ -2181,57 +2227,67 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2181
2227
|
}
|
|
2182
2228
|
const key = `${sourceSessionID}\u0000${reason}`;
|
|
2183
2229
|
const existing = freshSessionRedispatches.get(key);
|
|
2230
|
+
const queued = { status: "redispatch-queued", reason,
|
|
2231
|
+
source_session_id: sourceSessionID, retry_same_session: false };
|
|
2184
2232
|
if (existing !== undefined)
|
|
2185
|
-
return await existing.operation;
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2233
|
+
return existing.settled ? await existing.operation : queued;
|
|
2234
|
+
// Both session.create and promptAsync enter the host request scheduler. Defer the entire
|
|
2235
|
+
// redispatch until the child chat hook has returned its typed control error.
|
|
2236
|
+
const operation = new Promise((accept) => {
|
|
2237
|
+
setTimeout(() => {
|
|
2238
|
+
void (async () => {
|
|
2239
|
+
let targetSessionID;
|
|
2240
|
+
try {
|
|
2241
|
+
const created = await create.call(input.client.session, {
|
|
2242
|
+
query: { directory: input.worktree ?? input.directory },
|
|
2243
|
+
body: {},
|
|
2244
|
+
});
|
|
2245
|
+
const payload = isRecord(created) && "data" in created ? created.data : created;
|
|
2246
|
+
if (!isRecord(payload) || typeof payload.id !== "string" || payload.id.length === 0) {
|
|
2247
|
+
return freshSessionFallback(reason, fallbackAction);
|
|
2248
|
+
}
|
|
2249
|
+
targetSessionID = payload.id;
|
|
2250
|
+
const parentID = typeof payload.parentID === "string" ? payload.parentID
|
|
2251
|
+
: typeof payload.parentId === "string" ? payload.parentId
|
|
2252
|
+
: undefined;
|
|
2253
|
+
if (parentID !== undefined) {
|
|
2254
|
+
await deleteFreshSession(targetSessionID);
|
|
2255
|
+
return freshSessionFallback(reason, fallbackAction);
|
|
2256
|
+
}
|
|
2257
|
+
goalRootSessions.set(targetSessionID, goalRoot(sourceSessionID));
|
|
2258
|
+
const ticket = await issueGoalTicket(targetSessionID, `fresh-root:${reason}`);
|
|
2259
|
+
if (!ticket.issued)
|
|
2260
|
+
throw new Error("fresh coordinator ticket already outstanding");
|
|
2261
|
+
const sendFresh = send;
|
|
2262
|
+
const sent = await sendFresh.call(input.client.session, {
|
|
2263
|
+
path: { id: targetSessionID },
|
|
2264
|
+
query: { directory: input.worktree ?? input.directory },
|
|
2265
|
+
body: { agent: COORDINATOR_AGENT, parts: prompt.map((part) => part.type === "text"
|
|
2266
|
+
? { ...part, synthetic: true, metadata: ticket.metadata }
|
|
2267
|
+
: part) },
|
|
2268
|
+
});
|
|
2269
|
+
if (!promptAccepted(sent))
|
|
2270
|
+
throw new Error("fresh coordinator prompt rejected");
|
|
2271
|
+
appLogInfo("fresh-session.redispatched", sourceSessionID, {
|
|
2272
|
+
reason,
|
|
2273
|
+
targetSessionID: targetSessionID.slice(0, 128),
|
|
2274
|
+
});
|
|
2275
|
+
return {
|
|
2276
|
+
status: "redispatched",
|
|
2277
|
+
reason,
|
|
2278
|
+
source_session_id: sourceSessionID,
|
|
2279
|
+
target_session_id: targetSessionID,
|
|
2280
|
+
retry_same_session: false,
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
catch {
|
|
2284
|
+
if (targetSessionID !== undefined)
|
|
2285
|
+
await deleteFreshSession(targetSessionID);
|
|
2286
|
+
return freshSessionFallback(reason, fallbackAction);
|
|
2287
|
+
}
|
|
2288
|
+
})().then(accept);
|
|
2289
|
+
}, 0);
|
|
2290
|
+
});
|
|
2235
2291
|
const entry = { operation, settled: false };
|
|
2236
2292
|
freshSessionRedispatches.set(key, entry);
|
|
2237
2293
|
void operation.then((result) => {
|
|
@@ -2254,7 +2310,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2254
2310
|
break;
|
|
2255
2311
|
freshSessionRedispatches.delete(completed[0]);
|
|
2256
2312
|
}
|
|
2257
|
-
return
|
|
2313
|
+
return queued;
|
|
2258
2314
|
}
|
|
2259
2315
|
async function ensureLoaded() {
|
|
2260
2316
|
if (loaded?.gate !== undefined)
|
|
@@ -3425,9 +3481,6 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3425
3481
|
contract_fingerprint: structuralAdmission.contract_fingerprint, experience: experience.trace });
|
|
3426
3482
|
}
|
|
3427
3483
|
const coordinator = await getParallelCoordinator();
|
|
3428
|
-
if (await coordinator.targetCheckedOut(`refs/heads/${structuralAdmission.contract.provenance.target_branch}`)) {
|
|
3429
|
-
return JSON.stringify({ status: "sol-serial", reason: "target-checked-out", experience: experience.trace });
|
|
3430
|
-
}
|
|
3431
3484
|
const result = await coordinator.prepareFabric(contract, ownerRoot, executionPlanPath === undefined ? undefined : await readJson(resolve(executionPlanPath), INPUT_LIMITS.parallel));
|
|
3432
3485
|
if (result.status === "sol-serial")
|
|
3433
3486
|
return JSON.stringify({ ...result, experience: experience.trace });
|
|
@@ -5017,15 +5070,19 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5017
5070
|
const agent = info?.agent ?? message.agent;
|
|
5018
5071
|
if (typeof agent !== "string")
|
|
5019
5072
|
return undefined;
|
|
5020
|
-
|
|
5073
|
+
const parts = message.parts === undefined ? [] : message.parts;
|
|
5074
|
+
if (!Array.isArray(parts))
|
|
5021
5075
|
return undefined;
|
|
5022
|
-
const synthetic =
|
|
5076
|
+
const synthetic = syntheticPrompt(parts);
|
|
5023
5077
|
if (synthetic)
|
|
5024
5078
|
continue;
|
|
5079
|
+
const persistedParts = parts.length === 0 ? [] : freshSessionPrompt(parts);
|
|
5080
|
+
if (persistedParts === undefined)
|
|
5081
|
+
return undefined;
|
|
5025
5082
|
const messageID = info?.id ?? message.id;
|
|
5026
5083
|
if (typeof messageID !== "string" || messageID.length === 0)
|
|
5027
5084
|
return undefined;
|
|
5028
|
-
persistedTurn = { agent, synthetic: false, messageID };
|
|
5085
|
+
persistedTurn = { agent, synthetic: false, messageID, parts: persistedParts };
|
|
5029
5086
|
break;
|
|
5030
5087
|
}
|
|
5031
5088
|
return { hasForeignUserTurn: persistedTurn?.agent !== undefined && persistedTurn.agent !== COORDINATOR_AGENT,
|
|
@@ -5121,7 +5178,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5121
5178
|
return false;
|
|
5122
5179
|
await rememberCoordinatorRoot(sessionID);
|
|
5123
5180
|
if (persistedTurn !== undefined) {
|
|
5124
|
-
await acceptRealGoalTurn(sessionID, persistedTurn.messageID, persistedTurn.agent,
|
|
5181
|
+
await acceptRealGoalTurn(sessionID, persistedTurn.messageID, persistedTurn.agent, persistedTurn.parts);
|
|
5125
5182
|
goalDeclarationAuthority.set(sessionID, persistedTurn.messageID);
|
|
5126
5183
|
}
|
|
5127
5184
|
await pinAssetVersion(sessionID);
|
|
@@ -5560,16 +5617,23 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5560
5617
|
.replaceAll(CONTINUATION_MARKER, "")
|
|
5561
5618
|
.trimEnd();
|
|
5562
5619
|
}
|
|
5620
|
+
const hostRunOutcome = terminalRunOutcome(textOutput.text);
|
|
5563
5621
|
textOutput.text = await preserveActiveGoalContinuation(textInput.sessionID, textOutput.text, textInput.messageID);
|
|
5564
|
-
|
|
5622
|
+
// Preserve a host DONE claim for proof checks while allowing an ordinary local INTERRUPTED
|
|
5623
|
+
// response to remain presentation-only IN_PROGRESS continuation.
|
|
5624
|
+
const runOutcome = hostRunOutcome === "DONE" ? hostRunOutcome : terminalRunOutcome(textOutput.text);
|
|
5565
5625
|
const terminal = runOutcome === undefined || !isCoordinatorSession(textInput.sessionID)
|
|
5566
5626
|
? undefined
|
|
5567
5627
|
: await terminalGoalFromHostText(textInput.sessionID, textOutput.text);
|
|
5568
|
-
if (runOutcome === "DONE" && terminal
|
|
5569
|
-
(
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5628
|
+
if (runOutcome === "DONE" && terminal?.delivery === "running") {
|
|
5629
|
+
textOutput.text = replaceDoneTerminalStatus(textOutput.text, "status: IN_PROGRESS — durable delivery active; same sessionでjoinまたはstale reconcileが必要");
|
|
5630
|
+
await continuation.stopAutomaticRecovery(textInput.sessionID, false, true);
|
|
5631
|
+
}
|
|
5632
|
+
else if (runOutcome === "DONE" && terminal?.receipt === undefined &&
|
|
5633
|
+
terminal?.goal !== undefined && terminal.goal.goal_id !== null) {
|
|
5634
|
+
textOutput.text = replaceDoneTerminalStatus(textOutput.text, "status: INTERRUPTED — accepted criteria remain unproved\n" +
|
|
5635
|
+
"TRUE_INTERRUPTION: internal: accepted criteria remain unproved");
|
|
5636
|
+
await continuation.stopAutomaticRecovery(textInput.sessionID, false, true);
|
|
5573
5637
|
}
|
|
5574
5638
|
if (runOutcome !== "DONE" && terminal?.delivery === "ready" && terminal.receipt?.status === "succeeded") {
|
|
5575
5639
|
textOutput.text = replaceTerminalStatus(textOutput.text, "status: DONE");
|
|
@@ -5663,7 +5727,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5663
5727
|
observedChildTerminals.delete(chatInput.sessionID);
|
|
5664
5728
|
await serializeChatTransition(chatInput.sessionID, async () => {
|
|
5665
5729
|
const parentID = chatParentID(chatInput);
|
|
5666
|
-
const synthetic = output.parts
|
|
5730
|
+
const synthetic = syntheticPrompt(output.parts);
|
|
5667
5731
|
const selectedAgent = chatInput.agent ?? output.message.agent;
|
|
5668
5732
|
if (parentID !== undefined)
|
|
5669
5733
|
rememberParent(chatInput.sessionID, parentID);
|
|
@@ -5673,8 +5737,15 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5673
5737
|
output.message.agent = chatInput.agent;
|
|
5674
5738
|
}
|
|
5675
5739
|
const requestedCoordinator = selectedAgent === COORDINATOR_AGENT;
|
|
5740
|
+
const projectedCoordinatorParts = requestedCoordinator && !synthetic && output.parts.length > 0
|
|
5741
|
+
? freshSessionPrompt(output.parts)
|
|
5742
|
+
: undefined;
|
|
5743
|
+
if (requestedCoordinator && !synthetic && output.parts.length > 0 && projectedCoordinatorParts === undefined) {
|
|
5744
|
+
throw new Error("SORTIE_GOAL_CONTROL_DENIED: unsafe-message-parts");
|
|
5745
|
+
}
|
|
5676
5746
|
const messageID = realMessageID(chatInput, output) ?? (synthetic ? undefined
|
|
5677
|
-
:
|
|
5747
|
+
: projectedCoordinatorParts === undefined ? undefined
|
|
5748
|
+
: await persistedCurrentRealMessageID(chatInput.sessionID, selectedAgent, projectedCoordinatorParts));
|
|
5678
5749
|
if (requestedCoordinator && !coordinatorRoot && (parentID !== undefined || knownChildSessions.has(chatInput.sessionID)) &&
|
|
5679
5750
|
!synthetic && messageID !== undefined) {
|
|
5680
5751
|
await acceptRealGoalTurn(chatInput.sessionID, messageID, selectedAgent, output.parts);
|
|
@@ -5721,11 +5792,11 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5721
5792
|
// Some native hosts persist the user message only after this hook returns. Defer to the
|
|
5722
5793
|
// system-transform boundary, but retain no synthetic authority and accept only the exact
|
|
5723
5794
|
// final persisted real-user parts through persistedCurrentRealMessageID.
|
|
5724
|
-
pendingRealGoalTurns.set(chatInput.sessionID, { selectedAgent, parts: [
|
|
5795
|
+
pendingRealGoalTurns.set(chatInput.sessionID, { selectedAgent, parts: projectedCoordinatorParts ?? [] });
|
|
5725
5796
|
pruneParallelChildMap(pendingRealGoalTurns);
|
|
5726
5797
|
schedulePendingRealGoalRecovery(chatInput.sessionID);
|
|
5727
5798
|
}
|
|
5728
|
-
const prompt =
|
|
5799
|
+
const prompt = projectedCoordinatorParts;
|
|
5729
5800
|
if (prompt !== undefined)
|
|
5730
5801
|
coordinatorPrompts.set(chatInput.sessionID, prompt);
|
|
5731
5802
|
// Synthetic coordinator turns reach here only after consumeGoalTicket accepted the
|
|
@@ -6676,14 +6747,19 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6676
6747
|
return;
|
|
6677
6748
|
// Deletion is terminal for watchdog recovery. Disarm synchronously before any generic event
|
|
6678
6749
|
// processing can await, touch activity, or let a queued sweep recover the cancelled root.
|
|
6679
|
-
if (event.type === "session.deleted")
|
|
6750
|
+
if (event.type === "session.deleted") {
|
|
6680
6751
|
disarmDeletedCoordinatorTaskWatchdog(eventSessionID);
|
|
6752
|
+
await continuation.stopAutomaticRecovery(eventSessionID, false);
|
|
6753
|
+
}
|
|
6681
6754
|
if (event.type === "message.updated" && info !== undefined) {
|
|
6682
6755
|
rememberCoordinatorInterruption(eventSessionID, info);
|
|
6683
|
-
|
|
6756
|
+
if (pendingRealGoalTurns.has(eventSessionID))
|
|
6757
|
+
await recoverPendingRealGoalTurn(eventSessionID);
|
|
6758
|
+
else
|
|
6759
|
+
await acceptPersistedRealGoalEvent(eventSessionID, info);
|
|
6684
6760
|
}
|
|
6685
6761
|
if (pendingRealGoalTurns.has(eventSessionID) &&
|
|
6686
|
-
|
|
6762
|
+
event.type === "message.part.updated") {
|
|
6687
6763
|
await recoverPendingRealGoalTurn(eventSessionID);
|
|
6688
6764
|
}
|
|
6689
6765
|
const eventPartTime = isRecord(eventPart?.time) ? eventPart.time : undefined;
|
|
@@ -112,7 +112,15 @@ type SortieGoalSnapshot = Pick<GoalFlightState, "acceptance_contract" | "consume
|
|
|
112
112
|
export declare function createSortieResult(receipt: GoalTerminalReceipt, goal: SortieGoalSnapshot, metrics: RunMetrics | undefined, asOf?: string, records?: readonly GoalFlightEventRecord[]): SortieResult;
|
|
113
113
|
export type RunTerminalOutcome = "DONE" | "INTERRUPTED" | "BLOCKED" | "NEED_DECISION";
|
|
114
114
|
export declare function collectRunMetrics(client: RunMetricsClient | undefined, rootSessionID: string, directory?: string, now?: number, window?: RunMetricsWindow): Promise<RunMetrics | undefined>;
|
|
115
|
-
export
|
|
115
|
+
export interface SortieResultPresentation {
|
|
116
|
+
readonly implementation?: string;
|
|
117
|
+
readonly pending?: string;
|
|
118
|
+
readonly next?: string;
|
|
119
|
+
readonly commit?: string;
|
|
120
|
+
readonly statusSummary?: string;
|
|
121
|
+
readonly stopReason?: string;
|
|
122
|
+
}
|
|
123
|
+
export declare function formatSortieResult(result: SortieResult, presentation?: SortieResultPresentation): string;
|
|
116
124
|
export declare function formatRunMetrics(metrics: RunMetrics): string;
|
|
117
125
|
export declare function isDoneTerminalText(text: string): boolean;
|
|
118
126
|
export declare function terminalRunOutcome(text: string): RunTerminalOutcome | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildDebrief, renderDebrief, observeDebriefSession } from "./sortie-debrief.js";
|
|
1
|
+
import { buildDebrief, renderDebrief, renderDebriefProof, observeDebriefSession } from "./sortie-debrief.js";
|
|
2
2
|
import { goalFingerprint } from "../core/goal-bound.js";
|
|
3
3
|
import { renderCareer } from "./sortie-career.js";
|
|
4
4
|
const unavailable = (reason) => ({ availability: "unavailable", value: null, reason });
|
|
@@ -341,25 +341,61 @@ function duration(milliseconds) {
|
|
|
341
341
|
function metricText(metric, render) {
|
|
342
342
|
return metric.availability === "available" ? render(metric.value) : "計測不可";
|
|
343
343
|
}
|
|
344
|
-
|
|
344
|
+
const displayText = (value) => value?.replace(/[\r\n\t]+/gu, " ").trim() || "未取得";
|
|
345
|
+
const reportFence = (body) => {
|
|
346
|
+
const longest = Math.max(0, ...[...body.matchAll(/^~+/gmu)].map((match) => match[0].length));
|
|
347
|
+
const fence = "~".repeat(Math.max(3, longest + 1));
|
|
348
|
+
return `${fence}text\n${body}\n${fence}`;
|
|
349
|
+
};
|
|
350
|
+
export function formatSortieResult(result, presentation = {}) {
|
|
345
351
|
const criteria = metricText(result.proof.criteria, (entries) => {
|
|
346
352
|
const passing = entries.filter(({ status }) => status === "PASS").length;
|
|
347
353
|
return `${passing}/${entries.length}`;
|
|
348
354
|
});
|
|
349
|
-
const achievement = result.mission.status === "COMPLETED" ? "
|
|
355
|
+
const achievement = result.mission.status === "COMPLETED" ? "COMPLETED"
|
|
356
|
+
: result.mission.status === "INTERRUPTED" ? "INTERRUPTED"
|
|
357
|
+
: result.mission.status === "EXTERNAL_BLOCKER" ? "EXTERNAL_BLOCKER" : "USER_DECISION";
|
|
358
|
+
const summaryLabel = result.mission.status === "COMPLETED" ? "完了"
|
|
350
359
|
: result.mission.status === "INTERRUPTED" ? "中断(未完了)"
|
|
351
|
-
: result.mission.status === "EXTERNAL_BLOCKER" ? "外部要因で未完了"
|
|
352
|
-
: "ユーザー判断待ち(未完了)";
|
|
360
|
+
: result.mission.status === "EXTERNAL_BLOCKER" ? "外部要因で未完了" : "ユーザー判断待ち(未完了)";
|
|
353
361
|
const color = result.mission.status === "COMPLETED" ? "🟢" : result.mission.status === "EXTERNAL_BLOCKER" ? "🔴" : "🟡";
|
|
362
|
+
const proof = renderDebriefProof(result.debrief);
|
|
354
363
|
const body = [
|
|
355
|
-
|
|
356
|
-
|
|
364
|
+
"🐾 SORTIE DOGS — 帰還報告",
|
|
365
|
+
result.result_id[0],
|
|
366
|
+
"",
|
|
367
|
+
`${color} ${achievement} — ${displayText(presentation.statusSummary)}`,
|
|
368
|
+
"",
|
|
369
|
+
"⚔️ MISSION",
|
|
370
|
+
`経過 ⏱ ${metricText(result.speed.goal_wall_ms, duration)} ※待機含む`,
|
|
371
|
+
`最終達成条件 ◔ ${criteria}`,
|
|
372
|
+
`対象検証 ${proof.validation}`,
|
|
373
|
+
`SourceReview ${proof.review}`,
|
|
374
|
+
`Commit ${displayText(presentation.commit)}`,
|
|
375
|
+
"",
|
|
376
|
+
"🔧 実装",
|
|
377
|
+
displayText(presentation.implementation),
|
|
378
|
+
"",
|
|
379
|
+
"⏳ 未実施",
|
|
380
|
+
displayText(presentation.pending),
|
|
381
|
+
"",
|
|
382
|
+
"➡️ NEXT",
|
|
383
|
+
displayText(presentation.next),
|
|
384
|
+
"",
|
|
385
|
+
"🪙 COST / PACK",
|
|
386
|
+
`使用量 ${metricText(result.cost.total_tokens, (value) => `${value.toLocaleString("ja-JP")} tokens`)}`,
|
|
387
|
+
`host推定額 ${metricText(result.cost.cost_usd, (value) => `$${value.toFixed(4)}`)} ※実課金換算なし`,
|
|
388
|
+
"",
|
|
357
389
|
...renderDebrief(result.debrief),
|
|
358
|
-
|
|
359
|
-
"*最終応答生成前の計測*",
|
|
390
|
+
"",
|
|
360
391
|
...renderCareer(result.career),
|
|
361
|
-
|
|
362
|
-
|
|
392
|
+
"",
|
|
393
|
+
"🛑 STOP REASON",
|
|
394
|
+
displayText(presentation.stopReason ?? result.mission.stop_reason),
|
|
395
|
+
"",
|
|
396
|
+
"※使用量は最終応答生成前の計測",
|
|
397
|
+
].join("\n");
|
|
398
|
+
return `<details>\n<summary><strong>🐾 SORTIE DOGS — 帰還報告|${color} ${summaryLabel}</strong></summary>\n\n${reportFence(body)}\n\n</details>`;
|
|
363
399
|
}
|
|
364
400
|
export function formatRunMetrics(metrics) {
|
|
365
401
|
const elapsed = metrics.durationMilliseconds === undefined ? "duration unavailable" : `${duration(metrics.durationMilliseconds)} wall-clock`;
|
|
@@ -476,6 +512,7 @@ export function insertRunMetrics(text, metrics) {
|
|
|
476
512
|
return lines.join(newline);
|
|
477
513
|
}
|
|
478
514
|
export function insertSortieResult(text, result) {
|
|
515
|
+
const presentation = extractSortiePresentation(text);
|
|
479
516
|
const visible = sanitizeTerminalReport(text);
|
|
480
517
|
const checkpoint = terminalCheckpoint(visible);
|
|
481
518
|
if (checkpoint === undefined)
|
|
@@ -506,7 +543,7 @@ export function insertSortieResult(text, result) {
|
|
|
506
543
|
cleaned.splice(checkpoint.index + 1, 1);
|
|
507
544
|
let card;
|
|
508
545
|
try {
|
|
509
|
-
card = formatSortieResult(result);
|
|
546
|
+
card = formatSortieResult(result, presentation);
|
|
510
547
|
}
|
|
511
548
|
catch {
|
|
512
549
|
card = "<details>\n<summary><strong>🐾 SORTIE DOGS — 帰還報告</strong></summary>\n\n**確認:** 表示集計を取得できません。任務結果は先頭の状態を参照。\n\n</details>";
|
|
@@ -514,6 +551,23 @@ export function insertSortieResult(text, result) {
|
|
|
514
551
|
cleaned.splice(checkpoint.index + 1, 0, "", card, "");
|
|
515
552
|
return cleaned.join(newline).trimEnd();
|
|
516
553
|
}
|
|
554
|
+
function extractSortiePresentation(text) {
|
|
555
|
+
const first = topLevelLines(text).find(({ line }) => line.trim().length > 0)?.line ?? "";
|
|
556
|
+
const statusSummary = first.split(/\s+[—-]\s+/u).slice(1).join(" — ").trim() || undefined;
|
|
557
|
+
const section = (names) => {
|
|
558
|
+
const expression = new RegExp(`^[ \\t]*(?:#{1,6}[ \\t]*)?(?:\\*\\*)?(?:${names})(?:\\*\\*)?[ \\t]*:?[ \\t]*(?:\\*\\*)?[ \\t]*(.*)$`, "imu");
|
|
559
|
+
const match = expression.exec(text);
|
|
560
|
+
if (match === null)
|
|
561
|
+
return undefined;
|
|
562
|
+
if (match[1]?.trim())
|
|
563
|
+
return match[1].trim();
|
|
564
|
+
const tail = text.slice(match.index + match[0].length).split(/\r?\n/u);
|
|
565
|
+
return tail.find((line) => line.trim().length > 0 && !/^[ \\t]*(?:#{1,6}[ \\t]*)?(?:\\*\\*)?(?:変更点|実装|未実施|次|NEXT|Commit|コミット)/iu.test(line))?.trim();
|
|
566
|
+
};
|
|
567
|
+
const explicitStop = /^(?:TRUE_INTERRUPTION|TRUE_BLOCKER)[ \\t]*:[ \\t]*(.+)$/imu.exec(text)?.[1]?.trim();
|
|
568
|
+
return { statusSummary, implementation: section("変更点|実装"), pending: section("未実施"), next: section("次|NEXT"),
|
|
569
|
+
commit: section("Commit|コミット"), stopReason: explicitStop };
|
|
570
|
+
}
|
|
517
571
|
export function createGoalReport(result, receipt) {
|
|
518
572
|
const tokens = result.cost.total_tokens.availability === "available" && Number.isSafeInteger(result.cost.total_tokens.value)
|
|
519
573
|
? result.cost.total_tokens.value : null;
|
|
@@ -124,21 +124,25 @@ export async function collectCareer(directories, currentPath, current, read, max
|
|
|
124
124
|
}
|
|
125
125
|
export function renderCareer(career) {
|
|
126
126
|
if (career === undefined)
|
|
127
|
-
return ["
|
|
127
|
+
return ["📜 PACK RECORD", "保存履歴を取得できません"];
|
|
128
128
|
const terminal = career.goals - career.active;
|
|
129
|
-
const
|
|
130
|
-
const minutes = (metric) => metric.covered === 0 ? "計測不可" : `${(metric.sum / 60000).toFixed(1)}分(${metric.covered}/${terminal}任務)`;
|
|
131
|
-
const models = [...career.models].sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
|
|
132
|
-
const modelText = models.slice(0, 4).map((entry) => `${entry.model.replace(/[\\`*_{}\[\]()<>!|\r\n]/gu, "").slice(0, 120)} ${entry.tokens.toLocaleString("ja-JP")}`).join(" · ");
|
|
129
|
+
const minutes = (metric) => metric.covered === 0 ? "計測不可" : `${(metric.sum / 60000).toFixed(1)}分 ※${metric.covered}/${terminal}任務`;
|
|
133
130
|
return [
|
|
134
|
-
"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
131
|
+
"📜 PACK RECORD",
|
|
132
|
+
`🏁 完了 ${career.completed}`,
|
|
133
|
+
`🟡 中断 ${career.interrupted}`,
|
|
134
|
+
`⏳ 外部待機 ${career.external}`,
|
|
135
|
+
`❓ 指示待ち ${career.decision}`,
|
|
136
|
+
`🔄 進行中 ${career.active}`,
|
|
137
|
+
`↩️ 復帰 ${career.recoveries}`,
|
|
138
|
+
"",
|
|
139
|
+
`🪙 累積使用量 ${career.tokens.covered === 0 ? "計測不可" : `${career.tokens.sum.toLocaleString("ja-JP")} tokens`} ※${career.tokens.covered}/${terminal}任務`,
|
|
140
|
+
`⏱ 累積worker ${minutes(career.workerTime)}`,
|
|
141
|
+
`🕰 累積goal ${minutes(career.goalWall)} ※待機・重複含む`,
|
|
142
|
+
`⚡ 累積重複率 ${career.overlap.ratio === null ? "計測不可" : `${career.overlap.ratio.toFixed(2)}×`} ※${career.overlap.covered}/${terminal}任務・速度倍率ではありません`,
|
|
143
|
+
"",
|
|
144
|
+
"📦 保存範囲",
|
|
145
|
+
`${career.since?.slice(0, 10) ?? "開始日不明"}以降 / ${career.coverage.included} of ${career.coverage.files} files${career.coverage.unavailable || career.coverage.truncated ? " ※部分集計" : ""}`,
|
|
146
|
+
"※生涯戦績ではありません",
|
|
143
147
|
];
|
|
144
148
|
}
|
|
@@ -55,4 +55,8 @@ export interface Debrief {
|
|
|
55
55
|
export declare function observeDebriefSession(id: string, root: boolean, messages: readonly Record<string, unknown>[], window?: Span): DebriefSession;
|
|
56
56
|
export declare function buildDebrief(receipt: GoalTerminalReceipt, contract: GoalAcceptanceContract | null, observation: DebriefObservation | undefined, records?: readonly GoalFlightEventRecord[]): Debrief;
|
|
57
57
|
export declare function renderDebrief(debrief: Debrief | undefined): string[];
|
|
58
|
+
export declare function renderDebriefProof(debrief: Debrief | undefined): {
|
|
59
|
+
validation: string;
|
|
60
|
+
review: string;
|
|
61
|
+
};
|
|
58
62
|
export {};
|
|
@@ -254,6 +254,12 @@ export function buildDebrief(receipt, contract, observation, records) {
|
|
|
254
254
|
wallMilliseconds: unionDuration(spans) } } : {}) };
|
|
255
255
|
}
|
|
256
256
|
const label = (text) => text.replace(/[\r\n\t]/gu, " ").replace(/[\\`*_{}\[\]()<>!|]/gu, "").slice(0, 120);
|
|
257
|
+
const gauge = (percent) => {
|
|
258
|
+
const eighths = Math.max(0, Math.min(80, Math.round(percent * 0.8)));
|
|
259
|
+
const whole = Math.floor(eighths / 8), remainder = eighths % 8;
|
|
260
|
+
const partial = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"][remainder];
|
|
261
|
+
return `${"█".repeat(whole)}${partial}${" ".repeat(10 - whole - (remainder === 0 ? 0 : 1))}`;
|
|
262
|
+
};
|
|
257
263
|
export function renderDebrief(debrief) {
|
|
258
264
|
const pack = debrief?.pack == null ? null : [...debrief.pack].sort((a, b) => b.count - a.count || a.model.localeCompare(b.model));
|
|
259
265
|
const packVisible = pack?.slice(0, 4) ?? [];
|
|
@@ -264,21 +270,24 @@ export function renderDebrief(debrief) {
|
|
|
264
270
|
if (mix !== null && mix.length > 4)
|
|
265
271
|
visible.push({ model: "その他", tokens: mix.slice(4).reduce((sum, entry) => sum + entry.tokens, 0),
|
|
266
272
|
percent: mix.slice(4).reduce((sum, entry) => sum + entry.percent, 0) });
|
|
267
|
-
const
|
|
268
|
-
const filled = Math.max(0, Math.min(10, Math.round(percent / 10)));
|
|
269
|
-
return "█".repeat(filled) + "░".repeat(10 - filled);
|
|
270
|
-
};
|
|
271
|
-
const status = (value) => value === "PASS" ? "🟢 **PASS**" : value === "FAIL" ? "🔴 **FAIL**"
|
|
273
|
+
const status = (value) => value === "PASS" ? "🟢 PASS" : value === "FAIL" ? "🔴 FAIL"
|
|
272
274
|
: value === "WAIVED" ? "免除" : "未記録";
|
|
275
|
+
const counts = new Map(packVisible.map((entry) => [entry.model, entry.count]));
|
|
273
276
|
return [
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
...(mix === null ? ["モデル内訳 usage未取得"] : visible.map((entry) => {
|
|
278
|
+
const count = counts.get(entry.model);
|
|
279
|
+
return `🐕 ${label(entry.model)} ${gauge(entry.percent)} ${entry.percent.toFixed(1)}% ${entry.tokens.toLocaleString("ja-JP")} tokens${count === undefined ? "" : ` ×${count}`}`;
|
|
280
|
+
})),
|
|
281
|
+
`⚡ 実行重複率 ${debrief?.overlap !== undefined && debrief.overlap.wallMilliseconds > 0
|
|
282
|
+
? `${(debrief.overlap.workerMilliseconds / debrief.overlap.wallMilliseconds).toFixed(2)}×`
|
|
279
283
|
: pack?.length === 0 ? "対象なし(出撃なし)" : "稼働区間の記録不足"}`,
|
|
280
|
-
|
|
281
|
-
...(debrief?.notes?.length ? [
|
|
282
|
-
...(debrief?.traits.length ? [`**🏅 今回の戦績:** ${debrief.traits.join(" · ")}`] : []),
|
|
284
|
+
" ※worker区間・速度倍率ではありません",
|
|
285
|
+
...(debrief?.notes?.length ? [`計測範囲 ${debrief.notes.join(" · ")}`] : []),
|
|
283
286
|
];
|
|
284
287
|
}
|
|
288
|
+
export function renderDebriefProof(debrief) {
|
|
289
|
+
const status = (value) => value === "PASS" ? "🟢 PASS" : value === "FAIL" ? "🔴 FAIL"
|
|
290
|
+
: value === "WAIVED" ? "免除" : "未記録";
|
|
291
|
+
return { validation: status(debrief?.validation ?? "未確認"),
|
|
292
|
+
review: `${status(debrief?.review ?? "未確認")}${debrief?.reviewSource === "reviewer" ? "(reviewer報告)" : ""}` };
|
|
293
|
+
}
|
package/dist/runtime-assets.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface RuntimeAsset {
|
|
|
7
7
|
}
|
|
8
8
|
export declare const runtimeAssets: readonly [{
|
|
9
9
|
readonly name: "dog-coordinator";
|
|
10
|
-
readonly version: "0.3.
|
|
10
|
+
readonly version: "0.3.89-completion-proof-v1";
|
|
11
11
|
readonly installPath: "agent/dog-coordinator.md";
|
|
12
12
|
readonly content: `---
|
|
13
13
|
description: Canonical MkII coordinator packaged by Sortie-dogs
|
|
@@ -1391,6 +1391,9 @@ criterion -> changed or inspected implementation path -> concrete exercising tes
|
|
|
1391
1391
|
An aggregate validation command without that mapping is insufficient. Multi-form criteria must cover their
|
|
1392
1392
|
materially distinct syntax, value-shape, scope, and error paths; any missing path remains UNPROVEN and requires
|
|
1393
1393
|
continued implementation or validation. Include the complete trace in every high-risk SourceReview artifact.
|
|
1394
|
+
For accepted failure behavior, require result/error/state evidence and a valid case; for accepted composite
|
|
1395
|
+
or wrapped-value behavior, require public-entry-point evidence. Accept justified N/A dimensions and existing
|
|
1396
|
+
sufficient evidence without extra validation or review. Do not accept expectations copied from the candidate's output.
|
|
1394
1397
|
Any generated input/output pair in the changed manifest is high risk and requires SourceReview. DONE requires
|
|
1395
1398
|
generator command evidence, post-generation candidate identity, generated-output stability, and canonical
|
|
1396
1399
|
validation after generation. Reject evidence produced only before regeneration.
|
|
@@ -1468,10 +1471,10 @@ TERMINAL_STATUS_SEMANTICS_FIXTURE
|
|
|
1468
1471
|
END_TERMINAL_STATUS_SEMANTICS_FIXTURE
|
|
1469
1472
|
|
|
1470
1473
|
RUNTIME_ASSET_VERSION_SYNC_FIXTURE
|
|
1471
|
-
runtime_version: 0.3.
|
|
1474
|
+
runtime_version: 0.3.89-completion-proof-v1
|
|
1472
1475
|
shared_marker: src/asset-version.ts
|
|
1473
|
-
packaged_expectation: test/plugin-loader.test.ts uses 0.3.
|
|
1474
|
-
initialize_expectation: test/initialize.test.ts uses 0.3.
|
|
1476
|
+
packaged_expectation: test/plugin-loader.test.ts uses 0.3.89-completion-proof-v1
|
|
1477
|
+
initialize_expectation: test/initialize.test.ts uses 0.3.89-completion-proof-v1
|
|
1475
1478
|
rule: runtime asset versions, shared marker, packaged expectation, and initialize expectation change together
|
|
1476
1479
|
END_RUNTIME_ASSET_VERSION_SYNC_FIXTURE
|
|
1477
1480
|
|
|
@@ -1494,32 +1497,32 @@ END_INTERNAL_TERMINAL_PROOF_FIXTURE
|
|
|
1494
1497
|
`;
|
|
1495
1498
|
}, {
|
|
1496
1499
|
readonly name: "dog-worker";
|
|
1497
|
-
readonly version: "0.3.
|
|
1500
|
+
readonly version: "0.3.89-completion-proof-v1";
|
|
1498
1501
|
readonly installPath: "agent/dog-worker.md";
|
|
1499
1502
|
readonly content: string;
|
|
1500
1503
|
}, {
|
|
1501
1504
|
readonly name: "dog-luna-worker";
|
|
1502
|
-
readonly version: "0.3.
|
|
1505
|
+
readonly version: "0.3.89-completion-proof-v1";
|
|
1503
1506
|
readonly installPath: "agent/dog-luna-worker.md";
|
|
1504
1507
|
readonly content: string;
|
|
1505
1508
|
}, {
|
|
1506
1509
|
readonly name: "dog-scout";
|
|
1507
|
-
readonly version: "0.3.
|
|
1510
|
+
readonly version: "0.3.89-completion-proof-v1";
|
|
1508
1511
|
readonly installPath: "agent/dog-scout.md";
|
|
1509
1512
|
readonly content: "---\ndescription: Bounded evidence scout for dog-coordinator\nmode: subagent\nsteps: 8\npermission:\n bash: deny\n webfetch: deny\n task: deny\n question: deny\n glob: deny\n grep: deny\n edit: deny\n list: deny\n write: deny\n patch: deny\ntools:\n bash: false\n webfetch: false\n task: false\n question: false\n glob: false\n grep: false\n edit: false\n list: false\n write: false\n patch: false\n---\n# dog-scout\n\nAccept one concrete missing_evidence_code: manifest, validation, or owner-risk. Accept only an\nexplicit absolute project_root and a known_paths list of at most four paths from dog-coordinator.\nResolve only that evidence key from those paths under project_root; never resolve a path against the\nsession directory. Use Read only, with at most 120 lines and no more than one read per supplied path.\nDo not resolve a second key, explore, invoke another tool, retry, edit, stage, commit, or become user-facing.\n\nWhen project_root is missing, or a supplied path does not resolve under it, or a resolved path is\nunreadable, report that dispatch defect as the facts for the requested key and name the exact paths.\nDo not retry, guess another root, or answer from an unread path.\n\nReturn exactly one concise JSON object of at most 800 characters with exactly these keys:\nmissing_evidence_code, facts, evidence_paths, risks. Use no Markdown, code fence, commentary, or raw log. Return it only\nto dog-coordinator. Write the facts and risks prose in the language the dispatch uses for its own\nprose; keep the keys, paths, commands, and identifiers verbatim.\n";
|
|
1510
1513
|
}, {
|
|
1511
1514
|
readonly name: "dog-reviewer";
|
|
1512
|
-
readonly version: "0.3.
|
|
1515
|
+
readonly version: "0.3.89-completion-proof-v1";
|
|
1513
1516
|
readonly installPath: "agent/dog-reviewer.md";
|
|
1514
|
-
readonly content: "---\ndescription: Independent source reviewer for dog-coordinator\nmode: subagent\npermission:\n bash: deny\n webfetch: deny\n task: deny\n question: deny\n glob: deny\n grep: deny\n edit: deny\n list: deny\n write: deny\n patch: deny\n read: deny\ntools:\n bash: false\n webfetch: false\n task: false\n question: false\n glob: false\n grep: false\n edit: false\n list: false\n write: false\n patch: false\n read: false\n---\n# dog-reviewer\n\nAccept only one bounded SourceReview request from dog-coordinator after canonical\nvalidation for one high-risk candidate. Review only the supplied acceptance criteria, exact\nmanifest, changedLogicSummary, supplied changed-code excerpts, and validation evidence. Confirm every acceptance item explicitly\nmaps to at least one changedLogicSummary entry and assess that changed logic against the mapped\nacceptance item. Missing or incomplete coverage is a concrete finding, never PASS.\nRequire one indexed acceptance[i] -> changedLogicSummary[j] mapping line per acceptance item and\nreject a missing index or unequal mapping count before assessing the changed logic.\nAlso require each acceptance item to map to a concrete exercising test/input/branch and result. A broad\nsuite PASS without criterion-level exercise evidence is insufficient. When one item contains materially\ndifferent syntax forms, value shapes, scopes, or error paths, reject PASS unless representative traces cover\neach path or the artifact proves they share one implementation path.\nWhen changed files include generator inputs or checked-in generated outputs, require the canonical generator\ncommand, a stable post-generation diff, and validation executed after generation. Reject PASS if helper logic\nexists only in a generated output, regeneration removes behavior, or validation predates the generated candidate.\nDo not request raw logs or full source files, review low-risk candidates, expand scope, or dispatch\nanother agent.\nTreat those supplied fields as the complete bounded SourceReview artifact; use only that artifact and invoke no tools.\nDo not infer that a branch or exemption is absent from source because a prose summary omits it.\nIf the supplied excerpts do not establish a claim, report an evidence gap and request the exact\nbranch/helper excerpt in the next artifact; do not prescribe a source fix for an unproven defect.\n\nReturn one concise PASS or concrete-finding response only to dog-coordinator before the\ncoordinator commit. Write every finding, evidence, and required-fix sentence in the language the\nsupplied artifact uses for its own prose, one statement per line, and keep verdict values,\nidentifiers, paths, and commands verbatim. Do not implement, remediate, resolve blockers, edit,\nstage, commit, or become user-facing. Remain host-routed: do not require or identify a provider, vendor, model, variant,\nor transport.\n";
|
|
1517
|
+
readonly content: "---\ndescription: Independent source reviewer for dog-coordinator\nmode: subagent\npermission:\n bash: deny\n webfetch: deny\n task: deny\n question: deny\n glob: deny\n grep: deny\n edit: deny\n list: deny\n write: deny\n patch: deny\n read: deny\ntools:\n bash: false\n webfetch: false\n task: false\n question: false\n glob: false\n grep: false\n edit: false\n list: false\n write: false\n patch: false\n read: false\n---\n# dog-reviewer\n\nAccept only one bounded SourceReview request from dog-coordinator after canonical\nvalidation for one high-risk candidate. Review only the supplied acceptance criteria, exact\nmanifest, changedLogicSummary, supplied changed-code excerpts, and validation evidence. Confirm every acceptance item explicitly\nmaps to at least one changedLogicSummary entry and assess that changed logic against the mapped\nacceptance item. Missing or incomplete coverage is a concrete finding, never PASS.\nRequire one indexed acceptance[i] -> changedLogicSummary[j] mapping line per acceptance item and\nreject a missing index or unequal mapping count before assessing the changed logic.\nAlso require each acceptance item to map to a concrete exercising test/input/branch and result. A broad\nsuite PASS without criterion-level exercise evidence is insufficient. When one item contains materially\ndifferent syntax forms, value shapes, scopes, or error paths, reject PASS unless representative traces cover\neach path or the artifact proves they share one implementation path.\nCheck contract-derived expectations only for accepted behavior. For an applicable failure or composite-value\ncriterion, error-only or helper-only assertions can leave public result/state behavior unproved. Accept justified\nN/A dimensions; do not demand new behavior, new review rounds, or redundant checks outside accepted scope.\nWhen changed files include generator inputs or checked-in generated outputs, require the canonical generator\ncommand, a stable post-generation diff, and validation executed after generation. Reject PASS if helper logic\nexists only in a generated output, regeneration removes behavior, or validation predates the generated candidate.\nDo not request raw logs or full source files, review low-risk candidates, expand scope, or dispatch\nanother agent.\nTreat those supplied fields as the complete bounded SourceReview artifact; use only that artifact and invoke no tools.\nDo not infer that a branch or exemption is absent from source because a prose summary omits it.\nIf the supplied excerpts do not establish a claim, report an evidence gap and request the exact\nbranch/helper excerpt in the next artifact; do not prescribe a source fix for an unproven defect.\n\nReturn one concise PASS or concrete-finding response only to dog-coordinator before the\ncoordinator commit. Write every finding, evidence, and required-fix sentence in the language the\nsupplied artifact uses for its own prose, one statement per line, and keep verdict values,\nidentifiers, paths, and commands verbatim. Do not implement, remediate, resolve blockers, edit,\nstage, commit, or become user-facing. Remain host-routed: do not require or identify a provider, vendor, model, variant,\nor transport.\n";
|
|
1515
1518
|
}, {
|
|
1516
1519
|
readonly name: "dog-advisor";
|
|
1517
|
-
readonly version: "0.3.
|
|
1520
|
+
readonly version: "0.3.89-completion-proof-v1";
|
|
1518
1521
|
readonly installPath: "agent/dog-advisor.md";
|
|
1519
1522
|
readonly content: "---\ndescription: Focused technical advisor for dog-coordinator\nmode: subagent\npermission:\n bash: deny\n webfetch: deny\n task: deny\n question: deny\n glob: deny\n grep: deny\n edit: deny\n list: deny\n write: deny\n patch: deny\n read: deny\ntools:\n bash: false\n webfetch: false\n task: false\n question: false\n glob: false\n grep: false\n edit: false\n list: false\n write: false\n patch: false\n read: false\n---\n# dog-advisor\n\nAccept only one bounded Strategy request from dog-coordinator for one candidate and one focused\nquestion. Use only the supplied acceptance criteria, exact manifest, constraints, and concise\nevidence. Do not request raw logs or full source files, expand scope, or dispatch another agent.\nTreat those supplied fields as the complete bounded Strategy artifact; use only that artifact and invoke no tools.\nReject every SourceReview request and return the rejection only to dog-coordinator; SourceReview is\ndog-reviewer-only work.\n\nReturn concise options and one recommendation only to dog-coordinator. Write every option,\nrecommendation, and consideration in the language the supplied request uses for its own prose, one\nstatement per line, and keep identifiers, paths, and commands verbatim. Do not perform\nSourceReview, implement, remediate, resolve blockers, edit, stage, commit, or become user-facing.\nImplementation remains dog-worker work. Remain host-routed: do not require or identify a\nprovider, vendor, model, variant, or transport.\n";
|
|
1520
1523
|
}, {
|
|
1521
1524
|
readonly name: "sortie";
|
|
1522
|
-
readonly version: "0.3.
|
|
1525
|
+
readonly version: "0.3.89-completion-proof-v1";
|
|
1523
1526
|
readonly installPath: "command/sortie.md";
|
|
1524
1527
|
readonly content: "---\ndescription: Start the canonical Sortie-dogs MkII workflow\nagent: dog-coordinator\n---\nRequest: $ARGUMENTS\n\n1. If $ARGUMENTS is empty, request task context and stop; give project init guidance first.\n2. Do not preflight installed runtime assets. The plugin reports version skew without adding model\n turns; proceed from task evidence and project instructions.\n3. On restart or re-entry, reconstruct context from project-local durable artifacts and the\n latest bounded handoff or checkpoint. Preserve both manifests and ordered validation history;\n resume the same task through dog-coordinator with only the required delta.\n4. Otherwise transfer request and project context to dog-coordinator. Frontmatter is the single coordinator\n transfer; never route a worker to the user.\n";
|
|
1525
1528
|
}];
|
package/dist/runtime-assets.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GOAL_DECLARATION_FORMAT } from "./core/goal-declaration-format.js";
|
|
2
|
-
const ASSET_VERSION = "0.3.
|
|
2
|
+
const ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
3
3
|
// Kept local so source-mode CLI execution does not load the plugin graph.
|
|
4
4
|
const BACKLOG_DRAIN_CAPABILITY = "sortie_enable_backlog_drain";
|
|
5
5
|
const PARALLEL_PREPARE_CAPABILITY = "sortie_prepare_parallel_dispatch";
|
|
@@ -101,6 +101,8 @@ Validation budget exhaustion, host counters, local routing, and unavailable host
|
|
|
101
101
|
defects, never TRUE_BLOCKER: external and never a reason to ask the user for an internal route. Return the
|
|
102
102
|
typed defect to dog-coordinator for autonomous repair. A changed candidate may run the next declared
|
|
103
103
|
validation; an unchanged duplicate remains forbidden.
|
|
104
|
+
Run the exact declared canonical validation string in its own tool call. Do not prepend or append
|
|
105
|
+
formatting, generation, or cleanup commands: host evidence must match the declared command boundary.
|
|
104
106
|
|
|
105
107
|
Before returning canonical PASS, build a criterion-level trace for every accepted criterion. Each trace
|
|
106
108
|
must name the criterion, the changed implementation path or inspected existing path, the concrete test
|
|
@@ -108,6 +110,11 @@ case/input form or static branch that exercises it, and PASS or UNPROVEN. A broa
|
|
|
108
110
|
not prove every criterion. Split criteria that cover multiple syntax forms, value shapes, scopes, or error
|
|
109
111
|
paths into representative paths. If any accepted edge remains UNPROVEN, add a manifest-authorized check or
|
|
110
112
|
return the evidence gap; never report completion from aggregate validation alone.
|
|
113
|
+
Derive expected behavior from the accepted contract and existing public semantics, not the candidate.
|
|
114
|
+
Only when an accepted criterion covers failure behavior, check the public return/result, error, and
|
|
115
|
+
observable state together, paired with a valid case. For an accepted composite or wrapped-value feature,
|
|
116
|
+
exercise its public entry point as well as its helper. Mark inapplicable dimensions N/A with a short reason;
|
|
117
|
+
do not invent failure behavior, widen acceptance, or repeat a proved check to satisfy this guidance.
|
|
111
118
|
Treat generated-source boundaries as high risk. If a manifest changes a generator input, grammar, schema,
|
|
112
119
|
template, or a checked-in generated output, identify the repository's canonical generator and run it before
|
|
113
120
|
the final validation. Prove the regenerated output is stable and that canonical validation ran against that
|
|
@@ -1613,6 +1620,9 @@ criterion -> changed or inspected implementation path -> concrete exercising tes
|
|
|
1613
1620
|
An aggregate validation command without that mapping is insufficient. Multi-form criteria must cover their
|
|
1614
1621
|
materially distinct syntax, value-shape, scope, and error paths; any missing path remains UNPROVEN and requires
|
|
1615
1622
|
continued implementation or validation. Include the complete trace in every high-risk SourceReview artifact.
|
|
1623
|
+
For accepted failure behavior, require result/error/state evidence and a valid case; for accepted composite
|
|
1624
|
+
or wrapped-value behavior, require public-entry-point evidence. Accept justified N/A dimensions and existing
|
|
1625
|
+
sufficient evidence without extra validation or review. Do not accept expectations copied from the candidate's output.
|
|
1616
1626
|
Any generated input/output pair in the changed manifest is high risk and requires SourceReview. DONE requires
|
|
1617
1627
|
generator command evidence, post-generation candidate identity, generated-output stability, and canonical
|
|
1618
1628
|
validation after generation. Reject evidence produced only before regeneration.
|
|
@@ -1821,6 +1831,9 @@ Also require each acceptance item to map to a concrete exercising test/input/bra
|
|
|
1821
1831
|
suite PASS without criterion-level exercise evidence is insufficient. When one item contains materially
|
|
1822
1832
|
different syntax forms, value shapes, scopes, or error paths, reject PASS unless representative traces cover
|
|
1823
1833
|
each path or the artifact proves they share one implementation path.
|
|
1834
|
+
Check contract-derived expectations only for accepted behavior. For an applicable failure or composite-value
|
|
1835
|
+
criterion, error-only or helper-only assertions can leave public result/state behavior unproved. Accept justified
|
|
1836
|
+
N/A dimensions; do not demand new behavior, new review rounds, or redundant checks outside accepted scope.
|
|
1824
1837
|
When changed files include generator inputs or checked-in generated outputs, require the canonical generator
|
|
1825
1838
|
command, a stable post-generation diff, and validation executed after generation. Reject PASS if helper logic
|
|
1826
1839
|
exists only in a generated output, regeneration removes behavior, or validation predates the generated candidate.
|