sortie-dogs 0.10.18 → 0.10.19
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 +17 -74
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/goal-declaration-format.d.ts +13 -0
- package/dist/core/goal-declaration-format.js +68 -0
- package/dist/core/operator-proposal.d.ts +2 -0
- package/dist/core/operator-proposal.js +24 -2
- package/dist/core/operator-runtime.js +22 -1
- package/dist/plugin/index.d.ts +2 -0
- package/dist/plugin/index.js +13 -9
- package/dist/plugin/profiled.js +26 -10
- package/dist/plugin/receipt-presentation.d.ts +2 -0
- package/dist/plugin/receipt-presentation.js +9 -0
- package/dist/plugin/runtime-bridge.d.ts +1 -0
- package/dist/plugin/v2.d.ts +6 -3
- package/dist/plugin/v2.js +48 -39
- package/dist/runtime-assets-v010.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,8 +28,6 @@ implementation, validation, review, and model routing.
|
|
|
28
28
|
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) ·
|
|
29
29
|
[Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md)
|
|
30
30
|
|
|
31
|
-
Release: [v0.10.6](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.10.6)
|
|
32
|
-
|
|
33
31
|
> **Beta:** v0.10.x is under active stabilization. Runtime behavior,
|
|
34
32
|
> configuration, and generated assets may still change before 1.0.
|
|
35
33
|
|
|
@@ -85,78 +83,23 @@ The v0.10 profile is serial by design. The stable profile's Luna fabric and
|
|
|
85
83
|
parallel integration path are not exposed in this profile. More agents are not a
|
|
86
84
|
goal; preserving quality while reducing unnecessary expensive work is.
|
|
87
85
|
|
|
88
|
-
### SWE-bench
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
-
|
|
94
|
-
|
|
95
|
-
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
The pre-v0.10.6 local benchmark condition is closed historical evidence. New
|
|
106
|
-
development decisions use SWE-bench measurements collected under the frozen
|
|
107
|
-
contract in [Coding benchmark completion and correctness](docs/benchmark-completion-contract.md).
|
|
108
|
-
|
|
109
|
-
### v0.10.14 SWE-bench Lite dev23 baseline
|
|
110
|
-
|
|
111
|
-
The first complete v0.10.14 baseline was run against all 23 `dev` instances and
|
|
112
|
-
graded with the pinned official harness. It is a fixed candidate baseline, not a
|
|
113
|
-
leaderboard or general success-rate claim.
|
|
114
|
-
|
|
115
|
-
- Candidate: `sortie-dogs@0.10.14`, package SHA-256
|
|
116
|
-
`f2caf67268a5f69b67a252a0d84bfc5ad09db82684aa8c1e0f5c49f8d2986993`.
|
|
117
|
-
- Dataset: `princeton-nlp/SWE-bench_Lite`, revision
|
|
118
|
-
`6ec7bb89b9342f664a54a6e0a6ea6501d3437cc2`, split `dev`.
|
|
119
|
-
- Conditions: pass@1, `attempts_per_instance=1`, `retry_count=0`, per-instance
|
|
120
|
-
inference limit `$1.50`, four independent inference slots, and official
|
|
121
|
-
scoring with `max_workers=1`.
|
|
122
|
-
- Official score: **6/23 (26.1%)**. All 23 rows received an official result.
|
|
123
|
-
- Resolved instances: `marshmallow-code__marshmallow-1343`,
|
|
124
|
-
`marshmallow-code__marshmallow-1359`, `pydicom__pydicom-1256`,
|
|
125
|
-
`pydicom__pydicom-1694`, `pylint-dev__astroid-1333`, and
|
|
126
|
-
`sqlfluff__sqlfluff-1733`.
|
|
127
|
-
- Inference spend: `$15.7542788` under the separate `$50` v0.10.14 campaign.
|
|
128
|
-
- Environment deviations retained in the result: five `pvlib` rows hit the
|
|
129
|
-
NumPy 2.0 removal of `np.Inf`, and `pyvista__pyvista-4315` lacked
|
|
130
|
-
`libGL.so.1`. These rows remain in the denominator and were not silently
|
|
131
|
-
removed.
|
|
132
|
-
- Four non-successful inference terminals (`timeout` or `patch-index-failed`)
|
|
133
|
-
produced empty patches and are counted as unresolved official results.
|
|
134
|
-
|
|
135
|
-
This baseline is the comparison point for any later v0.10.15 candidate. A
|
|
136
|
-
later candidate must use its own package digest and preserve the same dataset,
|
|
137
|
-
official scoring boundary, and declared budget conditions before its score is
|
|
138
|
-
compared with this value.
|
|
139
|
-
|
|
140
|
-
## Historical local case study
|
|
141
|
-
|
|
142
|
-
These are completion-filtered local references on one frozen DeepSWE task,
|
|
143
|
-
`datacurve/anko-typed-variable-bindings`, collected from 2026-09-14 through
|
|
144
|
-
2026-09-18. They are not matched pairs or a leaderboard result.
|
|
145
|
-
|
|
146
|
-
- Bare OpenCode: Verified PASS `0/3`, F2P `3/27`, median agent wall `24.5 min`,
|
|
147
|
-
median completed-run API-equivalent cost `$3.53`.
|
|
148
|
-
- Sortie v0.9.12: Verified PASS `0/3`, F2P `23/27`, median wall `25.7 min`,
|
|
149
|
-
median completed-run cost `$2.85`; two additional interrupted attempts cost
|
|
150
|
-
an estimated `$6.20`.
|
|
151
|
-
- Sortie v0.10.3 one-shot: Verified PASS `0/1`, F2P `7/9`, wall `22.7 min`,
|
|
152
|
-
estimated cost `$3.79`.
|
|
153
|
-
- Sortie v0.10.5 one-shot: Verified PASS `1/1`, F2P `9/9`, P2P `94/94`, wall
|
|
154
|
-
`43.8 min`, estimated cost `$2.58`.
|
|
155
|
-
|
|
156
|
-
The v0.10.5 result is one verified success, not a success rate. Historical rate
|
|
157
|
-
schedules and endpoints differ, and the host-reported zero cost is not treated as
|
|
158
|
-
a bill. See [definitions, frozen inputs, and limitations](docs/benchmark-reference.md)
|
|
159
|
-
and [machine-readable values](docs/benchmarks/provisional-reference.json).
|
|
86
|
+
### SWE-bench evaluation
|
|
87
|
+
|
|
88
|
+
A frozen Sortie-dogs v0.10.14 build was evaluated on 23 fixed tasks.
|
|
89
|
+
|
|
90
|
+
- Official verifier PASS: 6 / 23
|
|
91
|
+
- Scored FAIL: 17 / 23
|
|
92
|
+
- Infrastructure blocked: 0 / 23
|
|
93
|
+
- Total estimated model cost: $15.75
|
|
94
|
+
- Median agent runtime: 15.2 min
|
|
95
|
+
|
|
96
|
+
The same Sortie-dogs candidate, model routes, budgets, tools, and verification
|
|
97
|
+
procedure were held fixed for all tasks. This is one frozen evaluation, not a
|
|
98
|
+
general success-rate claim.
|
|
99
|
+
|
|
100
|
+
Full methodology and per-task results: [benchmark details](docs/benchmark-v0.10.14-dev23.md)
|
|
101
|
+
|
|
102
|
+
Historical qualification references remain in [benchmark reference](docs/benchmark-reference.md).
|
|
160
103
|
|
|
161
104
|
## How v0.10.6 works
|
|
162
105
|
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
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.10.0-v0912-language4-cost-rpt10-compaction-ref2-proposal1-review-remediation1-surface4-proposal-recovery2-quality1-terminal2-route1-path1-permission2-prerequisite1-v2-bridge1-intent1-reflection1-cancel1-proposal-budget1-goal-recovery1-delegate-execute1-v2-depth1-process-replacement1-unstarted-v2-1";
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.10.0-v0912-language4-cost-rpt10-compaction-ref2-proposal1-review-remediation1-surface4-proposal-recovery2-quality1-terminal2-route1-path1-permission2-prerequisite1-v2-bridge1-intent1-reflection1-cancel1-proposal-budget1-goal-recovery1-delegate-execute1-v2-depth1-process-replacement1-unstarted-v2-1-cold-turn1-return-report-v2-2";
|
|
7
7
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
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.10.0-v0912-language4-cost-rpt10-compaction-ref2-proposal1-review-remediation1-surface4-proposal-recovery2-quality1-terminal2-route1-path1-permission2-prerequisite1-v2-bridge1-intent1-reflection1-cancel1-proposal-budget1-goal-recovery1-delegate-execute1-v2-depth1-process-replacement1-unstarted-v2-1";
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.10.0-v0912-language4-cost-rpt10-compaction-ref2-proposal1-review-remediation1-surface4-proposal-recovery2-quality1-terminal2-route1-path1-permission2-prerequisite1-v2-bridge1-intent1-reflection1-cancel1-proposal-budget1-goal-recovery1-delegate-execute1-v2-depth1-process-replacement1-unstarted-v2-1-cold-turn1-return-report-v2-2";
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
/** Shared planner guidance and admission enums; no inferred acceptance or dispatch authority. */
|
|
2
2
|
export declare const GOAL_DELIVERY_INTENTS: readonly ["design", "registration", "implementation", "repair", "controlled-change"];
|
|
3
3
|
export declare const GOAL_DELIVERY_MODES: readonly ["planning-only", "mvp-first", "repair-first", "controlled-change"];
|
|
4
|
+
export declare const GOAL_CRITERION_ENUMS: {
|
|
5
|
+
readonly build_boundary: readonly ["included", "excluded", "not-applicable"];
|
|
6
|
+
readonly proof_scope: readonly ["requested-full", "document-deliverable", "expected-negative"];
|
|
7
|
+
readonly expected_outcome: readonly ["pass", "fail"];
|
|
8
|
+
readonly source_binding: readonly ["declared", "current-protected"];
|
|
9
|
+
readonly candidate_binding: readonly ["declared", "current-protected"];
|
|
10
|
+
};
|
|
4
11
|
export declare function hasGoalCommandAliasConflict(values: Record<string, unknown>): boolean;
|
|
5
12
|
/** Shared root fields are defaults too. Explicit defaults and then per-criterion
|
|
6
13
|
* fields take precedence; no commands, identifiers or criteria are rewritten. */
|
|
7
14
|
export declare function goalDeclarationDefaults(object: Record<string, unknown>): Record<string, unknown>;
|
|
15
|
+
export interface GoalDeclarationFieldDiagnostic {
|
|
16
|
+
readonly pointer: string;
|
|
17
|
+
readonly expected: string;
|
|
18
|
+
}
|
|
19
|
+
/** Validate structured planner fields before immutable controls exist; expansion itself remains lossless. */
|
|
20
|
+
export declare function goalDeclarationFieldDiagnostics(object: Record<string, unknown>): readonly GoalDeclarationFieldDiagnostic[];
|
|
8
21
|
export declare const GOAL_DECLARATION_FORMAT: string;
|
|
9
22
|
/** Normalize a shared declaration without guessing acceptance, coverage, delivery, or validation. */
|
|
10
23
|
export declare function expandGoalDeclaration(value: unknown): string;
|
|
@@ -2,6 +2,13 @@ import { goalFingerprint } from "./goal-bound.js";
|
|
|
2
2
|
/** Shared planner guidance and admission enums; no inferred acceptance or dispatch authority. */
|
|
3
3
|
export const GOAL_DELIVERY_INTENTS = ["design", "registration", "implementation", "repair", "controlled-change"];
|
|
4
4
|
export const GOAL_DELIVERY_MODES = ["planning-only", "mvp-first", "repair-first", "controlled-change"];
|
|
5
|
+
export const GOAL_CRITERION_ENUMS = {
|
|
6
|
+
build_boundary: ["included", "excluded", "not-applicable"],
|
|
7
|
+
proof_scope: ["requested-full", "document-deliverable", "expected-negative"],
|
|
8
|
+
expected_outcome: ["pass", "fail"],
|
|
9
|
+
source_binding: ["declared", "current-protected"],
|
|
10
|
+
candidate_binding: ["declared", "current-protected"],
|
|
11
|
+
};
|
|
5
12
|
const CRITERION_FIELDS = ["criterion_id", "target", "entrypoint", "workload", "oracle_coverage", "build_boundary", "source",
|
|
6
13
|
"candidate", "source_binding", "candidate_binding", "fixture", "proof_scope", "expected_outcome", "validation_command"];
|
|
7
14
|
export function hasGoalCommandAliasConflict(values) {
|
|
@@ -23,6 +30,67 @@ export function goalDeclarationDefaults(object) {
|
|
|
23
30
|
? object.defaults : {};
|
|
24
31
|
return { ...normalizedCriterionFields(object), ...normalizedCriterionFields(defaults) };
|
|
25
32
|
}
|
|
33
|
+
/** Validate structured planner fields before immutable controls exist; expansion itself remains lossless. */
|
|
34
|
+
export function goalDeclarationFieldDiagnostics(object) {
|
|
35
|
+
const defects = new Map();
|
|
36
|
+
const add = (pointer, expected) => { defects.set(pointer, { pointer, expected }); };
|
|
37
|
+
const scalar = (value) => typeof value === "string" ? value.trim()
|
|
38
|
+
: typeof value === "boolean" || typeof value === "number" ? String(value) : undefined;
|
|
39
|
+
const singleLine = (value) => {
|
|
40
|
+
const text = scalar(value);
|
|
41
|
+
return text !== undefined && text.length > 0 && !/[\r\n]/u.test(String(value));
|
|
42
|
+
};
|
|
43
|
+
for (const [field, allowed, required] of [
|
|
44
|
+
["delivery_intent", GOAL_DELIVERY_INTENTS, true], ["delivery_mode", GOAL_DELIVERY_MODES, false],
|
|
45
|
+
["usable_path_established", ["true", "false"], true], ["controlled_change", ["true", "false"], true],
|
|
46
|
+
]) {
|
|
47
|
+
if ((required || object[field] !== undefined) && (!singleLine(object[field]) || !allowed.some(item => item === scalar(object[field])))) {
|
|
48
|
+
add(`/goal_declaration/${field}`, allowed.join(" | "));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (object.goal_acceptance_fingerprint !== undefined &&
|
|
52
|
+
!/^sha256:[a-f0-9]{64}$/u.test(String(object.goal_acceptance_fingerprint))) {
|
|
53
|
+
add("/goal_declaration/goal_acceptance_fingerprint", "sha256: followed by 64 lowercase hexadecimal characters");
|
|
54
|
+
}
|
|
55
|
+
const defaults = object.defaults !== null && typeof object.defaults === "object" && !Array.isArray(object.defaults)
|
|
56
|
+
? object.defaults : undefined;
|
|
57
|
+
const criteria = Array.isArray(object.criteria) ? object.criteria : [];
|
|
58
|
+
for (const [index, raw] of criteria.entries()) {
|
|
59
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
60
|
+
continue;
|
|
61
|
+
const criterion = raw;
|
|
62
|
+
const fieldValue = (field) => {
|
|
63
|
+
for (const [owner, prefix] of [[criterion, `/goal_declaration/criteria/${index}`],
|
|
64
|
+
[defaults, "/goal_declaration/defaults"], [object, "/goal_declaration"]]) {
|
|
65
|
+
if (!owner)
|
|
66
|
+
continue;
|
|
67
|
+
for (const key of [`goal_${field}`, field])
|
|
68
|
+
if (owner[key] !== undefined && owner[key] !== null) {
|
|
69
|
+
return { value: owner[key], pointer: `${prefix}/${key}` };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { value: undefined, pointer: `/goal_declaration/${defaults ? "defaults/" : ""}${field}` };
|
|
73
|
+
};
|
|
74
|
+
for (const field of ["target", "entrypoint", "workload", "source", "candidate", "fixture"]) {
|
|
75
|
+
const { value, pointer } = fieldValue(field);
|
|
76
|
+
if (!singleLine(value))
|
|
77
|
+
add(pointer, "nonblank single-line scalar");
|
|
78
|
+
}
|
|
79
|
+
for (const [field, allowed] of Object.entries(GOAL_CRITERION_ENUMS)) {
|
|
80
|
+
const { value, pointer } = fieldValue(field);
|
|
81
|
+
if (value === undefined && (field === "source_binding" || field === "candidate_binding"))
|
|
82
|
+
continue;
|
|
83
|
+
if (!singleLine(value) || !allowed.some(item => item === scalar(value)))
|
|
84
|
+
add(pointer, allowed.join(" | "));
|
|
85
|
+
}
|
|
86
|
+
const oracle = fieldValue("oracle_coverage");
|
|
87
|
+
if (!Array.isArray(oracle.value) || oracle.value.length === 0 ||
|
|
88
|
+
!oracle.value.every(item => typeof item === "string" && item.length > 0 && item.length <= 512) ||
|
|
89
|
+
new Set(oracle.value).size !== oracle.value.length)
|
|
90
|
+
add(oracle.pointer, "nonempty unique string array; each string has 1..512 characters");
|
|
91
|
+
}
|
|
92
|
+
return [...defects.values()];
|
|
93
|
+
}
|
|
26
94
|
export const GOAL_DECLARATION_FORMAT = `Declare a goal once, then reference it with goal_declaration_path in the Task prompt.
|
|
27
95
|
The referenced JSON may contain shared defaults and a criteria array; the host expands them privately.
|
|
28
96
|
An inline ext["sortie-dogs/goal-declaration"] in the registered handoff is also supported.
|
|
@@ -121,6 +121,8 @@ export declare class OperatorProposalRuntime {
|
|
|
121
121
|
private spendFile;
|
|
122
122
|
private readSpend;
|
|
123
123
|
private saveSpend;
|
|
124
|
+
/** A released investigation still belongs to its accepted order across a host restart. No grant is revived. */
|
|
125
|
+
continuationCheckpoint(root: string): Promise<string | undefined>;
|
|
124
126
|
read(root: string): Promise<OperatorProposalState | undefined>;
|
|
125
127
|
private readUnlocked;
|
|
126
128
|
private save;
|
|
@@ -324,6 +324,13 @@ export class OperatorProposalRuntime {
|
|
|
324
324
|
!Number.isSafeInteger(spend.reads) || spend.reads < 0 || !Number.isSafeInteger(spend.submissions) || spend.submissions < 0) {
|
|
325
325
|
throw new Error("operator-proposal-spend-invalid");
|
|
326
326
|
}
|
|
327
|
+
if (spend.retry !== undefined && (!record(spend.retry) || !/^[a-f0-9]{64}$/u.test(spend.retry.intent_hash) ||
|
|
328
|
+
!record(spend.retry.goal_binding) || !text(spend.retry.goal_binding.goal_id) ||
|
|
329
|
+
!Number.isSafeInteger(spend.retry.goal_binding.revision) || spend.retry.goal_binding.revision < 1 ||
|
|
330
|
+
!Number.isSafeInteger(spend.retry.goal_binding.scope_epoch) || spend.retry.goal_binding.scope_epoch < 1 ||
|
|
331
|
+
!/^sha256:[a-f0-9]{64}$/u.test(spend.retry.goal_binding.acceptance_fingerprint))) {
|
|
332
|
+
throw new Error("operator-proposal-spend-invalid");
|
|
333
|
+
}
|
|
327
334
|
return spend;
|
|
328
335
|
}
|
|
329
336
|
async saveSpend(spend) {
|
|
@@ -338,6 +345,19 @@ export class OperatorProposalRuntime {
|
|
|
338
345
|
await rm(temporary, { force: true }).catch(() => undefined);
|
|
339
346
|
}
|
|
340
347
|
}
|
|
348
|
+
/** A released investigation still belongs to its accepted order across a host restart. No grant is revived. */
|
|
349
|
+
async continuationCheckpoint(root) {
|
|
350
|
+
return this.serial(root, async () => {
|
|
351
|
+
const state = await this.readUnlocked(root);
|
|
352
|
+
if (state?.phase === "approved")
|
|
353
|
+
return undefined;
|
|
354
|
+
const retry = state?.goal_binding ? { intent_hash: state.intent_hash, goal_binding: state.goal_binding }
|
|
355
|
+
: state ? undefined : (await this.readSpend(root)).retry;
|
|
356
|
+
return retry === undefined ? undefined : JSON.stringify({ authority: "durable-proposal-continuity",
|
|
357
|
+
root_session_id: root, ...retry, status: state?.phase ?? "cancelled",
|
|
358
|
+
next_action: "Preserve the accepted goal and cumulative spend. Read operator_status; a cancelled proposal requires an explicit root retry decision and a new exact proposal Task, never revival of its old child." });
|
|
359
|
+
});
|
|
360
|
+
}
|
|
341
361
|
async read(root) {
|
|
342
362
|
return this.serial(root, () => this.readUnlocked(root));
|
|
343
363
|
}
|
|
@@ -489,7 +509,8 @@ export class OperatorProposalRuntime {
|
|
|
489
509
|
const spend = await this.readSpend(root);
|
|
490
510
|
const state = { schema_version: "0.1", profile: this.profile.id, root_session_id: root,
|
|
491
511
|
intent_id: `intent-${intentHash.slice(0, 24)}`, intent_hash: intentHash, intent, created_at: new Date().toISOString(), phase: "investigating",
|
|
492
|
-
goal_binding:
|
|
512
|
+
goal_binding: spend.retry?.intent_hash === intentHash ? structuredClone(spend.retry.goal_binding) : null,
|
|
513
|
+
proposal_call_id: null, proposal_session_id: null, read_count: spend.reads, read_paths: [], submission_count: spend.submissions, proposal_id: null,
|
|
493
514
|
...(spend.reads > 0 || spend.submissions > 0 ? { prior_spend: { reads: spend.reads, submissions: spend.submissions } } : {}),
|
|
494
515
|
proposal_revision: null, proposal_hash: null, proposal: null, approval_rationale: null };
|
|
495
516
|
this.assertBudgetAvailable(state);
|
|
@@ -647,7 +668,8 @@ export class OperatorProposalRuntime {
|
|
|
647
668
|
if (!state || state.phase === "approved")
|
|
648
669
|
return undefined;
|
|
649
670
|
await this.saveSpend({ schema_version: "0.1", profile: this.profile.id, root_session_id: root,
|
|
650
|
-
reads: state.read_count, submissions: state.submission_count
|
|
671
|
+
reads: state.read_count, submissions: state.submission_count,
|
|
672
|
+
...(state.goal_binding === null ? {} : { retry: { intent_hash: state.intent_hash, goal_binding: state.goal_binding } }) });
|
|
651
673
|
await rm(this.file(root), { force: true });
|
|
652
674
|
this.states.delete(root);
|
|
653
675
|
return state;
|
|
@@ -4,7 +4,7 @@ import { lstat, mkdir, open, readFile, realpath, rename, rm, unlink, writeFile }
|
|
|
4
4
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { acceptanceContinuityFingerprint, inspectAcceptanceContinuity, normalizeAcceptanceCriteria, ACCEPTANCE_CONTINUITY_EXTENSION, MAX_ACCEPTANCE_CONTINUITY_BYTES, MAX_ACCEPTANCE_CRITERIA } from "./acceptance-continuity.js";
|
|
7
|
-
import { expandGoalDeclaration, goalDeclarationDefaults, hasGoalCommandAliasConflict } from "./goal-declaration-format.js";
|
|
7
|
+
import { expandGoalDeclaration, goalDeclarationDefaults, goalDeclarationFieldDiagnostics, hasGoalCommandAliasConflict } from "./goal-declaration-format.js";
|
|
8
8
|
import { normalizeManifestPath, normalizeRelativePath } from "./path.js";
|
|
9
9
|
import { CONTRACT_TEXT_LIMITS, validateHandoffSchema, validateOperationManifestSchema } from "./validate-schema.js";
|
|
10
10
|
import { validateManifest } from "./validate-manifest.js";
|
|
@@ -235,6 +235,12 @@ export function parseOperatorPlan(value) {
|
|
|
235
235
|
return planError("/git_lifecycle/post_commit_validation", "operator-git-post-commit-validation-invalid", "canonical-declared-goal-validations-must-be-exclusive-contiguous-final-unit-suffix", "repair-proof-mapping");
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
|
+
const declarationDefects = goalDeclarationFieldDiagnostics(value.goal_declaration);
|
|
239
|
+
if (declarationDefects.length)
|
|
240
|
+
throw new OperatorContractError(declarationDefects.map(defect => ({
|
|
241
|
+
document: "plan", pointer: defect.pointer, code: "operator-goal-field-invalid", rule: "declared-goal-field",
|
|
242
|
+
repair_kind: "repair-field", repair_paths: [defect.pointer], expected: defect.expected,
|
|
243
|
+
})));
|
|
238
244
|
expandGoalDeclaration(value.goal_declaration);
|
|
239
245
|
return { ...value, acceptance: normalizeAcceptanceCriteria(value.acceptance) };
|
|
240
246
|
}
|
|
@@ -574,6 +580,21 @@ export class OperatorRuntime {
|
|
|
574
580
|
if (!record(patch) || !exactKeys(patch, ["op", "path", "value"]) || !["replace", "add"].includes(String(patch.op)) || typeof patch.path !== "string" || !Object.hasOwn(patch, "value")) {
|
|
575
581
|
throw new Error("operator-repair-invalid");
|
|
576
582
|
}
|
|
583
|
+
const goalFieldPath = /^\/goal_declaration\/(delivery_intent|delivery_mode|usable_path_established|controlled_change|goal_acceptance_fingerprint)$/.test(patch.path) ||
|
|
584
|
+
/^\/goal_declaration\/(?:defaults\/|criteria\/(?:0|[1-9]\d*)\/)?(?:goal_)?(?:target|entrypoint|workload|source|candidate|fixture|oracle_coverage|build_boundary|proof_scope|expected_outcome|source_binding|candidate_binding)$/.test(patch.path);
|
|
585
|
+
if (goalFieldPath && Array.isArray(draft.diagnostics) && draft.diagnostics.some((item) => item.document === "plan" && item.pointer === patch.path && item.code === "operator-goal-field-invalid")) {
|
|
586
|
+
// Only the exact invalid or missing field is editable; acceptance, valid declaration fields,
|
|
587
|
+
// budgets and scope never become mutable just because another field was diagnosed.
|
|
588
|
+
const keys = patch.path.slice(1).split("/");
|
|
589
|
+
let owner = draft.plan;
|
|
590
|
+
for (const key of keys.slice(0, -1))
|
|
591
|
+
owner = owner?.[key];
|
|
592
|
+
const key = keys.at(-1);
|
|
593
|
+
if (!record(owner) || (patch.op === "replace" && !Object.hasOwn(owner, key)))
|
|
594
|
+
throw new Error("operator-repair-path-forbidden");
|
|
595
|
+
owner[key] = patch.value;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
577
598
|
const readMatch = /^\/units\/(0|[1-9]\d*)\/read\/(0|[1-9]\d*)$/.exec(patch.path);
|
|
578
599
|
if (readMatch) {
|
|
579
600
|
const unitIndex = Number(readMatch[1]), pathIndex = Number(readMatch[2]);
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface OpenCodePluginInput {
|
|
|
13
13
|
childLifecycleCheckWaitMs?: number;
|
|
14
14
|
/** Installed-profile adapter only; never supplied by a model or project configuration. */
|
|
15
15
|
runtimeBridge?: RuntimeBridge;
|
|
16
|
+
/** V2 has no assistant-part update API; return the canonical panel with explicit acceptance. */
|
|
17
|
+
returnReportTransport?: "tool-result";
|
|
16
18
|
/** The host SDK client. Absent in hosts that construct the plugin without one. */
|
|
17
19
|
client?: SessionMessageReader & RunMetricsClient & ContinuationClient & OpenCodeModelAvailabilityClient & {
|
|
18
20
|
app?: {
|
package/dist/plugin/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
10
10
|
import { lstat, mkdir, open, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
11
11
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
12
12
|
import { RUNTIME_ASSET_VERSION } from "../asset-version.js";
|
|
13
|
-
import { GOAL_DECLARATION_FORMAT, GOAL_DELIVERY_INTENTS, GOAL_DELIVERY_MODES, expandGoalDeclaration } from "../core/goal-declaration-format.js";
|
|
13
|
+
import { GOAL_DECLARATION_FORMAT, GOAL_DELIVERY_INTENTS, GOAL_DELIVERY_MODES, GOAL_CRITERION_ENUMS, expandGoalDeclaration } from "../core/goal-declaration-format.js";
|
|
14
14
|
import { ACCEPTANCE_CONTINUITY_AUTHORITY, ACCEPTANCE_CONTINUITY_EXTENSION, ACCEPTANCE_CONTINUITY_SCHEMA_VERSION, acceptanceContinuityFingerprint, inspectAcceptanceContinuity, normalizeAcceptanceCriteria, } from "../core/acceptance-continuity.js";
|
|
15
15
|
import { resolveGlobalConfigRoot } from "../core/initialize.js";
|
|
16
16
|
import { admitLunaFabric } from "../core/luna-fabric-contract.js";
|
|
@@ -1770,11 +1770,14 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1770
1770
|
origin_user_message_id: ticket.origin_user_message_id,
|
|
1771
1771
|
} } };
|
|
1772
1772
|
}
|
|
1773
|
-
async function terminalGoal(sessionID, stopReason, status) {
|
|
1773
|
+
async function terminalGoal(sessionID, stopReason, status, explicitAcceptance = false) {
|
|
1774
1774
|
const ledger = await goalLedger(sessionID);
|
|
1775
1775
|
const state = (await ledger.readGoal()).state;
|
|
1776
1776
|
if (state.goal_id === null || state.receipt !== null || state.outstanding_reservations.length > 0 || state.acceptance_fingerprint === null)
|
|
1777
1777
|
return state.receipt ?? undefined;
|
|
1778
|
+
if (status === "succeeded" && !explicitAcceptance &&
|
|
1779
|
+
await input.runtimeBridge?.requiresExplicitAcceptance?.(sessionID) === true)
|
|
1780
|
+
return undefined;
|
|
1778
1781
|
const records = (await ledger.readGoal()).records;
|
|
1779
1782
|
const settledEvidence = records.flatMap(({ event }) => event.kind === "unit.settled" || event.kind === "unit.evidence-reconciled" ? event.evidence : []);
|
|
1780
1783
|
if (status === "succeeded" && (state.acceptance_contract === null ||
|
|
@@ -1857,7 +1860,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1857
1860
|
if (receipt === undefined && delivery === "failed") {
|
|
1858
1861
|
receipt = await terminalGoal(sessionID, "stopped", "stopped").catch(() => undefined);
|
|
1859
1862
|
}
|
|
1860
|
-
else if (receipt === undefined && proved
|
|
1863
|
+
else if (receipt === undefined && proved &&
|
|
1864
|
+
await input.runtimeBridge?.requiresExplicitAcceptance?.(sessionID) !== true) {
|
|
1861
1865
|
receipt = await terminalGoal(sessionID, "completed", "succeeded").catch(() => undefined);
|
|
1862
1866
|
}
|
|
1863
1867
|
else if (receipt === undefined && outcome === "INTERRUPTED") {
|
|
@@ -1934,19 +1938,19 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1934
1938
|
const expectedOutcome = values.goal_expected_outcome;
|
|
1935
1939
|
const sourceBinding = handoffValue(entries, ["goal_source_binding"]);
|
|
1936
1940
|
const candidateBinding = handoffValue(entries, ["goal_candidate_binding"]);
|
|
1937
|
-
if (buildBoundary !== undefined &&
|
|
1941
|
+
if (buildBoundary !== undefined && !GOAL_CRITERION_ENUMS.build_boundary.some(value => value === buildBoundary)) {
|
|
1938
1942
|
defects.push(contractDefect("contract", pointer("goal_build_boundary"), "goal_build_boundary_invalid"));
|
|
1939
1943
|
}
|
|
1940
|
-
if (proofScope !== undefined &&
|
|
1944
|
+
if (proofScope !== undefined && !GOAL_CRITERION_ENUMS.proof_scope.some(value => value === proofScope)) {
|
|
1941
1945
|
defects.push(contractDefect("contract", pointer("goal_proof_scope"), "goal_proof_scope_invalid"));
|
|
1942
1946
|
}
|
|
1943
|
-
if (expectedOutcome !== undefined &&
|
|
1947
|
+
if (expectedOutcome !== undefined && !GOAL_CRITERION_ENUMS.expected_outcome.some(value => value === expectedOutcome)) {
|
|
1944
1948
|
defects.push(contractDefect("contract", pointer("goal_expected_outcome"), "goal_expected_outcome_invalid"));
|
|
1945
1949
|
}
|
|
1946
|
-
if (sourceBinding !== undefined &&
|
|
1950
|
+
if (sourceBinding !== undefined && !GOAL_CRITERION_ENUMS.source_binding.some(value => value === sourceBinding)) {
|
|
1947
1951
|
defects.push(contractDefect("contract", pointer("goal_source_binding"), "goal_source_binding_invalid"));
|
|
1948
1952
|
}
|
|
1949
|
-
if (candidateBinding !== undefined &&
|
|
1953
|
+
if (candidateBinding !== undefined && !GOAL_CRITERION_ENUMS.candidate_binding.some(value => value === candidateBinding)) {
|
|
1950
1954
|
defects.push(contractDefect("contract", pointer("goal_candidate_binding"), "goal_candidate_binding_invalid"));
|
|
1951
1955
|
}
|
|
1952
1956
|
const declaredValidation = handoffValue(entries, ["validation"]);
|
|
@@ -7721,7 +7725,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
7721
7725
|
throw new Error("operator-goal-stopped");
|
|
7722
7726
|
// Same proof/freshness/reservation gate used by terminal text, requested
|
|
7723
7727
|
// explicitly by the root instead of inferred from a model's wording.
|
|
7724
|
-
const receipt = goal.receipt ?? await terminalGoal(sessionID, "completed", "succeeded");
|
|
7728
|
+
const receipt = goal.receipt ?? await terminalGoal(sessionID, "completed", "succeeded", true);
|
|
7725
7729
|
if (receipt?.status !== "succeeded")
|
|
7726
7730
|
return { status: "awaiting-evidence" };
|
|
7727
7731
|
// Acceptance must return its receipt to the active tool call. Revoking
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -8,7 +8,7 @@ import { relative, resolve, sep } from "node:path";
|
|
|
8
8
|
import { readFile, realpath } from "node:fs/promises";
|
|
9
9
|
import { BUILT_IN_MODEL_CATALOG } from "./model-routing.js";
|
|
10
10
|
import { goalFingerprint } from "../core/goal-bound.js";
|
|
11
|
-
import { decoratePreviewHeadings } from "./receipt-presentation.js";
|
|
11
|
+
import { decoratePreviewHeadings, returnReportPanel } from "./receipt-presentation.js";
|
|
12
12
|
import { sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
|
|
13
13
|
import { normalizeCommand } from "./gate.js";
|
|
14
14
|
import { normalizeRelativePath } from "../core/path.js";
|
|
@@ -266,7 +266,11 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
266
266
|
});
|
|
267
267
|
const runtimeBridge = {
|
|
268
268
|
profile, assetVersion,
|
|
269
|
-
continuationCheckpoint: root => operators.continuationCheckpoint(root),
|
|
269
|
+
continuationCheckpoint: async (root) => await operators.continuationCheckpoint(root) ?? await proposals.continuationCheckpoint(root),
|
|
270
|
+
requiresExplicitAcceptance: async (root) => {
|
|
271
|
+
const state = await operators.read(root);
|
|
272
|
+
return state !== undefined && state.phase !== "completed" && operatorTurnLifecycle.get(root) !== "historical";
|
|
273
|
+
},
|
|
270
274
|
ownsCanonicalValidation: async (root, taskID, child, command) => {
|
|
271
275
|
const state = await operators.read(root);
|
|
272
276
|
if (state === undefined || state.phase === "cancelled" || state.phase === "completed")
|
|
@@ -486,7 +490,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
486
490
|
await registerPreparedGoal(context.sessionID, state);
|
|
487
491
|
return preparedTask(state);
|
|
488
492
|
} };
|
|
489
|
-
tools[repair] = { description: "Repair a root-owned draft with named fields, git_lifecycle branch/start/message fields, or an already-declared criterion command. A diagnosed /units/i/read/j scope error may be replaced with a normalized repository-relative spelling of the same resource only: absolute inputs require an existing relative alias whose realpath is identical; read expansion, redirection, whole-array replacement and write-scope repair are forbidden. Empty patches revalidate the saved draft without resending it. Acceptance, unit count and write scope remain fixed.",
|
|
493
|
+
tools[repair] = { description: "Repair a root-owned draft with named fields, git_lifecycle branch/start/message fields, or an already-declared criterion command. A diagnosed operator-goal-field-invalid may repair only its exact invalid or missing goal declaration field using the reported expected values; valid fields, budgets and whole declaration replacement remain forbidden. A diagnosed /units/i/read/j scope error may be replaced with a normalized repository-relative spelling of the same resource only: absolute inputs require an existing relative alias whose realpath is identical; read expansion, redirection, whole-array replacement and write-scope repair are forbidden. Empty patches revalidate the saved draft without resending it. Acceptance, unit count and write scope remain fixed.",
|
|
490
494
|
args: { draft_id: stringSchema, patches_json: stringSchema }, execute: async (args, context) => {
|
|
491
495
|
await requireRoot(context.sessionID);
|
|
492
496
|
let patches;
|
|
@@ -654,16 +658,27 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
654
658
|
if (!["awaiting-acceptance", "completed"].includes(state.phase) || state.units.some(unit => unit.status !== "succeeded")) {
|
|
655
659
|
return JSON.stringify({ status: "not-ready", packet: operators.packet(state) });
|
|
656
660
|
}
|
|
657
|
-
const
|
|
661
|
+
const declarationFingerprint = await operators.completionGoalFingerprint(state);
|
|
658
662
|
if (state.phase === "awaiting-acceptance") {
|
|
659
|
-
await relinkRegisteredGoal(context.sessionID, state,
|
|
660
|
-
await control.assertActiveGoal(context.sessionID,
|
|
663
|
+
await relinkRegisteredGoal(context.sessionID, state, declarationFingerprint);
|
|
664
|
+
await control.assertActiveGoal(context.sessionID, declarationFingerprint);
|
|
661
665
|
}
|
|
662
|
-
const result = await control.completeRoot(context.sessionID,
|
|
666
|
+
const result = await control.completeRoot(context.sessionID, declarationFingerprint);
|
|
663
667
|
if (result.receipt)
|
|
664
668
|
await operators.terminal(context.sessionID, result.receipt);
|
|
669
|
+
let panel;
|
|
670
|
+
if (input.returnReportTransport === "tool-result" && result.receipt?.status === "succeeded") {
|
|
671
|
+
const text = `✅ **DONE** \`${state.runID}\` — ${state.acceptance.length} acceptance requirements verified.\n\n` +
|
|
672
|
+
`**変更点:** ${state.units.map(unit => unit.unit.title).join("; ")}\n\n` +
|
|
673
|
+
`**確認結果:** PASS — ${[...new Set(state.units.flatMap(unit => unit.unit.validation))].join("; ")}\n\n**次:** なし`;
|
|
674
|
+
const rendered = await control.renderReturnReport(context.sessionID, text, goalFingerprint(result.receipt)).catch(() => undefined);
|
|
675
|
+
if (rendered)
|
|
676
|
+
panel = returnReportPanel(rendered);
|
|
677
|
+
}
|
|
665
678
|
return JSON.stringify({ status: result.status, run_id: state.runID,
|
|
666
|
-
acceptance_fingerprint: state.acceptanceFingerprint, receipt: result.receipt ?? null
|
|
679
|
+
acceptance_fingerprint: state.acceptanceFingerprint, receipt: result.receipt ?? null,
|
|
680
|
+
...(panel ? { return_report: panel,
|
|
681
|
+
return_report_instruction: "Append return_report verbatim exactly once to your final answer as Markdown, outside any code fence. It is a host-authored receipt display, not a new task or input. Do not summarize, recalculate or request another model turn for it." } : {}) });
|
|
667
682
|
} };
|
|
668
683
|
tools[resume] = { description: "Reconcile a finished process-defect unit from native host validation records and unchanged source. For the exact first validation-only contract-repair continuation process defect, unchanged durable controls/candidate and available validation budget permit one same-worker validation-only retry; the result includes the exact Task to invoke. Never reimplements a unit or resets implementation/goal spend.",
|
|
669
684
|
args: { run_id: stringSchema, acceptance_fingerprint: stringSchema }, execute: async (args, context) => {
|
|
@@ -978,8 +993,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
978
993
|
output.parts.some(part => record(part) && part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0);
|
|
979
994
|
if (realTurn) {
|
|
980
995
|
const state = await operators.read(chat.sessionID);
|
|
981
|
-
if ((state !== undefined && (state.phase === "cancelled" || state.phase === "completed")) ||
|
|
982
|
-
operatorTurnLifecycle.get(chat.sessionID) === "cancelled")
|
|
996
|
+
if (((state !== undefined && (state.phase === "cancelled" || state.phase === "completed")) ||
|
|
997
|
+
operatorTurnLifecycle.get(chat.sessionID) === "cancelled") &&
|
|
998
|
+
await runtimeBridge.continuationCheckpoint(chat.sessionID) === undefined) {
|
|
983
999
|
operatorTurnLifecycle.set(chat.sessionID, "historical");
|
|
984
1000
|
const messageID = typeof chat.messageID === "string" && chat.messageID.length > 0
|
|
985
1001
|
? chat.messageID
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { GoalTerminalReceipt } from "../core/goal-bound.js";
|
|
2
|
+
/** Extract only the host card, excluding the terminal prose that follows it. */
|
|
3
|
+
export declare function returnReportPanel(rendered: string): string | undefined;
|
|
2
4
|
/** Cosmetic headings only; no acceptance or execution state is inferred. */
|
|
3
5
|
export declare function decoratePreviewHeadings(text: string): string;
|
|
4
6
|
/** Only a host-verified current receipt may supply a missing success heading. */
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { terminalRunOutcome } from "./run-metrics.js";
|
|
2
|
+
/** Extract only the host card, excluding the terminal prose that follows it. */
|
|
3
|
+
export function returnReportPanel(rendered) {
|
|
4
|
+
const summary = rendered.indexOf("<summary><strong>🐾 SORTIE DOGS — 帰還報告");
|
|
5
|
+
if (summary < 0)
|
|
6
|
+
return undefined;
|
|
7
|
+
const start = rendered.lastIndexOf("<details", summary);
|
|
8
|
+
const end = rendered.indexOf("\n</details>", summary);
|
|
9
|
+
return start < 0 || end < 0 ? undefined : rendered.slice(start, end + "\n</details>".length).trim();
|
|
10
|
+
}
|
|
2
11
|
/** Cosmetic headings only; no acceptance or execution state is inferred. */
|
|
3
12
|
export function decoratePreviewHeadings(text) {
|
|
4
13
|
const icons = { "変更点": "🔧", "確認結果": "🔍", "次": "➡️", changes: "🔧", validation: "🔍", next: "➡️" };
|
|
@@ -25,6 +25,7 @@ export interface RuntimeBridge {
|
|
|
25
25
|
readonly defaultModelCatalog?: import("./model-routing.js").ModelCatalog;
|
|
26
26
|
transformConfiguration?(value: unknown): unknown;
|
|
27
27
|
continuationCheckpoint?(rootSessionID: string): Promise<string | undefined>;
|
|
28
|
+
requiresExplicitAcceptance?(rootSessionID: string): Promise<boolean>;
|
|
28
29
|
ownsCanonicalValidation?(rootSessionID: string, unitID: string, childSessionID: string, command: string): Promise<boolean>;
|
|
29
30
|
onSerialSettlement?(settlement: SerialDispatchSettlement): Promise<void>;
|
|
30
31
|
onRootTerminal?(rootSessionID: string, receipt: GoalTerminalReceipt): Promise<void>;
|
package/dist/plugin/v2.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { OpenCodeHooks, OpenCodePlugin } from "./index.js";
|
|
2
|
-
export declare const V2_RETURN_REPORT_METADATA_KEY = "sortie-dogs/return-report";
|
|
3
2
|
type JsonObject = Record<string, unknown>;
|
|
4
3
|
type Registration = {
|
|
5
4
|
dispose(): Promise<void> | void;
|
|
@@ -33,6 +32,7 @@ export interface OpenCodeV2Context {
|
|
|
33
32
|
hook(name: "execute.before" | "execute.after", callback: (event: JsonObject) => Promise<void> | void): Promise<Registration>;
|
|
34
33
|
};
|
|
35
34
|
readonly session: {
|
|
35
|
+
list?(input: JsonObject): Promise<unknown>;
|
|
36
36
|
get(input: {
|
|
37
37
|
sessionID: string;
|
|
38
38
|
}): Promise<unknown>;
|
|
@@ -46,6 +46,9 @@ export interface OpenCodeV2Context {
|
|
|
46
46
|
switchModel(input: JsonObject): Promise<unknown>;
|
|
47
47
|
hook(name: "prompt" | "context" | "compaction", callback: (event: JsonObject) => Promise<void> | void): Promise<Registration>;
|
|
48
48
|
};
|
|
49
|
+
readonly message?: {
|
|
50
|
+
list(input: JsonObject): Promise<unknown>;
|
|
51
|
+
};
|
|
49
52
|
readonly permission: {
|
|
50
53
|
hook(name: "evaluate", callback: (event: JsonObject) => Promise<void> | void): Promise<Registration>;
|
|
51
54
|
};
|
|
@@ -60,8 +63,8 @@ export interface OpenCodeV2Plugin {
|
|
|
60
63
|
readonly id: string;
|
|
61
64
|
setup(context: OpenCodeV2Context): Promise<(() => void) | void>;
|
|
62
65
|
}
|
|
63
|
-
/**
|
|
64
|
-
export declare function
|
|
66
|
+
/** Observe terminal text for accounting only. Synthetic input is queued work, even with resume=false. */
|
|
67
|
+
export declare function createV2ReturnReportFinalizer(context: OpenCodeV2Context, hooks: OpenCodeHooks): (session: string) => Promise<void>;
|
|
65
68
|
export declare function createSortieDogsV2Plugin(legacyFactory?: OpenCodePlugin): OpenCodeV2Plugin;
|
|
66
69
|
export declare const SortieDogsV2Plugin: OpenCodeV2Plugin;
|
|
67
70
|
export default SortieDogsV2Plugin;
|
package/dist/plugin/v2.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { SortieDogsV010Plugin } from "./profiled.js";
|
|
3
|
-
const RETURN_REPORT_MARKER = "<summary><strong>🐾 SORTIE DOGS — 帰還報告";
|
|
4
|
-
export const V2_RETURN_REPORT_METADATA_KEY = "sortie-dogs/return-report";
|
|
5
2
|
function record(value) {
|
|
6
3
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
4
|
}
|
|
@@ -49,8 +46,17 @@ function legacyPart(value, session, message) {
|
|
|
49
46
|
return { ...value, sessionID: session, messageID: message };
|
|
50
47
|
const state = record(value.state) ? value.state : {};
|
|
51
48
|
const output = toolContentText(state.content);
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const name = string(value.name) ?? string(value.tool);
|
|
50
|
+
const tool = name === "subagent" ? "task" : name === "shell" ? "bash" : name === "patch" ? "apply_patch" : name;
|
|
51
|
+
const time = record(value.time) ? value.time : undefined;
|
|
52
|
+
const metadata = record(state.metadata) ? { ...state.metadata } : undefined;
|
|
53
|
+
if (metadata && Array.isArray(metadata.files))
|
|
54
|
+
metadata.files = metadata.files.map(file => record(file) && typeof file.patch === "string"
|
|
55
|
+
? { ...file, diff: file.patch } : file);
|
|
56
|
+
return { ...value, tool, callID: string(value.id), sessionID: session, messageID: message,
|
|
57
|
+
state: { ...state, input: legacyToolInput(name, state.input), ...(metadata ? { metadata } : {}),
|
|
58
|
+
...(time ? { time: { start: time.ran ?? time.created, end: time.completed } } : {}),
|
|
59
|
+
...(output === undefined ? {} : { output }) } };
|
|
54
60
|
}
|
|
55
61
|
function legacyMessage(value, session, agent) {
|
|
56
62
|
if (!record(value) || typeof value.id !== "string" || typeof value.type !== "string")
|
|
@@ -62,13 +68,15 @@ function legacyMessage(value, session, agent) {
|
|
|
62
68
|
}
|
|
63
69
|
if (value.type !== "assistant") {
|
|
64
70
|
if (value.type === "synthetic")
|
|
65
|
-
return { info: { id, sessionID: session, role: "
|
|
71
|
+
return { info: { id, sessionID: session, role: "user", agent, time: value.time },
|
|
66
72
|
parts: [{ id: `${id}-text`, sessionID: session, messageID: id, type: "text", text: String(value.text ?? ""), synthetic: true }] };
|
|
67
73
|
return undefined;
|
|
68
74
|
}
|
|
69
75
|
const model = modelReference(value.model);
|
|
70
76
|
return { info: { id, sessionID: session, role: "assistant", agent: string(value.agent) ?? agent,
|
|
71
|
-
...(model === undefined ? {} : { model
|
|
77
|
+
...(model === undefined ? {} : { model, providerID: model.providerID, modelID: model.modelID }),
|
|
78
|
+
...(record(value.providerState) && typeof value.providerState.serviceTier === "string" ? { serviceTier: value.providerState.serviceTier } : {}),
|
|
79
|
+
...(value.error === undefined ? {} : { error: value.error }), finish: value.finish, time: value.time, tokens: value.tokens, cost: value.cost },
|
|
72
80
|
parts: array(value.content).flatMap(part => {
|
|
73
81
|
const converted = legacyPart(part, session, id);
|
|
74
82
|
return converted === undefined ? [] : [converted];
|
|
@@ -77,7 +85,8 @@ function legacyMessage(value, session, agent) {
|
|
|
77
85
|
async function legacyMessages(context, id) {
|
|
78
86
|
const [info, history] = await Promise.all([
|
|
79
87
|
context.session.get({ sessionID: id }).catch(() => undefined),
|
|
80
|
-
context.
|
|
88
|
+
context.message ? nativePages(cursor => context.message.list({ sessionID: id, limit: 100, ...(cursor ? { cursor } : { order: "asc" }) }))
|
|
89
|
+
: context.session.context({ sessionID: id }),
|
|
81
90
|
]);
|
|
82
91
|
const agent = record(info) ? string(info.agent) : undefined;
|
|
83
92
|
return array(history).flatMap(value => {
|
|
@@ -85,6 +94,24 @@ async function legacyMessages(context, id) {
|
|
|
85
94
|
return converted === undefined ? [] : [converted];
|
|
86
95
|
});
|
|
87
96
|
}
|
|
97
|
+
async function nativePages(fetch) {
|
|
98
|
+
const items = [], seen = new Set();
|
|
99
|
+
let cursor;
|
|
100
|
+
for (let page = 0; page < 100; page += 1) {
|
|
101
|
+
const result = await fetch(cursor);
|
|
102
|
+
if (!record(result) || !Array.isArray(result.data) || !record(result.cursor))
|
|
103
|
+
throw new Error("v2-history-page-invalid");
|
|
104
|
+
items.push(...result.data);
|
|
105
|
+
const next = string(result.cursor.next);
|
|
106
|
+
if (!next)
|
|
107
|
+
return items;
|
|
108
|
+
if (seen.has(next))
|
|
109
|
+
throw new Error("v2-history-cursor-repeated");
|
|
110
|
+
seen.add(next);
|
|
111
|
+
cursor = next;
|
|
112
|
+
}
|
|
113
|
+
throw new Error("v2-history-page-limit");
|
|
114
|
+
}
|
|
88
115
|
function legacyClient(context) {
|
|
89
116
|
const directory = context.location.directory;
|
|
90
117
|
const session = {
|
|
@@ -101,7 +128,13 @@ function legacyClient(context) {
|
|
|
101
128
|
return { data: id === undefined || wanted === undefined ? undefined
|
|
102
129
|
: (await legacyMessages(context, id)).find(message => record(message.info) && message.info.id === wanted) };
|
|
103
130
|
},
|
|
104
|
-
children: async () =>
|
|
131
|
+
...(context.session.list ? { children: async (request) => {
|
|
132
|
+
const id = sessionID(request);
|
|
133
|
+
if (id === undefined)
|
|
134
|
+
throw new Error("v2-history-parent-missing");
|
|
135
|
+
return { data: await nativePages(cursor => context.session.list({ parentID: id, limit: 100,
|
|
136
|
+
...(cursor ? { cursor } : { order: "asc" }) })) };
|
|
137
|
+
} } : {}),
|
|
105
138
|
abort: async (request) => {
|
|
106
139
|
const id = sessionID(request);
|
|
107
140
|
return id === undefined ? undefined : await context.session.interrupt({ sessionID: id, resume: false });
|
|
@@ -185,24 +218,8 @@ function assistantText(message) {
|
|
|
185
218
|
return record(text) && typeof text.text === "string" ? text.text.trim() : undefined;
|
|
186
219
|
}
|
|
187
220
|
const V2_SUBAGENT_PROMPT_PREFIX = "You are a subagent spawned by another session.\n";
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (summary < 0)
|
|
191
|
-
return undefined;
|
|
192
|
-
const details = rendered.lastIndexOf("<details", summary);
|
|
193
|
-
return rendered.slice(details < 0 ? summary : details).trim();
|
|
194
|
-
}
|
|
195
|
-
function reportIdentity(session, sourceMessageID, panel) {
|
|
196
|
-
return createHash("sha256").update(`${session}\0${sourceMessageID}\0${panel}`).digest("hex");
|
|
197
|
-
}
|
|
198
|
-
function reportMetadata(message) {
|
|
199
|
-
if (!record(message) || message.type !== "synthetic" || !record(message.metadata))
|
|
200
|
-
return undefined;
|
|
201
|
-
const value = message.metadata[V2_RETURN_REPORT_METADATA_KEY];
|
|
202
|
-
return record(value) ? value : undefined;
|
|
203
|
-
}
|
|
204
|
-
/** V2 has no public assistant-part update API; publish one durable, non-resuming synthetic report card instead. */
|
|
205
|
-
export function createV2ReturnReportPublisher(context, hooks) {
|
|
221
|
+
/** Observe terminal text for accounting only. Synthetic input is queued work, even with resume=false. */
|
|
222
|
+
export function createV2ReturnReportFinalizer(context, hooks) {
|
|
206
223
|
const active = new Map();
|
|
207
224
|
return async (session) => {
|
|
208
225
|
const previous = active.get(session) ?? Promise.resolve();
|
|
@@ -217,15 +234,6 @@ export function createV2ReturnReportPublisher(context, hooks) {
|
|
|
217
234
|
const base = assistantText(source);
|
|
218
235
|
const output = { text: base };
|
|
219
236
|
await hooks["experimental.text.complete"]?.({ sessionID: session, messageID: source.id }, output);
|
|
220
|
-
const panel = reportPanel(output.text);
|
|
221
|
-
if (panel === undefined)
|
|
222
|
-
return;
|
|
223
|
-
const identity = reportIdentity(session, source.id, panel);
|
|
224
|
-
if (history.some(message => reportMetadata(message)?.identity === identity))
|
|
225
|
-
return;
|
|
226
|
-
await context.session.synthetic({ sessionID: session, id: `sortie-report-${identity}`, text: panel,
|
|
227
|
-
description: "Sortie Dogs return report", delivery: "queue", resume: false,
|
|
228
|
-
metadata: { [V2_RETURN_REPORT_METADATA_KEY]: { schema_version: "0.1", identity, source_message_id: source.id } } });
|
|
229
237
|
return;
|
|
230
238
|
}
|
|
231
239
|
}).finally(() => { if (active.get(session) === operation)
|
|
@@ -311,9 +319,10 @@ export function createSortieDogsV2Plugin(legacyFactory = SortieDogsV010Plugin) {
|
|
|
311
319
|
return {
|
|
312
320
|
id: "sortie-dogs.v010",
|
|
313
321
|
async setup(context) {
|
|
314
|
-
const hooks = await legacyFactory({ directory: context.location.directory, client: legacyClient(context)
|
|
322
|
+
const hooks = await legacyFactory({ directory: context.location.directory, client: legacyClient(context),
|
|
323
|
+
returnReportTransport: "tool-result" }, context.options ?? {});
|
|
315
324
|
await registerV2Hooks(context, hooks);
|
|
316
|
-
const
|
|
325
|
+
const finalize = createV2ReturnReportFinalizer(context, hooks);
|
|
317
326
|
const controller = new AbortController();
|
|
318
327
|
void (async () => {
|
|
319
328
|
for await (const event of context.event.subscribe({ signal: controller.signal })) {
|
|
@@ -327,7 +336,7 @@ export function createSortieDogsV2Plugin(legacyFactory = SortieDogsV010Plugin) {
|
|
|
327
336
|
if (id === undefined)
|
|
328
337
|
continue;
|
|
329
338
|
if (event.type === "session.execution.succeeded")
|
|
330
|
-
await
|
|
339
|
+
await finalize(id);
|
|
331
340
|
if (event.type === "session.created" || event.type === "session.deleted" || event.type === "session.idle") {
|
|
332
341
|
const info = event.type === "session.created" ? await context.session.get({ sessionID: id }).catch(() => ({ id })) : { id };
|
|
333
342
|
await hooks.event?.({ event: { type: String(event.type), properties: { sessionID: id, info } } });
|
|
@@ -23,6 +23,8 @@ Retain the canonical product's game-like user guidance and icons. Role separatio
|
|
|
23
23
|
Use the user's language for prose, concise icon-led plan/progress/evidence blocks, and the canonical terminal heading
|
|
24
24
|
after the host accepts the result. The host supplies the 🐾 return report, mission/proof, cost/pack and career panels;
|
|
25
25
|
do not fabricate scores, counts, medals or success, and do not suppress these panels as redundant decoration.
|
|
26
|
+
When complete_operator returns return_report, append that host-authored Markdown verbatim exactly once to the existing
|
|
27
|
+
final answer, outside any code fence. Do not calculate its values or turn the panel into another task or model call.
|
|
26
28
|
|
|
27
29
|
${canonicalFixture("READABLE_OUTPUT_FIXTURE")}
|
|
28
30
|
`;
|
|
@@ -219,7 +221,9 @@ directly (fast path); a larger plan returns ${operator}. Do not launch another i
|
|
|
219
221
|
Do not manually reconstruct, translate, shorten, or append to that returned prompt. Do not add task_id to resume a
|
|
220
222
|
fresh dispatch. If the exact Task is no longer in context, call operator_next once to retrieve it rather than guess.
|
|
221
223
|
If prepare reports an invalid plan, repair only the reported JSON pointer using its code and repair_kind; preserve the
|
|
222
|
-
original objective, acceptance, and unit count.
|
|
224
|
+
original objective, acceptance, and unit count. For operator-goal-field-invalid, use the exact expected enum or required
|
|
225
|
+
field value at the diagnosed pointer; add a missing field without replacing defaults, criteria, budgets or the whole declaration.
|
|
226
|
+
Do not regenerate the full plan or send placeholder/probe plans. If it reports
|
|
223
227
|
an immutable active contract, read operator_status and reuse the existing next Task. Cancel only when an actual scope
|
|
224
228
|
change or explicit stop requires it; do not cancel/recreate an unchanged plan to work around an admission error.
|
|
225
229
|
Cancellation does not close the accepted user order. A replacement repair plan must keep the exact previous ordered
|