sortie-dogs 0.10.20 → 0.10.23

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
@@ -1,11 +1,11 @@
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;
6
6
  }>;
7
7
  export declare const OPERATOR_PROPOSAL_BUDGET_CAPS: Readonly<{
8
- max_reads: 192;
8
+ max_reads: 228;
9
9
  max_submissions: 24;
10
10
  }>;
11
11
  export declare const OPERATOR_PROPOSAL_REVISION_CONTRACT: string;
@@ -94,6 +94,32 @@ export interface OperatorProposalState {
94
94
  /** Preparation may publish execution controls before approval commits; never revise that pinned plan. */
95
95
  approval_started?: boolean;
96
96
  root_revisions?: OperatorProposalRevision[];
97
+ /** Root-authorized cumulative increases; the submitted plan and its identity do not change. */
98
+ budget_revisions?: readonly {
99
+ readonly at: string;
100
+ readonly rationale: string;
101
+ readonly from: OperatorIntent["proposal_budget"];
102
+ readonly to: OperatorIntent["proposal_budget"];
103
+ readonly proposal_id: string;
104
+ readonly content_hash: string;
105
+ readonly goal_binding: OperatorProposalGoalBinding;
106
+ }[];
107
+ /** Submitted contracts displaced by a newly evidenced scope gap, never erased or approved. */
108
+ submitted_history?: readonly {
109
+ readonly archive_hash: string;
110
+ readonly proposal_id: string;
111
+ readonly revision: number;
112
+ readonly content_hash: string;
113
+ readonly missing_write_paths: readonly string[];
114
+ }[];
115
+ /** Immutable archive identities of earlier approved contracts on this root. */
116
+ approved_history?: readonly {
117
+ readonly archive_hash: string;
118
+ readonly intent_id: string;
119
+ readonly proposal_id: string;
120
+ readonly run_id: string;
121
+ readonly disposition: "completed" | "incomplete";
122
+ }[];
97
123
  }
98
124
  interface OperatorProposalIdentity {
99
125
  readonly proposal_id: string;
@@ -136,6 +162,7 @@ export declare class OperatorProposalRuntime {
136
162
  readonly projectRoot: string;
137
163
  constructor(projectRoot: string, profile: RuntimeProfile);
138
164
  private file;
165
+ private approvedArchiveFile;
139
166
  private spendFile;
140
167
  private readSpend;
141
168
  private saveSpend;
@@ -153,6 +180,13 @@ export declare class OperatorProposalRuntime {
153
180
  private resolveTask;
154
181
  begin(root: string, raw: unknown): Promise<OperatorProposalState>;
155
182
  private beginUnlocked;
183
+ private submittedArchiveFile;
184
+ /** Increase a submitted proposal's cumulative allowance without replacing its goal, plan or investigation. */
185
+ extendSubmittedBudget(root: string, actor: string, raw: unknown, currentGoal: OperatorProposalGoalBinding): Promise<OperatorProposalState>;
186
+ /** Reopen only a newly diagnosed scope gap in a submitted plan, preserving its exact archived identity. */
187
+ reopenSubmittedScope(root: string, actor: string, raw: unknown, currentGoal: OperatorProposalGoalBinding): Promise<OperatorProposalState>;
188
+ /** Explicit root-only transition after an approved contract's execution lane has ended. */
189
+ reviseApproved(root: string, actor: string, raw: unknown, readRun: () => Promise<OperatorState | undefined>, currentGoal: OperatorProposalGoalBinding, proveApprovedRunAncestry?: (oldGoalID: string, parentRunID: string) => Promise<boolean>): Promise<OperatorProposalState>;
156
190
  bindGoal(root: string, binding: OperatorProposalGoalBinding): Promise<OperatorProposalState>;
157
191
  private bindGoalUnlocked;
158
192
  admit(root: string, callID: string, args: unknown, reserveBudget?: () => Promise<void>): Promise<OperatorTask>;
@@ -9,7 +9,7 @@ const text = (value) => typeof value === "string" && value.trim().length > 0 &&
9
9
  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
- export const OPERATOR_PROPOSAL_BUDGET_CAPS = Object.freeze({ max_reads: 192, max_submissions: 24 });
12
+ export const OPERATOR_PROPOSAL_BUDGET_CAPS = Object.freeze({ max_reads: 228, max_submissions: 24 });
13
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
14
  "Pin the current submitted identity from operator_status. patches contains 1..32 {op,path,value} objects. " +
15
15
  "replace is allowed only at /coverage, /existing_surface, /uncovered, /negative_handling, /read_scope, /plan/units, /plan/acceptance_proof, " +
@@ -343,6 +343,9 @@ export class OperatorProposalRuntime {
343
343
  this.projectRoot = resolve(projectRoot);
344
344
  }
345
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
+ }
346
349
  spendFile(root) { return join(this.projectRoot, this.profile.stateDirectory, "operator-proposals", `${hash(root)}.spend.json`); }
347
350
  async readSpend(root) {
348
351
  let source;
@@ -367,6 +370,12 @@ export class OperatorProposalRuntime {
367
370
  !/^sha256:[a-f0-9]{64}$/u.test(spend.retry.goal_binding.acceptance_fingerprint))) {
368
371
  throw new Error("operator-proposal-spend-invalid");
369
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
+ }
370
379
  return spend;
371
380
  }
372
381
  async saveSpend(spend) {
@@ -543,11 +552,184 @@ export class OperatorProposalRuntime {
543
552
  goal_binding: spend.retry?.intent_hash === intentHash ? structuredClone(spend.retry.goal_binding) : null,
544
553
  proposal_call_id: null, proposal_session_id: null, read_count: spend.reads, read_paths: [], submission_count: spend.submissions, proposal_id: null,
545
554
  ...(spend.reads > 0 || spend.submissions > 0 ? { prior_spend: { reads: spend.reads, submissions: spend.submissions } } : {}),
546
- 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) } : {}) };
547
557
  this.assertBudgetAvailable(state);
548
558
  await this.save(state);
549
559
  return state;
550
560
  }
