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 +80 -60
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/goal-bound.d.ts +1 -1
- package/dist/core/goal-bound.js +1 -1
- package/dist/core/goal-declaration-format.d.ts +2 -0
- package/dist/core/goal-declaration-format.js +52 -3
- package/dist/plugin/continuation.js +16 -1
- package/dist/plugin/index.d.ts +2 -0
- package/dist/plugin/index.js +191 -29
- package/dist/plugin/run-metrics.js +51 -17
- package/dist/plugin/sortie-debrief.d.ts +4 -0
- package/dist/plugin/sortie-debrief.js +53 -20
- package/dist/runtime-assets.d.ts +58 -19
- package/dist/runtime-assets.js +72 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Sortie-dogs
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Add a bounded, cost-aware execution loop to OpenCode without taking OpenCode over.**
|
|
4
4
|
|
|
5
|
-
Sortie-dogs is an
|
|
6
|
-
|
|
7
|
-
|
|
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.**
|
|
10
|
-
> configuration and runtime assets may still change
|
|
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
|
[](https://www.npmjs.com/package/sortie-dogs)
|
|
13
13
|
[](LICENSE)
|
|
@@ -15,47 +15,63 @@ orchestration.
|
|
|
15
15
|
|
|
16
16
|

|
|
17
17
|
|
|
18
|
-
Sortie-dogs
|
|
19
|
-
|
|
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.
|
|
28
|
-
|
|
29
|
-
##
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -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.
|
|
5
|
+
export declare const RUNTIME_ASSET_VERSION = "0.3.86-codegen-proof-v1";
|
|
6
6
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -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.
|
|
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: "
|
|
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: {
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -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 ||
|
|
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 = `
|
|
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
|
-
|
|
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
|
|
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) {
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -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>;
|