sortie-dogs 0.10.14 → 0.10.15

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.
@@ -73,6 +73,11 @@ export interface OperatorProposalState {
73
73
  readonly intent: OperatorIntent;
74
74
  goal_binding: OperatorProposalGoalBinding | null;
75
75
  readonly created_at: string;
76
+ /** Frozen carry-in spend; unlike live counters it must not change the admitted Task identity. */
77
+ readonly prior_spend?: {
78
+ readonly reads: number;
79
+ readonly submissions: number;
80
+ };
76
81
  phase: "investigating" | "submitted" | "approved";
77
82
  proposal_call_id: string | null;
78
83
  proposal_session_id: string | null;
@@ -101,6 +101,25 @@ export class OperatorProposalBudgetError extends Error {
101
101
  }
102
102
  }
103
103
  function parseIntent(value) {
104
+ if (record(value) && Array.isArray(value.allow_read)) {
105
+ const diagnostics = [];
106
+ value.allow_read.forEach((path, index) => {
107
+ if (normalizedRelativePath(path))
108
+ return;
109
+ let expected = "existing repository-relative file or directory; external absolute paths are not proposal read scopes";
110
+ if (typeof path === "string") {
111
+ try {
112
+ expected = normalizeRelativePath(path);
113
+ }
114
+ catch { /* Never authorize an external or traversal path. */ }
115
+ }
116
+ diagnostics.push({ document: "proposal", pointer: `/allow_read/${index}`, code: "operator-intent-read-path-invalid",
117
+ rule: "normalized-relative-path", repair_kind: "repair-field", repair_paths: [`/allow_read/${index}`],
118
+ expected, actual_type: jsonType(path) });
119
+ });
120
+ if (diagnostics.length)
121
+ throw new OperatorContractError(diagnostics);
122
+ }
104
123
  const proposalBudget = record(value) && Object.hasOwn(value, "proposal_budget")
105
124
  ? value.proposal_budget
106
125
  : DEFAULT_OPERATOR_PROPOSAL_BUDGET;
@@ -392,6 +411,11 @@ export class OperatorProposalRuntime {
392
411
  `original_request: ${state.intent.original_request.text}`, "ordered_requirements:", ...state.intent.requirements.map(item => `- ${item.id} [${item.kind}]: ${item.text}`),
393
412
  `authoritative_refs: ${JSON.stringify(state.intent.authoritative_refs)}`, `allow_read: ${JSON.stringify(state.intent.allow_read)}`,
394
413
  `proposal_budget: ${JSON.stringify(state.intent.proposal_budget)}`,
414
+ "要件に相談や利用者判断が含まれる場合、workerから追加Taskを呼ぶ計画にしない。rootが派遣前に必要な相談を行い、確認済みの素材許諾・選択・画像参照・決定をunit.objectiveと許可された入力へ明記する。子はroot会話を自動継承しない。未確定の判断だけを未確定として保持し、確認済み事項を再質問させない。実機測定や利用者Goの未達をfixtureで代替しない。",
415
+ ...(state.prior_spend ? [`prior_proposal_spend: ${JSON.stringify(state.prior_spend)}; remaining_at_start: ${JSON.stringify({
416
+ reads: state.intent.proposal_budget.max_reads - state.prior_spend.reads,
417
+ submissions: state.intent.proposal_budget.max_submissions - state.prior_spend.submissions
418
+ })}. These are cumulative limits, not fresh grants. budget_estimate.proposal_reads must include prior_proposal_spend.reads plus reads charged in this investigation. Prior read paths are not evidence for this new child.`] : []),
395
419
  "proposal packet fields: schema_version, revision, coverage[{requirement_id,approach,validation}], existing_surface[{requirement_id,path,form}], uncovered[{requirement_id,reason}], negative_handling[{requirement_id,handling}], read_scope, write_scope, budget_estimate{proposal_reads,execution_units}, plan{schema_version,acceptance_proof,source_refs,goal_declaration,units, optional git_lifecycle}. revisionは正のJSON整数。例: \"revision\":1。文字列\"revision\":\"1\"は禁止し、hostは型変換しない。plan.acceptanceは生成・提出せず省略し、hostがdurable intent.requirementsのtextを同順・完全一致でmaterializeする。legacy互換として明示する場合だけ同じ全文・順序を完全一致で使い、null・部分集合・並べ替え・空白/大小文字変更は禁止。acceptance_ids等の代替field禁止。",
396
420
  "schema_versionはpacketとplanの両方で必ず文字列\"0.1\"。coverageはnegative/qualityを含む全ordered requirement IDを各1回含める(未対応だけuncoveredへ移す)。全declared criterion IDをacceptance_proofから参照し、各unitのacceptance_indicesはnonempty、全acceptance indexとexact validation commandを既存parser規則どおり完全coverageする。",
397
421
  "plan.acceptance_proofは各acceptanceに対応するcriterion IDの配列を並べた二重配列。複数acceptanceを同じexact commandが証明する場合はcriterion IDを共有可で、acceptanceごとの固有criterion作成は不要。goal_declarationはobject。delivery_intent=\"implementation\", delivery_mode=\"mvp-first\", usable_path_established=false, controlled_change=false, goal_budget_unitsは正整数。criteria各要素はcriterion_idとvalidation_commandを持つ。",
@@ -466,6 +490,7 @@ export class OperatorProposalRuntime {
466
490
  const state = { schema_version: "0.1", profile: this.profile.id, root_session_id: root,
467
491
  intent_id: `intent-${intentHash.slice(0, 24)}`, intent_hash: intentHash, intent, created_at: new Date().toISOString(), phase: "investigating",
468
492
  goal_binding: null, proposal_call_id: null, proposal_session_id: null, read_count: spend.reads, read_paths: [], submission_count: spend.submissions, proposal_id: null,
493
+ ...(spend.reads > 0 || spend.submissions > 0 ? { prior_spend: { reads: spend.reads, submissions: spend.submissions } } : {}),
469
494
  proposal_revision: null, proposal_hash: null, proposal: null, approval_rationale: null };
470
495
  this.assertBudgetAvailable(state);
471
496
  await this.save(state);
@@ -797,6 +797,7 @@ export class OperatorRuntime {
797
797
  "validation:", ...unit.validation.map(value => ` - ${value}`),
798
798
  "Execute validation in its declared order. Earlier entries may be approved generator, build, formatter, or exact cleanup commands required before canonical criterion tests. Every persistent or transient generator output must be declared in unit.write. Cleanup may remove only declared unit.write outputs and must be an explicit ordered command after generation and before post-commit or canonical validation; never add an ignore rule or remove an undeclared path. If any necessary command, input, output, or cleanup is missing, do not run an undeclared command or variant and do not use resume evidence tooling to invent permission; return a contract-repair decision.",
799
799
  "Preserve existing public API success and error return semantics unless acceptance explicitly changes them, and cover those compatibility boundaries in the declared validation.",
800
+ "Do not spawn nested subagents for consultation. Required consultations belong to the root before dispatch; use the confirmed decisions and evidence declared in the unit objective and inputs. If required consultation results or user decisions are missing, return the exact contract gap to the parent instead of attempting a deeper Task, inventing consent, or asking the user to repeat an already recorded decision.",
800
801
  ...(plan.git_lifecycle !== undefined && index === plan.units.length - 1 ? [
801
802
  `git_post_commit_validation: ${JSON.stringify(plan.git_lifecycle.post_commit_validation)}`,
802
803
  "Complete every source write before invoking any git_post_commit_validation command. Its first exact invocation is the host commit boundary; after it, source mutation and undeclared shell commands are denied. Run every listed command and return its real evidence.",
@@ -449,6 +449,7 @@ export function createProfiledPlugin(profile, assetVersion) {
449
449
  return JSON.stringify(operators.packet(state));
450
450
  control.enableUnits(state.rootSessionID, state.units.length);
451
451
  return JSON.stringify({ profile: profile.id, run_id: state.runID, acceptance_fingerprint: state.acceptanceFingerprint,
452
+ dispatch_instruction: "Dispatch this exact Task in the foreground. Omit background or set it to false; background=true is not supported by the operator lifecycle. Required consultations belong to the root before dispatch; confirmed user decisions must already be in the approved unit objective and inputs, not appended to this Task reference.",
452
453
  fast_path: state.units.length === 1, task: state.units.length === 1 ? operators.nextWorkerTask(state) : operators.dispatchTask(state) });
453
454
  }
454
455
  tools[prepare] = { description: `Freeze an approved serial operator plan. Invalid plans return bounded diagnostics and a draft_id for field-only repair; no worker is started. After operator-acceptance-remediation-required, cancel first and prepare only the same exact acceptance, a write scope within the prior approved union, and a Git lifecycle starting at the failed committed head; consumed goal budget is retained. Coordinator only. ${planContract}`,
@@ -549,17 +550,18 @@ export function createProfiledPlugin(profile, assetVersion) {
549
550
  const proposal = await proposals.read(context.sessionID);
550
551
  const proposalNextAction = proposal?.phase === "investigating"
551
552
  ? proposal.proposal_call_id !== null
552
- ? "proposal Task is already admitted; do not redispatch it or call operator_next. Continue submission repairs only in the same active claimed child. If that child terminated without submission, report the terminal proposal failure; remaining read or submission capacity does not authorize a new Task, budget reset, or replacement child. Only an explicit root decision to retry may call cancel_operator with reason=plain to release this grant, which discards the spent proposal accounting and never reuses the terminated child"
553
+ ? "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."
553
554
  : proposal.submission_count >= proposal.intent.proposal_budget.max_submissions
554
555
  ? "proposal submission budget exhausted; do not call operator_next; report the bounded proposal failure"
555
556
  : "proposal is not submitted; do not call operator_next; complete or repair the bounded proposal submission"
556
557
  : proposal?.phase === "submitted"
557
- ? "compare and approve the exact proposal; do not call operator_next before approval prepares a run"
558
+ ? "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."
558
559
  : "approved proposal has no operator run; do not call operator_next; reconcile the approval or preparation failure";
559
560
  return JSON.stringify(state ? { ...await operatorPacket(state), ...(draft ? { pending_draft: draft } : {}),
560
561
  ...(proposal ? { proposal: proposalIdentity(proposal) } : {}) }
561
562
  : draft ? { ...draft, ...(proposal ? { proposal: proposals.packet(proposal) } : {}) }
562
563
  : proposal ? { profile: profile.id, proposal: proposals.packet(proposal),
564
+ goal_binding: proposal.goal_binding, budget: await control.currentBudget(context.sessionID),
563
565
  ...(proposal.phase === "investigating" && proposal.proposal_call_id === null ? { task: proposals.referenceTask(proposal),
564
566
  dispatch_instruction: "Pass this short Task reference verbatim; the host expands it only inside the directly claimed proposal child." } : {}),
565
567
  next_action: proposalNextAction }
@@ -587,8 +589,11 @@ export function createProfiledPlugin(profile, assetVersion) {
587
589
  operatorTurnLifecycle.set(context.sessionID, "cancelled");
588
590
  return JSON.stringify({ profile: profile.id, status: "cancelled", scope: "proposal",
589
591
  released_proposal: discarded ? proposals.packet(discarded) : null,
592
+ retry_intent: discarded?.intent ?? null,
593
+ retained_goal_binding: discarded?.goal_binding ?? null,
590
594
  next_action: "the bounded proposal grant is released and its spent reads/submissions are not restored; " +
591
- "begin a new proposal investigation only on an explicit root decision to retry, and never reuse the cancelled child" });
595
+ "cumulative proposal and goal spend are preserved. On an explicit root decision to retry, pass retry_intent unchanged to begin_operator_proposal, " +
596
+ "preserve the same goal and ordered requirements, and dispatch only the new exact Task within the remaining budget. Never reuse the cancelled child or reset any budget" });
592
597
  }
593
598
  const current = pending;
594
599
  if (requestedReason === "acceptance-remediation" && current.decision !== "operator-acceptance-remediation-required")
@@ -772,6 +777,9 @@ export function createProfiledPlugin(profile, assetVersion) {
772
777
  catch (error) {
773
778
  if (error instanceof OperatorProposalBudgetError)
774
779
  return JSON.stringify(error.diagnostic);
780
+ if (error instanceof OperatorContractError)
781
+ return JSON.stringify({ status: "invalid-intent",
782
+ diagnostics: error.diagnostics, next_action: "Repair only the diagnosed fields and retry begin_operator_proposal; preserve original_request and every ordered requirement. No proposal was admitted and no budget was spent." });
775
783
  throw error;
776
784
  }
777
785
  state = await proposals.bindGoal(context.sessionID, await control.proposalGoalBinding(context.sessionID));
package/dist/plugin/v2.js CHANGED
@@ -136,6 +136,11 @@ function legacyToolName(name) {
136
136
  function legacyToolInput(name, input) {
137
137
  const value = record(input) ? { ...input } : {};
138
138
  if (name === "subagent") {
139
+ if (value.background === true && typeof value.prompt === "string" && value.prompt.startsWith("SORTIE_OPERATOR_")) {
140
+ throw new Error("operator-background-dispatch-not-supported: retry the same exact Task with background omitted or false; no admission or budget reservation occurred. Operator dispatch requires foreground completion for lifecycle accounting.");
141
+ }
142
+ if (value.background === false)
143
+ delete value.background;
139
144
  if (typeof value.agent === "string")
140
145
  value.subagent_type = value.agent;
141
146
  if (typeof value.sessionID === "string")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.10.14",
3
+ "version": "0.10.15",
4
4
  "description": "Bounded agent harness and validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",