561
+ submittedArchiveFile(root, intentID, proposalHash) {
562
+ return join(this.projectRoot, this.profile.stateDirectory, "operator-proposals", `${hash(root)}.${intentID}.${proposalHash}.submitted.json`);
563
+ }
564
+ /** Increase a submitted proposal's cumulative allowance without replacing its goal, plan or investigation. */
565
+ async extendSubmittedBudget(root, actor, raw, currentGoal) {
566
+ if (actor !== root)
567
+ throw new Error("operator-proposal-budget-revision-root-required");
568
+ return this.serial(root, async () => {
569
+ const state = await this.requiredUnlocked(root);
570
+ if (state.phase !== "submitted" || !state.proposal || !state.goal_binding || state.approval_started) {
571
+ throw new Error("operator-proposal-budget-revision-not-submitted");
572
+ }
573
+ if (!record(raw) || !exactSet(raw, ["proposal_id", "revision", "content_hash", "rationale", "proposal_budget"]) ||
574
+ !text(raw.proposal_id) || !Number.isSafeInteger(raw.revision) || !text(raw.content_hash) ||
575
+ !text(raw.rationale) || raw.rationale.length > 2000 || !record(raw.proposal_budget) ||
576
+ !exactSet(raw.proposal_budget, ["max_reads", "max_submissions"])) {
577
+ throw new Error("operator-proposal-budget-revision-invalid");
578
+ }
579
+ if (raw.proposal_id !== state.proposal_id || raw.revision !== state.proposal_revision ||
580
+ raw.content_hash !== state.proposal_hash)
581
+ throw new Error("operator-proposal-budget-revision-identity-mismatch");
582
+ if (JSON.stringify(currentGoal) !== JSON.stringify(state.goal_binding)) {
583
+ throw new Error("operator-proposal-budget-revision-goal-mismatch");
584
+ }
585
+ const before = state.intent.proposal_budget;
586
+ const budget = raw.proposal_budget;
587
+ const reads = budget.max_reads, submissions = budget.max_submissions;
588
+ if (!Number.isSafeInteger(reads) || !Number.isSafeInteger(submissions) ||
589
+ reads > OPERATOR_PROPOSAL_BUDGET_CAPS.max_reads || submissions > OPERATOR_PROPOSAL_BUDGET_CAPS.max_submissions ||
590
+ reads < before.max_reads || submissions < before.max_submissions ||
591
+ (reads === before.max_reads && submissions === before.max_submissions) ||
592
+ reads <= state.read_count || submissions <= state.submission_count) {
593
+ throw new Error("operator-proposal-budget-revision-range-invalid");
594
+ }
595
+ const next = { max_reads: reads, max_submissions: submissions };
596
+ const intent = { ...state.intent, proposal_budget: next };
597
+ // The intent hash protects the stored budget; the original intent ID and submitted proposal identity stay pinned.
598
+ const revised = { ...state, intent, intent_hash: hash(JSON.stringify(intent)),
599
+ budget_revisions: [...(state.budget_revisions ?? []), { at: new Date().toISOString(), rationale: raw.rationale,
600
+ from: { ...before }, to: next, proposal_id: state.proposal_id, content_hash: state.proposal_hash,
601
+ goal_binding: structuredClone(state.goal_binding) }] };
602
+ await this.save(revised);
603
+ return revised;
604
+ });
605
+ }
606
+ /** Reopen only a newly diagnosed scope gap in a submitted plan, preserving its exact archived identity. */
607
+ async reopenSubmittedScope(root, actor, raw, currentGoal) {
608
+ if (actor !== root)
609
+ throw new Error("operator-proposal-scope-reopen-root-required");
610
+ return this.serial(root, async () => {
611
+ const state = await this.requiredUnlocked(root);
612
+ if (state.phase !== "submitted" || !state.proposal || !state.goal_binding || state.approval_started) {
613
+ throw new Error("operator-proposal-scope-reopen-not-submitted");
614
+ }
615
+ if (!record(raw) || !exactSet(raw, ["proposal_id", "revision", "content_hash", "rationale", "missing_write_paths"]) ||
616
+ !text(raw.proposal_id) || !Number.isSafeInteger(raw.revision) || !text(raw.content_hash) ||
617
+ !text(raw.rationale) || raw.rationale.length > 2000 || !Array.isArray(raw.missing_write_paths) ||
618
+ raw.missing_write_paths.length < 1 || raw.missing_write_paths.length > 16 ||
619
+ !raw.missing_write_paths.every(normalizedRelativePath) ||
620
+ new Set(raw.missing_write_paths).size !== raw.missing_write_paths.length) {
621
+ throw new Error("operator-proposal-scope-reopen-invalid");
622
+ }
623
+ if (raw.proposal_id !== state.proposal_id || raw.revision !== state.proposal_revision ||
624
+ raw.content_hash !== state.proposal_hash)
625
+ throw new Error("operator-proposal-scope-reopen-identity-mismatch");
626
+ if (JSON.stringify(currentGoal) !== JSON.stringify(state.goal_binding)) {
627
+ throw new Error("operator-proposal-scope-reopen-goal-mismatch");
628
+ }
629
+ this.assertBudgetAvailable(state);
630
+ const oldWrites = state.proposal.write_scope;
631
+ const missing = raw.missing_write_paths;
632
+ if (missing.some(path => oldWrites.some(scope => path === scope || path.startsWith(`${scope}/`)) ||
633
+ !state.intent.allow_read.some(scope => path === scope || path.startsWith(`${scope}/`)))) {
634
+ throw new Error("operator-proposal-scope-reopen-path-invalid");
635
+ }
636
+ const archive = JSON.stringify({ submitted_proposal: state, missing_write_paths: missing, rationale: raw.rationale });
637
+ const archivePath = this.submittedArchiveFile(root, state.intent_id, state.proposal_hash);
638
+ try {
639
+ await writeFile(archivePath, archive, { flag: "wx", mode: 0o600 });
640
+ }
641
+ catch (error) {
642
+ if (error.code !== "EEXIST" || hash(await readFile(archivePath, "utf8")) !== hash(archive))
643
+ throw error;
644
+ }
645
+ const reopened = { ...state, phase: "investigating", created_at: new Date().toISOString(),
646
+ prior_spend: { reads: state.read_count, submissions: state.submission_count },
647
+ proposal_call_id: null, proposal_session_id: null, read_paths: [], proposal_id: null, proposal_revision: null,
648
+ proposal_hash: null, proposal: null, approval_rationale: null,
649
+ submitted_history: [...(state.submitted_history ?? []), { archive_hash: hash(archive), proposal_id: state.proposal_id,
650
+ revision: state.proposal_revision, content_hash: state.proposal_hash, missing_write_paths: missing }] };
651
+ await this.save(reopened);
652
+ return reopened;
653
+ });
654
+ }
655
+ /** Explicit root-only transition after an approved contract's execution lane has ended. */
656
+ async reviseApproved(root, actor, raw, readRun, currentGoal, proveApprovedRunAncestry) {
657
+ if (actor !== root)
658
+ throw new Error("operator-proposal-approved-revision-root-required");
659
+ return this.serial(root, async () => {
660
+ if (!record(raw) || !exactSet(raw, ["proposal_id", "revision", "content_hash", "operator_run_id", "rationale", "intent"]) ||
661
+ typeof raw.proposal_id !== "string" || !Number.isSafeInteger(raw.revision) || typeof raw.content_hash !== "string" ||
662
+ typeof raw.operator_run_id !== "string" || !text(raw.rationale)) {
663
+ throw new Error("operator-proposal-approved-revision-invalid");
664
+ }
665
+ const previous = await this.requiredUnlocked(root);
666
+ if (previous.phase !== "approved" || !previous.proposal || !previous.goal_binding) {
667
+ throw new Error("operator-proposal-approved-revision-not-approved");
668
+ }
669
+ if (raw.proposal_id !== previous.proposal_id || raw.revision !== previous.proposal_revision ||
670
+ raw.content_hash !== previous.proposal_hash)
671
+ throw new Error("operator-proposal-approved-revision-identity-mismatch");
672
+ const run = await readRun();
673
+ if (!run || !["cancelled", "completed"].includes(run.phase) || run.rootSessionID !== root ||
674
+ run.runID !== raw.operator_run_id ||
675
+ JSON.stringify(run.acceptance) !== JSON.stringify(previous.proposal.plan.acceptance)) {
676
+ throw new Error("operator-proposal-approved-revision-terminal-run-required");
677
+ }
678
+ const originalPlan = run.planHash === hash(JSON.stringify(previous.proposal.plan));
679
+ if (!originalPlan && (!run.parentRunID || !proveApprovedRunAncestry ||
680
+ !await proveApprovedRunAncestry(previous.goal_binding.goal_id, run.parentRunID))) {
681
+ throw new Error("operator-proposal-approved-revision-terminal-run-required");
682
+ }
683
+ if (!text(currentGoal.goal_id) || !Number.isSafeInteger(currentGoal.revision) || currentGoal.revision < 1 ||
684
+ !Number.isSafeInteger(currentGoal.scope_epoch) || currentGoal.scope_epoch < 1 ||
685
+ !/^sha256:[a-f0-9]{64}$/u.test(currentGoal.acceptance_fingerprint)) {
686
+ throw new Error("operator-proposal-goal-binding-invalid");
687
+ }
688
+ if (JSON.stringify(currentGoal) === JSON.stringify(previous.goal_binding)) {
689
+ throw new Error("operator-proposal-approved-revision-goal-revision-required");
690
+ }
691
+ const intent = parseIntent(raw.intent), intentHash = hash(JSON.stringify(intent));
692
+ if (intentHash === previous.intent_hash)
693
+ throw new Error("operator-proposal-approved-revision-intent-unchanged");
694
+ if (JSON.stringify(intent.requirements.slice(0, previous.intent.requirements.length)) !== JSON.stringify(previous.intent.requirements)) {
695
+ throw new Error("operator-proposal-approved-revision-acceptance-continuity-required");
696
+ }
697
+ const remainingReads = intent.proposal_budget.max_reads - previous.read_count;
698
+ const remainingSubmissions = intent.proposal_budget.max_submissions - previous.submission_count;
699
+ if (remainingReads <= 0 || remainingSubmissions <= 0) {
700
+ throw new OperatorProposalBudgetError({ status: "proposal-budget-exhausted",
701
+ code: remainingReads <= 0 ? "operator-proposal-read-budget-exhausted" : "operator-proposal-submission-budget-exhausted",
702
+ reads: previous.read_count, max_reads: intent.proposal_budget.max_reads, remaining_reads: Math.max(0, remainingReads),
703
+ submissions: previous.submission_count, max_submissions: intent.proposal_budget.max_submissions,
704
+ remaining_submissions: Math.max(0, remainingSubmissions),
705
+ required_minimum_budget: { max_reads: previous.read_count + 1, max_submissions: previous.submission_count + 1 } });
706
+ }
707
+ const archive = JSON.stringify({ approved_proposal: previous, terminal_run: {
708
+ run_id: run.runID, plan_hash: run.planHash, acceptance: run.acceptance, acceptance_fingerprint: run.acceptanceFingerprint,
709
+ phase: run.phase, decision: run.decision, receipt: run.receipt, generation: run.generation,
710
+ parent_run_id: originalPlan ? null : run.parentRunID
711
+ } });
712
+ const archivePath = this.approvedArchiveFile(root, previous.intent_id, previous.proposal_hash, run.runID);
713
+ try {
714
+ await writeFile(archivePath, archive, { flag: "wx", mode: 0o600 });
715
+ }
716
+ catch (error) {
717
+ if (error.code !== "EEXIST" || hash(await readFile(archivePath, "utf8")) !== hash(archive))
718
+ throw error;
719
+ }
720
+ const state = { schema_version: "0.1", profile: this.profile.id, root_session_id: root,
721
+ intent_id: `intent-${intentHash.slice(0, 24)}`, intent_hash: intentHash, intent, created_at: new Date().toISOString(),
722
+ phase: "investigating", goal_binding: structuredClone(currentGoal),
723
+ prior_spend: { reads: previous.read_count, submissions: previous.submission_count },
724
+ proposal_call_id: null, proposal_session_id: null, read_count: previous.read_count, read_paths: [],
725
+ submission_count: previous.submission_count, proposal_id: null, proposal_revision: null, proposal_hash: null,
726
+ proposal: null, approval_rationale: null,
727
+ approved_history: [...(previous.approved_history ?? []), { archive_hash: hash(archive), intent_id: previous.intent_id,
728
+ proposal_id: previous.proposal_id, run_id: run.runID, disposition: run.phase === "completed" ? "completed" : "incomplete" }] };
729
+ await this.save(state);
730
+ return state;
731
+ });
732
+ }
551
733
  async bindGoal(root, binding) {
552
734
  return this.serial(root, () => this.bindGoalUnlocked(root, binding));
553
735
  }
