sortie-dogs 0.9.7 → 0.9.9

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
@@ -1,13 +1,13 @@
1
1
  # Sortie-dogs
2
2
 
3
- **Give OpenCode a task; get a bounded, validated implementation loop instead of an open-ended agent run.**
3
+ **Add a bounded, cost-aware execution loop to OpenCode without taking OpenCode over.**
4
4
 
5
- Sortie-dogs is an execution harness for OpenCode, applying harness-engineering
6
- principles through mechanical write boundaries, validation, review, and bounded
7
- orchestration.
5
+ Sortie-dogs is an opt-in overlay, not a replacement. It stays passive until you
6
+ invoke `/sortie` or select `dog-coordinator`. Standard OpenCode agents and
7
+ unrelated sessions remain available and unchanged.
8
8
 
9
- > **Project status: Beta.** Stable in regular use. As a pre-1.0 release,
10
- > configuration and runtime assets may still change between releases.
9
+ > **Project status: Beta.** v0.9.x is under active stabilization. Runtime
10
+ > behavior, configuration, and runtime assets may still change before 1.0.
11
11
 
12
12
  [![npm](https://img.shields.io/npm/v/sortie-dogs)](https://www.npmjs.com/package/sortie-dogs)
13
13
  [![license](https://img.shields.io/npm/l/sortie-dogs)](LICENSE)
@@ -15,47 +15,63 @@ orchestration.
15
15
 
16
16
  ![Sortie-dogs coordinating a bounded implementation workflow](https://raw.githubusercontent.com/zufall-upon/Sortie-dogs/main/docs/assets/sortie-workflow.gif)
17
17
 
18
- Sortie-dogs is an opt-in OpenCode orchestration plugin. It turns a task into a
19
- scoped plan, optional evidence gathering, one bounded implementation unit, canonical
20
- validation, and evidence-backed completion—while preserving standard OpenCode
21
- agents and settings.
18
+ Sortie-dogs turns selected work into a scoped plan, optional evidence gathering,
19
+ bounded implementation, canonical validation, and evidence-backed completion.
22
20
 
23
21
  Requirements: Node.js 22.6 or newer, npm, and OpenCode.
24
22
 
25
23
  Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
26
24
 
27
- Release: [v0.9.7](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.7)
28
-
29
- ## Quick start
30
-
31
- Install the public npm package in the project and generate the project-local
32
- OpenCode runtime files:
25
+ Release: [v0.9.9](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.9)
26
+
27
+ ## Why Sortie-dogs?
28
+
29
+ ### Invisible until invited
30
+
31
+ Use normal OpenCode normally. Sortie activates only for `/sortie` or
32
+ `dog-coordinator`; it does not disable or replace OpenCode's standard agents.
33
+
34
+ ### Spend strong models only where they matter
35
+
36
+ Lower-cost models handle bounded retrieval and parallel volume work. Stronger
37
+ models are reserved for implementation, escalation, and independent review.
38
+
39
+ ### Return with proof
40
+
41
+ Writes stay scoped, and completion requires validation evidence. Every completed
42
+ run can return a concise Speed / Cost / Proof debrief.
43
+
44
+ ## Designed to coexist with OpenCode
45
+
46
+ Sortie-dogs adds a workflow to your existing setup rather than replacing it.
47
+
48
+ - It does not disable OpenCode's native agents or replace standard roles such as
49
+ `build`, `plan`, `explore`, or `general`.
50
+ - Ordinary sessions are not automatically converted into Sortie workflows.
51
+ - Project-local initialization is the recommended setup and does not change
52
+ user settings.
53
+ - Global runtime availability and cross-project reflection require separate,
54
+ explicit opt-in. Reflection is disabled by default.
55
+ - Unknown or user-owned runtime files are preserved rather than overwritten.
56
+ - Manual removal targets only known Sortie-owned runtime assets.
57
+
58
+ Use OpenCode normally. Invoke the pack only when you want it.
59
+
60
+ ## Quick start
61
+
62
+ Install the public npm package in the project and generate the project-local
63
+ OpenCode runtime files:
33
64
 
34
65
  ```sh
35
66
  npm install --save-dev sortie-dogs
36
- npx sortie-dogs init .
37
- ```
38
-
39
- Alternatively, install the CLI globally and initialize OpenCode's global
40
- configuration:
41
-
42
- ```sh
43
- npm install --global sortie-dogs
44
- sortie-dogs init --global
45
- ```
46
-
47
- This installs the canonical runtime assets in OpenCode's global configuration,
48
- so `dog-coordinator` can be selected from other projects without project-local
49
- initialization. Global initialization and project-local initialization are
50
- separate: `sortie-dogs init .` still writes runtime files only into that
51
- project. Project-local configuration and the plugin bridge below remain
52
- available when a project needs its own settings or dependency.
53
-
54
- Installing the runtime assets does not load the plugin, and without the plugin
55
- every role runs on whichever model the caller happened to use. Add the package
56
- to the `plugin` array of the OpenCode configuration the agents run under —
57
- `~/.config/opencode/opencode.json` for the global assets, or the project's
58
- `.opencode/opencode.json`:
67
+ npx sortie-dogs init .
68
+ ```
69
+
70
+ This is the recommended setup. Runtime assets stay project-local.
71
+
72
+ Installing the runtime assets does not load the plugin, and without the plugin
73
+ every role runs on whichever model the caller happened to use. Add the package
74
+ to the `plugin` array in the project's `.opencode/opencode.json`:
59
75
 
60
76
  ```json
61
77
  {
@@ -218,26 +234,7 @@ global file for durable global settings.
218
234
  unit, with a maximum of three records per run; routine bugs and external
219
235
  failures are never journaled.
220
236
 
221
- ## Why Sortie-dogs
222
-
223
- - **Focused when invited, invisible otherwise.** Activate it with `/sortie` or
224
- select `dog-coordinator`; ordinary OpenCode sessions remain unchanged.
225
- - **Evidence gathering without arbitrary dispatch ceilings.** A bounded scout resolves
226
- each concrete manifest, validation, or ownership gap whenever it appears; unchanged
227
- requests are not repeated.
228
- - **Writes stay inside the assignment.** Exact source or operation manifests
229
- gate edits and handoffs.
230
- - **Autonomous implementation routing.** An accepted scope with at least two safe independent
231
- units defaults to Luna fabric without opt-in. An explicit serial/no-parallel request wins;
232
- other work uses sequential workers. The separate parallel contract remains explicit.
233
- - **Runtime overlap protection.** Active equal or ancestor write scopes are rejected
234
- before mutation, and full validation waits until every parallel unit joins.
235
- - **Evidence before completion.** Canonical validation, risk-based review, and
236
- terminal evidence gate coordinator-owned completion and commits.
237
- - **Long work can recover.** Restart recovery and bounded compaction continue
238
- from retained handoff context rather than silently starting over.
239
-
240
- ## Example run
237
+ ## Example run
241
238
 
242
239
  An illustrative low-risk run stays bounded and reports its gates:
243
240
 
@@ -400,7 +397,30 @@ coordinator. `dog-reviewer` independently checks high-risk candidates after
400
397
  canonical validation. Neither role implements, stages, commits, or acts as a
401
398
  user-facing worker.
402
399
 
403
- ## Updates and migration
400
+ ## Optional global availability
401
+
402
+ If you intentionally want the Sortie roles available across projects, install
403
+ the CLI and runtime assets globally:
404
+
405
+ ```sh
406
+ npm install --global sortie-dogs
407
+ sortie-dogs init --global
408
+ ```
409
+
410
+ This writes canonical Sortie runtime assets to OpenCode's global configuration;
411
+ it does not make project-local initialization global. Load the plugin from the
412
+ global `~/.config/opencode/opencode.json` when using these assets:
413
+
414
+ ```json
415
+ {
416
+ "plugin": ["sortie-dogs"]
417
+ }
418
+ ```
419
+
420
+ Project-local configuration and plugin loading remain available when a project
421
+ needs its own settings or dependency.
422
+
423
+ ## Updates and migration
404
424
 
405
425
  After replacing the dependency with a newer release asset, run:
406
426
 
@@ -2,5 +2,5 @@
2
2
  * Version of the installable runtime assets. Kept in its own module so the plugin can compare an
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
- export declare const RUNTIME_ASSET_VERSION = "0.3.81-mission-debrief-v1";
5
+ export declare const RUNTIME_ASSET_VERSION = "0.3.86-codegen-proof-v1";
6
6
  export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION;
@@ -2,4 +2,4 @@
2
2
  * Version of the installable runtime assets. Kept in its own module so the plugin can compare an
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
- export const RUNTIME_ASSET_VERSION = "0.3.81-mission-debrief-v1";
5
+ export const RUNTIME_ASSET_VERSION = "0.3.86-codegen-proof-v1";
@@ -243,7 +243,7 @@ export interface GoalFlightState {
243
243
  }
244
244
  export declare class GoalBoundError extends Error {
245
245
  readonly code: "invalid" | "transition" | "budget" | "ticket" | "evidence";
246
- constructor(code: "invalid" | "transition" | "budget" | "ticket" | "evidence", message: string);
246
+ constructor(code: GoalBoundError["code"], message: string);
247
247
  }
248
248
  export declare function goalFingerprint(value: unknown): string;
249
249
  export declare function selectGoalDelivery(input: {
@@ -221,7 +221,7 @@ export function reduceGoalFlight(records) {
221
221
  event.consumed >= 0 && event.consumed <= event.limit && event.operation_id.length > 0 && event.evidence_key.length > 0, "invalid", "Validation admission is malformed.");
222
222
  if (event.decision === "ALLOW") {
223
223
  requireState(event.scope !== null && event.consumed === state.validation_budget.consumed + 1 &&
224
- (state.validation_budget.limit === null || state.validation_budget.limit === event.limit) &&
224
+ (state.validation_budget.limit === null || event.limit >= state.validation_budget.limit) &&
225
225
  !state.validation_budget.evidence_keys.includes(event.evidence_key) &&
226
226
  !state.validation_budget.reservations.some((entry) => entry.reservation_id === event.reservation_id), "budget", "Validation admission is stale, duplicated, or exhausted.");
227
227
  state = { ...state, validation_budget: { consumed: event.consumed, limit: event.limit,
@@ -2,3 +2,5 @@
2
2
  export declare const GOAL_DELIVERY_INTENTS: readonly ["design", "registration", "implementation", "repair", "controlled-change"];
3
3
  export declare const GOAL_DELIVERY_MODES: readonly ["planning-only", "mvp-first", "repair-first", "controlled-change"];
4
4
  export declare const GOAL_DECLARATION_FORMAT: string;
5
+ /** Normalize a shared declaration without guessing acceptance, coverage, delivery, or validation. */
6
+ export declare function expandGoalDeclaration(value: unknown): string;
@@ -1,12 +1,16 @@
1
+ import { goalFingerprint } from "./goal-bound.js";
1
2
  /** Shared planner guidance and admission enums; no inferred acceptance or dispatch authority. */
2
3
  export const GOAL_DELIVERY_INTENTS = ["design", "registration", "implementation", "repair", "controlled-change"];
3
4
  export const GOAL_DELIVERY_MODES = ["planning-only", "mvp-first", "repair-first", "controlled-change"];
4
- export const GOAL_DECLARATION_FORMAT = `Goal declaration belongs in the Task prompt, not in the handoff JSON or operation manifest.
5
+ export const GOAL_DECLARATION_FORMAT = `Declare a goal once, then reference it with goal_declaration_path in the Task prompt.
6
+ The referenced JSON may contain shared defaults and a criteria array; the host expands them privately.
7
+ An inline ext["sortie-dogs/goal-declaration"] in the registered handoff is also supported.
8
+ Existing accepted goals need no repeated declaration; budget-only revisions retain their criteria.
9
+ Legacy flat goal_* fields remain supported. No need to rewrite unrelated files or repeat common fields for each criterion.
5
10
  delivery_intent must be exactly one of: ${GOAL_DELIVERY_INTENTS.join(" | ")}. Never use prose or a boolean.
6
11
  delivery_mode is a separate optional enum: ${GOAL_DELIVERY_MODES.join(" | ")}.
7
12
  Use flat key: value lines. Each criterion begins with its own goal_criterion_id: line.
8
- Repeat the entire flat criterion block for multiple criteria. Do not use goal_criteria, goal_acceptance.criteria,
9
- YAML object/list wrappers, inline { goal_criterion_id: ... }, or JSON objects for these blocks.
13
+ For legacy flat input each criterion has a goal_criterion_id line. Prefer shared JSON defaults for multiple criteria.
10
14
  Replace every placeholder with the accepted task's actual value; preserve its acceptance and validation:
11
15
  goal_acceptance_fingerprint: <sha256: followed by exactly 64 lowercase hexadecimal characters>
12
16
  delivery_intent: implementation
@@ -31,3 +35,48 @@ goal_validation_command: <exact operation manifest validation command>
31
35
  The example's implementation/mvp-first selections apply only to an implementation without its usable path;
32
36
  select other enum values only from the accepted request. A declaration rejection launches no child.
33
37
  Repair the Task prompt's named fields, not unrelated files; retry only with the corrected declaration.`;
38
+ /** Normalize a shared declaration without guessing acceptance, coverage, delivery, or validation. */
39
+ export function expandGoalDeclaration(value) {
40
+ if (value === null || typeof value !== "object" || Array.isArray(value))
41
+ throw new Error("goal declaration must be an object");
42
+ const object = value;
43
+ if (!Array.isArray(object.criteria) || object.criteria.length === 0)
44
+ throw new Error("goal declaration requires criteria");
45
+ const defaults = object.defaults !== null && typeof object.defaults === "object" && !Array.isArray(object.defaults)
46
+ ? object.defaults : {};
47
+ const fields = ["criterion_id", "target", "entrypoint", "workload", "oracle_coverage", "build_boundary", "source",
48
+ "candidate", "source_binding", "candidate_binding", "fixture", "proof_scope", "expected_outcome", "validation_command"];
49
+ const scalar = (value) => {
50
+ if (Array.isArray(value))
51
+ return JSON.stringify(value);
52
+ if (typeof value === "string" && !/[\r\n]/u.test(value))
53
+ return value;
54
+ if (typeof value === "boolean" || typeof value === "number")
55
+ return String(value);
56
+ throw new Error("goal declaration field must be a single-line scalar or array");
57
+ };
58
+ const normalizedFields = (values) => Object.fromEntries(fields.flatMap(field => {
59
+ const found = values[`goal_${field}`] ?? values[field];
60
+ return found === undefined ? [] : [[`goal_${field}`, found]];
61
+ }));
62
+ const criteria = object.criteria.map(raw => {
63
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
64
+ throw new Error("goal criterion must be an object");
65
+ return { ...normalizedFields(defaults), ...normalizedFields(raw) };
66
+ });
67
+ const identity = { criteria, delivery_intent: object.delivery_intent, delivery_mode: object.delivery_mode,
68
+ usable_path_established: object.usable_path_established, controlled_change: object.controlled_change };
69
+ const lines = [`goal_acceptance_fingerprint: ${scalar(object.goal_acceptance_fingerprint ?? goalFingerprint(identity))}`];
70
+ for (const field of ["delivery_intent", "delivery_mode", "usable_path_established", "controlled_change",
71
+ "goal_budget_units", "goal_budget_time_ms", "goal_budget_cost_usd"]) {
72
+ if (object[field] !== undefined)
73
+ lines.push(`${field}: ${scalar(object[field])}`);
74
+ }
75
+ for (const criterion of criteria) {
76
+ criterion.goal_criterion_id ??= `criterion-${goalFingerprint(criterion).slice(7, 31)}`;
77
+ for (const field of fields)
78
+ if (criterion[`goal_${field}`] !== undefined)
79
+ lines.push(`goal_${field}: ${scalar(criterion[`goal_${field}`])}`);
80
+ }
81
+ return lines.join("\n");
82
+ }
@@ -483,6 +483,8 @@ export function createContinuationHooks(client, directory, policySource, timings
483
483
  return false;
484
484
  if (terminalCheckpoint(text))
485
485
  return false;
486
+ if (topLevelProtocolLines(text).some(({ line }) => /^status\s*:\s*IN_PROGRESS(?:\s|$)/iu.test(line)))
487
+ return true;
486
488
  if (/➡️\s*(?:次action|next_action)\s*:\s*\S/iu.test(text))
487
489
  return true;
488
490
  if (checkpointStatus(text) === "BLOCKED" && !trueBlockerReport(text))
@@ -1009,6 +1011,7 @@ export function createContinuationHooks(client, directory, policySource, timings
1009
1011
  state.stepRecoveryActive = false;
1010
1012
  clearTimer(state.stepRecoveryTimer);
1011
1013
  state.stepRecoveryTimer = undefined;
1014
+ state.stepRecoveryDeferredReport = undefined;
1012
1015
  resetRecoveryStall(state);
1013
1016
  if (state.notRequiredRevision !== state.turnRevision &&
1014
1017
  !output.text.includes(ROLLOVER_MARKER) &&
@@ -1031,7 +1034,17 @@ export function createContinuationHooks(client, directory, policySource, timings
1031
1034
  }
1032
1035
  else if (nonTerminalProgress(state.latestCoordinatorReport) ||
1033
1036
  (state.stepRecoveryActive && !terminalCheckpoint(state.latestCoordinatorReport))) {
1034
- if (input.allowStepRecoveryFallback !== false) {
1037
+ if (input.allowStepRecoveryFallback === false) {
1038
+ state.stepRecoveryDeferredReport = state.latestCoordinatorReport;
1039
+ }
1040
+ else if (state.stepRecoveryDeferredReport === state.latestCoordinatorReport) {
1041
+ // A completed text-part event is not message completion. Release its deferred
1042
+ // recovery only when the persisted assistant message later confirms completion.
1043
+ state.stepRecoveryDeferredReport = undefined;
1044
+ state.idleDeferred = true;
1045
+ }
1046
+ else {
1047
+ state.stepRecoveryDeferredReport = undefined;
1035
1048
  scheduleStepRecovery(input.sessionID, state, state.latestCoordinatorReport);
1036
1049
  }
1037
1050
  }
@@ -1162,6 +1175,7 @@ export function createContinuationHooks(client, directory, policySource, timings
1162
1175
  state.attempts = 0;
1163
1176
  state.directUsed = false;
1164
1177
  state.latestCoordinatorReport = undefined;
1178
+ state.stepRecoveryDeferredReport = undefined;
1165
1179
  if (!synthetic) {
1166
1180
  state.lastStepContinueReport = undefined;
1167
1181
  state.lastStepContinueRevision = undefined;
@@ -1182,6 +1196,7 @@ export function createContinuationHooks(client, directory, policySource, timings
1182
1196
  state.stepRecoveryTimer = undefined;
1183
1197
  if (tool !== CONTINUATION_CAPABILITY) {
1184
1198
  state.latestCoordinatorReport = undefined;
1199
+ state.stepRecoveryDeferredReport = undefined;
1185
1200
  }
1186
1201
  },
1187
1202
  blocksTool(sessionID) {
@@ -65,6 +65,8 @@ export interface OpenCodeHooks {
65
65
  /** Continuation observes the coordinator's completed final text to honour its fallback markers. */
66
66
  "experimental.text.complete"?: (input: {
67
67
  sessionID: string;
68
+ messageID?: string;
69
+ partID?: string;
68
70
  }, output: {
69
71
  text: string;
70
72
  }) => Promise<void>;