sortie-dogs 0.10.19 → 0.10.22

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 CHANGED
@@ -198,7 +198,7 @@ not invent, probe, or translate variant names.
198
198
  - `freeTierFallbackModels`: ordered global last-resort model IDs. Default:
199
199
  `opencode/deepseek-v4-flash-free`; `[]` disables this fallback.
200
200
  - `dedicatedWorkerModel`: canonical stable serial target, default
201
- `openai/gpt-5.6-sol` / `medium`. The v0.10 profile also supplies its explicit
201
+ `openai/gpt-6-sol` / `medium`. The v0.10 profile also supplies its explicit
202
202
  role routes below; do not infer the v0.10 worker route from this stable setting.
203
203
  - `consultation.strategy`: fixed advisor identity, optional `required`, and
204
204
  positive `maxCallsPerCandidate`; default one call and not required.
@@ -237,13 +237,13 @@ reuse the same evidence instead of spending the validation budget again.
237
237
 
238
238
  ### Default v0.10 routes
239
239
 
240
- - `dog-operator`: `openai/gpt-5.6-luna-fast` / `max`
241
- - `dogs-coordinator`: `openai/gpt-5.6-terra` / `xhigh`
242
- - `dog-worker-v010`: `openai/gpt-5.6-luna-fast` / `max`
243
- - `dog-scout-v010`: `openai/gpt-5.6-luna-fast` / `xhigh`
244
- - `dog-reviewer-v010`: `openai/gpt-5.6-terra` / `xhigh`
240
+ - `dog-operator`: `openai/gpt-6-luna` / `max`
241
+ - `dogs-coordinator`: `openai/gpt-6-sol` / `xhigh`
242
+ - `dog-worker-v010`: `openai/gpt-6-luna` / `max`
243
+ - `dog-scout-v010`: `openai/gpt-6-luna` / `xhigh`
244
+ - `dog-reviewer-v010`: `openai/gpt-6-sol` / `xhigh`
245
245
  - `dog-advisor-v010`: preferred declared `anthropic/claude-opus-5`, otherwise
246
- `openai/gpt-5.6-sol` / `xhigh`
246
+ `openai/gpt-6-sol` / `xhigh`
247
247
 
248
248
  An explicit model and variant selected in OpenCode remains authoritative for that
249
249
  session. Child role defaults fill absent native settings and may be overridden by
@@ -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;
@@ -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";
@@ -1,5 +1,5 @@
1
1
  import type { RuntimeProfile } from "./runtime-profile.js";
2
- import { type OperatorPlan, type OperatorTask } from "./operator-runtime.js";
2
+ import { type OperatorPlan, type OperatorState, type OperatorTask } from "./operator-runtime.js";
3
3
  export declare const DEFAULT_OPERATOR_PROPOSAL_BUDGET: Readonly<{
4
4
  max_reads: 45;
5
5
  max_submissions: 9;
@@ -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,31 @@ 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
+ /** Immutable archive identities of earlier approved contracts on this root. */
98
+ approved_history?: readonly {
99
+ readonly archive_hash: string;
100
+ readonly intent_id: string;
101
+ readonly proposal_id: string;
102
+ readonly run_id: string;
103
+ readonly disposition: "completed" | "incomplete";
104
+ }[];
105
+ }
106
+ interface OperatorProposalIdentity {
107
+ readonly proposal_id: string;
108
+ readonly revision: number;
109
+ readonly content_hash: string;
110
+ }
111
+ export interface OperatorProposalRevision {
112
+ readonly actor: string;
113
+ readonly at: string;
114
+ readonly rationale: string;
115
+ readonly from: OperatorProposalIdentity;
116
+ readonly to: OperatorProposalIdentity;
117
+ readonly patch_hash: string;
118
+ readonly paths: readonly string[];
93
119
  }
