sortie-dogs 0.10.10 → 0.10.12

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.
@@ -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";
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";
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";
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";
@@ -86,6 +86,24 @@ export interface OperatorProposalState {
86
86
  proposal: OperatorProposalPacket | null;
87
87
  approval_rationale: string | null;
88
88
  }
89
+ export interface OperatorProposalBudgetDiagnostic {
90
+ readonly status: "proposal-budget-exhausted";
91
+ readonly code: "operator-proposal-read-budget-exhausted" | "operator-proposal-submission-budget-exhausted";
92
+ readonly reads: number;
93
+ readonly max_reads: number;
94
+ readonly remaining_reads: number;
95
+ readonly submissions: number;
96
+ readonly max_submissions: number;
97
+ readonly remaining_submissions: number;
98
+ readonly required_minimum_budget: {
99
+ readonly max_reads: number;
100
+ readonly max_submissions: number;
101
+ };
102
+ }
103
+ export declare class OperatorProposalBudgetError extends Error {
104
+ readonly diagnostic: OperatorProposalBudgetDiagnostic;
105
+ constructor(diagnostic: OperatorProposalBudgetDiagnostic);
106
+ }
89
107
  /** Durable pre-execution lane. It grants investigation and submission, never source mutation or execution. */
90
108
  export declare class OperatorProposalRuntime {
91
109
  readonly profile: RuntimeProfile;
@@ -101,6 +119,7 @@ export declare class OperatorProposalRuntime {
101
119
  read(root: string): Promise<OperatorProposalState | undefined>;
102
120
  private readUnlocked;
103
121
  private save;
122
+ private assertBudgetAvailable;
104
123
  task(state: OperatorProposalState): OperatorTask;
105
124
  /** Root-visible bounded handle; the canonical prompt remains only in durable host state. */
106
125
  referenceTask(state: OperatorProposalState): OperatorTask;
@@ -93,6 +93,13 @@ const normalizedRelativePath = (value) => {
93
93
  return false;
94
94
  }
95
95
  };
96
+ export class OperatorProposalBudgetError extends Error {
97
+ diagnostic;
98
+ constructor(diagnostic) {
99
+ super(diagnostic.code);
100
+ this.diagnostic = diagnostic;
101
+ }
102
+ }
96
103
  function parseIntent(value) {
97
104
  const proposalBudget = record(value) && Object.hasOwn(value, "proposal_budget")
98
105
  ? value.proposal_budget
@@ -366,6 +373,18 @@ export class OperatorProposalRuntime {
366
373
  }
367
374
  this.states.set(state.root_session_id, structuredClone(state));
368
375
  }
376
+ assertBudgetAvailable(state) {
377
+ const remainingReads = state.intent.proposal_budget.max_reads - state.read_count;
378
+ const remainingSubmissions = state.intent.proposal_budget.max_submissions - state.submission_count;
379
+ if (remainingReads > 0 && remainingSubmissions > 0)
380
+ return;
381
+ throw new OperatorProposalBudgetError({ status: "proposal-budget-exhausted",
382
+ code: remainingReads <= 0 ? "operator-proposal-read-budget-exhausted" : "operator-proposal-submission-budget-exhausted",
383
+ reads: state.read_count, max_reads: state.intent.proposal_budget.max_reads, remaining_reads: Math.max(0, remainingReads),
384
+ submissions: state.submission_count, max_submissions: state.intent.proposal_budget.max_submissions,
385
+ remaining_submissions: Math.max(0, remainingSubmissions),
386
+ required_minimum_budget: { max_reads: state.read_count + 1, max_submissions: state.submission_count + 1 } });
387
+ }
369
388
  task(state) {
370
389
  return { subagent_type: this.profile.agentNames["dog-operator"], description: "承認前の調査と実行契約案作成",
371
390
  prompt: [`SORTIE_OPERATOR_PROPOSAL ${JSON.stringify({ root: state.root_session_id, intent_id: state.intent_id, intent_hash: state.intent_hash })}`,
@@ -435,8 +454,10 @@ export class OperatorProposalRuntime {
435
454
  async beginUnlocked(root, raw) {
436
455
  const intent = parseIntent(raw), intentHash = hash(JSON.stringify(intent)), previous = await this.readUnlocked(root);
437
456
  if (previous) {
438
- if (previous.intent_hash === intentHash)
457
+ if (previous.intent_hash === intentHash) {
458
+ this.assertBudgetAvailable(previous);
439
459
  return previous;
460
+ }
440
461
  if (previous.phase !== "approved")
441
462
  throw new Error("operator-proposal-active-intent-immutable");
442
463
  throw new Error("operator-proposal-new-intent-requires-explicit-root-revision");
@@ -446,6 +467,7 @@ export class OperatorProposalRuntime {
446
467
  intent_id: `intent-${intentHash.slice(0, 24)}`, intent_hash: intentHash, intent, created_at: new Date().toISOString(), phase: "investigating",
447
468
  goal_binding: null, proposal_call_id: null, proposal_session_id: null, read_count: spend.reads, read_paths: [], submission_count: spend.submissions, proposal_id: null,
448
469
  proposal_revision: null, proposal_hash: null, proposal: null, approval_rationale: null };
470
+ this.assertBudgetAvailable(state);
449
471
  await this.save(state);
450
472
  return state;
451
473
  }
@@ -474,6 +496,7 @@ export class OperatorProposalRuntime {
474
496
  const state = await this.requiredUnlocked(root);
475
497
  if (state.phase !== "investigating" || state.proposal_call_id !== null)
476
498
  throw new Error("operator-proposal-dispatch-not-authorized");
499
+ this.assertBudgetAvailable(state);
477
500
  const canonical = this.resolveTask(state, args);
478
501
  // Reserve only after the grant check, while competing admissions remain queued.
479
502
  await reserveBudget?.();
@@ -7573,6 +7573,10 @@ export const SortieDogsPlugin = async (input, options) => {
7573
7573
  registerGoalDeclaration: async (root, prompt) => {
7574
7574
  if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
7575
7575
  throw new Error("operator-coordinator-required");
7576
+ // In one-shot CLI turns OpenCode may persist the user message only after chat.message returns.
7577
+ // Proposal preparation can reach registration first, so recover that exact pending real turn here
7578
+ // instead of freezing a prepared contract that can never consume its user revision authority.
7579
+ await recoverPendingRealGoalTurn(root);
7576
7580
  const registered = await bindGoalDeclaration(root, prompt);
7577
7581
  if (registered?.goal_id === null || registered === undefined)
7578
7582
  throw new Error("operator-goal-registration-unavailable");
@@ -7588,6 +7592,7 @@ export const SortieDogsPlugin = async (input, options) => {
7588
7592
  relinkRegisteredGoal: async (root, request) => {
7589
7593
  if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
7590
7594
  throw new Error("operator-coordinator-required");
7595
+ await recoverPendingRealGoalTurn(root);
7591
7596
  await recoverCompletedGoalReservations(root);
7592
7597
  const ledger = await goalLedger(root), snapshot = await ledger.readGoal(), state = snapshot.state;
7593
7598
  if (state.phase !== "active" || state.receipt !== null)
@@ -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, OperatorProposalRuntime } from "../core/operator-proposal.js";
3
+ import { DEFAULT_OPERATOR_PROPOSAL_BUDGET, OPERATOR_APPROVAL_CONTRACT, OPERATOR_PROPOSAL_BUDGET_CAPS, 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";
@@ -760,7 +760,15 @@ export function createProfiledPlugin(profile, assetVersion) {
760
760
  tools[beginProposal] = { description: `Root-only: freeze original ordered requirements and grant one bounded read-only proposal investigation to dogs-coordinator. The result contains one exact short Task reference; pass task.subagent_type, task.description, and task.prompt byte-for-byte without appending or paraphrasing. The native parent retains that reference; the host expands it only inside the directly claimed proposal child. ${OPERATOR_INTENT_CONTRACT}`,
761
761
  args: { intent_json: intentSchema }, execute: async (args, context) => {
762
762
  await requireRoot(context.sessionID);
763
- let state = await proposals.begin(context.sessionID, JSON.parse(args.intent_json));
763
+ let state;
764
+ try {
765
+ state = await proposals.begin(context.sessionID, JSON.parse(args.intent_json));
766
+ }
767
+ catch (error) {
768
+ if (error instanceof OperatorProposalBudgetError)
769
+ return JSON.stringify(error.diagnostic);
770
+ throw error;
771
+ }
764
772
  state = await proposals.bindGoal(context.sessionID, await control.proposalGoalBinding(context.sessionID));
765
773
  return JSON.stringify({ ...proposals.packet(state), task: proposals.referenceTask(state),
766
774
  dispatch_instruction: "Pass this short Task reference verbatim; do not append or paraphrase it." });
@@ -1108,6 +1116,16 @@ export function createProfiledPlugin(profile, assetVersion) {
1108
1116
  await proposals.accountRead(root, request.sessionID, normalizeRelativePath(relative(input.directory, resolve(input.directory, args.filePath))));
1109
1117
  return;
1110
1118
  }
1119
+ if (who.role === "dog-operator" && request.tool === "execute") {
1120
+ const state = await operators.required(root);
1121
+ // OpenCode V2 exposes the delegate's operator_next capability through Code Mode's
1122
+ // execute conduit. Nested Sortie tools still enforce their own root/delegate identity;
1123
+ // denying the conduit prevents a valid multi-unit delegate from creating any worker.
1124
+ if (state.phase !== "running" || state.operatorSessionID !== request.sessionID) {
1125
+ throw new Error("operator-execute-owner-mismatch");
1126
+ }
1127
+ return;
1128
+ }
1111
1129
  if (who.role === "dog-operator" && request.tool === "read") {
1112
1130
  if (typeof args.filePath !== "string")
1113
1131
  throw new Error("operator-read-path-required");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.10.10",
3
+ "version": "0.10.12",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",