sortie-dogs 0.10.0 → 0.10.2
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 +43 -27
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/operator-proposal.d.ts +10 -0
- package/dist/core/operator-proposal.js +135 -23
- package/dist/core/operator-runtime.js +16 -0
- package/dist/plugin/profiled.js +78 -9
- package/dist/runtime-assets-v010.d.ts +1 -0
- package/dist/runtime-assets-v010.js +59 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -59,9 +59,13 @@ See [configuration details](#configuration) for model selection and other setup
|
|
|
59
59
|
> **Project status: Beta.** v0.10.x is under active stabilization. Runtime
|
|
60
60
|
> behavior, configuration, and runtime assets may still change before 1.0.
|
|
61
61
|
|
|
62
|
-
[](https://github.com/zufall-upon/Sortie-dogs/releases/latest)
|
|
63
|
+
[](https://www.npmjs.com/package/sortie-dogs)
|
|
64
|
+
[](https://github.com/zufall-upon/Sortie-dogs/actions/workflows/test.yml)
|
|
65
|
+
[](https://opencode.ai/)
|
|
66
|
+
[](https://www.typescriptlang.org/)
|
|
67
|
+
[](https://www.npmjs.com/package/sortie-dogs)
|
|
68
|
+
[](LICENSE)
|
|
65
69
|
|
|
66
70
|

|
|
67
71
|
|
|
@@ -70,7 +74,7 @@ bounded implementation, canonical validation, and evidence-backed completion.
|
|
|
70
74
|
|
|
71
75
|
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [テスト実行](docs/testing.md) · [CLI testing](docs/cli-testing.md)
|
|
72
76
|
|
|
73
|
-
Release: [v0.10.
|
|
77
|
+
Release: [v0.10.2](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.10.2)
|
|
74
78
|
|
|
75
79
|
## Latest local benchmark case study
|
|
76
80
|
|
|
@@ -88,38 +92,50 @@ Run configuration was fixed per product configuration:
|
|
|
88
92
|
children on `openai/gpt-5.6-sol` / `medium`, with the pinned Sortie package and runtime assets.
|
|
89
93
|
No Luna, Astra, or Opus messages were observed in these trials.
|
|
90
94
|
|
|
91
|
-
| Metric · one frozen task | Bare OpenCode | Sortie v0.9.12 | Sortie v0.10.
|
|
92
|
-
| --- | ---: | ---: | ---: |
|
|
93
|
-
| Attempts needed | 3 | 5 | 1 |
|
|
94
|
-
| Completed runs compared | 3 | 3 | 1 |
|
|
95
|
-
| Verified PASS | 0/3 | 0/3 | 0/1 |
|
|
96
|
-
| Task checks · F2P | 11.1% · 3/27 | 85.2% · 23/27 | 88.9% · 8/9 |
|
|
97
|
-
| Retained checks · P2P | 282/282 | 282/282 | 94/94 |
|
|
98
|
-
| Median agent wall | 24.5 min | 25.7 min |
|
|
99
|
-
| Median model steps | 43 | 39 |
|
|
100
|
-
| Implementation child sessions · total | 0 | 13 |
|
|
101
|
-
| Estimated API-equivalent cost · median completed run | $3.53 | $2.85 | **$
|
|
102
|
-
| Estimated cost · completed runs | $10.69 | $9.74 | **$
|
|
103
|
-
| Additional interrupted-attempt cost | $0 | $6.20 | $0 |
|
|
104
|
-
| Total cost to acquire completed runs | $10.69 | $15.94 | **$
|
|
95
|
+
| Metric · one frozen task | Bare OpenCode | Sortie v0.9.12 | Sortie v0.10.1 RC qualification |
|
|
96
|
+
| --- | ---: | ---: | ---: |
|
|
97
|
+
| Attempts needed | 3 | 5 | 1 |
|
|
98
|
+
| Completed runs compared | 3 | 3 | 1 |
|
|
99
|
+
| Verified PASS | 0/3 | 0/3 | 0/1 |
|
|
100
|
+
| Task checks · F2P | 11.1% · 3/27 | 85.2% · 23/27 | 88.9% · 8/9 |
|
|
101
|
+
| Retained checks · P2P | 282/282 | 282/282 | 94/94 |
|
|
102
|
+
| Median agent wall | 24.5 min | 25.7 min | 23.2 min · n=1 |
|
|
103
|
+
| Median model steps | 43 | 39 | 41 · CLI stream only |
|
|
104
|
+
| Implementation child sessions · total | 0 | 13 | 3 |
|
|
105
|
+
| Estimated API-equivalent cost · median completed run | $3.53 | $2.85 | **$4.48** · n=1 |
|
|
106
|
+
| Estimated cost · completed runs | $10.69 | $9.74 | **$4.48** |
|
|
107
|
+
| Additional interrupted-attempt cost · same fixed snapshot | $0 | $6.20 | $0 |
|
|
108
|
+
| Total cost to acquire completed runs · same fixed snapshot | $10.69 | $15.94 | **$4.48** |
|
|
105
109
|
|
|
106
110
|
All three Bare runs passed 1/9 task checks. The three completed Sortie runs passed 7/9, 8/9,
|
|
107
111
|
and 8/9. Every compared candidate retained 94/94 prior checks, but every official verifier still
|
|
108
112
|
returned reward 0. The two interrupted Sortie attempts are excluded from completed-run quality,
|
|
109
113
|
time, and cost aggregates; their attempt count and estimated cost remain visible above.
|
|
110
114
|
|
|
111
|
-
The v0.10 value is a standalone qualification-only reference, not a fourth matched run or a
|
|
112
|
-
FrontierHarness leaderboard result. It used no fresh Bare control and a localized Docker-free
|
|
113
|
-
verifier.
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
The v0.10.1 RC value is a standalone qualification-only reference, not a fourth matched run or a
|
|
116
|
+
FrontierHarness leaderboard result. It used no fresh Bare control and a localized Docker-free
|
|
117
|
+
verifier. The RC label identifies an unreleased working-tree snapshot; the benchmark tarball's
|
|
118
|
+
package metadata remained `0.10.0`, fixed by package SHA-256
|
|
119
|
+
`67582ee023c3bd57813262e292c067a3f90f30d61c01d8caf6f91f09ae78f61d`.
|
|
120
|
+
|
|
121
|
+
Its estimated API-equivalent cost is **$4.478613**: $3.23 Sol and $1.25 Terra when rounded to cents.
|
|
122
|
+
The audit prices all root and descendant assistant messages: 84 priced requests, 4,215,698 tokens,
|
|
123
|
+
100% pricing coverage, and no unpriced requests. It uses the product's 2026-09-14 Standard schedule
|
|
124
|
+
per request, including cached input, reasoning as output, and the long-context rate band. The earlier
|
|
125
|
+
delivery-incomplete package was a different source snapshot (`32dea0a3…`): it spent **$4.276578**
|
|
126
|
+
across 90 priced requests and 3,704,649 tokens, with no terminal outcome or verifier. It is not folded
|
|
127
|
+
into this RC's same-snapshot acquisition cost. Observed spend across both distinct snapshots was
|
|
128
|
+
**$8.755191**, which is development spend rather than this RC's acquisition cost. Historical Bare and
|
|
129
|
+
v0.9.12 costs retain their 2026-07-30 schedule; their cost cells are not same-rate comparisons with
|
|
130
|
+
this RC estimate.
|
|
116
131
|
|
|
117
132
|

|
|
118
133
|
|
|
119
|
-
Cost
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
These are API-equivalent
|
|
134
|
+
Cost audits use deduplicated root and descendant session tokens, grouped by the model that produced
|
|
135
|
+
each message. Historical values retain their frozen 2026-07-30 short-context schedule; the RC uses
|
|
136
|
+
the product's 2026-09-14 per-request schedule described above. Completed-run cost shows execution
|
|
137
|
+
efficiency; same-snapshot acquisition cost adds interrupted attempts. These are API-equivalent
|
|
138
|
+
estimates, not invoices.
|
|
123
139
|
|
|
124
140
|
The product objective is **more verified outcomes per unit of cost and time without weakening the
|
|
125
141
|
accepted goal**. This small, single-task local case study does not establish that claim, isolate
|
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-ref1-proposal1-review-remediation1-surface3";
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.10.0-v0912-language4-cost-rpt10-compaction-ref1-proposal1-review-remediation1-surface3-proposal-recovery2-quality1-terminal1";
|
|
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-ref1-proposal1-review-remediation1-surface3";
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.10.0-v0912-language4-cost-rpt10-compaction-ref1-proposal1-review-remediation1-surface3-proposal-recovery2-quality1-terminal1";
|
|
@@ -95,6 +95,9 @@ export declare class OperatorProposalRuntime {
|
|
|
95
95
|
readonly projectRoot: string;
|
|
96
96
|
constructor(projectRoot: string, profile: RuntimeProfile);
|
|
97
97
|
private file;
|
|
98
|
+
private spendFile;
|
|
99
|
+
private readSpend;
|
|
100
|
+
private saveSpend;
|
|
98
101
|
read(root: string): Promise<OperatorProposalState | undefined>;
|
|
99
102
|
private readUnlocked;
|
|
100
103
|
private save;
|
|
@@ -117,9 +120,16 @@ export declare class OperatorProposalRuntime {
|
|
|
117
120
|
accountRead(root: string, actor: string, path?: string): Promise<OperatorProposalState>;
|
|
118
121
|
private accountReadUnlocked;
|
|
119
122
|
submit(root: string, actor: string, raw: unknown): Promise<OperatorProposalState>;
|
|
123
|
+
submitJSON(root: string, actor: string, source: string): Promise<OperatorProposalState>;
|
|
120
124
|
private submitUnlocked;
|
|
121
125
|
approve(root: string, raw: unknown, commit?: boolean): Promise<OperatorProposalState>;
|
|
122
126
|
private approveUnlocked;
|
|
127
|
+
/**
|
|
128
|
+
* Release a pre-approval proposal grant on explicit root cancellation. Without this the root holds an
|
|
129
|
+
* immutable investigating intent whose admitted child can no longer bind, so no retry path exists at all.
|
|
130
|
+
* An approved proposal is already connected to the execution lane and stays durable.
|
|
131
|
+
*/
|
|
132
|
+
discardPreApproval(root: string): Promise<OperatorProposalState | undefined>;
|
|
123
133
|
required(root: string): Promise<OperatorProposalState>;
|
|
124
134
|
assertExecutionApproved(root: string, planHash: string): Promise<void>;
|
|
125
135
|
private requiredUnlocked;
|
|
@@ -120,23 +120,63 @@ function parseIntent(value) {
|
|
|
120
120
|
function parsePacket(value, state) {
|
|
121
121
|
if (Buffer.byteLength(JSON.stringify(value)) > 128 * 1024)
|
|
122
122
|
throw new Error("operator-proposal-too-large");
|
|
123
|
-
if (!record(value)
|
|
124
|
-
return proposalError("/", "operator-proposal-invalid", "
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
123
|
+
if (!record(value))
|
|
124
|
+
return proposalError("/", "operator-proposal-invalid", "object", { expected: "object", actual_type: jsonType(value) });
|
|
125
|
+
const diagnostics = [];
|
|
126
|
+
const invalid = (pointer, rule, expected, actual, code = "operator-proposal-invalid") => {
|
|
127
|
+
diagnostics.push({ document: "proposal", pointer, code, rule, repair_kind: "repair-field",
|
|
128
|
+
repair_paths: [pointer], expected, actual_type: jsonType(actual) });
|
|
129
|
+
};
|
|
130
|
+
const fields = ["schema_version", "revision", "coverage", "existing_surface", "uncovered", "negative_handling", "read_scope", "write_scope", "budget_estimate", "plan"];
|
|
131
|
+
for (const key of Object.keys(value).filter(key => !fields.includes(key)).sort()) {
|
|
132
|
+
invalid(`/${pointerToken(key)}`, "unknown-field", "remove-unknown-field", value[key]);
|
|
133
|
+
}
|
|
134
|
+
for (const key of fields)
|
|
135
|
+
if (!Object.hasOwn(value, key))
|
|
136
|
+
invalid(`/${key}`, "required", "required-field", undefined);
|
|
137
|
+
if (Object.hasOwn(value, "schema_version") && value.schema_version !== "0.1") {
|
|
138
|
+
invalid("/schema_version", "schema-version", 'literal-"0.1"', value.schema_version);
|
|
139
|
+
}
|
|
140
|
+
if (Object.hasOwn(value, "revision") && (!Number.isSafeInteger(value.revision) || value.revision < 1)) {
|
|
141
|
+
invalid("/revision", "positive-integer", "positive-integer", value.revision, "operator-proposal-revision-invalid");
|
|
142
|
+
}
|
|
143
|
+
for (const key of ["coverage", "existing_surface", "uncovered", "negative_handling", "read_scope", "write_scope"]) {
|
|
144
|
+
if (!Object.hasOwn(value, key))
|
|
145
|
+
continue;
|
|
146
|
+
const items = value[key];
|
|
147
|
+
if (!Array.isArray(items)) {
|
|
148
|
+
invalid(`/${key}`, "array", "array", items);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (key === "read_scope" || key === "write_scope")
|
|
152
|
+
items.forEach((item, index) => {
|
|
153
|
+
if (!normalizedRelativePath(item))
|
|
154
|
+
invalid(`/${key}/${index}`, "normalized-relative-path", "nonempty repository-relative path; forward slashes; no trailing slash, dot segments, traversal, or absolute path", item);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (Object.hasOwn(value, "budget_estimate")) {
|
|
158
|
+
const budget = value.budget_estimate;
|
|
159
|
+
if (!record(budget))
|
|
160
|
+
invalid("/budget_estimate", "object", "object", budget);
|
|
161
|
+
else {
|
|
162
|
+
const keys = ["proposal_reads", "execution_units"];
|
|
163
|
+
for (const key of Object.keys(budget).filter(key => !keys.includes(key)).sort()) {
|
|
164
|
+
invalid(`/budget_estimate/${pointerToken(key)}`, "unknown-field", "remove-unknown-field", budget[key]);
|
|
165
|
+
}
|
|
166
|
+
for (const key of keys)
|
|
167
|
+
if (!Number.isSafeInteger(budget[key])) {
|
|
168
|
+
invalid(`/budget_estimate/${key}`, Object.hasOwn(budget, key) ? "integer" : "required", "integer", budget[key]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
136
171
|
}
|
|
172
|
+
if (diagnostics.length > 0)
|
|
173
|
+
throw new OperatorContractError(diagnostics);
|
|
174
|
+
// The shape checks above are batched for repair; semantic checks below remain strict.
|
|
175
|
+
const coverage = value.coverage, uncovered = value.uncovered, negatives = value.negative_handling;
|
|
176
|
+
const budget = value.budget_estimate;
|
|
137
177
|
const requirementIDs = new Set(state.intent.requirements.map(item => item.id));
|
|
138
178
|
const coverageIDs = new Set(), uncoveredIDs = new Set();
|
|
139
|
-
for (const item of
|
|
179
|
+
for (const item of coverage) {
|
|
140
180
|
if (!record(item) || !exact(item, ["requirement_id", "approach", "validation"]) || !identifier(item.requirement_id) ||
|
|
141
181
|
!requirementIDs.has(item.requirement_id) || coverageIDs.has(item.requirement_id) || !text(item.approach) || !text(item.validation))
|
|
142
182
|
throw new Error("operator-proposal-coverage-invalid");
|
|
@@ -159,7 +199,7 @@ function parsePacket(value, state) {
|
|
|
159
199
|
}
|
|
160
200
|
if ([...coverageIDs].some(id => !surfaced.has(id)))
|
|
161
201
|
throw new Error("operator-proposal-existing-surface-incomplete");
|
|
162
|
-
for (const item of
|
|
202
|
+
for (const item of uncovered) {
|
|
163
203
|
if (!record(item) || !exact(item, ["requirement_id", "reason"]) || !identifier(item.requirement_id) || !requirementIDs.has(item.requirement_id) ||
|
|
164
204
|
coverageIDs.has(item.requirement_id) || uncoveredIDs.has(item.requirement_id) || !text(item.reason))
|
|
165
205
|
throw new Error("operator-proposal-uncovered-invalid");
|
|
@@ -169,7 +209,7 @@ function parsePacket(value, state) {
|
|
|
169
209
|
throw new Error("operator-proposal-requirement-coverage-incomplete");
|
|
170
210
|
const negativeIDs = new Set(state.intent.requirements.filter(item => item.kind === "negative").map(item => item.id));
|
|
171
211
|
const handled = new Set();
|
|
172
|
-
for (const item of
|
|
212
|
+
for (const item of negatives) {
|
|
173
213
|
if (!record(item) || !exact(item, ["requirement_id", "handling"]) || !identifier(item.requirement_id) || !negativeIDs.has(item.requirement_id) || handled.has(item.requirement_id) || !text(item.handling))
|
|
174
214
|
throw new Error("operator-proposal-negative-handling-invalid");
|
|
175
215
|
handled.add(item.requirement_id);
|
|
@@ -204,9 +244,17 @@ function parsePacket(value, state) {
|
|
|
204
244
|
if (plan.acceptance.length !== acceptance.length || plan.acceptance.some((item, index) => item !== acceptance[index]))
|
|
205
245
|
throw new Error("operator-proposal-acceptance-rewritten");
|
|
206
246
|
const writes = [...new Set(plan.units.flatMap(unit => unit.write))].sort();
|
|
207
|
-
if (JSON.stringify([...new Set(value.write_scope)].sort()) !== JSON.stringify(writes))
|
|
208
|
-
|
|
209
|
-
|
|
247
|
+
if (JSON.stringify([...new Set(value.write_scope)].sort()) !== JSON.stringify(writes)) {
|
|
248
|
+
proposalError("/write_scope", "operator-proposal-write-scope-mismatch", "exact-union-of-unit-write-scopes", { repair_paths: ["/write_scope", "/plan/units"] });
|
|
249
|
+
}
|
|
250
|
+
for (const [unitIndex, unit] of plan.units.entries())
|
|
251
|
+
for (const [pathIndex, path] of unit.read.entries()) {
|
|
252
|
+
const executionReads = [...value.read_scope, ...plan.units.slice(0, unitIndex + 1).flatMap(item => item.write)];
|
|
253
|
+
if (!executionReads.some(scope => path === scope || path.startsWith(`${scope}/`))) {
|
|
254
|
+
proposalError(`/plan/units/${unitIndex}/read/${pathIndex}`, "operator-proposal-unit-read-scope-expanded", "unit-input-within-declared-read-scope-or-generated-write-scope");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (budget.proposal_reads !== state.read_count || budget.execution_units !== plan.units.length)
|
|
210
258
|
throw new Error("operator-proposal-budget-estimate-mismatch");
|
|
211
259
|
return { ...structuredClone(materialized), plan };
|
|
212
260
|
}
|
|
@@ -233,6 +281,37 @@ export class OperatorProposalRuntime {
|
|
|
233
281
|
this.projectRoot = resolve(projectRoot);
|
|
234
282
|
}
|
|
235
283
|
file(root) { return join(this.projectRoot, this.profile.stateDirectory, "operator-proposals", `${hash(root)}.json`); }
|
|
284
|
+
spendFile(root) { return join(this.projectRoot, this.profile.stateDirectory, "operator-proposals", `${hash(root)}.spend.json`); }
|
|
285
|
+
async readSpend(root) {
|
|
286
|
+
let source;
|
|
287
|
+
try {
|
|
288
|
+
source = await readFile(this.spendFile(root), "utf8");
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (error.code === "ENOENT") {
|
|
292
|
+
return { schema_version: "0.1", profile: this.profile.id, root_session_id: root, reads: 0, submissions: 0 };
|
|
293
|
+
}
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
const spend = JSON.parse(source);
|
|
297
|
+
if (spend.schema_version !== "0.1" || spend.profile !== this.profile.id || spend.root_session_id !== root ||
|
|
298
|
+
!Number.isSafeInteger(spend.reads) || spend.reads < 0 || !Number.isSafeInteger(spend.submissions) || spend.submissions < 0) {
|
|
299
|
+
throw new Error("operator-proposal-spend-invalid");
|
|
300
|
+
}
|
|
301
|
+
return spend;
|
|
302
|
+
}
|
|
303
|
+
async saveSpend(spend) {
|
|
304
|
+
const directory = join(this.projectRoot, this.profile.stateDirectory, "operator-proposals");
|
|
305
|
+
await mkdir(directory, { recursive: true });
|
|
306
|
+
const temporary = `${this.spendFile(spend.root_session_id)}.${randomUUID()}.tmp`;
|
|
307
|
+
try {
|
|
308
|
+
await writeFile(temporary, JSON.stringify(spend), { flag: "wx", mode: 0o600 });
|
|
309
|
+
await rename(temporary, this.spendFile(spend.root_session_id));
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
236
315
|
async read(root) {
|
|
237
316
|
return this.serial(root, () => this.readUnlocked(root));
|
|
238
317
|
}
|
|
@@ -304,6 +383,11 @@ export class OperatorProposalRuntime {
|
|
|
304
383
|
"Git mutationが明示要件の時だけplan.git_lifecycleを追加する。exact shape: {\"branch_create\":{\"branch\":\"validated destination branch\",\"start_ref\":\"existing commit/ref\"},\"commit\":{\"message\":\"single-line commit message\"},\"post_commit_validation\":[\"exact declared validation command\"]}。post_commit_validationは既存canonical command identityでgoal criterion commandと一致し、final unit.validationの同順・連続suffixでなければならない。全non-post validationを先に実行し、全source write完了後にsuffixを実行する。hostはworker spend前にclean root、existing start_ref、nonexisting destinationを検証し、fixed argvでbranch作成する。suffix先頭command直前に全unit.write union内変更を明示stageしてcommitし、以後のsource writeを拒否する。既存validation executorが各post-commit commandのfresh evidenceを記録し、全evidenceなしのroot acceptanceは禁止。任意Git command、git add -A、commit -a、amend、force、push、既存branch上書きは禁止。git_lifecycle省略時のGit mutationはない。",
|
|
305
384
|
"acceptance_indicesは0-based。各unitのacceptance_indicesに置く全indexについて、そのacceptance_proof[index]が参照するcriterionのresolved validation_commandを同じunit.validationへ文字列完全一致で最低1個含める。範囲外index禁止。さらに各unitはそれ以前のunitにないgoal criterion validation_commandを少なくとも1個validationへ含める。同じvalidation commandだけを全unitで再利用禁止。順序付き2 unitなら例としてunit 1に中間状態を許すcommand、unit 2に最終commandを割り当てる。",
|
|
306
385
|
"existing_surfaceは各covered requirementにつき最低1件。対象codebaseで、その要件が制約・変更する既存構文や既存経路を実際にReadして特定し、そのfileのproject相対pathと見つけた形(form)を書く。formは文面の言い換えでなく、observedした構文形・node種別・dispatch分岐を書く。生成・宣言・束縛の経路と変更・代入の経路は別項目として挙げる。単数形しか要件に書かれていなくても、grammarやdispatchに複数値・分配・入れ子・暗黙形があればそれも挙げる。既存形が無いと判断した場合も、探索したfileのpathと「既存形なし」の根拠をformへ書く。pathはこのTaskで実際にReadしたfileでなければhostが拒否する。",
|
|
386
|
+
"調査順: authoritative_refsと既知entrypointから始める。対象pathが分かっているならdirectory巡回を挟まず直接そこへ進み、path未知の時だけ許可scope内のdirectoryやindexをReadして実在を確認する。名前を推測しただけのReadをしない。各requirementについて、それが変更する既存経路、symbol、validation oracle、build recipeを結び付ける。巨大な概念書やtest群を既定で全文読込せず、既知location・目次・周辺範囲から必要なoffset/limitを選ぶ。部分Readは探索手段であって完了条件ではない。既存分岐、関連呼出先、値の表現、負要件の影響範囲が不明なら範囲を広げ、必要ならfile全体を読む。読込量の少なさ自体を達成度にしない。authoritative Makefile、言語のgenerator directive、package/build scriptは後回しにせず関連実装経路と併せて早期に確認する。同じ内容の再読は、新しい未解決点、参照先、範囲不足、source変化のいずれかを理由とする。Read以外のtoolは要求しない。",
|
|
387
|
+
"記述は既存fieldで簡潔に行う。raw sourceやlogの全文再掲、同じplanの自然文による二重転記をしない。ただしcoverage、existing_surface、ordered validation、read/write、negative_handling、uncoveredは省略しない。初回提出で全requirementを扱えるproposalを目指し、調査できなかった点はuncoveredへ正直に残す。薄いproposalを出してrootへ追加調査を戻す往復を前提にしない。",
|
|
388
|
+
"提出前に正本commandと照合する。観測済みのexecutable、引数、test path、build target範囲をそのまま保持し、全体buildを一部targetへ縮めたり未確認の絶対executable pathを補完したりしない。検証script自身の副作用まで確認し、全生成物をunit.writeへ宣言する。unit.readはread_scopeまたは宣言済みwrite_scope内。再提出は診断fieldとactual_readsだけを直し、既に正しいcommand・scopeを再生成しない。",
|
|
389
|
+
"unit.validationとcriterionのvalidation_commandには実行commandだけを書く。手動GUI確認、root専有review、運用上の予算会計を説明文やラベル付きの疑似commandへ変換しない。fixture・新規Sessionの自動生成は実Backendの既存Session表示の証拠とは限らず、oracleが実際に確認する対象と副作用を照合する。現行契約で表現できない要件はuncoveredへ理由を残し、無関係なtestを全要件の証明として割り当てない。",
|
|
390
|
+
"status=submittedが返ったら、この調査Taskは完了。追加toolなしで親へ返す。汎用の続行指示が来てもoperator_nextやworker起動へ進まない。提出は承認でも実行許可でもない。",
|
|
307
391
|
"許可sourceをReadで1回以上調査後、返答前にproposal packetをsortie_v010_submit_operator_proposalへ提出する。invalid-proposal返却時だけ、そのcodeに該当するfieldを修正する。返却されたactual_readsはhostのcanonical accountingであり、再提出時はcodeに関係なくbudget_estimate.proposal_readsをその値へ一致させる。これはread budgetをresetせず、成功/失敗tool表示からcountを推測しない。失敗したReadが一律に未計上とも仮定しない。有限submission budget内で再提出する。proseだけ返して終了禁止。未対応要件はuncoveredへ明示する。承認・実行を主張しない。"].join("\n") };
|
|
308
392
|
}
|
|
309
393
|
/** Root-visible bounded handle; the canonical prompt remains only in durable host state. */
|
|
@@ -357,9 +441,10 @@ export class OperatorProposalRuntime {
|
|
|
357
441
|
throw new Error("operator-proposal-active-intent-immutable");
|
|
358
442
|
throw new Error("operator-proposal-new-intent-requires-explicit-root-revision");
|
|
359
443
|
}
|
|
444
|
+
const spend = await this.readSpend(root);
|
|
360
445
|
const state = { schema_version: "0.1", profile: this.profile.id, root_session_id: root,
|
|
361
446
|
intent_id: `intent-${intentHash.slice(0, 24)}`, intent_hash: intentHash, intent, created_at: new Date().toISOString(), phase: "investigating",
|
|
362
|
-
goal_binding: null, proposal_call_id: null, proposal_session_id: null, read_count:
|
|
447
|
+
goal_binding: null, proposal_call_id: null, proposal_session_id: null, read_count: spend.reads, read_paths: [], submission_count: spend.submissions, proposal_id: null,
|
|
363
448
|
proposal_revision: null, proposal_hash: null, proposal: null, approval_rationale: null };
|
|
364
449
|
await this.save(state);
|
|
365
450
|
return state;
|
|
@@ -448,9 +533,19 @@ export class OperatorProposalRuntime {
|
|
|
448
533
|
return state;
|
|
449
534
|
}
|
|
450
535
|
async submit(root, actor, raw) {
|
|
451
|
-
return this.serial(root, () => this.submitUnlocked(root, actor, raw));
|
|
536
|
+
return this.serial(root, () => this.submitUnlocked(root, actor, () => raw));
|
|
537
|
+
}
|
|
538
|
+
async submitJSON(root, actor, source) {
|
|
539
|
+
return this.serial(root, () => this.submitUnlocked(root, actor, () => {
|
|
540
|
+
try {
|
|
541
|
+
return JSON.parse(source);
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
return proposalError("/", "operator-proposal-json-invalid", "json");
|
|
545
|
+
}
|
|
546
|
+
}));
|
|
452
547
|
}
|
|
453
|
-
async submitUnlocked(root, actor,
|
|
548
|
+
async submitUnlocked(root, actor, readPacket) {
|
|
454
549
|
const state = await this.requiredUnlocked(root);
|
|
455
550
|
if (state.proposal_session_id !== actor || state.phase !== "investigating")
|
|
456
551
|
throw new Error("operator-proposal-submit-grant-invalid");
|
|
@@ -458,7 +553,7 @@ export class OperatorProposalRuntime {
|
|
|
458
553
|
throw new Error("operator-proposal-submission-budget-exhausted");
|
|
459
554
|
let packet;
|
|
460
555
|
try {
|
|
461
|
-
packet = parsePacket(
|
|
556
|
+
packet = parsePacket(readPacket(), state);
|
|
462
557
|
}
|
|
463
558
|
catch (error) {
|
|
464
559
|
state.submission_count++;
|
|
@@ -493,6 +588,23 @@ export class OperatorProposalRuntime {
|
|
|
493
588
|
}
|
|
494
589
|
return state;
|
|
495
590
|
}
|
|
591
|
+
/**
|
|
592
|
+
* Release a pre-approval proposal grant on explicit root cancellation. Without this the root holds an
|
|
593
|
+
* immutable investigating intent whose admitted child can no longer bind, so no retry path exists at all.
|
|
594
|
+
* An approved proposal is already connected to the execution lane and stays durable.
|
|
595
|
+
*/
|
|
596
|
+
async discardPreApproval(root) {
|
|
597
|
+
return this.serial(root, async () => {
|
|
598
|
+
const state = await this.readUnlocked(root);
|
|
599
|
+
if (!state || state.phase === "approved")
|
|
600
|
+
return undefined;
|
|
601
|
+
await this.saveSpend({ schema_version: "0.1", profile: this.profile.id, root_session_id: root,
|
|
602
|
+
reads: state.read_count, submissions: state.submission_count });
|
|
603
|
+
await rm(this.file(root), { force: true });
|
|
604
|
+
this.states.delete(root);
|
|
605
|
+
return state;
|
|
606
|
+
});
|
|
607
|
+
}
|
|
496
608
|
async required(root) { const state = await this.read(root); if (!state)
|
|
497
609
|
throw new Error("operator-proposal-missing"); return state; }
|
|
498
610
|
async assertExecutionApproved(root, planHash) {
|
|
@@ -41,6 +41,15 @@ export function operatorGitPathAuthorized(path, scopes, platform = process.platf
|
|
|
41
41
|
}
|
|
42
42
|
const contractError = (diagnostic) => { throw new OperatorContractError([diagnostic]); };
|
|
43
43
|
const planError = (pointer, code, rule, repair_kind = "repair-field") => contractError({ document: "plan", pointer, code, rule, repair_kind });
|
|
44
|
+
function rejectValidationAnnotation(command, pointer) {
|
|
45
|
+
// A bare multi-character label followed by ':' and whitespace is an instruction annotation,
|
|
46
|
+
// not a supported executable validation. Do not interpret or strip it into a fake passing oracle.
|
|
47
|
+
// This is deliberately not a shell parser or a claim of semantic proof; quoted commands, drive
|
|
48
|
+
// paths, URLs in arguments, and shell syntax remain the executor's responsibility.
|
|
49
|
+
if (/^\s*[A-Za-z][A-Za-z0-9_-]*(?:\s+[A-Za-z][A-Za-z0-9_-]*)*:(?:\s|$)/u.test(command)) {
|
|
50
|
+
planError(pointer, "operator-validation-annotation-invalid", "executable-command-not-instruction-label");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
44
53
|
function strings(value, nonempty = false) {
|
|
45
54
|
return Array.isArray(value) && (!nonempty || value.length > 0) && value.every(text);
|
|
46
55
|
}
|
|
@@ -104,6 +113,12 @@ export function parseOperatorPlan(value) {
|
|
|
104
113
|
return contractError({ document: "plan", pointer: `/goal_declaration/criteria/${proofCommands.size}/goal_validation_command`,
|
|
105
114
|
code: "operator-goal-command-required", rule: "declared-validation-command-for-criterion", repair_kind: "repair-proof-mapping",
|
|
106
115
|
repair_paths: [`/goal_declaration/criteria/${proofCommands.size}/goal_validation_command`] });
|
|
116
|
+
const commandOwner = raw.goal_validation_command != null || raw.validation_command != null ? raw
|
|
117
|
+
: record(declaration.defaults) && (declaration.defaults.goal_validation_command != null || declaration.defaults.validation_command != null)
|
|
118
|
+
? declaration.defaults : declaration;
|
|
119
|
+
const commandPrefix = commandOwner === raw ? `/goal_declaration/criteria/${proofCommands.size}`
|
|
120
|
+
: commandOwner === declaration ? "/goal_declaration" : "/goal_declaration/defaults";
|
|
121
|
+
rejectValidationAnnotation(command, `${commandPrefix}/${commandOwner.goal_validation_command != null ? "goal_validation_command" : "validation_command"}`);
|
|
107
122
|
proofCommands.set(id, command);
|
|
108
123
|
}
|
|
109
124
|
if (!Array.isArray(value.acceptance_proof) || value.acceptance_proof.length !== value.acceptance.length ||
|
|
@@ -119,6 +134,7 @@ export function parseOperatorPlan(value) {
|
|
|
119
134
|
!identifier(unit.id) || ids.has(unit.id) || !text(unit.title) || !text(unit.objective) ||
|
|
120
135
|
!strings(unit.read) || !strings(unit.write, true) || !strings(unit.validation, true))
|
|
121
136
|
return planError(`/units/${unitIndex}`, "operator-unit-invalid", "operator-unit-shape");
|
|
137
|
+
unit.validation.forEach((command, index) => rejectValidationAnnotation(command, `/units/${unitIndex}/validation/${index}`));
|
|
122
138
|
ids.add(unit.id);
|
|
123
139
|
if (!Array.isArray(unit.acceptance_indices) || unit.acceptance_indices.length === 0 ||
|
|
124
140
|
unit.acceptance_indices.some(index => !Number.isSafeInteger(index) || index < 0 || index >= acceptanceCount ||
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -286,8 +286,11 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
286
286
|
: { ...(record(stringSchema) ? stringSchema : { type: "string" }), description: OPERATOR_INTENT_CONTRACT };
|
|
287
287
|
const proposalContract = "proposal_json must encode one JSON object with exactly these required fields and types: " +
|
|
288
288
|
'schema_version string "0.1"; revision positive integer, for example "revision":1, never string "revision":"1"; ' +
|
|
289
|
-
"coverage array of {requirement_id:string,approach:string,validation:string};
|
|
290
|
-
"
|
|
289
|
+
"coverage array of {requirement_id:string,approach:string,validation:string}; " +
|
|
290
|
+
"existing_surface array of {requirement_id:string,path:string,form:string}, with at least one observed form per covered requirement and each path actually Read by this child; " +
|
|
291
|
+
"uncovered array of {requirement_id:string,reason:string}; " +
|
|
292
|
+
"negative_handling array of {requirement_id:string,handling:string}, containing only and all IDs whose intent kind is negative; " +
|
|
293
|
+
"read_scope string array; write_scope string array; all scope and existing_surface paths must be normalized repository-relative paths with forward slashes, no trailing slash, dot segments, traversal, or absolute paths; " +
|
|
291
294
|
"budget_estimate object with proposal_reads:integer and execution_units:integer; plan object with schema_version:string, " +
|
|
292
295
|
"acceptance_proof:string[][], source_refs:string[], goal_declaration:object, units:object[], and optional git_lifecycle with exact shape " +
|
|
293
296
|
"Each unit.validation is the complete ordered execution list, not a tests-only list: commands required by observed authoritative Makefiles, language generator directives, or repository scripts for generation, build, formatting, and exact cleanup precede post-commit or canonical criterion tests. Put every required input in unit.read and every persistent or transient generated output in unit.write. Cleanup may remove only declared unit.write outputs; never approve arbitrary ignore rules or removal of undeclared paths. Do not guess a tool-specific command or output, claim an unobserved capability, or add a preparatory or cleanup command as a goal criterion unless it independently proves acceptance. The host preserves declared order and authority but does not statically discover or inject every build dependency or generator output. " +
|
|
@@ -329,7 +332,15 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
329
332
|
await control?.stopAutomaticRecovery(root);
|
|
330
333
|
const proposal = await proposals.read(root);
|
|
331
334
|
if (proposal?.phase === "investigating" && proposal.proposal_call_id !== null) {
|
|
332
|
-
|
|
335
|
+
// The admitted proposal Task already settles this reservation when its child returns. A terminal
|
|
336
|
+
// settlement is final, so an explicit cancellation must release the grant instead of failing on it.
|
|
337
|
+
try {
|
|
338
|
+
await control?.settleProposalBudget(root, proposal.intent_id, proposal.proposal_call_id, "cancelled");
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
if (!(error instanceof Error) || error.message !== "operator-proposal-budget-settlement-conflict")
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
333
344
|
}
|
|
334
345
|
const children = await operators.interrupted(root, reason);
|
|
335
346
|
for (const child of children) {
|
|
@@ -369,6 +380,14 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
369
380
|
catch (error) {
|
|
370
381
|
if (error instanceof OperatorContractError)
|
|
371
382
|
return JSON.stringify({ status: "invalid-plan", diagnostics: error.diagnostics, diagnostics_truncated: error.diagnostics_truncated });
|
|
383
|
+
// An immutable active contract is a local routing defect, not an external blocker. Return the
|
|
384
|
+
// existing durable state and its next action so the root continues instead of retrying prepare.
|
|
385
|
+
if (error instanceof Error && error.message === "operator-active-contract-immutable") {
|
|
386
|
+
return JSON.stringify({ status: "active-contract-immutable", code: error.message,
|
|
387
|
+
packet: await operatorPacket(await operators.required(context.sessionID)),
|
|
388
|
+
next_action: `This root already owns an immutable active contract. Do not resend a plan or cancel an unchanged contract: ` +
|
|
389
|
+
`read ${status} and continue the existing run's next_task_ref or next_action. Cancel only for an actual scope change or explicit stop.` });
|
|
390
|
+
}
|
|
372
391
|
throw error;
|
|
373
392
|
}
|
|
374
393
|
await registerPreparedGoal(context.sessionID, state);
|
|
@@ -425,7 +444,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
425
444
|
const proposal = await proposals.read(context.sessionID);
|
|
426
445
|
const proposalNextAction = proposal?.phase === "investigating"
|
|
427
446
|
? proposal.proposal_call_id !== null
|
|
428
|
-
? "proposal Task is already admitted; do not redispatch it or call operator_next. Continue submission repairs only in the same active claimed child. If that child terminated without submission, report the terminal proposal failure; remaining read or submission capacity does not authorize a new Task, budget reset, or replacement child"
|
|
447
|
+
? "proposal Task is already admitted; do not redispatch it or call operator_next. Continue submission repairs only in the same active claimed child. If that child terminated without submission, report the terminal proposal failure; remaining read or submission capacity does not authorize a new Task, budget reset, or replacement child. Only an explicit root decision to retry may call cancel_operator with no reason to release this grant, which discards the spent proposal accounting and never reuses the terminated child"
|
|
429
448
|
: proposal.submission_count >= proposal.intent.proposal_budget.max_submissions
|
|
430
449
|
? "proposal submission budget exhausted; do not call operator_next; report the bounded proposal failure"
|
|
431
450
|
: "proposal is not submitted; do not call operator_next; complete or repair the bounded proposal submission"
|
|
@@ -441,13 +460,29 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
441
460
|
next_action: proposalNextAction }
|
|
442
461
|
: { status: "absent", profile: profile.id });
|
|
443
462
|
} };
|
|
444
|
-
tools[cancel] = { description: "Revoke this root's operator grant and stop only its owned children before releasing core state. reason is a closed set: omit it for a plain cancellation, including replanning
|
|
463
|
+
tools[cancel] = { description: "Revoke this root's operator grant and stop only its owned children before releasing core state. Before approval it instead releases the bounded proposal grant, including one whose admitted child already terminated, so the root can begin a new investigation; it never reuses that child or restores spent proposal budget. reason is a closed set: omit it for a plain cancellation, including replanning, contract revision, and any pre-approval proposal release, and never send free text such as a written justification. Use reason=acceptance-remediation only for decision=operator-acceptance-remediation-required. At awaiting-acceptance, reason=review-blocking authorizes a same-goal review-remediation replacement only within the exact acceptance, committed head, approved write union, and retained remaining budget.",
|
|
445
464
|
args: { reason: optionalStringSchema }, execute: async (args, context) => {
|
|
446
465
|
await requireRoot(context.sessionID);
|
|
447
466
|
const requestedReason = args.reason;
|
|
448
467
|
if (requestedReason !== undefined && requestedReason !== "review-blocking" && requestedReason !== "acceptance-remediation")
|
|
449
468
|
throw new Error("operator-cancel-reason-invalid");
|
|
450
|
-
const
|
|
469
|
+
const pending = await operators.read(context.sessionID);
|
|
470
|
+
if (!pending) {
|
|
471
|
+
// A pre-approval proposal owns no execution lane. Its admitted child can never be rebound, so without
|
|
472
|
+
// this release the root can neither resume, redispatch, nor start any replacement investigation.
|
|
473
|
+
const proposal = await proposals.read(context.sessionID);
|
|
474
|
+
if (!proposal || proposal.phase === "approved")
|
|
475
|
+
throw new Error("operator-run-missing");
|
|
476
|
+
if (requestedReason !== undefined)
|
|
477
|
+
throw new Error("operator-cancel-reason-invalid");
|
|
478
|
+
await stop(context.sessionID, "explicit-cancellation", false);
|
|
479
|
+
const discarded = await proposals.discardPreApproval(context.sessionID);
|
|
480
|
+
return JSON.stringify({ profile: profile.id, status: "cancelled", scope: "proposal",
|
|
481
|
+
released_proposal: discarded ? proposals.packet(discarded) : null,
|
|
482
|
+
next_action: "the bounded proposal grant is released and its spent reads/submissions are not restored; " +
|
|
483
|
+
"begin a new proposal investigation only on an explicit root decision to retry, and never reuse the cancelled child" });
|
|
484
|
+
}
|
|
485
|
+
const current = pending;
|
|
451
486
|
if (requestedReason === "acceptance-remediation" && current.decision !== "operator-acceptance-remediation-required")
|
|
452
487
|
throw new Error("operator-cancel-reason-invalid");
|
|
453
488
|
if (requestedReason === "review-blocking" && current.phase !== "awaiting-acceptance")
|
|
@@ -483,8 +518,14 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
483
518
|
await control.authorizeOperatorContractRepairValidation(context.sessionID, taskID, unit.childSessionID, args.repair_fingerprint);
|
|
484
519
|
}
|
|
485
520
|
catch (error) {
|
|
521
|
+
// The repair is aborted and the run keeps a durable decision. Return that preserved state so an
|
|
522
|
+
// unauthorized resume cannot leave the root without a recognizable terminal or next action.
|
|
486
523
|
applied = await operators.abortAppliedContractRepair(context.sessionID, "operator-contract-repair-validation-resume-unavailable");
|
|
487
|
-
|
|
524
|
+
return JSON.stringify({ status: "contract-repair-validation-unavailable",
|
|
525
|
+
code: error instanceof Error ? error.message : "operator-contract-repair-validation-resume-unavailable",
|
|
526
|
+
packet: await operatorPacket(applied),
|
|
527
|
+
next_action: `The validation-only resume was not authorized and this repair is closed. Do not retry ${resolveContractRepair} or ` +
|
|
528
|
+
`${resume} for the same fingerprint: read ${status}, then either continue the reported decision or return one terminal checkpoint naming this refusal.` });
|
|
488
529
|
}
|
|
489
530
|
return JSON.stringify({ status: "repair-applied", run_id: applied.runID, unit_id: unit.unit.id,
|
|
490
531
|
repair_generation: applied.repairGeneration, task: operators.nextWorkerTask(applied) });
|
|
@@ -610,7 +651,10 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
610
651
|
if (!root)
|
|
611
652
|
throw new Error("operator-proposal-session-inactive");
|
|
612
653
|
try {
|
|
613
|
-
|
|
654
|
+
const submitted = await proposals.submitJSON(root, context.sessionID, args.proposal_json);
|
|
655
|
+
return JSON.stringify({ ...proposals.packet(submitted),
|
|
656
|
+
next_action: "Proposal submitted. This investigation child must now return to its parent without further tools. " +
|
|
657
|
+
"Do not call operator_next, start a worker, or claim approval; the root must compare and approve the proposal." });
|
|
614
658
|
}
|
|
615
659
|
catch (error) {
|
|
616
660
|
const contract = error instanceof OperatorContractError;
|
|
@@ -618,6 +662,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
618
662
|
throw error;
|
|
619
663
|
const state = await proposals.required(root);
|
|
620
664
|
return JSON.stringify({ status: "invalid-proposal", code: error.message, actual_reads: state.read_count,
|
|
665
|
+
remaining_reads: state.intent.proposal_budget.max_reads - state.read_count,
|
|
621
666
|
submissions: state.submission_count, remaining_submissions: state.intent.proposal_budget.max_submissions - state.submission_count,
|
|
622
667
|
...(contract ? { diagnostics: error.diagnostics, diagnostics_truncated: error.diagnostics_truncated } : {}) });
|
|
623
668
|
}
|
|
@@ -1118,6 +1163,14 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1118
1163
|
(output.system ??= []).push(`SORTIE_RUNTIME_PROFILE ${profile.id}; marker ${assetVersion}. ` +
|
|
1119
1164
|
`Shared MkII protocol role names are logical: ${protocolMap}. Use only ${profile.toolPrefix} tools for this profile. ` +
|
|
1120
1165
|
"Never rewrite user acceptance or evidence to rename protocol roles. Final acceptance belongs only to the root coordinator.");
|
|
1166
|
+
const proposal = await proposals.read(root);
|
|
1167
|
+
if (proposal?.phase === "investigating" && proposal.proposal_session_id === request.sessionID) {
|
|
1168
|
+
(output.system ??= []).push(`SORTIE_PROPOSAL_PHASE investigating; intent=${proposal.intent_id}; root=${root}; child=${request.sessionID}. ` +
|
|
1169
|
+
`This durable phase remains authoritative after compaction even when the latest message is a generic continuation. ` +
|
|
1170
|
+
`Continue the admitted read-only investigation and submit through ${submitProposal}. Do not call ${next} or dispatch workers: no execution run exists yet. ` +
|
|
1171
|
+
`actual_reads=${proposal.read_count}; remaining_reads=${proposal.intent.proposal_budget.max_reads - proposal.read_count}; ` +
|
|
1172
|
+
`submissions=${proposal.submission_count}; remaining_submissions=${proposal.intent.proposal_budget.max_submissions - proposal.submission_count}.`);
|
|
1173
|
+
}
|
|
1121
1174
|
},
|
|
1122
1175
|
"experimental.text.complete": async (request, output) => {
|
|
1123
1176
|
const role = (await identity(request.sessionID)).role;
|
|
@@ -1142,6 +1195,18 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1142
1195
|
if (!root)
|
|
1143
1196
|
return;
|
|
1144
1197
|
if ((await identity(request.sessionID)).role === "dog-operator") {
|
|
1198
|
+
const proposal = await proposals.read(root);
|
|
1199
|
+
if (proposal?.phase === "investigating" && proposal.proposal_session_id === request.sessionID) {
|
|
1200
|
+
(output.context ??= []).push(`Proposal continuation: ${JSON.stringify({
|
|
1201
|
+
root, child: request.sessionID, intent_id: proposal.intent_id, intent_hash: proposal.intent_hash,
|
|
1202
|
+
intent: proposal.intent, goal_binding: proposal.goal_binding, read_paths: proposal.read_paths,
|
|
1203
|
+
actual_reads: proposal.read_count, remaining_reads: proposal.intent.proposal_budget.max_reads - proposal.read_count,
|
|
1204
|
+
submissions: proposal.submission_count, remaining_submissions: proposal.intent.proposal_budget.max_submissions - proposal.submission_count,
|
|
1205
|
+
})}. Preserve the latest proposal draft and field diagnostics in the summary. Continue only in this same claimed read-only proposal child. ` +
|
|
1206
|
+
`Repair only diagnosed fields, then call ${submitProposal}; do not call ${next}, dispatch Tasks, execute work, or reset budgets. ` +
|
|
1207
|
+
"This is investigation, not an execution run. Compaction grants no new reads or submissions.");
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1145
1210
|
const state = await operators.required(root);
|
|
1146
1211
|
(output.context ??= []).push(`Operator continuation: root=${root}; run=${state.runID}; generation=${state.generation}; contract=${state.planHash}. ` +
|
|
1147
1212
|
`Read ${next} for current authoritative state. Do not reconstruct acceptance or reset the queue.`);
|
|
@@ -1150,9 +1215,13 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1150
1215
|
await core["experimental.session.compacting"]?.(request, output);
|
|
1151
1216
|
},
|
|
1152
1217
|
"experimental.compaction.autocontinue": async (request, output) => {
|
|
1153
|
-
|
|
1218
|
+
const root = await rootFor(request.sessionID);
|
|
1219
|
+
if (!root)
|
|
1154
1220
|
return;
|
|
1155
1221
|
if ((await identity(request.sessionID)).role === "dog-operator") {
|
|
1222
|
+
const proposal = await proposals.read(root);
|
|
1223
|
+
if (proposal?.phase === "investigating" && proposal.proposal_session_id === request.sessionID)
|
|
1224
|
+
return;
|
|
1156
1225
|
output.enabled = false;
|
|
1157
1226
|
return;
|
|
1158
1227
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type RuntimeAsset } from "./runtime-assets.ts";
|
|
2
2
|
export declare const PREVIEW_PRESENTATION_POLICY: string;
|
|
3
|
+
export declare const PREVIEW_TERMINAL_REPORT_POLICY: string;
|
|
3
4
|
export declare const COMMUNICATION_LANGUAGE_POLICY = "\n## Communication language continuity\n\nUse the language of the user's latest instruction sentences for all user-visible communication,\nincluding progress, Task descriptions/titles, delegated questions, handoff prose, findings and final replies.\nJapanese instructions require Japanese communication; English instructions require English communication.\nMixed-language identifiers or quoted English documents do not change the user's instruction language.\nIf the latest message supplies no detectable language, preserve the previous instruction language.\n\nBefore delegating, write the question and every prose field in that same language and explicitly ask the\nchild to reply in it. Do not translate a Japanese request into English merely because these agent instructions\nor examples are English. Child sessions and their prompts are visible to the user: parent-only translation\nafter an English exchange is not sufficient. All descendants preserve the requested language; it takes\nprecedence over the language of protocol boilerplate. Do not add a separate translation pass or model call.\n\nKeep protocol keys, enums, commands, paths, identifiers, model names, exact quoted evidence and code verbatim.\nFor example strategy_trigger, architecture-choice, review_phase and PASS must not be localized. Translate\ntheir explanatory prose, not these tokens. Preserve immutable criteria and host-generated Task packets\nverbatim; author their user-controlled prose in the correct language before the contract is frozen.\nGenerated control labels are not a reason to switch the surrounding explanation to English.\n";
|
|
4
5
|
export declare const runtimeAssets: readonly RuntimeAsset[];
|
|
@@ -8,10 +8,14 @@ const coordinator = profileAgent(profile, "dog-coordinator");
|
|
|
8
8
|
const operator = profileAgent(profile, "dog-operator");
|
|
9
9
|
const worker = profileAgent(profile, "dog-worker");
|
|
10
10
|
const canonicalCoordinator = canonicalAssets.find(asset => asset.name === "dog-coordinator").content;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/** Reuse the canonical fixture bodies so this profile cannot drift from the shared terminal vocabulary. */
|
|
12
|
+
function canonicalFixture(marker) {
|
|
13
|
+
const start = canonicalCoordinator.indexOf(`${marker}\n`);
|
|
14
|
+
const end = start < 0 ? -1 : canonicalCoordinator.indexOf(`END_${marker}`, start);
|
|
15
|
+
if (start < 0 || end < 0)
|
|
16
|
+
throw new Error(`canonical-fixture-missing:${marker}`);
|
|
17
|
+
return canonicalCoordinator.slice(start, end + `END_${marker}`.length);
|
|
18
|
+
}
|
|
15
19
|
export const PREVIEW_PRESENTATION_POLICY = `
|
|
16
20
|
## Sortie presentation continuity
|
|
17
21
|
|
|
@@ -20,7 +24,34 @@ Use the user's language for prose, concise icon-led plan/progress/evidence block
|
|
|
20
24
|
after the host accepts the result. The host supplies the 🐾 return report, mission/proof, cost/pack and career panels;
|
|
21
25
|
do not fabricate scores, counts, medals or success, and do not suppress these panels as redundant decoration.
|
|
22
26
|
|
|
23
|
-
${
|
|
27
|
+
${canonicalFixture("READABLE_OUTPUT_FIXTURE")}
|
|
28
|
+
`;
|
|
29
|
+
export const PREVIEW_TERMINAL_REPORT_POLICY = `
|
|
30
|
+
## Terminal report contract
|
|
31
|
+
|
|
32
|
+
A task turn that ends without another tool call is a terminal return. Its first non-empty line must be one
|
|
33
|
+
machine checkpoint: exactly one of DONE, INTERRUPTED, BLOCKED, or NEED_DECISION with that status icon,
|
|
34
|
+
followed by a short conclusion in the user's language. Never close a task turn with bare prose, an unlabeled
|
|
35
|
+
summary, a plan, a progress note, or a preamble, and never leave the run without one of these four tokens.
|
|
36
|
+
The status token, its icon, TRUE_INTERRUPTION and TRUE_BLOCKER are protocol tokens: keep them verbatim
|
|
37
|
+
even when the surrounding conclusion is translated. Translate only the display labels and keep their order.
|
|
38
|
+
|
|
39
|
+
DONE requires a succeeded ${profile.toolPrefix}complete_operator receipt for the accepted goal; the host renders
|
|
40
|
+
the measured return report from that receipt. Without it, return INTERRUPTED, BLOCKED, or NEED_DECISION naming
|
|
41
|
+
the exact unresolved condition. An exhausted budget, an unapproved or failed proposal, a terminated child, a
|
|
42
|
+
refused contract operation, or an unreachable acceptance is an INTERRUPTED return, never a silent stop.
|
|
43
|
+
A genuine interruption also requires the canonical machine line \`TRUE_INTERRUPTION: user: <condition>\` or
|
|
44
|
+
\`TRUE_INTERRUPTION: internal: <condition>\`; without it the host keeps the run on its same-session continuation path.
|
|
45
|
+
|
|
46
|
+
A refused control operation is a local process defect, not a terminal blocker. Read the returned status and
|
|
47
|
+
next_action, apply that one correction, and continue in the same turn. Never reissue an unchanged refused
|
|
48
|
+
request: an active contract returns ${profile.toolPrefix}operator_status and the existing next Task, and an
|
|
49
|
+
unavailable contract-repair validation resume returns the preserved run state and its decision. When the same
|
|
50
|
+
refusal repeats with unchanged state, stop retrying and return one INTERRUPTED checkpoint naming that refusal.
|
|
51
|
+
|
|
52
|
+
${canonicalFixture("TERMINAL_STATUS_SEMANTICS_FIXTURE")}
|
|
53
|
+
|
|
54
|
+
${canonicalFixture("TERMINAL_OUTPUT_TEMPLATE")}
|
|
24
55
|
`;
|
|
25
56
|
export const COMMUNICATION_LANGUAGE_POLICY = `
|
|
26
57
|
## Communication language continuity
|
|
@@ -102,9 +133,18 @@ their literal ASCII keys; translated prose belongs in the values. Do not guess a
|
|
|
102
133
|
For a nontrivial request whose source facts, unit boundaries, or exact validation contract still require investigation,
|
|
103
134
|
do not author a giant speculative plan at the root. Freeze the original request once with stable ordered requirement IDs,
|
|
104
135
|
including every negative and quality condition, authoritative references, finite proposal read/submission budgets, and the
|
|
105
|
-
maximum read prefixes. Call ${profile.toolPrefix}begin_operator_proposal. Dispatch its exact ${operator} Task unchanged.
|
|
136
|
+
maximum read prefixes. Call ${profile.toolPrefix}begin_operator_proposal. Dispatch its exact ${operator} Task unchanged.
|
|
137
|
+
Freeze product requirements from the original request, not extra implementation criteria invented from workflow
|
|
138
|
+
bookkeeping. Keep proposal read/submission allowances in proposal_budget and host counters; do not turn spent
|
|
139
|
+
budgets or your own reporting obligations into worker validation commands. Preserve any explicit user requirement.
|
|
106
140
|
That child remains the same logical operations role: it may inspect only the host-approved read prefixes and submit one
|
|
107
141
|
requirement-mapped proposal; it cannot edit, use shell, dispatch a worker/advisor/scout, widen read scope, or execute work.
|
|
142
|
+
An admitted proposal child is never redispatched or replaced. If it terminates without a submitted proposal, report that
|
|
143
|
+
terminal failure first; only an explicit decision to retry may call ${profile.toolPrefix}cancel_operator with no reason to
|
|
144
|
+
release the grant before freezing the requirements again. Spent proposal reads and submissions are never restored.
|
|
145
|
+
Do not cancel/reinvestigate the same known contract defect merely to get another draft. Preserve exact observed
|
|
146
|
+
commands and correction evidence. If a user-only acceptance condition cannot be expressed by the existing contract,
|
|
147
|
+
report that specific limitation for a scope/design decision instead of inventing a passing test or another investigation.
|
|
108
148
|
|
|
109
149
|
The proposal must map every original requirement ID to approach and validation, explicitly list uncovered IDs, preserve
|
|
110
150
|
negative-condition handling, and include exact read/write/unit/GoalDeclaration plans and budget estimates. Its summary is
|
|
@@ -246,7 +286,7 @@ Use ${profile.toolPrefix}cancel_operator to stop an active grant before changing
|
|
|
246
286
|
operator-acceptance-remediation-required replacement action, or performing the bounded awaiting-acceptance
|
|
247
287
|
reason=review-blocking replacement above. Agent switching revokes this
|
|
248
288
|
runtime's ownership; do not restart it from a stale summary. The initial preview supports the serial lane only.
|
|
249
|
-
${PREVIEW_PRESENTATION_POLICY}
|
|
289
|
+
${PREVIEW_PRESENTATION_POLICY}${PREVIEW_TERMINAL_REPORT_POLICY}
|
|
250
290
|
`;
|
|
251
291
|
const operatorContent = `---
|
|
252
292
|
description: Sortie-dogs ${V010_RUNTIME_ASSET_VERSION} hidden dogs-coordinator operations delegate; no source or acceptance authority.
|
|
@@ -267,10 +307,14 @@ tools:
|
|
|
267
307
|
# ${operator}
|
|
268
308
|
|
|
269
309
|
You operate one coordinator-approved serial queue for runtime ${V010_RUNTIME_ASSET_VERSION}. You are not a second coordinator.
|
|
270
|
-
When the prompt starts SORTIE_OPERATOR_PROPOSAL
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
310
|
+
When the prompt starts SORTIE_OPERATOR_PROPOSAL or the host supplies SORTIE_PROPOSAL_PHASE investigating,
|
|
311
|
+
perform only its bounded read investigation and submit the complete packet
|
|
312
|
+
through ${profile.toolPrefix}submit_operator_proposal before returning; a prose-only return is forbidden. If it returns invalid-proposal,
|
|
313
|
+
repair only the named code (using actual_reads for a budget estimate mismatch) within the finite submission budget.
|
|
314
|
+
A successful status=submitted ends this investigation Task: return to the parent without further tools, even if a generic
|
|
315
|
+
continuation asks for the next step. Submission is not execution admission; never call operator_next or dispatch a worker.
|
|
316
|
+
For an admitted execution queue, call
|
|
317
|
+
${profile.toolPrefix}operator_next. If it returns a task, pass its subagent_type, description, and prompt unchanged to Task.
|
|
274
318
|
After the worker returns, inspect the host's bounded packet and call next again only when the queue still has pending work.
|
|
275
319
|
The worker owns implementation, diagnosis, correction and declared validation inside its Task invocation. Do not duplicate it.
|
|
276
320
|
|
|
@@ -280,8 +324,10 @@ or replace a child to bypass a refusal or a budget. The plugin enforces root/pro
|
|
|
280
324
|
|
|
281
325
|
Keep coordination concise and use the handoff's language. Do not return intermediate progress merely to wake the coordinator.
|
|
282
326
|
On awaiting-decision, cancelled, or awaiting-acceptance, stop and return the packet's status and unresolved evidence. Do not
|
|
283
|
-
claim the feature is accepted; only the root coordinator can do that. After compaction
|
|
284
|
-
|
|
327
|
+
claim the feature is accepted; only the root coordinator can do that. After compaction in the proposal phase, preserve the
|
|
328
|
+
same child, intent, read evidence and remaining budgets; continue investigation/submission repair, never call next.
|
|
329
|
+
Only in the execution phase, call next to read authoritative queue state and its current short Task reference rather than
|
|
330
|
+
reconstructing criteria from a summary. Never use a standalone/generic worker as a fallback.
|
|
285
331
|
`;
|
|
286
332
|
/**
|
|
287
333
|
* A constraint added to an existing construct is only complete when every existing form that reaches
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sortie-dogs",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"homepage": "https://github.com/zufall-upon/Sortie-dogs#readme",
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public",
|
|
26
|
-
"tag": "
|
|
26
|
+
"tag": "latest"
|
|
27
27
|
},
|
|
28
28
|
"type": "module",
|
|
29
29
|
"engines": {
|