94
120
  export interface OperatorProposalBudgetDiagnostic {
95
121
  readonly status: "proposal-budget-exhausted";
@@ -112,12 +138,13 @@ export declare class OperatorProposalBudgetError extends Error {
112
138
  /** Durable pre-execution lane. It grants investigation and submission, never source mutation or execution. */
113
139
  export declare class OperatorProposalRuntime {
114
140
  readonly profile: RuntimeProfile;
115
- private readonly states;
116
141
  private readonly pending;
117
142
  private serial;
143
+ private locked;
118
144
  readonly projectRoot: string;
119
145
  constructor(projectRoot: string, profile: RuntimeProfile);
120
146
  private file;
147
+ private approvedArchiveFile;
121
148
  private spendFile;
122
149
  private readSpend;
123
150
  private saveSpend;
@@ -135,6 +162,8 @@ export declare class OperatorProposalRuntime {
135
162
  private resolveTask;
136
163
  begin(root: string, raw: unknown): Promise<OperatorProposalState>;
137
164
  private beginUnlocked;
165
+ /** Explicit root-only transition after an approved contract's execution lane has ended. */
166
+ reviseApproved(root: string, actor: string, raw: unknown, readRun: () => Promise<OperatorState | undefined>, currentGoal: OperatorProposalGoalBinding): Promise<OperatorProposalState>;
138
167
  bindGoal(root: string, binding: OperatorProposalGoalBinding): Promise<OperatorProposalState>;
139
168
  private bindGoalUnlocked;
140
169
  admit(root: string, callID: string, args: unknown, reserveBudget?: () => Promise<void>): Promise<OperatorTask>;
@@ -148,6 +177,11 @@ export declare class OperatorProposalRuntime {
148
177
  submit(root: string, actor: string, raw: unknown): Promise<OperatorProposalState>;
149
178
  submitJSON(root: string, actor: string, source: string): Promise<OperatorProposalState>;
150
179
  private submitUnlocked;
180
+ /** A read-free root correction of an ended child's submitted packet, never a new investigation grant. */
181
+ reviseJSON(root: string, actor: string, source: string): Promise<OperatorProposalState>;
182
+ private patchedPacket;
183
+ /** Close revision before any prepare/register side effect, including when that later step fails. */
184
+ pinApproval(root: string, raw: unknown): Promise<void>;
151
185
  approve(root: string, raw: unknown, commit?: boolean): Promise<OperatorProposalState>;
152
186
  private approveUnlocked;
153
187
  /**
@@ -161,3 +195,4 @@ export declare class OperatorProposalRuntime {
161
195
  private requiredUnlocked;
162
196
  packet(state: OperatorProposalState): unknown;
163
197
  }
198
+ 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,12 +312,40 @@ 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;
307
343
  this.projectRoot = resolve(projectRoot);
308
344
  }
309
345
  file(root) { return join(this.projectRoot, this.profile.stateDirectory, "operator-proposals", `${hash(root)}.json`); }
346
+ approvedArchiveFile(root, intentID, proposalHash, runID) {
347
+ return join(this.projectRoot, this.profile.stateDirectory, "operator-proposals", `${hash(root)}.${intentID}.${proposalHash}.${hash(runID).slice(0, 16)}.approved.json`);
348
+ }
310
349
  spendFile(root) { return join(this.projectRoot, this.profile.stateDirectory, "operator-proposals", `${hash(root)}.spend.json`); }
311
350
  async readSpend(root) {
312
351
  let source;
@@ -331,6 +370,12 @@ export class OperatorProposalRuntime {
331
370
  !/^sha256:[a-f0-9]{64}$/u.test(spend.retry.goal_binding.acceptance_fingerprint))) {
332
371
  throw new Error("operator-proposal-spend-invalid");
333
372
  }
373
+ if (spend.approved_history !== undefined && (!Array.isArray(spend.approved_history) ||
374
+ spend.approved_history.some(item => !record(item) || !/^[a-f0-9]{64}$/u.test(String(item.archive_hash)) ||
375
+ !identifier(item.intent_id) || !identifier(item.proposal_id) || !text(item.run_id) ||
376
+ !["completed", "incomplete"].includes(String(item.disposition))))) {
377
+ throw new Error("operator-proposal-spend-invalid");
378
+ }
334
379
  return spend;
335
380
  }
336
381
  async saveSpend(spend) {
@@ -362,9 +407,6 @@ export class OperatorProposalRuntime {
362
407
  return this.serial(root, () => this.readUnlocked(root));
363
408
  }
364
409
  async readUnlocked(root) {
365
- const cached = this.states.get(root);
366
- if (cached)
367
- return structuredClone(cached);
368
410
  let source;
369
411
  try {
370
412
  source = await readFile(this.file(root), "utf8");
@@ -396,8 +438,7 @@ export class OperatorProposalRuntime {
396
438
  else {
397
439
  throw new Error("operator-proposal-state-invalid");
398
440
  }
399
- this.states.set(root, state);
400
- return structuredClone(state);
441
+ return state;
401
442
  }
402
443
  async save(state) {
403
444
  const directory = join(this.projectRoot, this.profile.stateDirectory, "operator-proposals");
@@ -410,7 +451,6 @@ export class OperatorProposalRuntime {
410
451
  finally {
411
452
  await rm(temporary, { force: true }).catch(() => undefined);
412
453
  }
413
- this.states.set(state.root_session_id, structuredClone(state));
414
454
  }
415
455
  assertBudgetAvailable(state) {
416
456
  const remainingReads = state.intent.proposal_budget.max_reads - state.read_count;
@@ -512,11 +552,85 @@ export class OperatorProposalRuntime {
512
552
  goal_binding: spend.retry?.intent_hash === intentHash ? structuredClone(spend.retry.goal_binding) : null,
513
553
  proposal_call_id: null, proposal_session_id: null, read_count: spend.reads, read_paths: [], submission_count: spend.submissions, proposal_id: null,
514
554
  ...(spend.reads > 0 || spend.submissions > 0 ? { prior_spend: { reads: spend.reads, submissions: spend.submissions } } : {}),
515
- proposal_revision: null, proposal_hash: null, proposal: null, approval_rationale: null };
555
+ proposal_revision: null, proposal_hash: null, proposal: null, approval_rationale: null,
556
+ ...(spend.approved_history ? { approved_history: structuredClone(spend.approved_history) } : {}) };
516
557
  this.assertBudgetAvailable(state);
517
558
  await this.save(state);
518
559
  return state;
519
560
  }
561
+ /** Explicit root-only transition after an approved contract's execution lane has ended. */
562
+ async reviseApproved(root, actor, raw, readRun, currentGoal) {
563
+ if (actor !== root)
564
+ throw new Error("operator-proposal-approved-revision-root-required");
565
+ return this.serial(root, async () => {
566
+ if (!record(raw) || !exactSet(raw, ["proposal_id", "revision", "content_hash", "operator_run_id", "rationale", "intent"]) ||
567
+ typeof raw.proposal_id !== "string" || !Number.isSafeInteger(raw.revision) || typeof raw.content_hash !== "string" ||
568
+ typeof raw.operator_run_id !== "string" || !text(raw.rationale)) {
569
+ throw new Error("operator-proposal-approved-revision-invalid");
570
+ }
571
+ const previous = await this.requiredUnlocked(root);
572
+ if (previous.phase !== "approved" || !previous.proposal || !previous.goal_binding) {
573
+ throw new Error("operator-proposal-approved-revision-not-approved");
574
+ }
575
+ if (raw.proposal_id !== previous.proposal_id || raw.revision !== previous.proposal_revision ||
576
+ raw.content_hash !== previous.proposal_hash)
577
+ throw new Error("operator-proposal-approved-revision-identity-mismatch");
578
+ const run = await readRun();
579
+ if (!run || !["cancelled", "completed"].includes(run.phase) || run.rootSessionID !== root ||
580
+ run.runID !== raw.operator_run_id || run.planHash !== hash(JSON.stringify(previous.proposal.plan)) ||
581
+ JSON.stringify(run.acceptance) !== JSON.stringify(previous.proposal.plan.acceptance)) {
582
+ throw new Error("operator-proposal-approved-revision-terminal-run-required");
583
+ }
584
+ if (!text(currentGoal.goal_id) || !Number.isSafeInteger(currentGoal.revision) || currentGoal.revision < 1 ||
585
+ !Number.isSafeInteger(currentGoal.scope_epoch) || currentGoal.scope_epoch < 1 ||
586
+ !/^sha256:[a-f0-9]{64}$/u.test(currentGoal.acceptance_fingerprint)) {
587
+ throw new Error("operator-proposal-goal-binding-invalid");
588
+ }
589
+ if (JSON.stringify(currentGoal) === JSON.stringify(previous.goal_binding)) {
590
+ throw new Error("operator-proposal-approved-revision-goal-revision-required");
591
+ }
592
+ const intent = parseIntent(raw.intent), intentHash = hash(JSON.stringify(intent));
593
+ if (intentHash === previous.intent_hash)
594
+ throw new Error("operator-proposal-approved-revision-intent-unchanged");
595
+ if (currentGoal.goal_id === previous.goal_binding.goal_id &&
596
+ JSON.stringify(intent.requirements.slice(0, previous.intent.requirements.length)) !== JSON.stringify(previous.intent.requirements)) {
597
+ throw new Error("operator-proposal-approved-revision-acceptance-continuity-required");
598
+ }
599
+ const remainingReads = intent.proposal_budget.max_reads - previous.read_count;
600
+ const remainingSubmissions = intent.proposal_budget.max_submissions - previous.submission_count;
601
+ if (remainingReads <= 0 || remainingSubmissions <= 0) {
602
+ throw new OperatorProposalBudgetError({ status: "proposal-budget-exhausted",
603
+ code: remainingReads <= 0 ? "operator-proposal-read-budget-exhausted" : "operator-proposal-submission-budget-exhausted",
604
+ reads: previous.read_count, max_reads: intent.proposal_budget.max_reads, remaining_reads: Math.max(0, remainingReads),
605
+ submissions: previous.submission_count, max_submissions: intent.proposal_budget.max_submissions,
606
+ remaining_submissions: Math.max(0, remainingSubmissions),
607
+ required_minimum_budget: { max_reads: previous.read_count + 1, max_submissions: previous.submission_count + 1 } });
608
+ }
609
+ const archive = JSON.stringify({ approved_proposal: previous, terminal_run: {
610
+ run_id: run.runID, plan_hash: run.planHash, acceptance: run.acceptance, acceptance_fingerprint: run.acceptanceFingerprint,
611
+ phase: run.phase, decision: run.decision, receipt: run.receipt, generation: run.generation
612
+ } });
613
+ const archivePath = this.approvedArchiveFile(root, previous.intent_id, previous.proposal_hash, run.runID);
614
+ try {
615
+ await writeFile(archivePath, archive, { flag: "wx", mode: 0o600 });
616
+ }
617
+ catch (error) {
618
+ if (error.code !== "EEXIST" || hash(await readFile(archivePath, "utf8")) !== hash(archive))
619
+ throw error;
620
+ }
621
+ const state = { schema_version: "0.1", profile: this.profile.id, root_session_id: root,
622
+ intent_id: `intent-${intentHash.slice(0, 24)}`, intent_hash: intentHash, intent, created_at: new Date().toISOString(),
623
+ phase: "investigating", goal_binding: structuredClone(currentGoal),
624
+ prior_spend: { reads: previous.read_count, submissions: previous.submission_count },
625
+ proposal_call_id: null, proposal_session_id: null, read_count: previous.read_count, read_paths: [],
626
+ submission_count: previous.submission_count, proposal_id: null, proposal_revision: null, proposal_hash: null,
627
+ proposal: null, approval_rationale: null,
628
+ approved_history: [...(previous.approved_history ?? []), { archive_hash: hash(archive), intent_id: previous.intent_id,
629
+ proposal_id: previous.proposal_id, run_id: run.runID, disposition: run.phase === "completed" ? "completed" : "incomplete" }] };
630
+ await this.save(state);
631
+ return state;
632
+ });
633
+ }
520
634
  async bindGoal(root, binding) {
521
635
  return this.serial(root, () => this.bindGoalUnlocked(root, binding));
522
636
  }
@@ -638,6 +752,132 @@ export class OperatorProposalRuntime {
638
752
  await this.save(state);
639
753
  return state;
640
754
  }
755
+ /** A read-free root correction of an ended child's submitted packet, never a new investigation grant. */
756
+ async reviseJSON(root, actor, source) {
757
+ return this.serial(root, async () => {
758
+ if (root !== actor)
759
+ throw new Error("operator-proposal-revision-root-required");
760
+ const state = await this.requiredUnlocked(root);
761
+ if (state.phase !== "submitted" || !state.proposal || state.approval_started)
762
+ throw new Error("operator-proposal-not-revisable");
763
+ if (state.submission_count >= state.intent.proposal_budget.max_submissions)
764
+ throw new Error("operator-proposal-submission-budget-exhausted");
765
+ let request, candidate;
766
+ try {
767
+ if (Buffer.byteLength(source) > 128 * 1024)
768
+ proposalError("/", "operator-proposal-revision-too-large", "maxBytes");
769
+ let raw;
770
+ try {
771
+ raw = JSON.parse(source);
772
+ }
773
+ catch {
774
+ proposalError("/", "operator-proposal-revision-json-invalid", "json");
775
+ }
776
+ if (!record(raw) || !exactSet(raw, ["proposal_id", "revision", "content_hash", "rationale", "patches"]) ||
777
+ !text(raw.proposal_id) || !Number.isSafeInteger(raw.revision) || !text(raw.content_hash) ||
778
+ !text(raw.rationale) || raw.rationale.length > 2000 || !Array.isArray(raw.patches) ||
779
+ raw.patches.length < 1 || raw.patches.length > 32) {
780
+ return proposalError("/", "operator-proposal-revision-invalid", "exact-identity-rationale-bounded-patches");
781
+ }
782
+ request = raw;
783
+ if (raw.proposal_id !== state.proposal_id || raw.revision !== state.proposal_revision || raw.content_hash !== state.proposal_hash) {
784
+ throw new Error("operator-proposal-revision-identity-mismatch");
785
+ }
786
+ candidate = this.patchedPacket(state, raw.patches);
787
+ }
788
+ catch (error) {
789
+ if (error instanceof Error && error.message === "operator-proposal-revision-identity-mismatch")
790
+ throw error;
791
+ // Rejections retain the old body/identity; only the same finite attempt counter advances.
792
+ state.submission_count++;
793
+ await this.save(state);
794
+ throw error;
795
+ }
796
+ const from = { proposal_id: state.proposal_id, revision: state.proposal_revision, content_hash: state.proposal_hash };
797
+ state.submission_count++;
798
+ state.proposal = candidate;
799
+ state.proposal_revision = candidate.revision;
800
+ state.proposal_hash = hash(JSON.stringify(candidate));
801
+ state.proposal_id = proposalID(state.intent_id, candidate.revision, state.proposal_hash);
802
+ (state.root_revisions ??= []).push({ actor, at: new Date().toISOString(), rationale: request.rationale,
803
+ from, to: { proposal_id: state.proposal_id, revision: candidate.revision, content_hash: state.proposal_hash },
804
+ patch_hash: hash(JSON.stringify(request.patches)), paths: request.patches.map(patch => patch.path) });
805
+ await this.save(state);
806
+ return state;
807
+ });
808
+ }
809
+ patchedPacket(state, patches) {
810
+ const before = state.proposal;
811
+ const draft = structuredClone(before);
812
+ const paths = [];
813
+ for (const [index, patch] of patches.entries()) {
814
+ const pointer = `/patches/${index}`;
815
+ if (!record(patch) || !exactSet(patch, ["op", "path", "value"]) || typeof patch.path !== "string") {
816
+ proposalError(pointer, "operator-proposal-revision-patch-invalid", "exact-op-path-value");
817
+ }
818
+ const entry = patch;
819
+ const command = /^\/plan\/goal_declaration\/criteria\/(0|[1-9][0-9]*)\/(validation_command|goal_validation_command)$/u.test(entry.path);
820
+ const replace = /^\/(coverage|existing_surface|uncovered|negative_handling|read_scope|plan\/(units|acceptance_proof))$/u.test(entry.path) ||
821
+ /^\/plan\/units\/(0|[1-9][0-9]*)\/(title|objective|read|validation|acceptance_indices)$/u.test(entry.path);
822
+ if (!(command && (entry.op === "add" || entry.op === "replace") || replace && entry.op === "replace") ||
823
+ paths.some(path => path === entry.path || path.startsWith(`${entry.path}/`) || entry.path.startsWith(`${path}/`))) {
824
+ proposalError(`${pointer}/path`, "operator-proposal-revision-path-denied", "nonoverlapping-allowlisted-field");
825
+ }
826
+ paths.push(entry.path);
827
+ const tokens = entry.path.slice(1).split("/");
828
+ let owner = draft;
829
+ for (const token of tokens.slice(0, -1)) {
830
+ if (owner === null || typeof owner !== "object" || !Object.hasOwn(owner, token)) {
831
+ proposalError(`${pointer}/path`, "operator-proposal-revision-path-missing", "existing-parent");
832
+ }
833
+ owner = owner[token];
834
+ }
835
+ const key = tokens.at(-1);
836
+ if (owner === null || typeof owner !== "object" || (entry.op === "replace" && !Object.hasOwn(owner, key)) ||
837
+ (entry.op === "add" && Object.hasOwn(owner, key))) {
838
+ proposalError(`${pointer}/path`, "operator-proposal-revision-path-missing", "replace-existing-or-add-absent-field");
839
+ }
840
+ owner[key] = structuredClone(entry.value);
841
+ }
842
+ const normalizeReads = (value) => Array.isArray(value) ? value.map(path => {
843
+ if (typeof path !== "string")
844
+ return path;
845
+ try {
846
+ return normalizeRelativePath(path);
847
+ }
848
+ catch {
849
+ return proposalError("/read_scope", "operator-proposal-revision-read-invalid", "safe-relative-spelling");
850
+ }
851
+ }) : value;
852
+ draft.read_scope = normalizeReads(draft.read_scope);
853
+ if (Array.isArray(draft.plan.units))
854
+ for (const unit of draft.plan.units)
855
+ if (record(unit))
856
+ unit.read = normalizeReads(unit.read);
857
+ draft.revision = before.revision + 1;
858
+ draft.budget_estimate = { proposal_reads: state.read_count, execution_units: draft.plan.units?.length };
859
+ const candidate = parsePacket(draft, state);
860
+ const within = (path, scopes) => scopes.some(scope => path === scope || path.startsWith(`${scope}/`));
861
+ if (candidate.read_scope.some(path => !within(path, before.read_scope)) ||
862
+ candidate.plan.units.flatMap(unit => unit.read).some(path => !within(path, before.plan.units.flatMap(unit => unit.read)))) {
863
+ proposalError("/read_scope", "operator-proposal-revision-read-expanded", "read-authority-only-shrinks");
864
+ }
865
+ if (candidate.plan.units.length > before.plan.units.length) {
866
+ proposalError("/plan/units", "operator-proposal-revision-units-expanded", "execution-unit-count-cannot-increase");
867
+ }
868
+ if (JSON.stringify({ ...candidate, revision: before.revision }) === JSON.stringify(before)) {
869
+ proposalError("/patches", "operator-proposal-revision-no-change", "substantive-correction-required");
870
+ }
871
+ return candidate;
872
+ }
873
+ /** Close revision before any prepare/register side effect, including when that later step fails. */
874
+ async pinApproval(root, raw) {
875
+ return this.serial(root, async () => {
876
+ const state = await this.approveUnlocked(root, raw, false);
877
+ state.approval_started = true;
878
+ await this.save(state);
879
+ });
880
+ }
641
881
  async approve(root, raw, commit = true) {
642
882
  return this.serial(root, () => this.approveUnlocked(root, raw, commit));
643
883
  }
@@ -669,9 +909,9 @@ export class OperatorProposalRuntime {
669
909
  return undefined;
670
910
  await this.saveSpend({ schema_version: "0.1", profile: this.profile.id, root_session_id: root,
671
911
  reads: state.read_count, submissions: state.submission_count,
912
+ ...(state.approved_history ? { approved_history: state.approved_history } : {}),
672
913
  ...(state.goal_binding === null ? {} : { retry: { intent_hash: state.intent_hash, goal_binding: state.goal_binding } }) });
673
914
  await rm(this.file(root), { force: true });
674
- this.states.delete(root);
675
915
  return state;
676
916
  });
677
917
  }
@@ -693,6 +933,7 @@ export class OperatorProposalRuntime {
693
933
  remaining_reads: state.intent.proposal_budget.max_reads - state.read_count, submissions: state.submission_count,
694
934
  remaining_submissions: state.intent.proposal_budget.max_submissions - state.submission_count,
695
935
  task_admitted: state.proposal_call_id !== null, uncovered: state.proposal?.uncovered ?? [], proposal: state.proposal,
696
- approval_rationale: state.approval_rationale };
936
+ approval_rationale: state.approval_rationale, root_revisions: state.root_revisions ?? [], approved_history: state.approved_history ?? [],
937
+ revision_available: state.phase === "submitted" && !state.approval_started && state.submission_count < state.intent.proposal_budget.max_submissions };
697
938
  }
698
939
  }
