sortie-dogs 0.10.18 → 0.10.20
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 +27 -1
- package/dist/core/operator-proposal.js +192 -13
- 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 +54 -13
- 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 +18 -2
- 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-root-revision1";
|
|
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-root-revision1";
|
|
@@ -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.
|
|
@@ -8,6 +8,7 @@ export declare const OPERATOR_PROPOSAL_BUDGET_CAPS: Readonly<{
|
|
|
8
8
|
max_reads: 192;
|
|
9
9
|
max_submissions: 24;
|
|
10
10
|
}>;
|
|
11
|
+
export declare const OPERATOR_PROPOSAL_REVISION_CONTRACT: string;
|
|
11
12
|
export declare const OPERATOR_APPROVAL_CONTRACT: string;
|
|
12
13
|
export interface OperatorIntentRequirement {
|
|
13
14
|
readonly id: string;
|
|
@@ -90,6 +91,23 @@ export interface OperatorProposalState {
|
|
|
90
91
|
proposal_hash: string | null;
|
|
91
92
|
proposal: OperatorProposalPacket | null;
|
|
92
93
|
approval_rationale: string | null;
|
|
94
|
+
/** Preparation may publish execution controls before approval commits; never revise that pinned plan. */
|
|
95
|
+
approval_started?: boolean;
|
|
96
|
+
root_revisions?: OperatorProposalRevision[];
|
|
97
|
+
}
|
|
98
|
+
interface OperatorProposalIdentity {
|
|
99
|
+
readonly proposal_id: string;
|
|
100
|
+
readonly revision: number;
|
|
101
|
+
readonly content_hash: string;
|
|
102
|
+
}
|
|
103
|
+
export interface OperatorProposalRevision {
|
|
104
|
+
readonly actor: string;
|
|
105
|
+
readonly at: string;
|
|
106
|
+
readonly rationale: string;
|
|
107
|
+
readonly from: OperatorProposalIdentity;
|
|
108
|
+
readonly to: OperatorProposalIdentity;
|
|
109
|
+
readonly patch_hash: string;
|
|
110
|
+
readonly paths: readonly string[];
|
|
93
111
|
}
|
|
94
112
|
export interface OperatorProposalBudgetDiagnostic {
|
|
95
113
|
readonly status: "proposal-budget-exhausted";
|
|
@@ -112,15 +130,17 @@ export declare class OperatorProposalBudgetError extends Error {
|
|
|
112
130
|
/** Durable pre-execution lane. It grants investigation and submission, never source mutation or execution. */
|
|
113
131
|
export declare class OperatorProposalRuntime {
|
|
114
132
|
readonly profile: RuntimeProfile;
|
|
115
|
-
private readonly states;
|
|
116
133
|
private readonly pending;
|
|
117
134
|
private serial;
|
|
135
|
+
private locked;
|
|
118
136
|
readonly projectRoot: string;
|
|
119
137
|
constructor(projectRoot: string, profile: RuntimeProfile);
|
|
120
138
|
private file;
|
|
121
139
|
private spendFile;
|
|
122
140
|
private readSpend;
|
|
123
141
|
private saveSpend;
|
|
142
|
+
/** A released investigation still belongs to its accepted order across a host restart. No grant is revived. */
|
|
143
|
+
continuationCheckpoint(root: string): Promise<string | undefined>;
|
|
124
144
|
read(root: string): Promise<OperatorProposalState | undefined>;
|
|
125
145
|
private readUnlocked;
|
|
126
146
|
private save;
|
|
@@ -146,6 +166,11 @@ export declare class OperatorProposalRuntime {
|
|
|
146
166
|
submit(root: string, actor: string, raw: unknown): Promise<OperatorProposalState>;
|
|
147
167
|
submitJSON(root: string, actor: string, source: string): Promise<OperatorProposalState>;
|
|
148
168
|
private submitUnlocked;
|
|
169
|
+
/** A read-free root correction of an ended child's submitted packet, never a new investigation grant. */
|
|
170
|
+
reviseJSON(root: string, actor: string, source: string): Promise<OperatorProposalState>;
|
|
171
|
+
private patchedPacket;
|
|
172
|
+
/** Close revision before any prepare/register side effect, including when that later step fails. */
|
|
173
|
+
pinApproval(root: string, raw: unknown): Promise<void>;
|
|
149
174
|
approve(root: string, raw: unknown, commit?: boolean): Promise<OperatorProposalState>;
|
|
150
175
|
private approveUnlocked;
|
|
151
176
|
/**
|
|
@@ -159,3 +184,4 @@ export declare class OperatorProposalRuntime {
|
|
|
159
184
|
private requiredUnlocked;
|
|
160
185
|
packet(state: OperatorProposalState): unknown;
|
|
161
186
|
}
|
|
187
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { normalizeRelativePath } from "./path.js";
|
|
5
5
|
import { OperatorContractError, parseOperatorPlan } from "./operator-runtime.js";
|
|
@@ -10,6 +10,18 @@ const ORIGINAL_REQUEST_MAX_CHARACTERS = 128 * 1024;
|
|
|
10
10
|
const ORIGINAL_REQUEST_MAX_BYTES = 128 * 1024;
|
|
11
11
|
export const DEFAULT_OPERATOR_PROPOSAL_BUDGET = Object.freeze({ max_reads: 45, max_submissions: 9 });
|
|
12
12
|
export const OPERATOR_PROPOSAL_BUDGET_CAPS = Object.freeze({ max_reads: 192, max_submissions: 24 });
|
|
13
|
+
export const OPERATOR_PROPOSAL_REVISION_CONTRACT = "revision_json must encode exactly {proposal_id:string,revision:positive integer,content_hash:string,rationale:nonblank single-line string,patches:array}. " +
|
|
14
|
+
"Pin the current submitted identity from operator_status. patches contains 1..32 {op,path,value} objects. " +
|
|
15
|
+
"replace is allowed only at /coverage, /existing_surface, /uncovered, /negative_handling, /read_scope, /plan/units, /plan/acceptance_proof, " +
|
|
16
|
+
"/plan/units/<index>/{title,objective,read,validation,acceptance_indices}; add or replace is also allowed at " +
|
|
17
|
+
"/plan/goal_declaration/criteria/<index>/{validation_command,goal_validation_command}. No duplicate or overlapping paths. " +
|
|
18
|
+
"Read spellings may normalize separators and dot segments without traversal; read authority can only shrink. " +
|
|
19
|
+
"The exact write union, ordered acceptance, source_refs, intent, authoritative_refs, goal binding, goal defaults, criterion identities, git_lifecycle and budgets are immutable. " +
|
|
20
|
+
"Unit count cannot increase. The host derives the next revision, hash and budget_estimate, validates the complete packet, and records root-patch provenance atomically. " +
|
|
21
|
+
"Each authorized attempt, including malformed input or rejected patches, consumes one remaining submission; foreign/stale calls do not spend the live grant. No reads or execution units are granted or restored. " +
|
|
22
|
+
"Approval preparation or approval closes this revision lane. Revision never restarts a child, prepares a run, or approves anything. " +
|
|
23
|
+
"Compare every original requirement again before explicit approval with the new identity. Use only already observed evidence. " +
|
|
24
|
+
"Keep root-owned push/global apply and user-only acceptance pending until separately evidenced; worker tests never prove those obligations. Unrepresentable or unobserved requirements stay uncovered.";
|
|
13
25
|
const OPERATOR_APPROVAL_IDENTITY_FIELDS = ["proposal_id", "revision", "content_hash", "compared_requirement_ids", "decision"];
|
|
14
26
|
const OPERATOR_APPROVAL_RATIONALE_FIELDS = ["rationale", "comparison_rationale"];
|
|
15
27
|
export const OPERATOR_APPROVAL_CONTRACT = "approval_json must encode one JSON object using exactly one six-field shape. " +
|
|
@@ -287,10 +299,9 @@ function parsePacket(value, state) {
|
|
|
287
299
|
/** Durable pre-execution lane. It grants investigation and submission, never source mutation or execution. */
|
|
288
300
|
export class OperatorProposalRuntime {
|
|
289
301
|
profile;
|
|
290
|
-
states = new Map();
|
|
291
302
|
pending = new Map();
|
|
292
303
|
async serial(root, action) {
|
|
293
|
-
const result = (this.pending.get(root) ?? Promise.resolve()).then(action);
|
|
304
|
+
const result = (this.pending.get(root) ?? Promise.resolve()).then(() => this.locked(root, action));
|
|
294
305
|
const tail = result.then(() => undefined, () => undefined);
|
|
295
306
|
this.pending.set(root, tail);
|
|
296
307
|
try {
|
|
@@ -301,6 +312,31 @@ export class OperatorProposalRuntime {
|
|
|
301
312
|
this.pending.delete(root);
|
|
302
313
|
}
|
|
303
314
|
}
|
|
315
|
+
async locked(root, action) {
|
|
316
|
+
await mkdir(join(this.projectRoot, this.profile.stateDirectory, "operator-proposals"), { recursive: true });
|
|
317
|
+
const lock = `${this.file(root)}.lock`;
|
|
318
|
+
let handle;
|
|
319
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
320
|
+
try {
|
|
321
|
+
handle = await open(lock, "wx", 0o600);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
if (error.code !== "EEXIST")
|
|
326
|
+
throw error;
|
|
327
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (!handle)
|
|
331
|
+
throw new Error("operator-proposal-state-busy");
|
|
332
|
+
try {
|
|
333
|
+
return await action();
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
await handle.close();
|
|
337
|
+
await rm(lock, { force: true });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
304
340
|
projectRoot;
|
|
305
341
|
constructor(projectRoot, profile) {
|
|
306
342
|
this.profile = profile;
|
|
@@ -324,6 +360,13 @@ export class OperatorProposalRuntime {
|
|
|
324
360
|
!Number.isSafeInteger(spend.reads) || spend.reads < 0 || !Number.isSafeInteger(spend.submissions) || spend.submissions < 0) {
|
|
325
361
|
throw new Error("operator-proposal-spend-invalid");
|
|
326
362
|
}
|
|
363
|
+
if (spend.retry !== undefined && (!record(spend.retry) || !/^[a-f0-9]{64}$/u.test(spend.retry.intent_hash) ||
|
|
364
|
+
!record(spend.retry.goal_binding) || !text(spend.retry.goal_binding.goal_id) ||
|
|
365
|
+
!Number.isSafeInteger(spend.retry.goal_binding.revision) || spend.retry.goal_binding.revision < 1 ||
|
|
366
|
+
!Number.isSafeInteger(spend.retry.goal_binding.scope_epoch) || spend.retry.goal_binding.scope_epoch < 1 ||
|
|
367
|
+
!/^sha256:[a-f0-9]{64}$/u.test(spend.retry.goal_binding.acceptance_fingerprint))) {
|
|
368
|
+
throw new Error("operator-proposal-spend-invalid");
|
|
369
|
+
}
|
|
327
370
|
return spend;
|
|
328
371
|
}
|
|
329
372
|
async saveSpend(spend) {
|
|
@@ -338,13 +381,23 @@ export class OperatorProposalRuntime {
|
|
|
338
381
|
await rm(temporary, { force: true }).catch(() => undefined);
|
|
339
382
|
}
|
|
340
383
|
}
|
|
384
|
+
/** A released investigation still belongs to its accepted order across a host restart. No grant is revived. */
|
|
385
|
+
async continuationCheckpoint(root) {
|
|
386
|
+
return this.serial(root, async () => {
|
|
387
|
+
const state = await this.readUnlocked(root);
|
|
388
|
+
if (state?.phase === "approved")
|
|
389
|
+
return undefined;
|
|
390
|
+
const retry = state?.goal_binding ? { intent_hash: state.intent_hash, goal_binding: state.goal_binding }
|
|
391
|
+
: state ? undefined : (await this.readSpend(root)).retry;
|
|
392
|
+
return retry === undefined ? undefined : JSON.stringify({ authority: "durable-proposal-continuity",
|
|
393
|
+
root_session_id: root, ...retry, status: state?.phase ?? "cancelled",
|
|
394
|
+
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." });
|
|
395
|
+
});
|
|
396
|
+
}
|
|
341
397
|
async read(root) {
|
|
342
398
|
return this.serial(root, () => this.readUnlocked(root));
|
|
343
399
|
}
|
|
344
400
|
async readUnlocked(root) {
|
|
345
|
-
const cached = this.states.get(root);
|
|
346
|
-
if (cached)
|
|
347
|
-
return structuredClone(cached);
|
|
348
401
|
let source;
|
|
349
402
|
try {
|
|
350
403
|
source = await readFile(this.file(root), "utf8");
|
|
@@ -376,8 +429,7 @@ export class OperatorProposalRuntime {
|
|
|
376
429
|
else {
|
|
377
430
|
throw new Error("operator-proposal-state-invalid");
|
|
378
431
|
}
|
|
379
|
-
|
|
380
|
-
return structuredClone(state);
|
|
432
|
+
return state;
|
|
381
433
|
}
|
|
382
434
|
async save(state) {
|
|
383
435
|
const directory = join(this.projectRoot, this.profile.stateDirectory, "operator-proposals");
|
|
@@ -390,7 +442,6 @@ export class OperatorProposalRuntime {
|
|
|
390
442
|
finally {
|
|
391
443
|
await rm(temporary, { force: true }).catch(() => undefined);
|
|
392
444
|
}
|
|
393
|
-
this.states.set(state.root_session_id, structuredClone(state));
|
|
394
445
|
}
|
|
395
446
|
assertBudgetAvailable(state) {
|
|
396
447
|
const remainingReads = state.intent.proposal_budget.max_reads - state.read_count;
|
|
@@ -489,7 +540,8 @@ export class OperatorProposalRuntime {
|
|
|
489
540
|
const spend = await this.readSpend(root);
|
|
490
541
|
const state = { schema_version: "0.1", profile: this.profile.id, root_session_id: root,
|
|
491
542
|
intent_id: `intent-${intentHash.slice(0, 24)}`, intent_hash: intentHash, intent, created_at: new Date().toISOString(), phase: "investigating",
|
|
492
|
-
goal_binding:
|
|
543
|
+
goal_binding: spend.retry?.intent_hash === intentHash ? structuredClone(spend.retry.goal_binding) : null,
|
|
544
|
+
proposal_call_id: null, proposal_session_id: null, read_count: spend.reads, read_paths: [], submission_count: spend.submissions, proposal_id: null,
|
|
493
545
|
...(spend.reads > 0 || spend.submissions > 0 ? { prior_spend: { reads: spend.reads, submissions: spend.submissions } } : {}),
|
|
494
546
|
proposal_revision: null, proposal_hash: null, proposal: null, approval_rationale: null };
|
|
495
547
|
this.assertBudgetAvailable(state);
|
|
@@ -617,6 +669,132 @@ export class OperatorProposalRuntime {
|
|
|
617
669
|
await this.save(state);
|
|
618
670
|
return state;
|
|
619
671
|
}
|
|
672
|
+
/** A read-free root correction of an ended child's submitted packet, never a new investigation grant. */
|
|
673
|
+
async reviseJSON(root, actor, source) {
|
|
674
|
+
return this.serial(root, async () => {
|
|
675
|
+
if (root !== actor)
|
|
676
|
+
throw new Error("operator-proposal-revision-root-required");
|
|
677
|
+
const state = await this.requiredUnlocked(root);
|
|
678
|
+
if (state.phase !== "submitted" || !state.proposal || state.approval_started)
|
|
679
|
+
throw new Error("operator-proposal-not-revisable");
|
|
680
|
+
if (state.submission_count >= state.intent.proposal_budget.max_submissions)
|
|
681
|
+
throw new Error("operator-proposal-submission-budget-exhausted");
|
|
682
|
+
let request, candidate;
|
|
683
|
+
try {
|
|
684
|
+
if (Buffer.byteLength(source) > 128 * 1024)
|
|
685
|
+
proposalError("/", "operator-proposal-revision-too-large", "maxBytes");
|
|
686
|
+
let raw;
|
|
687
|
+
try {
|
|
688
|
+
raw = JSON.parse(source);
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
proposalError("/", "operator-proposal-revision-json-invalid", "json");
|
|
692
|
+
}
|
|
693
|
+
if (!record(raw) || !exactSet(raw, ["proposal_id", "revision", "content_hash", "rationale", "patches"]) ||
|
|
694
|
+
!text(raw.proposal_id) || !Number.isSafeInteger(raw.revision) || !text(raw.content_hash) ||
|
|
695
|
+
!text(raw.rationale) || raw.rationale.length > 2000 || !Array.isArray(raw.patches) ||
|
|
696
|
+
raw.patches.length < 1 || raw.patches.length > 32) {
|
|
697
|
+
return proposalError("/", "operator-proposal-revision-invalid", "exact-identity-rationale-bounded-patches");
|
|
698
|
+
}
|
|
699
|
+
request = raw;
|
|
700
|
+
if (raw.proposal_id !== state.proposal_id || raw.revision !== state.proposal_revision || raw.content_hash !== state.proposal_hash) {
|
|
701
|
+
throw new Error("operator-proposal-revision-identity-mismatch");
|
|
702
|
+
}
|
|
703
|
+
candidate = this.patchedPacket(state, raw.patches);
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
if (error instanceof Error && error.message === "operator-proposal-revision-identity-mismatch")
|
|
707
|
+
throw error;
|
|
708
|
+
// Rejections retain the old body/identity; only the same finite attempt counter advances.
|
|
709
|
+
state.submission_count++;
|
|
710
|
+
await this.save(state);
|
|
711
|
+
throw error;
|
|
712
|
+
}
|
|
713
|
+
const from = { proposal_id: state.proposal_id, revision: state.proposal_revision, content_hash: state.proposal_hash };
|
|
714
|
+
state.submission_count++;
|
|
715
|
+
state.proposal = candidate;
|
|
716
|
+
state.proposal_revision = candidate.revision;
|
|
717
|
+
state.proposal_hash = hash(JSON.stringify(candidate));
|
|
718
|
+
state.proposal_id = proposalID(state.intent_id, candidate.revision, state.proposal_hash);
|
|
719
|
+
(state.root_revisions ??= []).push({ actor, at: new Date().toISOString(), rationale: request.rationale,
|
|
720
|
+
from, to: { proposal_id: state.proposal_id, revision: candidate.revision, content_hash: state.proposal_hash },
|
|
721
|
+
patch_hash: hash(JSON.stringify(request.patches)), paths: request.patches.map(patch => patch.path) });
|
|
722
|
+
await this.save(state);
|
|
723
|
+
return state;
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
patchedPacket(state, patches) {
|
|
727
|
+
const before = state.proposal;
|
|
728
|
+
const draft = structuredClone(before);
|
|
729
|
+
const paths = [];
|
|
730
|
+
for (const [index, patch] of patches.entries()) {
|
|
731
|
+
const pointer = `/patches/${index}`;
|
|
732
|
+
if (!record(patch) || !exactSet(patch, ["op", "path", "value"]) || typeof patch.path !== "string") {
|
|
733
|
+
proposalError(pointer, "operator-proposal-revision-patch-invalid", "exact-op-path-value");
|
|
734
|
+
}
|
|
735
|
+
const entry = patch;
|
|
736
|
+
const command = /^\/plan\/goal_declaration\/criteria\/(0|[1-9][0-9]*)\/(validation_command|goal_validation_command)$/u.test(entry.path);
|
|
737
|
+
const replace = /^\/(coverage|existing_surface|uncovered|negative_handling|read_scope|plan\/(units|acceptance_proof))$/u.test(entry.path) ||
|
|
738
|
+
/^\/plan\/units\/(0|[1-9][0-9]*)\/(title|objective|read|validation|acceptance_indices)$/u.test(entry.path);
|
|
739
|
+
if (!(command && (entry.op === "add" || entry.op === "replace") || replace && entry.op === "replace") ||
|
|
740
|
+
paths.some(path => path === entry.path || path.startsWith(`${entry.path}/`) || entry.path.startsWith(`${path}/`))) {
|
|
741
|
+
proposalError(`${pointer}/path`, "operator-proposal-revision-path-denied", "nonoverlapping-allowlisted-field");
|
|
742
|
+
}
|
|
743
|
+
paths.push(entry.path);
|
|
744
|
+
const tokens = entry.path.slice(1).split("/");
|
|
745
|
+
let owner = draft;
|
|
746
|
+
for (const token of tokens.slice(0, -1)) {
|
|
747
|
+
if (owner === null || typeof owner !== "object" || !Object.hasOwn(owner, token)) {
|
|
748
|
+
proposalError(`${pointer}/path`, "operator-proposal-revision-path-missing", "existing-parent");
|
|
749
|
+
}
|
|
750
|
+
owner = owner[token];
|
|
751
|
+
}
|
|
752
|
+
const key = tokens.at(-1);
|
|
753
|
+
if (owner === null || typeof owner !== "object" || (entry.op === "replace" && !Object.hasOwn(owner, key)) ||
|
|
754
|
+
(entry.op === "add" && Object.hasOwn(owner, key))) {
|
|
755
|
+
proposalError(`${pointer}/path`, "operator-proposal-revision-path-missing", "replace-existing-or-add-absent-field");
|
|
756
|
+
}
|
|
757
|
+
owner[key] = structuredClone(entry.value);
|
|
758
|
+
}
|
|
759
|
+
const normalizeReads = (value) => Array.isArray(value) ? value.map(path => {
|
|
760
|
+
if (typeof path !== "string")
|
|
761
|
+
return path;
|
|
762
|
+
try {
|
|
763
|
+
return normalizeRelativePath(path);
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
return proposalError("/read_scope", "operator-proposal-revision-read-invalid", "safe-relative-spelling");
|
|
767
|
+
}
|
|
768
|
+
}) : value;
|
|
769
|
+
draft.read_scope = normalizeReads(draft.read_scope);
|
|
770
|
+
if (Array.isArray(draft.plan.units))
|
|
771
|
+
for (const unit of draft.plan.units)
|
|
772
|
+
if (record(unit))
|
|
773
|
+
unit.read = normalizeReads(unit.read);
|
|
774
|
+
draft.revision = before.revision + 1;
|
|
775
|
+
draft.budget_estimate = { proposal_reads: state.read_count, execution_units: draft.plan.units?.length };
|
|
776
|
+
const candidate = parsePacket(draft, state);
|
|
777
|
+
const within = (path, scopes) => scopes.some(scope => path === scope || path.startsWith(`${scope}/`));
|
|
778
|
+
if (candidate.read_scope.some(path => !within(path, before.read_scope)) ||
|
|
779
|
+
candidate.plan.units.flatMap(unit => unit.read).some(path => !within(path, before.plan.units.flatMap(unit => unit.read)))) {
|
|
780
|
+
proposalError("/read_scope", "operator-proposal-revision-read-expanded", "read-authority-only-shrinks");
|
|
781
|
+
}
|
|
782
|
+
if (candidate.plan.units.length > before.plan.units.length) {
|
|
783
|
+
proposalError("/plan/units", "operator-proposal-revision-units-expanded", "execution-unit-count-cannot-increase");
|
|
784
|
+
}
|
|
785
|
+
if (JSON.stringify({ ...candidate, revision: before.revision }) === JSON.stringify(before)) {
|
|
786
|
+
proposalError("/patches", "operator-proposal-revision-no-change", "substantive-correction-required");
|
|
787
|
+
}
|
|
788
|
+
return candidate;
|
|
789
|
+
}
|
|
790
|
+
/** Close revision before any prepare/register side effect, including when that later step fails. */
|
|
791
|
+
async pinApproval(root, raw) {
|
|
792
|
+
return this.serial(root, async () => {
|
|
793
|
+
const state = await this.approveUnlocked(root, raw, false);
|
|
794
|
+
state.approval_started = true;
|
|
795
|
+
await this.save(state);
|
|
796
|
+
});
|
|
797
|
+
}
|
|
620
798
|
async approve(root, raw, commit = true) {
|
|
621
799
|
return this.serial(root, () => this.approveUnlocked(root, raw, commit));
|
|
622
800
|
}
|
|
@@ -647,9 +825,9 @@ export class OperatorProposalRuntime {
|
|
|
647
825
|
if (!state || state.phase === "approved")
|
|
648
826
|
return undefined;
|
|
649
827
|
await this.saveSpend({ schema_version: "0.1", profile: this.profile.id, root_session_id: root,
|
|
650
|
-
reads: state.read_count, submissions: state.submission_count
|
|
828
|
+
reads: state.read_count, submissions: state.submission_count,
|
|
829
|
+
...(state.goal_binding === null ? {} : { retry: { intent_hash: state.intent_hash, goal_binding: state.goal_binding } }) });
|
|
651
830
|
await rm(this.file(root), { force: true });
|
|
652
|
-
this.states.delete(root);
|
|
653
831
|
return state;
|
|
654
832
|
});
|
|
655
833
|
}
|
|
@@ -671,6 +849,7 @@ export class OperatorProposalRuntime {
|
|
|
671
849
|
remaining_reads: state.intent.proposal_budget.max_reads - state.read_count, submissions: state.submission_count,
|
|
672
850
|
remaining_submissions: state.intent.proposal_budget.max_submissions - state.submission_count,
|
|
673
851
|
task_admitted: state.proposal_call_id !== null, uncovered: state.proposal?.uncovered ?? [], proposal: state.proposal,
|
|
674
|
-
approval_rationale: state.approval_rationale
|
|
852
|
+
approval_rationale: state.approval_rationale, root_revisions: state.root_revisions ?? [],
|
|
853
|
+
revision_available: state.phase === "submitted" && !state.approval_started && state.submission_count < state.intent.proposal_budget.max_submissions };
|
|
675
854
|
}
|
|
676
855
|
}
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { V010_RUNTIME_ASSET_VERSION } from "../asset-version.js";
|
|
2
2
|
import { OperatorContractError, OperatorRuntime } from "../core/operator-runtime.js";
|
|
3
|
-
import { DEFAULT_OPERATOR_PROPOSAL_BUDGET, OPERATOR_APPROVAL_CONTRACT, OPERATOR_PROPOSAL_BUDGET_CAPS, OperatorProposalBudgetError, OperatorProposalRuntime } from "../core/operator-proposal.js";
|
|
3
|
+
import { DEFAULT_OPERATOR_PROPOSAL_BUDGET, OPERATOR_APPROVAL_CONTRACT, OPERATOR_PROPOSAL_BUDGET_CAPS, OPERATOR_PROPOSAL_REVISION_CONTRACT, OperatorProposalBudgetError, OperatorProposalRuntime } from "../core/operator-proposal.js";
|
|
4
4
|
import { CANONICAL_AGENT_ROLES, canonicalAgent, profileAgent, profileTool, V010_RUNTIME_PROFILE } from "../core/runtime-profile.js";
|
|
5
5
|
import { SortieDogsPlugin as canonicalPlugin } from "./index.js";
|
|
6
6
|
import { taskChildSessionID } from "./task-result-repair.js";
|
|
@@ -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")
|
|
@@ -391,6 +395,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
391
395
|
const approvalSchema = record(stringSchema) && typeof stringSchema.describe === "function"
|
|
392
396
|
? stringSchema.describe(OPERATOR_APPROVAL_CONTRACT)
|
|
393
397
|
: { ...(record(stringSchema) ? stringSchema : { type: "string" }), description: OPERATOR_APPROVAL_CONTRACT };
|
|
398
|
+
const revisionSchema = record(stringSchema) && typeof stringSchema.describe === "function"
|
|
399
|
+
? stringSchema.describe(OPERATOR_PROPOSAL_REVISION_CONTRACT)
|
|
400
|
+
: { ...(record(stringSchema) ? stringSchema : { type: "string" }), description: OPERATOR_PROPOSAL_REVISION_CONTRACT };
|
|
394
401
|
const planContract = "plan_json must encode the exact operator plan object: schema_version, acceptance, acceptance_proof, source_refs, " +
|
|
395
402
|
"goal_declaration, units, and optional git_lifecycle only. git_lifecycle exact shape is " +
|
|
396
403
|
'{branch_create:{branch:string,start_ref:string},commit:{message:string},post_commit_validation:string[],remediation_reserve?:string[],remediation_scope_expansion?:string[]}. Its presence is root authorization for only a clean, non-overwriting ' +
|
|
@@ -416,6 +423,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
416
423
|
const beginProposal = profileTool(profile, "sortie_begin_operator_proposal");
|
|
417
424
|
const submitProposal = profileTool(profile, "sortie_submit_operator_proposal");
|
|
418
425
|
const approveProposal = profileTool(profile, "sortie_approve_operator_proposal");
|
|
426
|
+
const reviseProposal = profileTool(profile, "sortie_revise_operator_proposal");
|
|
419
427
|
async function stop(root, reason, retireRoot = true) {
|
|
420
428
|
if (retireRoot)
|
|
421
429
|
retired.add(root);
|
|
@@ -486,7 +494,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
486
494
|
await registerPreparedGoal(context.sessionID, state);
|
|
487
495
|
return preparedTask(state);
|
|
488
496
|
} };
|
|
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.",
|
|
497
|
+
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
498
|
args: { draft_id: stringSchema, patches_json: stringSchema }, execute: async (args, context) => {
|
|
491
499
|
await requireRoot(context.sessionID);
|
|
492
500
|
let patches;
|
|
@@ -555,7 +563,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
555
563
|
? "proposal submission budget exhausted; do not call operator_next; report the bounded proposal failure"
|
|
556
564
|
: "proposal is not submitted; do not call operator_next; complete or repair the bounded proposal submission"
|
|
557
565
|
: proposal?.phase === "submitted"
|
|
558
|
-
?
|
|
566
|
+
? `compare every original requirement with the exact proposal. For a known semantic defect, use ${reviseProposal} with its current identity and bounded field patches, then compare again; do not redispatch the ended child or cancel/reinvestigate the same defect. Approval preparation pins the plan and closes revision. Do not call operator_next before approval prepares a run. Approval authorizes implementation, not product acceptance. Preserve every required live measurement, consultation, root-owned push/global apply and user approval as pending acceptance obligations; canonical commands do not substitute for them. Use the host budget counters, not plan.goal_declaration.goal_budget_units, for remaining capacity; approval validates the retained goal binding and execution budget. A bounded diagnostic run may use cancel_operator after admission without inventing a plan validation command for cancellation.`
|
|
559
567
|
: "approved proposal has no operator run; do not call operator_next; reconcile the approval or preparation failure";
|
|
560
568
|
return JSON.stringify(state ? { ...await operatorPacket(state), ...(draft ? { pending_draft: draft } : {}),
|
|
561
569
|
...(proposal ? { proposal: proposalIdentity(proposal) } : {}) }
|
|
@@ -654,16 +662,27 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
654
662
|
if (!["awaiting-acceptance", "completed"].includes(state.phase) || state.units.some(unit => unit.status !== "succeeded")) {
|
|
655
663
|
return JSON.stringify({ status: "not-ready", packet: operators.packet(state) });
|
|
656
664
|
}
|
|
657
|
-
const
|
|
665
|
+
const declarationFingerprint = await operators.completionGoalFingerprint(state);
|
|
658
666
|
if (state.phase === "awaiting-acceptance") {
|
|
659
|
-
await relinkRegisteredGoal(context.sessionID, state,
|
|
660
|
-
await control.assertActiveGoal(context.sessionID,
|
|
667
|
+
await relinkRegisteredGoal(context.sessionID, state, declarationFingerprint);
|
|
668
|
+
await control.assertActiveGoal(context.sessionID, declarationFingerprint);
|
|
661
669
|
}
|
|
662
|
-
const result = await control.completeRoot(context.sessionID,
|
|
670
|
+
const result = await control.completeRoot(context.sessionID, declarationFingerprint);
|
|
663
671
|
if (result.receipt)
|
|
664
672
|
await operators.terminal(context.sessionID, result.receipt);
|
|
673
|
+
let panel;
|
|
674
|
+
if (input.returnReportTransport === "tool-result" && result.receipt?.status === "succeeded") {
|
|
675
|
+
const text = `✅ **DONE** \`${state.runID}\` — ${state.acceptance.length} acceptance requirements verified.\n\n` +
|
|
676
|
+
`**変更点:** ${state.units.map(unit => unit.unit.title).join("; ")}\n\n` +
|
|
677
|
+
`**確認結果:** PASS — ${[...new Set(state.units.flatMap(unit => unit.unit.validation))].join("; ")}\n\n**次:** なし`;
|
|
678
|
+
const rendered = await control.renderReturnReport(context.sessionID, text, goalFingerprint(result.receipt)).catch(() => undefined);
|
|
679
|
+
if (rendered)
|
|
680
|
+
panel = returnReportPanel(rendered);
|
|
681
|
+
}
|
|
665
682
|
return JSON.stringify({ status: result.status, run_id: state.runID,
|
|
666
|
-
acceptance_fingerprint: state.acceptanceFingerprint, receipt: result.receipt ?? null
|
|
683
|
+
acceptance_fingerprint: state.acceptanceFingerprint, receipt: result.receipt ?? null,
|
|
684
|
+
...(panel ? { return_report: panel,
|
|
685
|
+
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
686
|
} };
|
|
668
687
|
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
688
|
args: { run_id: stringSchema, acceptance_fingerprint: stringSchema }, execute: async (args, context) => {
|
|
@@ -808,6 +827,26 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
808
827
|
...(contract ? { diagnostics: error.diagnostics, diagnostics_truncated: error.diagnostics_truncated } : {}) });
|
|
809
828
|
}
|
|
810
829
|
} };
|
|
830
|
+
tools[reviseProposal] = { description: "Root-only: revise an unapproved submitted proposal using a hash-pinned field patch and the remaining submission allowance. " + OPERATOR_PROPOSAL_REVISION_CONTRACT,
|
|
831
|
+
args: { revision_json: revisionSchema }, execute: async (args, context) => {
|
|
832
|
+
await requireRoot(context.sessionID);
|
|
833
|
+
return serializeDispatchTransition(context.sessionID, async () => {
|
|
834
|
+
try {
|
|
835
|
+
const revised = await proposals.reviseJSON(context.sessionID, context.sessionID, args.revision_json);
|
|
836
|
+
return JSON.stringify({ ...proposals.packet(revised),
|
|
837
|
+
next_action: "Compare all original ordered requirements again. Explicitly approve only this new identity if every obligation is genuinely covered; no execution or approval was granted by revision." });
|
|
838
|
+
}
|
|
839
|
+
catch (error) {
|
|
840
|
+
const contract = error instanceof OperatorContractError;
|
|
841
|
+
if (!contract && (!(error instanceof Error) || !error.message.startsWith("operator-proposal-")))
|
|
842
|
+
throw error;
|
|
843
|
+
const state = await proposals.read(context.sessionID);
|
|
844
|
+
return JSON.stringify({ status: "invalid-revision", code: error.message,
|
|
845
|
+
...(state ? { proposal: proposalIdentity(state) } : {}),
|
|
846
|
+
...(contract ? { diagnostics: error.diagnostics, diagnostics_truncated: error.diagnostics_truncated } : {}) });
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
} };
|
|
811
850
|
tools[approveProposal] = { description: "Root-only: record semantic comparison of the exact proposal revision/hash, then connect its immutable plan to the existing execution lane. " + OPERATOR_APPROVAL_CONTRACT,
|
|
812
851
|
args: { approval_json: approvalSchema }, execute: async (args, context) => {
|
|
813
852
|
await requireRoot(context.sessionID);
|
|
@@ -837,6 +876,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
837
876
|
if (approved.goal_binding === null)
|
|
838
877
|
throw new Error("operator-proposal-goal-binding-missing");
|
|
839
878
|
await control.assertProposalExecutionBudget(context.sessionID, approved.goal_binding, proposal.plan.units.length);
|
|
879
|
+
await proposals.pinApproval(context.sessionID, approval);
|
|
840
880
|
const prepared = await operators.propose(context.sessionID, proposal.plan);
|
|
841
881
|
if (prepared.status !== "prepared")
|
|
842
882
|
throw new Error("operator-proposal-approved-plan-invalid");
|
|
@@ -846,7 +886,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
846
886
|
});
|
|
847
887
|
} };
|
|
848
888
|
const ownTools = new Set([prepare, repair, next, status, cancel, complete, resume, resolveContractRepair,
|
|
849
|
-
beginProposal, submitProposal, approveProposal]);
|
|
889
|
+
beginProposal, submitProposal, approveProposal, reviseProposal]);
|
|
850
890
|
const protocolMap = CANONICAL_AGENT_ROLES.map(role => `${role}=${profileAgent(profile, role)}`).join(", ");
|
|
851
891
|
function rememberRendered(key, text) {
|
|
852
892
|
renderedParts.set(key, goalFingerprint(text));
|
|
@@ -978,8 +1018,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
978
1018
|
output.parts.some(part => record(part) && part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0);
|
|
979
1019
|
if (realTurn) {
|
|
980
1020
|
const state = await operators.read(chat.sessionID);
|
|
981
|
-
if ((state !== undefined && (state.phase === "cancelled" || state.phase === "completed")) ||
|
|
982
|
-
operatorTurnLifecycle.get(chat.sessionID) === "cancelled")
|
|
1021
|
+
if (((state !== undefined && (state.phase === "cancelled" || state.phase === "completed")) ||
|
|
1022
|
+
operatorTurnLifecycle.get(chat.sessionID) === "cancelled") &&
|
|
1023
|
+
await runtimeBridge.continuationCheckpoint(chat.sessionID) === undefined) {
|
|
983
1024
|
operatorTurnLifecycle.set(chat.sessionID, "historical");
|
|
984
1025
|
const messageID = typeof chat.messageID === "string" && chat.messageID.length > 0
|
|
985
1026
|
? 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
|
`;
|
|
@@ -166,7 +168,19 @@ report that specific limitation for a scope/design decision instead of inventing
|
|
|
166
168
|
The proposal must map every original requirement ID to approach and validation, explicitly list uncovered IDs, preserve
|
|
167
169
|
negative-condition handling, and include exact read/write/unit/GoalDeclaration plans and budget estimates. Its summary is
|
|
168
170
|
not the source of truth and proposal reads are not acceptance evidence. Compare the submitted proposal directly with the
|
|
169
|
-
original request.
|
|
171
|
+
original request. For a known defect in a submitted, unapproved proposal, the root may call
|
|
172
|
+
${profile.toolPrefix}revise_operator_proposal with revision_json containing the current proposal_id, revision, content_hash,
|
|
173
|
+
a substantive rationale, and 1..32 allowlisted {op,path,value} patches. Follow its exact schema; do not resend the full packet.
|
|
174
|
+
Correct coverage/uncovered, observed surface claims, negative handling, unit/milestone/proof mapping, or shrink/normalize reads
|
|
175
|
+
using only the completed investigation's evidence. Ordered requirements/acceptance, references, goal binding, budgets,
|
|
176
|
+
criterion identities/defaults, Git authority and the exact write union stay fixed; execution unit count cannot increase.
|
|
177
|
+
Successful and invalid patch attempts share the remaining submission allowance. Revision grants no new read, child or worker
|
|
178
|
+
and requires no live proposal child. The host atomically records provenance and derives the next revision/hash.
|
|
179
|
+
Approval preparation closes revision even if preparation later fails. Preserve that pinned plan for approval recovery.
|
|
180
|
+
Never claim worker tests prove root-owned push/global apply or user-only acceptance; keep those obligations explicitly pending
|
|
181
|
+
until their actual evidence exists. If existing contracts cannot express a required obligation, keep it uncovered and report
|
|
182
|
+
the exact limitation instead of substituting a passing test. A root patch is not acceptance or automatic approval.
|
|
183
|
+
Compare every original requirement again after revision. Only the root may call ${profile.toolPrefix}approve_operator_proposal with the exact proposal_id,
|
|
170
184
|
revision, content_hash, ordered compared_requirement_ids, decision=approve, and a substantive comparison rationale.
|
|
171
185
|
Uncovered requirements, stale hashes/revisions, another root, widened scope, rewritten acceptance, or reset proposal
|
|
172
186
|
accounting must be rejected. Hash matching records identity; it never replaces this semantic root decision. Approval alone
|
|
@@ -219,7 +233,9 @@ directly (fast path); a larger plan returns ${operator}. Do not launch another i
|
|
|
219
233
|
Do not manually reconstruct, translate, shorten, or append to that returned prompt. Do not add task_id to resume a
|
|
220
234
|
fresh dispatch. If the exact Task is no longer in context, call operator_next once to retrieve it rather than guess.
|
|
221
235
|
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.
|
|
236
|
+
original objective, acceptance, and unit count. For operator-goal-field-invalid, use the exact expected enum or required
|
|
237
|
+
field value at the diagnosed pointer; add a missing field without replacing defaults, criteria, budgets or the whole declaration.
|
|
238
|
+
Do not regenerate the full plan or send placeholder/probe plans. If it reports
|
|
223
239
|
an immutable active contract, read operator_status and reuse the existing next Task. Cancel only when an actual scope
|
|
224
240
|
change or explicit stop requires it; do not cancel/recreate an unchanged plan to work around an admission error.
|
|
225
241
|
Cancellation does not close the accepted user order. A replacement repair plan must keep the exact previous ordered
|