sortie-dogs 0.10.19 → 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/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-cold-turn1-return-report-v2-2";
|
|
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-cold-turn1-return-report-v2-2";
|
|
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";
|
|
@@ -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,9 +130,9 @@ 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;
|
|
@@ -148,6 +166,11 @@ export declare class OperatorProposalRuntime {
|
|
|
148
166
|
submit(root: string, actor: string, raw: unknown): Promise<OperatorProposalState>;
|
|
149
167
|
submitJSON(root: string, actor: string, source: string): Promise<OperatorProposalState>;
|
|
150
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>;
|
|
151
174
|
approve(root: string, raw: unknown, commit?: boolean): Promise<OperatorProposalState>;
|
|
152
175
|
private approveUnlocked;
|
|
153
176
|
/**
|
|
@@ -161,3 +184,4 @@ export declare class OperatorProposalRuntime {
|
|
|
161
184
|
private requiredUnlocked;
|
|
162
185
|
packet(state: OperatorProposalState): unknown;
|
|
163
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;
|
|
@@ -362,9 +398,6 @@ export class OperatorProposalRuntime {
|
|
|
362
398
|
return this.serial(root, () => this.readUnlocked(root));
|
|
363
399
|
}
|
|
364
400
|
async readUnlocked(root) {
|
|
365
|
-
const cached = this.states.get(root);
|
|
366
|
-
if (cached)
|
|
367
|
-
return structuredClone(cached);
|
|
368
401
|
let source;
|
|
369
402
|
try {
|
|
370
403
|
source = await readFile(this.file(root), "utf8");
|
|
@@ -396,8 +429,7 @@ export class OperatorProposalRuntime {
|
|
|
396
429
|
else {
|
|
397
430
|
throw new Error("operator-proposal-state-invalid");
|
|
398
431
|
}
|
|
399
|
-
|
|
400
|
-
return structuredClone(state);
|
|
432
|
+
return state;
|
|
401
433
|
}
|
|
402
434
|
async save(state) {
|
|
403
435
|
const directory = join(this.projectRoot, this.profile.stateDirectory, "operator-proposals");
|
|
@@ -410,7 +442,6 @@ export class OperatorProposalRuntime {
|
|
|
410
442
|
finally {
|
|
411
443
|
await rm(temporary, { force: true }).catch(() => undefined);
|
|
412
444
|
}
|
|
413
|
-
this.states.set(state.root_session_id, structuredClone(state));
|
|
414
445
|
}
|
|
415
446
|
assertBudgetAvailable(state) {
|
|
416
447
|
const remainingReads = state.intent.proposal_budget.max_reads - state.read_count;
|
|
@@ -638,6 +669,132 @@ export class OperatorProposalRuntime {
|
|
|
638
669
|
await this.save(state);
|
|
639
670
|
return state;
|
|
640
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
|
+
}
|
|
641
798
|
async approve(root, raw, commit = true) {
|
|
642
799
|
return this.serial(root, () => this.approveUnlocked(root, raw, commit));
|
|
643
800
|
}
|
|
@@ -671,7 +828,6 @@ export class OperatorProposalRuntime {
|
|
|
671
828
|
reads: state.read_count, submissions: state.submission_count,
|
|
672
829
|
...(state.goal_binding === null ? {} : { retry: { intent_hash: state.intent_hash, goal_binding: state.goal_binding } }) });
|
|
673
830
|
await rm(this.file(root), { force: true });
|
|
674
|
-
this.states.delete(root);
|
|
675
831
|
return state;
|
|
676
832
|
});
|
|
677
833
|
}
|
|
@@ -693,6 +849,7 @@ export class OperatorProposalRuntime {
|
|
|
693
849
|
remaining_reads: state.intent.proposal_budget.max_reads - state.read_count, submissions: state.submission_count,
|
|
694
850
|
remaining_submissions: state.intent.proposal_budget.max_submissions - state.submission_count,
|
|
695
851
|
task_admitted: state.proposal_call_id !== null, uncovered: state.proposal?.uncovered ?? [], proposal: state.proposal,
|
|
696
|
-
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 };
|
|
697
854
|
}
|
|
698
855
|
}
|
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";
|
|
@@ -395,6 +395,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
395
395
|
const approvalSchema = record(stringSchema) && typeof stringSchema.describe === "function"
|
|
396
396
|
? stringSchema.describe(OPERATOR_APPROVAL_CONTRACT)
|
|
397
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 };
|
|
398
401
|
const planContract = "plan_json must encode the exact operator plan object: schema_version, acceptance, acceptance_proof, source_refs, " +
|
|
399
402
|
"goal_declaration, units, and optional git_lifecycle only. git_lifecycle exact shape is " +
|
|
400
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 ' +
|
|
@@ -420,6 +423,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
420
423
|
const beginProposal = profileTool(profile, "sortie_begin_operator_proposal");
|
|
421
424
|
const submitProposal = profileTool(profile, "sortie_submit_operator_proposal");
|
|
422
425
|
const approveProposal = profileTool(profile, "sortie_approve_operator_proposal");
|
|
426
|
+
const reviseProposal = profileTool(profile, "sortie_revise_operator_proposal");
|
|
423
427
|
async function stop(root, reason, retireRoot = true) {
|
|
424
428
|
if (retireRoot)
|
|
425
429
|
retired.add(root);
|
|
@@ -559,7 +563,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
559
563
|
? "proposal submission budget exhausted; do not call operator_next; report the bounded proposal failure"
|
|
560
564
|
: "proposal is not submitted; do not call operator_next; complete or repair the bounded proposal submission"
|
|
561
565
|
: proposal?.phase === "submitted"
|
|
562
|
-
?
|
|
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.`
|
|
563
567
|
: "approved proposal has no operator run; do not call operator_next; reconcile the approval or preparation failure";
|
|
564
568
|
return JSON.stringify(state ? { ...await operatorPacket(state), ...(draft ? { pending_draft: draft } : {}),
|
|
565
569
|
...(proposal ? { proposal: proposalIdentity(proposal) } : {}) }
|
|
@@ -823,6 +827,26 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
823
827
|
...(contract ? { diagnostics: error.diagnostics, diagnostics_truncated: error.diagnostics_truncated } : {}) });
|
|
824
828
|
}
|
|
825
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
|
+
} };
|
|
826
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,
|
|
827
851
|
args: { approval_json: approvalSchema }, execute: async (args, context) => {
|
|
828
852
|
await requireRoot(context.sessionID);
|
|
@@ -852,6 +876,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
852
876
|
if (approved.goal_binding === null)
|
|
853
877
|
throw new Error("operator-proposal-goal-binding-missing");
|
|
854
878
|
await control.assertProposalExecutionBudget(context.sessionID, approved.goal_binding, proposal.plan.units.length);
|
|
879
|
+
await proposals.pinApproval(context.sessionID, approval);
|
|
855
880
|
const prepared = await operators.propose(context.sessionID, proposal.plan);
|
|
856
881
|
if (prepared.status !== "prepared")
|
|
857
882
|
throw new Error("operator-proposal-approved-plan-invalid");
|
|
@@ -861,7 +886,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
861
886
|
});
|
|
862
887
|
} };
|
|
863
888
|
const ownTools = new Set([prepare, repair, next, status, cancel, complete, resume, resolveContractRepair,
|
|
864
|
-
beginProposal, submitProposal, approveProposal]);
|
|
889
|
+
beginProposal, submitProposal, approveProposal, reviseProposal]);
|
|
865
890
|
const protocolMap = CANONICAL_AGENT_ROLES.map(role => `${role}=${profileAgent(profile, role)}`).join(", ");
|
|
866
891
|
function rememberRendered(key, text) {
|
|
867
892
|
renderedParts.set(key, goalFingerprint(text));
|
|
@@ -168,7 +168,19 @@ report that specific limitation for a scope/design decision instead of inventing
|
|
|
168
168
|
The proposal must map every original requirement ID to approach and validation, explicitly list uncovered IDs, preserve
|
|
169
169
|
negative-condition handling, and include exact read/write/unit/GoalDeclaration plans and budget estimates. Its summary is
|
|
170
170
|
not the source of truth and proposal reads are not acceptance evidence. Compare the submitted proposal directly with the
|
|
171
|
-
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,
|
|
172
184
|
revision, content_hash, ordered compared_requirement_ids, decision=approve, and a substantive comparison rationale.
|
|
173
185
|
Uncovered requirements, stale hashes/revisions, another root, widened scope, rewritten acceptance, or reset proposal
|
|
174
186
|
accounting must be rejected. Hash matching records identity; it never replaces this semantic root decision. Approval alone
|