@@ -23,7 +23,7 @@ export interface AdaptiveReviewProvider {
23
23
  readonly fingerprint: string;
24
24
  }>;
25
25
  }
26
- export declare const DEFAULT_ADAPTIVE_REMEDIATION_MODEL = "openai/gpt-5.6-sol";
26
+ export declare const DEFAULT_ADAPTIVE_REMEDIATION_MODEL = "openai/gpt-6-sol";
27
27
  export interface AdaptiveRemediationSessionClient extends OpenCodeModelAvailabilityClient {
28
28
  session: {
29
29
  create(input: {
@@ -10,7 +10,7 @@ import { produceWorktreeCommitArtifact, runContainedValidation } from "../core/w
10
10
  import { openCodeModel } from "./model-routing-hook.js";
11
11
  const SHA = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u;
12
12
  const HASH = /^[a-f0-9]{64}$/u;
13
- export const DEFAULT_ADAPTIVE_REMEDIATION_MODEL = "openai/gpt-5.6-sol";
13
+ export const DEFAULT_ADAPTIVE_REMEDIATION_MODEL = "openai/gpt-6-sol";
14
14
  const responseData = (value) => {
15
15
  if (value === null || typeof value !== "object")
16
16
  return {};
@@ -1,9 +1,9 @@
1
1
  export declare const MODEL_COST_PRICING_SNAPSHOT: {
2
- readonly checkedAt: "2026-09-14";
2
+ readonly checkedAt: "2026-09-23";
3
3
  readonly currency: "USD";
4
4
  readonly unit: "1M tokens";
5
- readonly sources: readonly ["https://developers.openai.com/api/docs/pricing", "https://developers.openai.com/api/docs/models/gpt-6-astra", "https://developers.openai.com/api/docs/models/gpt-5.6-sol", "https://developers.openai.com/api/docs/models/gpt-5.6-terra", "https://developers.openai.com/api/docs/models/gpt-5.6-luna", "https://platform.claude.com/docs/en/about-claude/pricing"];
6
- readonly assumptions: readonly ["OpenAI Standard pricing; requests above 272,000 total input/cache tokens use 2x input/cache and 1.5x output pricing.", "Anthropic Standard pricing with the 5-minute cache-write rate; Fast, Batch, Flex, regional, tool, and subscription charges are excluded.", "variant and serviceTier are separate; model variants do not alter the selected Standard token price.", "gpt-5.6-luna-fast has no published model page; its rates are the resolved host model catalog entry for openai/gpt-5.6-luna-fast, exactly twice the Luna Standard schedule. That entry declares no separate long-context band, so the shared OpenAI band above is applied rather than a second assumed schedule."];
5
+ readonly sources: readonly ["https://developers.openai.com/api/docs/pricing", "https://developers.openai.com/api/docs/models/gpt-6-astra", "https://developers.openai.com/api/docs/models/gpt-6-sol", "https://developers.openai.com/api/docs/models/gpt-6-luna", "https://developers.openai.com/api/docs/models/gpt-5.6-sol", "https://developers.openai.com/api/docs/models/gpt-5.6-terra", "https://developers.openai.com/api/docs/models/gpt-5.6-luna", "https://platform.claude.com/docs/en/about-claude/pricing"];
6
+ readonly assumptions: readonly ["OpenAI Standard pricing; requests above 272,000 total input/cache tokens use 2x input/cache and 1.5x output pricing.", "Anthropic Standard pricing with the 5-minute cache-write rate; Fast, Batch, Flex, regional, tool, and subscription charges are excluded.", "variant and serviceTier are separate; model variants do not alter the selected Standard token price.", "GPT-6 Sol and GPT-6 Luna use their official Standard schedules; Fast mode is a service tier priced separately and does not create a second model ID.", "gpt-5.6-luna-fast is retained only as a compatibility price for historical host catalog entries; it has no published model page and is exactly twice the GPT-5.6 Luna Standard schedule."];
7
7
  };
8
8
  export interface ModelCostUsage {
9
9
  readonly providerID: string | undefined;
@@ -1,10 +1,12 @@
1
1
  export const MODEL_COST_PRICING_SNAPSHOT = {
2
- checkedAt: "2026-09-14",
2
+ checkedAt: "2026-09-23",
3
3
  currency: "USD",
4
4
  unit: "1M tokens",
5
5
  sources: [
6
6
  "https://developers.openai.com/api/docs/pricing",
7
7
  "https://developers.openai.com/api/docs/models/gpt-6-astra",
8
+ "https://developers.openai.com/api/docs/models/gpt-6-sol",
9
+ "https://developers.openai.com/api/docs/models/gpt-6-luna",
8
10
  "https://developers.openai.com/api/docs/models/gpt-5.6-sol",
9
11
  "https://developers.openai.com/api/docs/models/gpt-5.6-terra",
10
12
  "https://developers.openai.com/api/docs/models/gpt-5.6-luna",
@@ -14,11 +16,14 @@ export const MODEL_COST_PRICING_SNAPSHOT = {
14
16
  "OpenAI Standard pricing; requests above 272,000 total input/cache tokens use 2x input/cache and 1.5x output pricing.",
15
17
  "Anthropic Standard pricing with the 5-minute cache-write rate; Fast, Batch, Flex, regional, tool, and subscription charges are excluded.",
16
18
  "variant and serviceTier are separate; model variants do not alter the selected Standard token price.",
17
- "gpt-5.6-luna-fast has no published model page; its rates are the resolved host model catalog entry for openai/gpt-5.6-luna-fast, exactly twice the Luna Standard schedule. That entry declares no separate long-context band, so the shared OpenAI band above is applied rather than a second assumed schedule.",
19
+ "GPT-6 Sol and GPT-6 Luna use their official Standard schedules; Fast mode is a service tier priced separately and does not create a second model ID.",
20
+ "gpt-5.6-luna-fast is retained only as a compatibility price for historical host catalog entries; it has no published model page and is exactly twice the GPT-5.6 Luna Standard schedule.",
18
21
  ],
19
22
  };
20
23
  const OPENAI = {
21
24
  "gpt-6-astra": { input: 10, cacheRead: 1, cacheWrite: 12.5, output: 50 },
25
+ "gpt-6-sol": { input: 2, cacheRead: 0.2, cacheWrite: 2.5, output: 10 },
26
+ "gpt-6-luna": { input: 0.1, cacheRead: 0.01, cacheWrite: 0.125, output: 0.5 },
22
27
  "gpt-5.6-sol": { input: 4, cacheRead: 0.4, cacheWrite: 5, output: 20 },
23
28
  "gpt-5.6": { input: 4, cacheRead: 0.4, cacheWrite: 5, output: 20 },
24
29
  "gpt-5.6-terra": { input: 2, cacheRead: 0.2, cacheWrite: 2.5, output: 12 },
@@ -8,17 +8,17 @@ export interface RoleModelRoute {
8
8
  }
9
9
  export type ModelRoutingConfig = Readonly<Record<string, RoleModelRoute>>;
10
10
  /** The stable serial implementation route stays on Sol when Luna fan-out is unsafe or unavailable. */
11
- export declare const DEDICATED_WORKER_MODEL = "openai/gpt-5.6-sol";
11
+ export declare const DEDICATED_WORKER_MODEL = "openai/gpt-6-sol";
12
12
  export declare const DEDICATED_WORKER_VARIANT = "medium";
13
13
  /** Luna executes only units admitted to the fabric; it is not the serial worker's fallback target. */
14
- export declare const LUNA_FABRIC_WORKER_MODEL = "openai/gpt-5.6-luna";
14
+ export declare const LUNA_FABRIC_WORKER_MODEL = "openai/gpt-6-luna";
15
15
  export declare const LUNA_FABRIC_WORKER_VARIANT = "max";
16
16
  export declare const LUNA_FABRIC_WORKER_ROLE = "dog-luna-worker";
17
- /** The coordinator uses Terra High so routing and progress decisions do not bottleneck the workflow. */
18
- export declare const DEFAULT_COORDINATOR_MODEL = "openai/gpt-5.6-terra";
17
+ /** The coordinator uses Sol 6 High so routing and progress decisions do not bottleneck the workflow. */
18
+ export declare const DEFAULT_COORDINATOR_MODEL = "openai/gpt-6-sol";
19
19
  export declare const DEFAULT_COORDINATOR_VARIANT = "high";
20
20
  /** Compatibility names retained for the pre-v0.8 explicit Sol target. */
21
- export declare const ESCALATION_WORKER_MODEL = "openai/gpt-5.6-sol";
21
+ export declare const ESCALATION_WORKER_MODEL = "openai/gpt-6-sol";
22
22
  export declare const ESCALATION_WORKER_VARIANT = "medium";
23
23
  export declare const DEDICATED_WORKER_ROLES: readonly ["implementation", "remediation", "blocker-resolution", "sol-worker-mk2a2", "dog-worker"];
24
24
  /** Ordered last-resort targets used only when the host proves a policy target unavailable. */
@@ -37,8 +37,8 @@ export declare const LUNA_FABRIC_WORKER_TARGET: ModelTarget;
37
37
  export declare const LUNA_FABRIC_WORKER_ROUTING: ModelRoutingConfig;
38
38
  export declare function fixedWorkerRouting(dedicatedTarget?: ModelTarget): ModelRoutingConfig;
39
39
  export declare const FIXED_MODEL_ROUTING: ModelRoutingConfig;
40
- export declare const RECOMMENDED_LUNA_MODEL = "openai/gpt-5.6-luna";
41
- /** Coordinator state and routing use Terra High; bounded evidence retrieval uses Luna High. */
40
+ export declare const RECOMMENDED_LUNA_MODEL = "openai/gpt-6-luna";
41
+ /** Coordinator state and routing use Sol 6 High; bounded evidence retrieval uses Luna High. */
42
42
  export declare const DEFAULT_COORDINATOR_ROUTING: ModelRoutingConfig;
43
43
  /** Evidence gathering is retrieval rather than reasoning, so the scout stays one effort tier lower. */
44
44
  export declare const RECOMMENDED_SCOUT_VARIANT = "high";
@@ -1,13 +1,13 @@
1
1
  import { CONSULTATION_ROLE_POLICY } from "../core/consultation.js";
2
2
  /** The stable serial implementation route stays on Sol when Luna fan-out is unsafe or unavailable. */
3
- export const DEDICATED_WORKER_MODEL = "openai/gpt-5.6-sol";
3
+ export const DEDICATED_WORKER_MODEL = "openai/gpt-6-sol";
4
4
  export const DEDICATED_WORKER_VARIANT = "medium";
5
5
  /** Luna executes only units admitted to the fabric; it is not the serial worker's fallback target. */
6
- export const LUNA_FABRIC_WORKER_MODEL = "openai/gpt-5.6-luna";
6
+ export const LUNA_FABRIC_WORKER_MODEL = "openai/gpt-6-luna";
7
7
  export const LUNA_FABRIC_WORKER_VARIANT = "max";
8
8
  export const LUNA_FABRIC_WORKER_ROLE = "dog-luna-worker";
9
- /** The coordinator uses Terra High so routing and progress decisions do not bottleneck the workflow. */
10
- export const DEFAULT_COORDINATOR_MODEL = "openai/gpt-5.6-terra";
9
+ /** The coordinator uses Sol 6 High so routing and progress decisions do not bottleneck the workflow. */
10
+ export const DEFAULT_COORDINATOR_MODEL = "openai/gpt-6-sol";
11
11
  export const DEFAULT_COORDINATOR_VARIANT = "high";
12
12
  /** Compatibility names retained for the pre-v0.8 explicit Sol target. */
13
13
  export const ESCALATION_WORKER_MODEL = DEDICATED_WORKER_MODEL;
@@ -64,7 +64,7 @@ export function fixedWorkerRouting(dedicatedTarget = DEFAULT_DEDICATED_WORKER_TA
64
64
  export const FIXED_MODEL_ROUTING = fixedWorkerRouting();
65
65
  const fixedModelRoleSet = new Set(Object.keys(FIXED_MODEL_ROUTING));
66
66
  export const RECOMMENDED_LUNA_MODEL = LUNA_FABRIC_WORKER_MODEL;
67
- /** Coordinator state and routing use Terra High; bounded evidence retrieval uses Luna High. */
67
+ /** Coordinator state and routing use Sol 6 High; bounded evidence retrieval uses Luna High. */
68
68
  export const DEFAULT_COORDINATOR_ROUTING = Object.freeze({
69
69
  "dog-coordinator": Object.freeze({
70
70
  preferred: Object.freeze({
@@ -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";
@@ -22,17 +22,17 @@ const SERIAL_OPTIONAL_ARGUMENTS = new Map([
22
22
  "scope", "trigger", "cause", "prevention", "evidence", "evidenceRef", "id", "promotedRef", "confirmation",
23
23
  ])],
24
24
  ]);
25
- const PREVIEW_WORKER_ROUTE = Object.freeze({ model: "openai/gpt-5.6-luna-fast", variant: "max" });
26
- const PREVIEW_SCOUT_ROUTE = Object.freeze({ model: "openai/gpt-5.6-luna-fast", variant: "xhigh" });
25
+ const PREVIEW_WORKER_ROUTE = Object.freeze({ model: "openai/gpt-6-luna", variant: "max" });
26
+ const PREVIEW_SCOUT_ROUTE = Object.freeze({ model: "openai/gpt-6-luna", variant: "xhigh" });
27
27
  /**
28
28
  * Contract authorship, not throughput. Qualification observed a cheaper operations model emit
29
29
  * structurally valid but under-scoped contracts: a write union narrower than the remediation the
30
30
  * review it also schedules demands, which strands an otherwise complete run on NEED_DECISION.
31
31
  */
32
- const PREVIEW_OPERATIONS_ROUTE = Object.freeze({ model: "openai/gpt-5.6-terra", variant: "xhigh" });
33
- const PREVIEW_PRIMARY_ROUTE = Object.freeze({ model: "openai/gpt-5.6-luna-fast", variant: "max" });
32
+ const PREVIEW_OPERATIONS_ROUTE = Object.freeze({ model: "openai/gpt-6-sol", variant: "xhigh" });
33
+ const PREVIEW_PRIMARY_ROUTE = Object.freeze({ model: "openai/gpt-6-luna", variant: "max" });
34
34
  /** Review must be able to reject the worker's output, so it never shares the worker's model family. */
35
- const PREVIEW_REVIEW_ROUTE = Object.freeze({ model: "openai/gpt-5.6-terra", variant: "xhigh" });
35
+ const PREVIEW_REVIEW_ROUTE = Object.freeze({ model: "openai/gpt-6-sol", variant: "xhigh" });
36
36
  /** Every preview route the profile can bind a role to. Catalog declaration reads this one list. */
37
37
  const PREVIEW_ROUTES = Object.freeze([
38
38
  PREVIEW_PRIMARY_ROUTE, PREVIEW_WORKER_ROUTE, PREVIEW_SCOUT_ROUTE, PREVIEW_OPERATIONS_ROUTE,
@@ -55,9 +55,16 @@ export function processRemediationReplacementPacket(code, packet, cancelTool, pr
55
55
  */
56
56
  export function previewModelCatalog(routes = PREVIEW_ROUTES, base = BUILT_IN_MODEL_CATALOG.global ?? []) {
57
57
  const variantsOf = (model) => [...new Set(routes.filter(route => route.model === model).map(route => route.variant))];
58
- const listed = new Set(base.map(entry => entry.model));
58
+ const mergedBase = new Map();
59
+ for (const entry of base) {
60
+ const prior = mergedBase.get(entry.model);
61
+ mergedBase.set(entry.model, prior === undefined
62
+ ? entry
63
+ : { model: entry.model, variants: [...new Set([...(prior.variants ?? []), ...(entry.variants ?? [])])] });
64
+ }
65
+ const listed = new Set(mergedBase.keys());
59
66
  return [
60
- ...base.map(entry => {
67
+ ...[...mergedBase.values()].map(entry => {
61
68
  const variants = variantsOf(entry.model);
62
69
  return variants.length === 0
63
70
  ? entry
@@ -395,6 +402,16 @@ export function createProfiledPlugin(profile, assetVersion) {
395
402
  const approvalSchema = record(stringSchema) && typeof stringSchema.describe === "function"
396
403
  ? stringSchema.describe(OPERATOR_APPROVAL_CONTRACT)
397
404
  : { ...(record(stringSchema) ? stringSchema : { type: "string" }), description: OPERATOR_APPROVAL_CONTRACT };
405
+ const revisionSchema = record(stringSchema) && typeof stringSchema.describe === "function"
406
+ ? stringSchema.describe(OPERATOR_PROPOSAL_REVISION_CONTRACT)
407
+ : { ...(record(stringSchema) ? stringSchema : { type: "string" }), description: OPERATOR_PROPOSAL_REVISION_CONTRACT };
408
+ const approvedRevisionContract = "revision_json must encode exactly {proposal_id:string,revision:positive integer,content_hash:string,operator_run_id:string,rationale:nonblank single-line string,intent:object}. " +
409
+ "The intent has the exact begin_operator_proposal intent_json shape and a cumulative proposal_budget with room above all retained reads/submissions. " +
410
+ "Pin the approved proposal identity and the cancelled/completed run ID from operator_status. For the same goal, the new requirements must begin with every old ordered requirement byte-for-byte. Only a root acting for a newly authorized active goal may revise; active/prepared runs, stale identity or unchanged goal binding are denied. " +
411
+ "Old acceptance, terminal status and proposal accounting remain durable. This returns an investigation Task, not implementation or acceptance.";
412
+ const approvedRevisionSchema = record(stringSchema) && typeof stringSchema.describe === "function"
413
+ ? stringSchema.describe(approvedRevisionContract)
414
+ : { ...(record(stringSchema) ? stringSchema : { type: "string" }), description: approvedRevisionContract };
398
415
  const planContract = "plan_json must encode the exact operator plan object: schema_version, acceptance, acceptance_proof, source_refs, " +
399
416
  "goal_declaration, units, and optional git_lifecycle only. git_lifecycle exact shape is " +
400
417
  '{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 +437,8 @@ export function createProfiledPlugin(profile, assetVersion) {
420
437
  const beginProposal = profileTool(profile, "sortie_begin_operator_proposal");
421
438
  const submitProposal = profileTool(profile, "sortie_submit_operator_proposal");
422
439
  const approveProposal = profileTool(profile, "sortie_approve_operator_proposal");
440
+ const reviseProposal = profileTool(profile, "sortie_revise_operator_proposal");
441
+ const reviseApprovedIntent = profileTool(profile, "sortie_revise_approved_operator_intent");
423
442
  async function stop(root, reason, retireRoot = true) {
424
443
  if (retireRoot)
425
444
  retired.add(root);
@@ -559,7 +578,7 @@ export function createProfiledPlugin(profile, assetVersion) {
559
578
  ? "proposal submission budget exhausted; do not call operator_next; report the bounded proposal failure"
560
579
  : "proposal is not submitted; do not call operator_next; complete or repair the bounded proposal submission"
561
580
  : proposal?.phase === "submitted"
562
- ? "compare and approve the exact proposal; do not call operator_next before approval prepares a run. Approval authorizes implementation, not product acceptance. Preserve every required live measurement, consultation 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."
581
+ ? `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
582
  : "approved proposal has no operator run; do not call operator_next; reconcile the approval or preparation failure";
564
583
  return JSON.stringify(state ? { ...await operatorPacket(state), ...(draft ? { pending_draft: draft } : {}),
565
584
  ...(proposal ? { proposal: proposalIdentity(proposal) } : {}) }
@@ -801,6 +820,33 @@ export function createProfiledPlugin(profile, assetVersion) {
801
820
  return JSON.stringify({ ...proposals.packet(state), task: proposals.referenceTask(state),
802
821
  dispatch_instruction: "Pass this short Task reference verbatim; do not append or paraphrase it." });
803
822
  } };
823
+ tools[reviseApprovedIntent] = { description: `Root-only: after a terminal cancelled/completed run, explicitly replace its approved proposal with a new user-authorized intent. Supply revision_json with exactly {proposal_id,revision,content_hash,operator_run_id,rationale,intent}; pin the approved identity and terminal run from operator_status. The intent has the exact ${beginProposal} intent_json shape and a cumulative proposal_budget strictly above retained reads/submissions. A current active goal with a changed binding and no outstanding reservations is required. The prior approved contract, ordered acceptance, terminal disposition and cumulative proposal spend are archived; the old run is never accepted, revived or overwritten. Active/prepared runs and stale identities are rejected. This grants one new investigation Task, not approval or execution.`,
824
+ args: { revision_json: approvedRevisionSchema }, execute: async (args, context) => {
825
+ await requireRoot(context.sessionID);
826
+ return serializeDispatchTransition(context.sessionID, async () => {
827
+ let request;
828
+ try {
829
+ request = JSON.parse(args.revision_json);
830
+ }
831
+ catch {
832
+ return JSON.stringify({ status: "invalid-approved-revision", code: "operator-proposal-approved-revision-json-invalid" });
833
+ }
834
+ try {
835
+ const binding = await control.proposalGoalBinding(context.sessionID);
836
+ await control.assertActiveGoal(context.sessionID, binding.acceptance_fingerprint);
837
+ const revised = await proposals.reviseApproved(context.sessionID, context.sessionID, request, () => new OperatorRuntime(input.directory, profile).read(context.sessionID), binding);
838
+ return JSON.stringify({ ...proposals.packet(revised), task: proposals.referenceTask(revised),
839
+ dispatch_instruction: "Dispatch this new exact Task reference once; do not reuse the prior approved contract or its child." });
840
+ }
841
+ catch (error) {
842
+ if (error instanceof OperatorProposalBudgetError)
843
+ return JSON.stringify(error.diagnostic);
844
+ if (!(error instanceof Error) || !error.message.startsWith("operator-proposal-"))
845
+ throw error;
846
+ return JSON.stringify({ status: "invalid-approved-revision", code: error.message });
847
+ }
848
+ });
849
+ } };
804
850
  tools[submitProposal] = { description: "Proposal-child-only: submit a requirement-mapped contract proposal without editing source or starting workers. " + proposalContract + " Omit plan.acceptance; the host derives its exact ordered text from durable intent.requirements. A legacy explicit plan.acceptance is accepted only when byte-exact in content and order; null, subsets, reorder, normalization, and acceptance_ids aliases are rejected.",
805
851
  args: { proposal_json: proposalSchema }, execute: async (args, context) => {
806
852
  const root = await rootFor(context.sessionID);
@@ -823,6 +869,26 @@ export function createProfiledPlugin(profile, assetVersion) {
823
869
  ...(contract ? { diagnostics: error.diagnostics, diagnostics_truncated: error.diagnostics_truncated } : {}) });
824
870
  }
825
871
  } };
872
+ 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,
873
+ args: { revision_json: revisionSchema }, execute: async (args, context) => {
874
+ await requireRoot(context.sessionID);
875
+ return serializeDispatchTransition(context.sessionID, async () => {
876
+ try {
877
+ const revised = await proposals.reviseJSON(context.sessionID, context.sessionID, args.revision_json);
878
+ return JSON.stringify({ ...proposals.packet(revised),
879
+ 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." });
880
+ }
881
+ catch (error) {
882
+ const contract = error instanceof OperatorContractError;
883
+ if (!contract && (!(error instanceof Error) || !error.message.startsWith("operator-proposal-")))
884
+ throw error;
885
+ const state = await proposals.read(context.sessionID);
886
+ return JSON.stringify({ status: "invalid-revision", code: error.message,
887
+ ...(state ? { proposal: proposalIdentity(state) } : {}),
888
+ ...(contract ? { diagnostics: error.diagnostics, diagnostics_truncated: error.diagnostics_truncated } : {}) });
889
+ }
890
+ });
891
+ } };
826
892
  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
893
  args: { approval_json: approvalSchema }, execute: async (args, context) => {
828
894
  await requireRoot(context.sessionID);
@@ -852,6 +918,7 @@ export function createProfiledPlugin(profile, assetVersion) {
852
918
  if (approved.goal_binding === null)
853
919
  throw new Error("operator-proposal-goal-binding-missing");
854
920
  await control.assertProposalExecutionBudget(context.sessionID, approved.goal_binding, proposal.plan.units.length);
921
+ await proposals.pinApproval(context.sessionID, approval);
855
922
  const prepared = await operators.propose(context.sessionID, proposal.plan);
856
923
  if (prepared.status !== "prepared")
857
924
  throw new Error("operator-proposal-approved-plan-invalid");
@@ -861,7 +928,7 @@ export function createProfiledPlugin(profile, assetVersion) {
861
928
  });
862
929
  } };
863
930
  const ownTools = new Set([prepare, repair, next, status, cancel, complete, resume, resolveContractRepair,
864
- beginProposal, submitProposal, approveProposal]);
931
+ beginProposal, submitProposal, approveProposal, reviseProposal, reviseApprovedIntent]);
865
932
  const protocolMap = CANONICAL_AGENT_ROLES.map(role => `${role}=${profileAgent(profile, role)}`).join(", ");
866
933
  function rememberRendered(key, text) {
867
934
  renderedParts.set(key, goalFingerprint(text));
@@ -89,7 +89,7 @@ permission rejection shows a different project root, do not repeat that path; re
89
89
  const coordinatorContent = `---
90
90
  description: Sortie-dogs ${V010_RUNTIME_ASSET_VERSION} primary dog-operator — strategic authority with a bounded operations delegate.
91
91
  mode: primary
92
- model: openai/gpt-5.6-luna-fast
92
+ model: openai/gpt-6-luna
93
93
  variant: max
94
94
  permission:
95
95
  question: allow
@@ -153,6 +153,14 @@ Every requirement entry must include kind="requirement" | "negative" | "quality"
153
153
  Use only authoritative_refs and allow_read for begin intent scope. project_root, source_refs, max_read_prefixes,
154
154
  authoritative_references, and other aliases are invalid. Do not guess a smaller contract after rejection: use this exact
155
155
  shape, preserving all ordered requirements, and omit proposal_budget only when the deterministic host default is intended.
156
+ If the root already has an approved proposal, first finish or cancel its operator run. After a genuinely new user-authorized
157
+ goal is active, use ${profile.toolPrefix}revise_approved_operator_intent with revision_json containing exactly the old
158
+ proposal_id, revision, content_hash, terminal operator_run_id, a single-line rationale, and the complete new intent object.
159
+ Pin the identity from operator_status. The host refuses active/prepared runs, stale identities, unchanged goal bindings,
160
+ and budgets without room above cumulative proposal spend. It archives the old approved intent, ordered acceptance and
161
+ terminal result before granting one new investigation Task. Within the same goal, retain the old ordered requirements
162
+ byte-for-byte as a prefix of the new intent; a separate user-authorized goal may have a different order. No child, unit,
163
+ acceptance or budget is revived or reset.
156
164
  Freeze product requirements from the original request, not extra implementation criteria invented from workflow
157
165
  bookkeeping. Keep proposal read/submission allowances in proposal_budget and host counters; do not turn spent
158
166
  budgets or your own reporting obligations into worker validation commands. Preserve any explicit user requirement.
@@ -168,7 +176,19 @@ report that specific limitation for a scope/design decision instead of inventing
168
176
  The proposal must map every original requirement ID to approach and validation, explicitly list uncovered IDs, preserve
169
177
  negative-condition handling, and include exact read/write/unit/GoalDeclaration plans and budget estimates. Its summary is
170
178
  not the source of truth and proposal reads are not acceptance evidence. Compare the submitted proposal directly with the
171
- original request. Only the root may call ${profile.toolPrefix}approve_operator_proposal with the exact proposal_id,
179
+ original request. For a known defect in a submitted, unapproved proposal, the root may call
180
+ ${profile.toolPrefix}revise_operator_proposal with revision_json containing the current proposal_id, revision, content_hash,
181
+ a substantive rationale, and 1..32 allowlisted {op,path,value} patches. Follow its exact schema; do not resend the full packet.
182
+ Correct coverage/uncovered, observed surface claims, negative handling, unit/milestone/proof mapping, or shrink/normalize reads
183
+ using only the completed investigation's evidence. Ordered requirements/acceptance, references, goal binding, budgets,
184
+ criterion identities/defaults, Git authority and the exact write union stay fixed; execution unit count cannot increase.
185
+ Successful and invalid patch attempts share the remaining submission allowance. Revision grants no new read, child or worker
186
+ and requires no live proposal child. The host atomically records provenance and derives the next revision/hash.
187
+ Approval preparation closes revision even if preparation later fails. Preserve that pinned plan for approval recovery.
188
+ Never claim worker tests prove root-owned push/global apply or user-only acceptance; keep those obligations explicitly pending
189
+ until their actual evidence exists. If existing contracts cannot express a required obligation, keep it uncovered and report
190
+ the exact limitation instead of substituting a passing test. A root patch is not acceptance or automatic approval.
191
+ Compare every original requirement again after revision. Only the root may call ${profile.toolPrefix}approve_operator_proposal with the exact proposal_id,
172
192
  revision, content_hash, ordered compared_requirement_ids, decision=approve, and a substantive comparison rationale.
173
193
  Uncovered requirements, stale hashes/revisions, another root, widened scope, rewritten acceptance, or reset proposal
174
194
  accounting must be rejected. Hash matching records identity; it never replaces this semantic root decision. Approval alone
@@ -12,7 +12,7 @@ export declare const runtimeAssets: readonly [{
12
12
  readonly content: `---
13
13
  description: Canonical MkII coordinator packaged by Sortie-dogs
14
14
  mode: primary
15
- model: openai/gpt-5.6-terra
15
+ model: openai/gpt-6-sol
16
16
  variant: high
17
17
  permission:
18
18
  question: allow
@@ -414,7 +414,7 @@ uses the run's shared time/cost limits. Use the existing compiled plan and Luna
414
414
  Call sortie_prepare_failure_swarm. Dispatch only its returned ready descriptors with
415
415
  dog-luna-worker and one failure_swarm_descriptor JSON line. The plugin binds source scope,
416
416
  read-only authority, distinct causes, cumulative budget, and the shared cancellable lifecycle.
417
- No diagnosis child may write or select a remedy. After findings finish, the coordinator (Terra by
417
+ No diagnosis child may write or select a remedy. After findings finish, the coordinator (Sol 6 by
418
418
  default, or the user's explicitly selected coordinator) calls sortie_select_failure_diagnosis
419
419
  with swarm_id and one selection_json containing diagnosis_id, capsule_id, recovery_kind,
420
420
  proposal, and budget_request. Preserve its immutable scope/acceptance/validation contract and
@@ -241,7 +241,7 @@ export const runtimeAssets = [
241
241
  content: `---
242
242
  description: Canonical MkII coordinator packaged by Sortie-dogs
243
243
  mode: primary
244
- model: openai/gpt-5.6-terra
244
+ model: openai/gpt-6-sol
245
245
  variant: high
246
246
  permission:
247
247
  question: allow
@@ -643,7 +643,7 @@ uses the run's shared time/cost limits. Use the existing compiled plan and Luna
643
643
  Call sortie_prepare_failure_swarm. Dispatch only its returned ready descriptors with
644
644
  dog-luna-worker and one failure_swarm_descriptor JSON line. The plugin binds source scope,
645
645
  read-only authority, distinct causes, cumulative budget, and the shared cancellable lifecycle.
646
- No diagnosis child may write or select a remedy. After findings finish, the coordinator (Terra by
646
+ No diagnosis child may write or select a remedy. After findings finish, the coordinator (Sol 6 by
647
647
  default, or the user's explicitly selected coordinator) calls sortie_select_failure_diagnosis
648
648
  with swarm_id and one selection_json containing diagnosis_id, capsule_id, recovery_kind,
649
649
  proposal, and budget_request. Preserve its immutable scope/acceptance/validation contract and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.10.19",
3
+ "version": "0.10.22",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",