sortie-dogs 0.2.13 → 0.2.14

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
@@ -144,11 +144,22 @@ Optional settings in `.opencode/sortie-dogs.json`:
144
144
  {
145
145
  "operationManifestPath": "operation-manifest.json",
146
146
  "handoffPaths": ["handoff.json"],
147
- "readOnlyTools": ["my_mcp_search"],
148
- "dedicatedWorkerModel": { "model": "provider/model", "variant": "deep" },
149
- "continuation": { "enabled": true, "maxAutoContinues": 3 }
150
- }
151
- ```
147
+ "readOnlyTools": ["my_mcp_search"],
148
+ "dedicatedWorkerModel": { "model": "provider/model", "variant": "deep" },
149
+ "continuation": { "enabled": true, "maxAutoContinues": 3 },
150
+ "reflection": {
151
+ "enabled": false,
152
+ "layers": { "run": true, "project": true, "global": false }
153
+ }
154
+ }
155
+ ```
156
+
157
+ The same schema may be saved globally as
158
+ `~/.config/opencode/sortie-dogs.json` (on Windows,
159
+ `%USERPROFILE%\.config\opencode\sortie-dogs.json`). Precedence is built-in
160
+ defaults, global file, project file, `SORTIE_DOGS_CONFIG`, then plugin factory
161
+ options. OpenCode plugin normalization may omit factory options, so use the
162
+ global file for durable global settings.
152
163
 
153
164
  - `operationManifestPath` moves the manifest; the path is project-relative.
154
165
  - `handoffPaths` lists the handoff files the plugin inspects. A worker can only
@@ -170,8 +181,15 @@ Optional settings in `.opencode/sortie-dogs.json`:
170
181
  `dog-coordinator` session is ever resumed: a child session is never promoted and
171
182
  another coordinator is never adopted. Set `enabled` to `false` to keep every
172
183
  batch manual, raise or lower `maxAutoContinues` (default `3`, maximum `10`) to
173
- change the ceiling, and set `summarizeModel` to pin the compaction model when
174
- the host default is unsuitable.
184
+ change the ceiling, and set `summarizeModel` to pin the compaction model when
185
+ the host default is unsuitable. Normal OpenCode auto-compaction keeps the
186
+ host's auto-continue behavior; Sortie suppresses it only while its own
187
+ explicitly queued rollover owns the resume.
188
+ - `reflection` is an opt-in process-prevention companion for an activated root
189
+ `dog-coordinator`. It is disabled by default. Run and project layers default
190
+ to enabled after opt-in; the cross-project global storage layer remains
191
+ disabled unless explicitly enabled. Child and non-coordinator sessions fail
192
+ closed, and `SORTIE_REFLECTION=0` is an immediate kill switch.
175
193
 
176
194
  ## Why Sortie-dogs
177
195
 
@@ -99,3 +99,4 @@ export declare const DEFAULT_PLUGIN_OPTIONS: Readonly<Omit<Required<SortieDogsPl
99
99
  export declare function resolvePluginConfiguration(...values: readonly unknown[]): PluginConfiguration;
100
100
  /** Resolve the plugin's fixed source boundaries: project-local first, environment and host global. */
101
101
  export declare function resolvePluginConfigurationSources(projectValue: unknown, environmentValue: unknown, hostValue: unknown): PluginConfigurationSources;
102
+ export declare function resolvePluginConfigurationSourcesWithGlobal(globalValue: unknown, projectValue: unknown, environmentValue: unknown, hostValue: unknown): PluginConfigurationSources;
@@ -372,17 +372,23 @@ export function resolvePluginConfiguration(...values) {
372
372
  }
373
373
  /** Resolve the plugin's fixed source boundaries: project-local first, environment and host global. */