@@ -826,6 +1008,7 @@ export class OperatorProposalRuntime {
826
1008
  return undefined;
827
1009
  await this.saveSpend({ schema_version: "0.1", profile: this.profile.id, root_session_id: root,
828
1010
  reads: state.read_count, submissions: state.submission_count,
1011
+ ...(state.approved_history ? { approved_history: state.approved_history } : {}),
829
1012
  ...(state.goal_binding === null ? {} : { retry: { intent_hash: state.intent_hash, goal_binding: state.goal_binding } }) });
830
1013
  await rm(this.file(root), { force: true });
831
1014
  return state;
@@ -846,10 +1029,13 @@ export class OperatorProposalRuntime {
846
1029
  packet(state) {
847
1030
  return { status: state.phase, intent_id: state.intent_id, intent_hash: state.intent_hash,
848
1031
  proposal_id: state.proposal_id, revision: state.proposal_revision, content_hash: state.proposal_hash, reads: state.read_count,
849
- remaining_reads: state.intent.proposal_budget.max_reads - state.read_count, submissions: state.submission_count,
1032
+ max_reads: state.intent.proposal_budget.max_reads, remaining_reads: state.intent.proposal_budget.max_reads - state.read_count,
1033
+ submissions: state.submission_count, max_submissions: state.intent.proposal_budget.max_submissions,
850
1034
  remaining_submissions: state.intent.proposal_budget.max_submissions - state.submission_count,
851
1035
  task_admitted: state.proposal_call_id !== null, uncovered: state.proposal?.uncovered ?? [], proposal: state.proposal,
852
1036
  approval_rationale: state.approval_rationale, root_revisions: state.root_revisions ?? [],
1037
+ budget_revisions: state.budget_revisions ?? [], submitted_history: state.submitted_history ?? [],
1038
+ approved_history: state.approved_history ?? [],
853
1039
  revision_available: state.phase === "submitted" && !state.approval_started && state.submission_count < state.intent.proposal_budget.max_submissions };
854
1040
  }
855
1041
  }
@@ -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 {};
@@ -1617,13 +1617,63 @@ export const SortieDogsPlugin = async (input, options) => {
1617
1617
  !isRecord(part.state) || !["completed", "error"].includes(String(part.state.status)) ||
1618
1618
  !isRecord(part.state.input) || typeof part.state.input.prompt !== "string")
1619
1619
  continue;
1620
- const proposal = reservation.unit_id.startsWith("proposal:");
1621
1620
  const role = String(part.state.input.subagent_type);
1622
- if (proposal ? role !== "dog-operator" : !["dog-worker", "dog-luna-worker"].includes(role))
1621
+ const prompt = part.state.input.prompt;
1622
+ const proposal = reservation.unit_id.startsWith("proposal:");
1623
+ let unitID;
1624
+ // The profiled client maps the external V2 `dogs-coordinator` to this canonical role.
1625
+ if (role === "dog-operator" &&
1626
+ (prompt.startsWith("SORTIE_OPERATOR_DELEGATE_REF ") ||
1627
+ prompt.startsWith("SORTIE_OPERATOR_PROPOSAL_TASK_REF "))) {
1628
+ // Native V2 stores the short Task handle, not the expanded child prompt. The
1629
+ // handle's owner and unit must agree with the durable reservation and call ID.
1630
+ const prefix = prompt.startsWith("SORTIE_OPERATOR_DELEGATE_REF ")
1631
+ ? "SORTIE_OPERATOR_DELEGATE_REF " : "SORTIE_OPERATOR_PROPOSAL_TASK_REF ";
1632
+ let ref;
1633
+ try {
1634
+ ref = JSON.parse(prompt.slice(prefix.length));
1635
+ }
1636
+ catch {
1637
+ continue;
1638
+ }
1639
+ if (!isRecord(ref) || ref.r !== root || !/^[a-f0-9]{64}$/u.test(String(ref.h)) ||
1640
+ (prefix.startsWith("SORTIE_OPERATOR_DELEGATE") && !/^[a-f0-9]{64}$/u.test(String(ref.p))))
1641
+ continue;
1642
+ if (prefix.startsWith("SORTIE_OPERATOR_DELEGATE")) {
1643
+ if (ref.k !== "delegate" || typeof ref.n !== "string" ||
1644
+ !Number.isSafeInteger(ref.g) || ref.g < 1 ||
1645
+ !reservation.unit_id.startsWith(`${ref.n}-`) ||
1646
+ !/^[1-9][0-9]*$/u.test(reservation.unit_id.slice(ref.n.length + 1)))
1647
+ continue;
1648
+ unitID = reservation.unit_id;
1649
+ }
1650
+ else {
1651
+ if (ref.k !== "proposal" || typeof ref.i !== "string" ||
1652
+ reservation.unit_id !== `proposal:${ref.i}`)
1653
+ continue;
1654
+ unitID = reservation.unit_id;
1655
+ }
1656
+ }
1657
+ else if (["dog-worker", "dog-luna-worker"].includes(role)) {
1658
+ if (prompt.startsWith("SORTIE_OPERATOR_TASK_REF ")) {
1659
+ let ref;
1660
+ try {
1661
+ ref = JSON.parse(prompt.slice("SORTIE_OPERATOR_TASK_REF ".length));
1662
+ }
1663
+ catch {
1664
+ continue;
1665
+ }
1666
+ if (!isRecord(ref) || ref.r !== root || !/^[a-f0-9]{64}$/u.test(String(ref.h)) ||
1667
+ !/^[a-f0-9]{64}$/u.test(String(ref.p)) || typeof ref.t !== "string")
1668
+ continue;
1669
+ unitID = ref.t;
1670
+ }
1671
+ else {
1672
+ unitID = handoffValue(handoffEntries(prompt), ["task_id"]) ?? part.callID;
1673
+ }
1674
+ }
1675
+ if (unitID === undefined || (proposal && role !== "dog-operator"))
1623
1676
  continue;
1624
- const unitID = proposal
1625
- ? reservation.unit_id
1626
- : handoffValue(handoffEntries(part.state.input.prompt), ["task_id"]) ?? part.callID;
1627
1677
  if (unitID !== reservation.unit_id || goalFingerprint({ goal_id: state.goal_id, unit_id: unitID,
1628
1678
  call_id: part.callID }) !== reservation.reservation_id)
1629
1679
  continue;
@@ -4028,12 +4078,13 @@ export const SortieDogsPlugin = async (input, options) => {
4028
4078
  async function serializeChatTransition(sessionID, operation) {
4029
4079
  const previous = chatTransitions.get(sessionID) ?? Promise.resolve();
4030
4080
  const current = previous.catch(() => undefined).then(operation);
4031
- chatTransitions.set(sessionID, current);
4081
+ const tail = current.then(() => undefined, () => undefined);
4082
+ chatTransitions.set(sessionID, tail);
4032
4083
  try {
4033
- await current;
4084
+ return await current;
4034
4085
  }
4035
4086
  finally {
4036
- if (chatTransitions.get(sessionID) === current)
4087
+ if (chatTransitions.get(sessionID) === tail)
4037
4088
  chatTransitions.delete(sessionID);
4038
4089
  }
4039
4090
  }
@@ -7338,11 +7389,132 @@ export const SortieDogsPlugin = async (input, options) => {
7338
7389
  assertActiveGoal: async (root, fingerprint) => {
7339
7390
  if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
7340
7391
  throw new Error("operator-coordinator-required");
7392
+ await recoverCompletedGoalReservations(root);
7341
7393
  const goal = await currentGoal(root);
7342
7394
  if (!goal || goal.phase !== "active" || goal.receipt !== null || goal.acceptance_fingerprint !== fingerprint || goal.outstanding_reservations.length) {
7343
7395
  throw new Error("operator-resume-goal-identity-mismatch");
7344
7396
  }
7345
7397
  },
7398
+ reconcileAbortedOperatorOrphan: root => serializeChatTransition(root, async () => {
7399
+ if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
7400
+ throw new Error("operator-coordinator-required");
7401
+ const ledger = await goalLedger(root), initial = (await ledger.readGoal()).state;
7402
+ if (initial.phase !== "active" || initial.goal_id === null || initial.receipt !== null) {
7403
+ return { status: "unproven", reason: "active-goal-required" };
7404
+ }
7405
+ if (initial.outstanding_reservations.length === 0)
7406
+ return { status: "no-orphan" };
7407
+ if (initial.outstanding_reservations.length !== 1)
7408
+ return { status: "unproven", reason: "multiple-reservations" };
7409
+ const reservation = initial.outstanding_reservations[0];
7410
+ const unitID = reservation.unit_id, runID = unitID.replace(/-[1-9][0-9]*$/u, "");
7411
+ if (reservation.session_id !== root || runID === unitID || !/^operator-[a-f0-9-]+$/u.test(runID) ||
7412
+ input.client?.session?.children === undefined || input.client.session.messages === undefined ||
7413
+ input.client.session.abort === undefined) {
7414
+ return { status: "unproven", reservation_id: reservation.reservation_id, reason: "owned-native-lineage-unavailable" };
7415
+ }
7416
+ const sessionAPI = input.client.session;
7417
+ const messages = async (id) => {
7418
+ const response = await sessionAPI.messages({ path: { id }, query: { directory: input.directory } });
7419
+ return isRecord(response) && Array.isArray(response.data) ? response.data.filter(isRecord) : [];
7420
+ };
7421
+ const children = async (id) => {
7422
+ const response = await sessionAPI.children({ path: { id }, query: { directory: input.directory } });
7423
+ return isRecord(response) && Array.isArray(response.data) ? response.data.filter(isRecord) : [];
7424
+ };
7425
+ const nativeTaskParts = (history) => history.flatMap(message => isRecord(message.info) && message.info.role === "assistant" && Array.isArray(message.parts)
7426
+ ? message.parts.filter(part => isRecord(part) && part.type === "tool" && part.tool === "task") : []);
7427
+ const refFrom = (part, prefix) => {
7428
+ const state = isRecord(part.state) ? part.state : undefined;
7429
+ const toolInput = isRecord(state?.input) ? state.input : undefined;
7430
+ if (typeof toolInput?.prompt !== "string" || !toolInput.prompt.startsWith(prefix))
7431
+ return undefined;
7432
+ try {
7433
+ const ref = JSON.parse(toolInput.prompt.slice(prefix.length));
7434
+ return isRecord(ref) ? ref : undefined;
7435
+ }
7436
+ catch {
7437
+ return undefined;
7438
+ }
7439
+ };
7440
+ const rootParts = nativeTaskParts(await messages(root));
7441
+ const parentTasks = rootParts.filter(part => {
7442
+ const state = isRecord(part.state) ? part.state : undefined;
7443
+ const ref = refFrom(part, "SORTIE_OPERATOR_DELEGATE_REF ");
7444
+ return state?.status === "error" && isRecord(state.error) && state.error.type === "aborted" &&
7445
+ ref?.k === "delegate" && ref.r === root && ref.n === runID &&
7446
+ /^[a-f0-9]{64}$/u.test(String(ref.p)) && /^[a-f0-9]{64}$/u.test(String(ref.h)) &&
7447
+ Number.isSafeInteger(ref.g) && ref.g > 0 &&
7448
+ isRecord(state.time) && typeof state.time.end === "number";
7449
+ });
7450
+ if (parentTasks.length !== 1)
7451
+ return { status: "unproven", reservation_id: reservation.reservation_id, reason: "terminal-parent-unproven" };
7452
+ const parentEnd = parentTasks[0].state.time.end;
7453
+ const parentRef = refFrom(parentTasks[0], "SORTIE_OPERATOR_DELEGATE_REF ");
7454
+ const candidates = [];
7455
+ for (const delegate of await children(root)) {
7456
+ if (typeof delegate.id !== "string" || delegate.parentID !== root || delegate.agent !== "dog-operator")
7457
+ continue;
7458
+ const parts = nativeTaskParts(await messages(delegate.id));
7459
+ for (const part of parts) {
7460
+ const ref = refFrom(part, "SORTIE_OPERATOR_TASK_REF ");
7461
+ if (ref?.r !== root || ref.n !== runID || ref.t !== unitID || typeof part.callID !== "string" ||
7462
+ ref.p !== parentRef.p || ref.g !== parentRef.g || !/^[a-f0-9]{64}$/u.test(String(ref.h)) ||
7463
+ goalFingerprint({ goal_id: initial.goal_id, unit_id: unitID, call_id: part.callID }) !== reservation.reservation_id ||
7464
+ !isRecord(part.state) || part.state.status !== "running")
7465
+ continue;
7466
+ for (const worker of await children(delegate.id)) {
7467
+ if (typeof worker.id !== "string" || worker.parentID !== delegate.id ||
7468
+ !["dog-worker", "dog-luna-worker"].includes(String(worker.agent)))
7469
+ continue;
7470
+ const history = await messages(worker.id);
7471
+ const claimed = history.some(message => isRecord(message.info) && message.info.role === "user" &&
7472
+ Array.isArray(message.parts) && message.parts.some(textPart => isRecord(textPart) &&
7473
+ textPart.type === "text" && typeof textPart.text === "string" &&
7474
+ handoffValue(handoffEntries(textPart.text), ["task_id"]) === unitID));
7475
+ const lastActivity = Math.max(0, ...history.flatMap(message => isRecord(message.info) && isRecord(message.info.time) &&
7476
+ typeof message.info.time.created === "number" ? [message.info.time.created] : []));
7477
+ if (claimed && lastActivity > 0 && lastActivity <= parentEnd) {
7478
+ candidates.push({ delegate: delegate.id, worker: worker.id, callID: part.callID });
7479
+ }
7480
+ }
7481
+ }
7482
+ }
7483
+ if (candidates.length !== 1)
7484
+ return { status: "unproven", reservation_id: reservation.reservation_id, reason: "stale-child-lineage-unproven" };
7485
+ const candidate = candidates[0];
7486
+ for (const id of [candidate.worker, candidate.delegate]) {
7487
+ const acknowledgement = await sessionAPI.abort({ path: { id }, query: { directory: input.directory } });
7488
+ const response = isRecord(acknowledgement) && isRecord(acknowledgement.data) ? acknowledgement.data : acknowledgement;
7489
+ if (!isRecord(response) || typeof response.interrupted !== "boolean") {
7490
+ return { status: "unproven", reservation_id: reservation.reservation_id, reason: "native-interrupt-unconfirmed" };
7491
+ }
7492
+ }
7493
+ const current = (await ledger.readGoal()).state;
7494
+ if (current.phase !== "active" || current.receipt !== null ||
7495
+ current.goal_id !== initial.goal_id || current.revision !== initial.revision ||
7496
+ current.scope_epoch !== initial.scope_epoch || current.acceptance_fingerprint !== initial.acceptance_fingerprint ||
7497
+ current.outstanding_reservations.length !== 1 ||
7498
+ current.outstanding_reservations[0].reservation_id !== reservation.reservation_id) {
7499
+ return { status: "unproven", reservation_id: reservation.reservation_id, reason: "goal-changed-during-interrupt" };
7500
+ }
7501
+ await ledger.appendGoal({ kind: "unit.settled", at: new Date().toISOString(), reservation_id: reservation.reservation_id,
7502
+ receipt_id: goalFingerprint({ orphan_interrupted: candidate, reservation_id: reservation.reservation_id }),
7503
+ goal_id: initial.goal_id, unit_id: unitID, disposition: "cancelled", result_class: "interrupted",
7504
+ progress_fingerprint: null, evidence: [], elapsed_ms: null, cost_usd: null });
7505
+ return { status: "settled", reservation_id: reservation.reservation_id, unit_id: unitID };
7506
+ }),
7507
+ proveApprovedRunAncestry: async (root, oldGoalID, parentRunID) => {
7508
+ if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
7509
+ throw new Error("operator-coordinator-required");
7510
+ const { records } = await (await goalLedger(root)).readGoal();
7511
+ if (!records.some(({ event }) => event.kind === "goal.accepted" && event.goal_id === oldGoalID))
7512
+ return false;
7513
+ return records.some(({ event }) => event.kind === "dispatch.reserved" && event.goal_id === oldGoalID &&
7514
+ event.session_id === root && event.unit_id.startsWith(`${parentRunID}-`) &&
7515
+ records.some(({ event: settled }) => settled.kind === "unit.settled" && settled.goal_id === oldGoalID &&
7516
+ settled.unit_id === event.unit_id && settled.reservation_id === event.reservation_id));
7517
+ },
7346
7518
  retainOperatorContractRepairWorker: async (root, taskID, childSessionID) => {
7347
7519
  if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
7348
7520
  throw new Error("operator-coordinator-required");
@@ -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({
@@ -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
@@ -398,6 +405,31 @@ export function createProfiledPlugin(profile, assetVersion) {
398
405
  const revisionSchema = record(stringSchema) && typeof stringSchema.describe === "function"
399
406
  ? stringSchema.describe(OPERATOR_PROPOSAL_REVISION_CONTRACT)
400
407
  : { ...(record(stringSchema) ? stringSchema : { type: "string" }), description: OPERATOR_PROPOSAL_REVISION_CONTRACT };
408
+ const budgetRevisionContract = "Root-only: increase the cumulative read/submission limits of this exact unapproved submitted proposal. " +
409
+ "revision_json must encode exactly {proposal_id,revision,content_hash,rationale,proposal_budget:{max_reads,max_submissions}}. " +
410
+ "Pin the submitted identity from operator_status and supply user-authorized cumulative limits, strictly above retained spend and no lower than old limits. " +
411
+ "The current goal binding must still match. This changes neither proposal identity, ordered requirements, plan, goal, read evidence nor spent counters; " +
412
+ "it grants no new investigation Task, approval, execution or acceptance. Recompare the proposal before separately revising or approving it.";
413
+ const budgetRevisionSchema = record(stringSchema) && typeof stringSchema.describe === "function"
414
+ ? stringSchema.describe(budgetRevisionContract)
415
+ : { ...(record(stringSchema) ? stringSchema : { type: "string" }), description: budgetRevisionContract };
416
+ const scopeReopenContract = "Root-only: after observing a newly missing implementation write path, reopen this exact unapproved submitted proposal " +
417
+ "for a bounded new investigation without cancellation or budget reset. revision_json must encode exactly " +
418
+ "{proposal_id,revision,content_hash,rationale,missing_write_paths:string[]}. Pin its current identity; every path must be an exact " +
419
+ "normalized repository-relative path absent from the submitted write scope and within the frozen intent.allow_read. " +
420
+ "The submitted contract is archived, ordered requirements, goal binding, intent, original source refs and cumulative spend are retained; " +
421
+ "old child and read paths grant no authority to the new child. This consumes a new goal dispatch unit only when its returned Task is admitted. " +
422
+ "Do not use this to retry a known field defect correctable by revise_operator_proposal or to claim any old acceptance.";
423
+ const scopeReopenSchema = record(stringSchema) && typeof stringSchema.describe === "function"
424
+ ? stringSchema.describe(scopeReopenContract)
425
+ : { ...(record(stringSchema) ? stringSchema : { type: "string" }), description: scopeReopenContract };
426
+ 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}. " +
427
+ "The intent has the exact begin_operator_proposal intent_json shape and a cumulative proposal_budget with room above all retained reads/submissions. " +
428
+ "Pin the approved proposal identity and the cancelled/completed run ID from operator_status. The new requirements must begin with every old ordered requirement byte-for-byte even if the active goal ID has changed. Only a root acting for a newly authorized active goal may revise; active/prepared runs, stale identity or unchanged goal binding are denied. " +
429
+ "Old acceptance, terminal status and proposal accounting remain durable. This returns an investigation Task, not implementation or acceptance.";
430
+ const approvedRevisionSchema = record(stringSchema) && typeof stringSchema.describe === "function"
431
+ ? stringSchema.describe(approvedRevisionContract)
432
+ : { ...(record(stringSchema) ? stringSchema : { type: "string" }), description: approvedRevisionContract };
401
433
  const planContract = "plan_json must encode the exact operator plan object: schema_version, acceptance, acceptance_proof, source_refs, " +
402
434
  "goal_declaration, units, and optional git_lifecycle only. git_lifecycle exact shape is " +
403
435
  '{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 ' +
@@ -424,6 +456,10 @@ export function createProfiledPlugin(profile, assetVersion) {
424
456
  const submitProposal = profileTool(profile, "sortie_submit_operator_proposal");
425
457
  const approveProposal = profileTool(profile, "sortie_approve_operator_proposal");
426
458
  const reviseProposal = profileTool(profile, "sortie_revise_operator_proposal");
459
+ const extendProposalBudget = profileTool(profile, "sortie_extend_operator_proposal_budget");
460
+ const reopenProposalScope = profileTool(profile, "sortie_reopen_operator_proposal_scope");
461
+ const reconcileOrphan = profileTool(profile, "sortie_reconcile_orphaned_operator_dispatch");
462
+ const reviseApprovedIntent = profileTool(profile, "sortie_revise_approved_operator_intent");
427
463
  async function stop(root, reason, retireRoot = true) {
428
464
  if (retireRoot)
429
465
  retired.add(root);
@@ -560,13 +596,16 @@ export function createProfiledPlugin(profile, assetVersion) {
560
596
  ? proposal.proposal_call_id !== null
561
597
  ? "proposal Task is already admitted; do not redispatch it or call operator_next. Continue submission repairs only in the same active claimed child. If that child terminated or was interrupted without submission, an explicit root decision to retry may call cancel_operator with reason=plain to release this grant. Cancellation stops owned children and durably preserves cumulative reads/submissions and goal spend. Pass the returned retry_intent unchanged to begin_operator_proposal, then dispatch only its new exact Task within the remaining budget. Never reuse the terminated child, reset spend, or replace the goal or ordered requirements. Exhaustion requires an explicit cumulative budget revision, not a reset."
562
598
  : proposal.submission_count >= proposal.intent.proposal_budget.max_submissions
563
- ? "proposal submission budget exhausted; do not call operator_next; report the bounded proposal failure"
599
+ ? `proposal submission budget exhausted; do not call operator_next. For the exact same unapproved submitted proposal and active goal, an explicitly user-authorized cumulative increase can use ${extendProposalBudget}; retain all previous spend and compare the unchanged proposal again. Otherwise report the bounded failure.`
564
600
  : "proposal is not submitted; do not call operator_next; complete or repair the bounded proposal submission"
565
601
  : proposal?.phase === "submitted"
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.`
567
- : "approved proposal has no operator run; do not call operator_next; reconcile the approval or preparation failure";
602
+ ? `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. If exhausted, an explicitly user-authorized cumulative increase on this same submitted identity uses ${extendProposalBudget}. An observed missing implementation write path inside the frozen allow_read needs separate root-authorized ${reopenProposalScope}; this archives the old submission and returns a new read-only Task while retaining all spend. 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.`
603
+ : state?.phase === "cancelled"
604
+ ? `approved proposal remains pinned to a cancelled run. Do not call operator_next or reuse its Task. If the currently active goal differs, use ${reconcileOrphan} for one proven aborted native child reservation, then ${reviseApprovedIntent} with the exact cancelled run and retained ordered acceptance for a newly authorized investigation.`
605
+ : "approved proposal has no operator run; do not call operator_next; reconcile the approval or preparation failure";
568
606
  return JSON.stringify(state ? { ...await operatorPacket(state), ...(draft ? { pending_draft: draft } : {}),
569
- ...(proposal ? { proposal: proposalIdentity(proposal) } : {}) }
607
+ ...(proposal ? { proposal: proposalIdentity(proposal), proposal_next_action: proposalNextAction } : {}),
608
+ ...(state.phase === "cancelled" ? { orphan_recovery_action: `If the active goal has one reservation from an aborted owned V2 Task, use ${reconcileOrphan} only after native lineage and interrupt proof. If a terminal replacement run is still bound to a previous approved goal, use ${reviseApprovedIntent} with its exact identity, preserved ordered requirements and cumulative spend; it starts fresh investigation, not acceptance.` } : {}) }
570
609
  : draft ? { ...draft, ...(proposal ? { proposal: proposals.packet(proposal) } : {}) }
571
610
  : proposal ? { profile: profile.id, proposal: proposals.packet(proposal),
572
611
  goal_binding: proposal.goal_binding, budget: await control.currentBudget(context.sessionID),
@@ -805,6 +844,33 @@ export function createProfiledPlugin(profile, assetVersion) {
805
844
  return JSON.stringify({ ...proposals.packet(state), task: proposals.referenceTask(state),
806
845
  dispatch_instruction: "Pass this short Task reference verbatim; do not append or paraphrase it." });
807
846
  } };
847
+ 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.`,
848
+ args: { revision_json: approvedRevisionSchema }, execute: async (args, context) => {
849
+ await requireRoot(context.sessionID);
850
+ return serializeDispatchTransition(context.sessionID, async () => {
851
+ let request;
852
+ try {
853
+ request = JSON.parse(args.revision_json);
854
+ }
855
+ catch {
856
+ return JSON.stringify({ status: "invalid-approved-revision", code: "operator-proposal-approved-revision-json-invalid" });
857
+ }
858
+ try {
859
+ const binding = await control.proposalGoalBinding(context.sessionID);
860
+ await control.assertActiveGoal(context.sessionID, binding.acceptance_fingerprint);
861
+ const revised = await proposals.reviseApproved(context.sessionID, context.sessionID, request, () => new OperatorRuntime(input.directory, profile).read(context.sessionID), binding, (oldGoalID, parentRunID) => control.proveApprovedRunAncestry(context.sessionID, oldGoalID, parentRunID));
862
+ return JSON.stringify({ ...proposals.packet(revised), task: proposals.referenceTask(revised),
863
+ dispatch_instruction: "Dispatch this new exact Task reference once; do not reuse the prior approved contract or its child." });
864
+ }
865
+ catch (error) {
866
+ if (error instanceof OperatorProposalBudgetError)
867
+ return JSON.stringify(error.diagnostic);
868
+ if (!(error instanceof Error) || !error.message.startsWith("operator-proposal-"))
869
+ throw error;
870
+ return JSON.stringify({ status: "invalid-approved-revision", code: error.message });
871
+ }
872
+ });
873
+ } };
808
874
  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.",
809
875
  args: { proposal_json: proposalSchema }, execute: async (args, context) => {
810
876
  const root = await rootFor(context.sessionID);
@@ -847,6 +913,57 @@ export function createProfiledPlugin(profile, assetVersion) {
847
913
  }
848
914
  });
849
915
  } };
916
+ tools[extendProposalBudget] = { description: budgetRevisionContract,
917
+ args: { revision_json: budgetRevisionSchema }, execute: async (args, context) => {
918
+ await requireRoot(context.sessionID);
919
+ return serializeDispatchTransition(context.sessionID, async () => {
920
+ try {
921
+ const binding = await control.proposalGoalBinding(context.sessionID);
922
+ const revised = await proposals.extendSubmittedBudget(context.sessionID, context.sessionID, JSON.parse(args.revision_json), binding);
923
+ return JSON.stringify({ ...proposals.packet(revised),
924
+ next_action: "Compare all original ordered requirements against this unchanged proposal. Revise diagnosed fields within the remaining submission budget or explicitly approve; no worker was started." });
925
+ }
926
+ catch (error) {
927
+ if (error instanceof SyntaxError)
928
+ return JSON.stringify({ status: "invalid-budget-revision", code: "operator-proposal-budget-revision-json-invalid" });
929
+ if (!(error instanceof Error) || !error.message.startsWith("operator-proposal-"))
930
+ throw error;
931
+ return JSON.stringify({ status: "invalid-budget-revision", code: error.message });
932
+ }
933
+ });
934
+ } };
935
+ tools[reopenProposalScope] = { description: scopeReopenContract,
936
+ args: { revision_json: scopeReopenSchema }, execute: async (args, context) => {
937
+ await requireRoot(context.sessionID);
938
+ return serializeDispatchTransition(context.sessionID, async () => {
939
+ try {
940
+ const binding = await control.proposalGoalBinding(context.sessionID);
941
+ await control.assertActiveGoal(context.sessionID, binding.acceptance_fingerprint);
942
+ const reopened = await proposals.reopenSubmittedScope(context.sessionID, context.sessionID, JSON.parse(args.revision_json), binding);
943
+ return JSON.stringify({ ...proposals.packet(reopened), task: proposals.referenceTask(reopened),
944
+ dispatch_instruction: "Dispatch this new exact read-only Task once; investigate the newly missing surfaces and submit a complete plan. Never reuse the archived child or its read paths." });
945
+ }
946
+ catch (error) {
947
+ if (error instanceof OperatorProposalBudgetError)
948
+ return JSON.stringify(error.diagnostic);
949
+ if (error instanceof SyntaxError)
950
+ return JSON.stringify({ status: "invalid-scope-reopen", code: "operator-proposal-scope-reopen-json-invalid" });
951
+ if (!(error instanceof Error) || !error.message.startsWith("operator-proposal-"))
952
+ throw error;
953
+ return JSON.stringify({ status: "invalid-scope-reopen", code: error.message });
954
+ }
955
+ });
956
+ } };
957
+ tools[reconcileOrphan] = { description: "Root-only: after explicitly cancelling the operator, reconcile one orphaned goal reservation from an aborted native delegate and its still-running historical child Task. The host requires exact goal/call/Task identity, owned parent-child lineage, native timestamps, and acknowledged interruption of both owned sessions. It records interrupted lifecycle spend with no validation evidence or acceptance. If any proof is absent it returns unproven without settling the reservation; never use worker prose as proof.",
958
+ args: {}, execute: async (_args, context) => {
959
+ await requireRoot(context.sessionID);
960
+ return serializeDispatchTransition(context.sessionID, async () => {
961
+ const run = await operators.read(context.sessionID);
962
+ if (run?.phase !== "cancelled")
963
+ return JSON.stringify({ status: "unproven", reason: "cancelled-operator-required" });
964
+ return JSON.stringify(await control.reconcileAbortedOperatorOrphan(context.sessionID));
965
+ });
966
+ } };
850
967
  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,
851
968
  args: { approval_json: approvalSchema }, execute: async (args, context) => {
852
969
  await requireRoot(context.sessionID);
@@ -886,7 +1003,8 @@ export function createProfiledPlugin(profile, assetVersion) {
886
1003
  });
887
1004
  } };
888
1005
  const ownTools = new Set([prepare, repair, next, status, cancel, complete, resume, resolveContractRepair,
889
- beginProposal, submitProposal, approveProposal, reviseProposal]);
1006
+ beginProposal, submitProposal, approveProposal, reviseProposal, extendProposalBudget, reopenProposalScope,
1007
+ reconcileOrphan, reviseApprovedIntent]);
890
1008
  const protocolMap = CANONICAL_AGENT_ROLES.map(role => `${role}=${profileAgent(profile, role)}`).join(", ");
891
1009
  function rememberRendered(key, text) {
892
1010
  renderedParts.set(key, goalFingerprint(text));
@@ -62,6 +62,13 @@ export interface RuntimeBridge {
62
62
  assertProposalExecutionBudget(rootSessionID: string, binding: OperatorProposalGoalBinding, executionUnits: number): Promise<void>;
63
63
  hasNoGoalReservation(rootSessionID: string, unitID: string, callID: string): Promise<boolean>;
64
64
  assertActiveGoal(rootSessionID: string, goalFingerprint: string): Promise<void>;
65
+ reconcileAbortedOperatorOrphan(rootSessionID: string): Promise<{
66
+ readonly status: "settled" | "no-orphan" | "unproven";
67
+ readonly reservation_id?: string;
68
+ readonly unit_id?: string;
69
+ readonly reason?: string;
70
+ }>;
71
+ proveApprovedRunAncestry(rootSessionID: string, oldGoalID: string, parentRunID: string): Promise<boolean>;
65
72
  retainOperatorContractRepairWorker(rootSessionID: string, taskID: string, childSessionID: string): Promise<void>;
66
73
  assertOperatorContractRepairValidationAvailable(rootSessionID: string, taskID: string, childSessionID: string): Promise<void>;
67
74
  remediationScopeExpansionAuthority(rootSessionID: string): Promise<string | undefined>;
@@ -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.
@@ -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.20",
3
+ "version": "0.10.23",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",