374
374
  export function resolvePluginConfigurationSources(projectValue, environmentValue, hostValue) {
375
- const configured = resolvePluginConfiguration(projectValue, environmentValue, hostValue);
375
+ return resolvePluginConfigurationSourcesWithGlobal(undefined, projectValue, environmentValue, hostValue);
376
+ }
377
+ export function resolvePluginConfigurationSourcesWithGlobal(globalValue, projectValue, environmentValue, hostValue) {
378
+ const configured = resolvePluginConfiguration(globalValue, projectValue, environmentValue, hostValue);
376
379
  if (configured.kind === "invalid")
377
380
  return configured;
381
+ const globalLayer = parseLayer(globalValue);
378
382
  const projectLayer = parseLayer(projectValue);
379
383
  const environmentLayer = parseLayer(environmentValue);
380
384
  const hostLayer = parseLayer(hostValue);
381
- if (projectLayer === undefined || environmentLayer === undefined || hostLayer === undefined) {
385
+ if (globalLayer === undefined || projectLayer === undefined ||
386
+ environmentLayer === undefined || hostLayer === undefined) {
382
387
  return { kind: "invalid" };
383
388
  }
384
389
  const globalModelRouting = Object.fromEntries(Object.entries({
385
390
  ...recommendedRoleRouting(configured.dedicatedWorkerModel),
391
+ ...(globalLayer.modelRouting ?? {}),
386
392
  ...(environmentLayer.modelRouting ?? {}),
387
393
  ...(hostLayer.modelRouting ?? {}),
388
394
  }).filter(([role]) => !isFixedModelRole(role)));
@@ -125,7 +125,7 @@ export type ContinuationPolicySource = ContinuationPolicy | (() => ContinuationP
125
125
  * without an agent field, or answers for a different directory, instead of failing silently.
126
126
  */
127
127
  export type LocalIdentitySource = (sessionID: string) => ContinuationIdentity | undefined;
128
- export type RolloverAbort = "identity-unavailable" | "child-session" | "summarize-unavailable" | "terminal-identity-rejected";
128
+ export type RolloverAbort = "identity-unavailable" | "child-session" | "summarize-unavailable" | "retries-exhausted" | "terminal-identity-rejected";
129
129
  export interface ContinuationToolContext {
130
130
  readonly sessionID: string;
131
131
  readonly agent?: string | undefined;
@@ -304,9 +304,17 @@ export function createContinuationHooks(client, directory, policySource, timings
304
304
  unrefTimer(setTimeout(async () => {
305
305
  const completed = await runRollover(sessionID);
306
306
  const state = sessions.get(sessionID);
307
+ if (!completed && (state?.cooldownTimer !== undefined || state?.active === true))
308
+ return;
307
309
  if (!completed && state?.pendingRollover === true && attempt < timings.scheduleAttempts) {
308
310
  scheduleRollover(sessionID, attempt + 1);
309
311
  }
312
+ else if (!completed && state?.pendingRollover === true) {
313
+ state.pendingRollover = false;
314
+ state.promptPending = false;
315
+ state.continueReport = undefined;
316
+ warnRollover(sessionID, "retries-exhausted");
317
+ }
310
318
  }, timings.scheduleMilliseconds * (attempt + 1)));
311
319
  }
312
320
  function queueRollover(sessionID, report, resume) {
@@ -446,7 +454,7 @@ export function createContinuationHooks(client, directory, policySource, timings
446
454
  identity.agent !== policy().agent)
447
455
  return;
448
456
  }
449
- if (input.overflow !== true || pending)
457
+ if (pending)
450
458
  output.enabled = false;
451
459
  },
452
460
  async sessionIdle(sessionID) {
@@ -5,7 +5,7 @@ import { RUNTIME_ASSET_VERSION } from "../asset-version.js";
5
5
  import { normalizeRelativePath, RelativePathError } from "../core/path.js";
6
6
  import { validateManifest } from "../core/validate-manifest.js";
7
7
  import { safeSchemaPointer, validateHandoffSchema, validateOperationManifestSchema, } from "../core/validate-schema.js";
8
- import { DEFAULT_PLUGIN_OPTIONS, resolvePluginConfigurationSources, } from "./config.js";
8
+ import { DEFAULT_PLUGIN_OPTIONS, resolvePluginConfiguration, resolvePluginConfigurationSourcesWithGlobal, } from "./config.js";
9
9
  import { CONTINUATION_CAPABILITY, createContinuationHooks, } from "./continuation.js";
10
10
  import { WriteDeniedError, createProjectPaths, createWriteGate, describeUnclassifiedCommand, isKnownReadOnlyTool, normalizeCommand, resolveProjectRoot, safePath, } from "./gate.js";
11
11
  import { createModelRoutingHook, } from "./model-routing-hook.js";
@@ -190,6 +190,22 @@ async function readOptionalProjectConfig(project) {
190
190
  throw error;
191
191
  }
192
192
  }
193
+ async function readOptionalGlobalConfig() {
194
+ try {
195
+ const value = await readJson(join(configRoot(), "sortie-dogs.json"), INPUT_LIMITS.config);
196
+ if (resolvePluginConfiguration(value).kind === "invalid") {
197
+ console.warn("[sortie-dogs] global configuration ignored: invalid or unavailable");
198
+ return undefined;
199
+ }
200
+ return value;
201
+ }
202
+ catch (error) {
203
+ if (isAbsentPathError(error))
204
+ return undefined;
205
+ console.warn("[sortie-dogs] global configuration ignored: invalid or unavailable");
206
+ return undefined;
207
+ }
208
+ }
193
209
  function readEnvironmentConfig() {
194
210
  const source = process.env[ENV_CONFIG];
195
211
  if (source === undefined || source.length === 0)
@@ -411,11 +427,12 @@ export const SortieDogsPlugin = async (input, options) => {
411
427
  let loading;
412
428
  let manifestAbsent = false;
413
429
  let assetVersionReported = false;
430
+ const globalConfig = await readOptionalGlobalConfig();
414
431
  // Project config read is required discovery for its opt-in; no reflection storage/version read
415
432
  // occurs unless that resolved config enables reflection. It stays isolated from write-gate load.
416
433
  try {
417
434
  project = await createProjectPaths(resolveProjectRoot(input));
418
- const probed = resolvePluginConfigurationSources(await readOptionalProjectConfig(project), readEnvironmentConfig(), options);
435
+ const probed = resolvePluginConfigurationSourcesWithGlobal(globalConfig, await readOptionalProjectConfig(project), readEnvironmentConfig(), options);
419
436
  if (probed.kind === "configured" && reflectionEnabled(probed.reflection)) {
420
437
  reflectionVersion = await nearestPackageVersion();
421
438
  reflectionConfiguration = probed.reflection;
@@ -463,7 +480,7 @@ export const SortieDogsPlugin = async (input, options) => {
463
480
  await reportAssetVersionSkew(project);
464
481
  const projectConfig = await readOptionalProjectConfig(project);
465
482
  const environmentConfig = readEnvironmentConfig();
466
- const parsed = resolvePluginConfigurationSources(projectConfig, environmentConfig, options);
483
+ const parsed = resolvePluginConfigurationSourcesWithGlobal(globalConfig, projectConfig, environmentConfig, options);
467
484
  if (parsed.kind === "invalid")
468
485
  throw new WriteDeniedError("manifest-unavailable", "<unknown>");
469
486
  loaded = loadConfigured(parsed, input.worktree ?? project.root, input.client);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sortie-dogs",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Bounded, validated orchestration loop plugin for OpenCode",
5
5
  "keywords": [
6
6
  "opencode",