sortie-dogs 0.6.0 → 0.6.1

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
@@ -20,7 +20,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
20
20
 
21
21
  Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
22
22
 
23
- Release: [v0.6.0](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.6.0)
23
+ Release: [v0.6.1](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.6.1)
24
24
 
25
25
  ## Quick start
26
26
 
@@ -151,7 +151,8 @@ Optional settings in `.opencode/sortie-dogs.json`:
151
151
  "continuation": { "enabled": true, "maxAutoContinues": 10 },
152
152
  "reflection": {
153
153
  "enabled": false,
154
- "layers": { "run": true, "project": true, "global": false }
154
+ "layers": { "run": true, "project": true, "global": false },
155
+ "maxInjectedTokens": 500
155
156
  }
156
157
  }
157
158
  ```
@@ -201,6 +202,10 @@ global file for durable global settings.
201
202
  to enabled after opt-in; the cross-project global storage layer remains
202
203
  disabled unless explicitly enabled. Child and non-coordinator sessions fail
203
204
  closed, and `SORTIE_REFLECTION=0` is an immediate kill switch. The coordinator
205
+ injects the governing `REFLECTION_POLICY` only while reflection is enabled.
206
+ `maxInjectedTokens` budgets the dynamic `SORTIE_PROCESS_REFLECTIONS` heading
207
+ and persisted entry lines; the policy is outside that entry budget.
208
+ The coordinator
204
209
  evaluates it only after a resolved blocker/review defect and at a terminal
205
210
  unit, with a maximum of three records per run; routine bugs and external
206
211
  failures are never journaled.
@@ -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.49-run-metrics-v1";
5
+ export declare const RUNTIME_ASSET_VERSION = "0.3.50-observability-reflection-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.49-run-metrics-v1";
5
+ export const RUNTIME_ASSET_VERSION = "0.3.50-observability-reflection-v1";
@@ -7,6 +7,7 @@ export interface ReflectionConfiguration {
7
7
  readonly global: boolean;
8
8
  };
9
9
  readonly maxInjectedEntries: number;
10
+ /** Token budget for the dynamic SORTIE_PROCESS_REFLECTIONS heading and persisted entry lines; excludes the governing REFLECTION_POLICY. */
10
11
  readonly maxInjectedTokens: number;
11
12
  }
12
13
  export type ReflectionPolicyInput = Partial<Omit<ReflectionConfiguration, "layers">> & {
@@ -170,10 +170,21 @@ export interface ContinuationHooks {
170
170
  sessionIdle(sessionID: string): Promise<void>;
171
171
  forgetSession(sessionID: string): void;
172
172
  }
173
+ export type ContinuationTransitionType = "continuation.queued" | "continuation.compacted" | "continuation.resumed" | "continuation.not_required";
174
+ export type ContinuationTransitionReason = "continuation-requested" | "compaction-only" | "summarize-accepted" | "prompt-accepted" | "terminal-checkpoint";
175
+ export interface ContinuationTransition {
176
+ readonly type: ContinuationTransitionType;
177
+ readonly sessionID: string;
178
+ readonly epoch: number;
179
+ readonly reason: ContinuationTransitionReason;
180
+ readonly attempts: number;
181
+ readonly resumeAttempts: number;
182
+ }
183
+ export type ContinuationTransitionObserver = (transition: ContinuationTransition) => void;
173
184
  /**
174
185
  * Build the continuation runtime. Every hook fails closed: an unreadable session, an absent client,
175
186
  * or a rejected identity leaves the coordinator exactly where it was, with manual continuation
176
187
  * still available to the user.
177
188
  */
178
- export declare function createContinuationHooks(client: ContinuationClient | undefined, directory: string, policySource: ContinuationPolicySource, timings?: ContinuationTimings, localIdentity?: LocalIdentitySource): ContinuationHooks;
189
+ export declare function createContinuationHooks(client: ContinuationClient | undefined, directory: string, policySource: ContinuationPolicySource, timings?: ContinuationTimings, localIdentity?: LocalIdentitySource, transitionObserver?: ContinuationTransitionObserver): ContinuationHooks;
179
190
  export {};
@@ -214,9 +214,38 @@ function sessionInfo(response) {
214
214
  * or a rejected identity leaves the coordinator exactly where it was, with manual continuation
215
215
  * still available to the user.
216
216
  */
217
- export function createContinuationHooks(client, directory, policySource, timings = DEFAULT_TIMINGS, localIdentity) {
217
+ export function createContinuationHooks(client, directory, policySource, timings = DEFAULT_TIMINGS, localIdentity, transitionObserver) {
218
218
  const sessions = new Map();
219
219
  const warned = new Set();
220
+ function observeTransition(type, sessionID, state, reason) {
221
+ try {
222
+ transitionObserver?.({
223
+ type,
224
+ sessionID,
225
+ epoch: state.rolloverEpoch,
226
+ reason,
227
+ attempts: state.attempts,
228
+ resumeAttempts: state.resumeAttempts,
229
+ });
230
+ }
231
+ catch {
232
+ // Lifecycle telemetry is best effort and must never affect continuation.
233
+ }
234
+ }
235
+ function observeResumed(sessionID, state, epoch) {
236
+ if (state.compactedTransitionEpoch !== epoch || state.resumedTransitionEpoch === epoch)
237
+ return;
238
+ state.resumedTransitionEpoch = epoch;
239
+ observeTransition("continuation.resumed", sessionID, state, "prompt-accepted");
240
+ }
241
+ function observeCompacted(sessionID, state, epoch) {
242
+ if (state.compactedTransitionEpoch === epoch)
243
+ return;
244
+ state.compactedTransitionEpoch = epoch;
245
+ observeTransition("continuation.compacted", sessionID, state, "summarize-accepted");
246
+ if (state.resumeIssuedEpoch === epoch)
247
+ observeResumed(sessionID, state, epoch);
248
+ }
220
249
  /**
221
250
  * A rollover that cannot start is otherwise indistinguishable from a coordinator that never asked
222
251
  * for one, so every abort names its own reason exactly once per session.
@@ -532,6 +561,7 @@ export function createContinuationHooks(client, directory, policySource, timings
532
561
  state.recoverySummaryValidated = false;
533
562
  // The accepted prompt now belongs to the host loop, not this rollover request.
534
563
  state.active = false;
564
+ observeResumed(sessionID, state, epoch);
535
565
  return true;
536
566
  }
537
567
  catch (error) {
@@ -608,6 +638,7 @@ export function createContinuationHooks(client, directory, policySource, timings
608
638
  state.promptPending = false;
609
639
  state.compactedRollover = true;
610
640
  state.lastRollover = Date.now();
641
+ observeCompacted(sessionID, state, operationEpoch);
611
642
  }
612
643
  if (state.resumeIssuingEpoch === operationEpoch ||
613
644
  state.resumeIssuedEpoch === operationEpoch) {
@@ -694,6 +725,7 @@ export function createContinuationHooks(client, directory, policySource, timings
694
725
  if (resume && countAttempt)
695
726
  state.attempts += 1;
696
727
  const epoch = state.rolloverEpoch;
728
+ observeTransition("continuation.queued", sessionID, state, resume ? "continuation-requested" : "compaction-only");
697
729
  // session.idle can be lost when a one-shot CLI host exits. Keep this zero-delay timer referenced
698
730
  // so the rollover reaches the host after the current plugin hook returns but before process exit.
699
731
  setTimeout(async () => {
@@ -835,8 +867,11 @@ export function createContinuationHooks(client, directory, policySource, timings
835
867
  * One-shot CLI hosts can exit as soon as the compaction assistant finishes, before the
836
868
  * compacted event or summarize response. Its text-complete hook is the last awaited boundary
837
869
  * where the summary message already exists and a resume prompt can still join the same loop.
838
- */
870
+ */
839
871
  if (ownedCompactionSummary || trimmed.startsWith(ROLLOVER_TOKEN)) {
872
+ if (ownedCompactionSummary) {
873
+ observeCompacted(input.sessionID, state, state.rolloverEpoch);
874
+ }
840
875
  await arbitrateResume(input.sessionID, state);
841
876
  if (state.resumeIssuedEpoch === state.rolloverEpoch)
842
877
  return;
@@ -850,6 +885,12 @@ export function createContinuationHooks(client, directory, policySource, timings
850
885
  clearTimer(state.stepRecoveryTimer);
851
886
  state.stepRecoveryTimer = undefined;
852
887
  resetRecoveryStall(state);
888
+ if (state.notRequiredRevision !== state.turnRevision &&
889
+ !output.text.includes(ROLLOVER_MARKER) &&
890
+ !output.text.includes(CONTINUATION_MARKER)) {
891
+ state.notRequiredRevision = state.turnRevision;
892
+ observeTransition("continuation.not_required", input.sessionID, state, "terminal-checkpoint");
893
+ }
853
894
  }
854
895
  if (batchCheckpointNeedsContinuation(state.latestCoordinatorReport)) {
855
896
  if (input.allowCheckpointContinuation === false) {
@@ -951,8 +992,11 @@ export function createContinuationHooks(client, directory, policySource, timings
951
992
  },
952
993
  async sessionCompacted(sessionID) {
953
994
  const state = sessions.get(sessionID);
954
- if (state !== undefined)
995
+ if (state?.pendingRollover === true &&
996
+ (state.active || state.promptPending || state.compactingEpoch === state.rolloverEpoch)) {
997
+ observeCompacted(sessionID, state, state.rolloverEpoch);
955
998
  await arbitrateResume(sessionID, state);
999
+ }
956
1000
  },
957
1001
  async compactionAutoContinue(input, output) {
958
1002
  const state = sessions.get(input.sessionID);
@@ -10,6 +10,19 @@ export interface OpenCodePluginInput {
10
10
  worktree?: string;
11
11
  /** The host SDK client. Absent in hosts that construct the plugin without one. */
12
12
  client?: SessionMessageReader & RunMetricsClient & ContinuationClient & OpenCodeModelAvailabilityClient & {
13
+ app?: {
14
+ log?: (request: {
15
+ body: {
16
+ service: string;
17
+ level: "debug" | "info" | "error" | "warn";
18
+ message: string;
19
+ extra?: Record<string, unknown>;
20
+ };
21
+ query?: {
22
+ directory?: string;
23
+ };
24
+ }) => unknown;
25
+ };
13
26
  tui?: {
14
27
  showToast?: (request: {
15
28
  body: {
@@ -17,7 +17,7 @@ import { WriteDeniedError, canonicalManifestReadScopes, canonicalManifestWriteSc
17
17
  import { BACKLOG_DRAIN_CAPABILITY, FastLaneController } from "./fast-lane.js";
18
18
  import { createModelRoutingHook, } from "./model-routing-hook.js";
19
19
  import { createTaskResultRepairHook, lastAssistantText, markConsultationFallbackRetry, taskChildSessionID, } from "./task-result-repair.js";
20
- import { configRoot, nearestPackageVersion, reflectionEnabled, ReflectionError, ReflectionStore } from "../reflection/index.js";
20
+ import { configRoot, nearestPackageVersion, REFLECTION_POLICY, reflectionEnabled, ReflectionError, ReflectionStore } from "../reflection/index.js";
21
21
  import { collectRunMetrics, insertRunMetrics, isDoneTerminalText } from "./run-metrics.js";
22
22
  const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024, parallel: 512 * 1024 };
23
23
  const INSPECTION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
@@ -803,6 +803,83 @@ export const SortieDogsPlugin = async (input, options) => {
803
803
  let bootstrapCompleted = false;
804
804
  let assetVersionReported = false;
805
805
  const globalConfig = await readOptionalGlobalConfig();
806
+ const sessionOperationMetrics = new Map();
807
+ function appLogInfo(message, sessionID, extra) {
808
+ const app = input.client?.app;
809
+ const log = app?.log;
810
+ if (log === undefined)
811
+ return;
812
+ try {
813
+ const result = log.call(app, {
814
+ body: {
815
+ service: "sortie-dogs",
816
+ level: "info",
817
+ message,
818
+ extra: { sessionID: sessionID.slice(0, 128), ...extra },
819
+ },
820
+ query: { directory: input.directory },
821
+ });
822
+ void Promise.resolve(result).catch(() => undefined);
823
+ }
824
+ catch {
825
+ // Host lifecycle telemetry is best effort.
826
+ }
827
+ }
828
+ function pruneSessionOperationMetrics(now, reserveSlot = false) {
829
+ for (const [sessionID, metrics] of sessionOperationMetrics) {
830
+ if (metrics.touched + ACTIVE_SESSION_CACHE.ttlMilliseconds <= now) {
831
+ sessionOperationMetrics.delete(sessionID);
832
+ }
833
+ }
834
+ const limit = ACTIVE_SESSION_CACHE.maximum - (reserveSlot ? 1 : 0);
835
+ while (sessionOperationMetrics.size > limit) {
836
+ sessionOperationMetrics.delete(sessionOperationMetrics.keys().next().value);
837
+ }
838
+ }
839
+ function operationMetricsFor(sessionID) {
840
+ const now = Date.now();
841
+ pruneSessionOperationMetrics(now);
842
+ const existing = sessionOperationMetrics.get(sessionID);
843
+ if (existing !== undefined) {
844
+ existing.touched = now;
845
+ sessionOperationMetrics.delete(sessionID);
846
+ sessionOperationMetrics.set(sessionID, existing);
847
+ return existing;
848
+ }
849
+ pruneSessionOperationMetrics(now, true);
850
+ const created = {
851
+ touched: now,
852
+ operations: {
853
+ hostSessionIdentity: { count: 0, elapsedMilliseconds: 0 },
854
+ bootstrapControlState: { count: 0, elapsedMilliseconds: 0 },
855
+ collectRunMetrics: { count: 0, elapsedMilliseconds: 0 },
856
+ },
857
+ };
858
+ sessionOperationMetrics.set(sessionID, created);
859
+ return created;
860
+ }
861
+ async function measureSessionOperation(sessionID, operation, run) {
862
+ const started = performance.now();
863
+ try {
864
+ return await run();
865
+ }
866
+ finally {
867
+ const measurement = operationMetricsFor(sessionID).operations[operation];
868
+ measurement.count += 1;
869
+ measurement.elapsedMilliseconds += Math.max(0, performance.now() - started);
870
+ }
871
+ }
872
+ function operationMetricsSnapshot(sessionID) {
873
+ const operations = sessionOperationMetrics.get(sessionID)?.operations;
874
+ return {
875
+ hostSessionIdentityCount: operations?.hostSessionIdentity.count ?? 0,
876
+ hostSessionIdentityElapsedMilliseconds: Math.round(operations?.hostSessionIdentity.elapsedMilliseconds ?? 0),
877
+ bootstrapControlStateCount: operations?.bootstrapControlState.count ?? 0,
878
+ bootstrapControlStateElapsedMilliseconds: Math.round(operations?.bootstrapControlState.elapsedMilliseconds ?? 0),
879
+ collectRunMetricsCount: operations?.collectRunMetrics.count ?? 0,
880
+ collectRunMetricsElapsedMilliseconds: Math.round(operations?.collectRunMetrics.elapsedMilliseconds ?? 0),
881
+ };
882
+ }
806
883
  // Project config read is required discovery for its opt-in; no reflection storage/version read
807
884
  // occurs unless that resolved config enables reflection. It stays isolated from write-gate load.
808
885
  try {
@@ -844,7 +921,12 @@ export const SortieDogsPlugin = async (input, options) => {
844
921
  */
845
922
  (sessionID) => isCoordinatorSession(sessionID)
846
923
  ? { agent: COORDINATOR_AGENT, parentID: undefined }
847
- : undefined);
924
+ : undefined, (transition) => appLogInfo(transition.type, transition.sessionID, {
925
+ epoch: transition.epoch,
926
+ reason: transition.reason,
927
+ attempts: transition.attempts,
928
+ resumeAttempts: transition.resumeAttempts,
929
+ }));
848
930
  const completedCoordinatorMessages = new Set();
849
931
  const completedCoordinatorParts = new Set();
850
932
  async function ensureLoaded() {
@@ -2361,6 +2443,7 @@ export const SortieDogsPlugin = async (input, options) => {
2361
2443
  }
2362
2444
  function expireSession(sessionID) {
2363
2445
  activeSessions.delete(sessionID);
2446
+ sessionOperationMetrics.delete(sessionID);
2364
2447
  abandonSessionLease(sessionID);
2365
2448
  sessionAuthorizations.delete(sessionID);
2366
2449
  if (!childHasInFlightParentTask(sessionID)) {
@@ -2381,6 +2464,7 @@ export const SortieDogsPlugin = async (input, options) => {
2381
2464
  }
2382
2465
  function evictSession(sessionID) {
2383
2466
  activeSessions.delete(sessionID);
2467
+ sessionOperationMetrics.delete(sessionID);
2384
2468
  abandonSessionLease(sessionID);
2385
2469
  sessionAuthorizations.delete(sessionID);
2386
2470
  bindingPins.delete(sessionID);
@@ -2479,10 +2563,10 @@ export const SortieDogsPlugin = async (input, options) => {
2479
2563
  if (get === undefined)
2480
2564
  return undefined;
2481
2565
  try {
2482
- const response = await get.call(input.client.session, {
2566
+ const response = await measureSessionOperation(sessionID, "hostSessionIdentity", () => get.call(input.client.session, {
2483
2567
  path: { id: sessionID },
2484
2568
  query: { directory: input.directory },
2485
- });
2569
+ }));
2486
2570
  const payload = isRecord(response) && "data" in response ? response.data : response;
2487
2571
  if (!isRecord(payload))
2488
2572
  return undefined;
@@ -2906,9 +2990,13 @@ export const SortieDogsPlugin = async (input, options) => {
2906
2990
  }
2907
2991
  if ((isCoordinatorSession(textInput.sessionID) || await recoverCoordinatorRoot(textInput.sessionID)) &&
2908
2992
  isDoneTerminalText(textOutput.text)) {
2909
- const metrics = await collectRunMetrics(input.client, textInput.sessionID, input.directory).catch(() => undefined);
2993
+ const metrics = await measureSessionOperation(textInput.sessionID, "collectRunMetrics", () => collectRunMetrics(input.client, textInput.sessionID, input.directory).catch(() => undefined));
2910
2994
  if (metrics !== undefined)
2911
2995
  textOutput.text = insertRunMetrics(textOutput.text, metrics);
2996
+ appLogInfo("run-metrics.snapshot", textInput.sessionID, {
2997
+ available: metrics !== undefined,
2998
+ ...operationMetricsSnapshot(textInput.sessionID),
2999
+ });
2912
3000
  }
2913
3001
  await completeContinuationText(textInput.sessionID, textOutput.text, false);
2914
3002
  },
@@ -3057,22 +3145,33 @@ export const SortieDogsPlugin = async (input, options) => {
3057
3145
  }
3058
3146
  }
3059
3147
  }
3148
+ const heading = "SORTIE_PROCESS_REFLECTIONS";
3149
+ const prefix = `${REFLECTION_POLICY}\n\n${heading}`;
3150
+ if (transformOutput.system !== undefined) {
3151
+ const retained = transformOutput.system.filter((item) => item !== REFLECTION_POLICY && !item.startsWith(`${prefix}\n`));
3152
+ if (retained.length !== transformOutput.system.length)
3153
+ transformOutput.system = retained;
3154
+ }
3060
3155
  if (!reflectionStartup || !(await beginReflection(transformInput.sessionID)))
3061
3156
  return;
3062
3157
  const config = reflectionConfiguration;
3063
3158
  try {
3064
3159
  if (!config)
3065
3160
  return;
3066
- const heading = "SORTIE_PROCESS_REFLECTIONS";
3067
- const buckets = ["run", "project", "global"]
3068
- .filter((layer) => config.layers[layer])
3069
- .map((layer) => ({ layer, ...(layer === "global" ? {} : { run: transformInput.sessionID }) }));
3070
- const budget = Math.max(0, config.maxInjectedTokens - Buffer.byteLength(`${heading}\n`, "utf8"));
3071
- const text = await reflectionStore.injectBuckets(buckets, config.maxInjectedEntries, budget, reflectionVersion);
3072
- if (text)
3073
- transformOutput.system = [...(transformOutput.system ?? []), `${heading}\n${text}`];
3074
- }
3075
- catch { /* reflection is strictly non-invasive */ }
3161
+ let element = REFLECTION_POLICY;
3162
+ try {
3163
+ const buckets = ["run", "project", "global"]
3164
+ .filter((layer) => config.layers[layer])
3165
+ .map((layer) => ({ layer, ...(layer === "global" ? {} : { run: transformInput.sessionID }) }));
3166
+ // Historical persisted configs budget the dynamic heading and entry payload, not policy.
3167
+ const entryBudget = Math.max(0, config.maxInjectedTokens - Buffer.byteLength(`${heading}\n`, "utf8"));
3168
+ const text = await reflectionStore.injectBuckets(buckets, config.maxInjectedEntries, entryBudget, reflectionVersion);
3169
+ if (text)
3170
+ element = `${prefix}\n${text}`;
3171
+ }
3172
+ catch { /* persisted entries are best effort; the active policy still applies */ }
3173
+ transformOutput.system = [...(transformOutput.system ?? []), element];
3174
+ }
3076
3175
  finally {
3077
3176
  endReflection(transformInput.sessionID);
3078
3177
  }
@@ -3227,7 +3326,7 @@ export const SortieDogsPlugin = async (input, options) => {
3227
3326
  const bootstrap = bootstrapRequired && !exactCoordinatorDirectOperation &&
3228
3327
  !coordinatorCapability && !sessionGateCapability &&
3229
3328
  !sessionAuthorizations.has(toolInput.sessionID) && (coordinatorRoot || coordinatorRoots.size > 0)
3230
- ? await bootstrapControlState()
3329
+ ? await measureSessionOperation(toolInput.sessionID, "bootstrapControlState", bootstrapControlState)
3231
3330
  : undefined;
3232
3331
  if (coordinatorRoot && bootstrapRequired && !exactCoordinatorDirectOperation &&
3233
3332
  !coordinatorCapability && !sessionGateCapability) {
@@ -3637,7 +3736,7 @@ export const SortieDogsPlugin = async (input, options) => {
3637
3736
  if (event.type === "session.compacted")
3638
3737
  await continuation.sessionCompacted(eventSessionID);
3639
3738
  if (event.type === "session.idle" && isCoordinatorSession(eventSessionID)) {
3640
- const bootstrap = await bootstrapControlState();
3739
+ const bootstrap = await measureSessionOperation(eventSessionID, "bootstrapControlState", bootstrapControlState);
3641
3740
  if (bootstrapRequired && bootstrap?.usable === true && bootstrap.missing.length > 0) {
3642
3741
  if (!bootstrapIdleWarnings.has(eventSessionID)) {
3643
3742
  bootstrapIdleWarnings.add(eventSessionID);
@@ -1,3 +1,4 @@
1
1
  export { DEFAULT_REFLECTION, configRoot, nearestPackageVersion, projectKey, reflectionEnabled } from "./config.js";
2
+ export { REFLECTION_POLICY } from "./policy.js";
2
3
  export { ReflectionStore, ReflectionError, estimateInjectionTokens, normalizeField } from "./store.js";
3
4
  export type { ReflectionLayer } from "./config.js";
@@ -1,2 +1,3 @@
1
1
  export { DEFAULT_REFLECTION, configRoot, nearestPackageVersion, projectKey, reflectionEnabled } from "./config.js";
2
+ export { REFLECTION_POLICY } from "./policy.js";
2
3
  export { ReflectionStore, ReflectionError, estimateInjectionTokens, normalizeField } from "./store.js";
@@ -0,0 +1 @@
1
+ export declare const REFLECTION_POLICY = "## Bounded process reflection\n\nReflection is an opt-in prevention checkpoint, not routine journaling. If the\nsortie_reflection capability is unavailable, continue without it and never block the task. When\navailable, record a user correction immediately after acknowledging it and constraining the current\nremediation, even when that remediation remains open. Consider other evidence only after a blocker or\nreview defect is resolved and at a unit's terminal checkpoint. Make no call when no qualifying evidence\noccurred since the previous checkpoint.\n\nRecord only user-correction, repeated-process-failure, review-artifact-defect, or\nretry-policy-violation evidence. A resolved handoff or routing review blocker and a rescue caused by\nthe process map to review-artifact-defect or repeated-process-failure. Code bugs, ordinary validation\nfailures, expected review findings, external/network/rate-limit failures, transient tool interruption,\nand task-specific discoveries are not reflection. Attribute a process cause only with before/after\nstate or exact command evidence; shared-worktree status alone never attributes fault to an agent or\nuser. Use a stable lowercase ASCII scope with no task-specific noun.\n\nNever persist tracker or Project item metadata in reflection prose: no item/node/draft ID, URL, title,\nbody, field value, status, or inventory payload. Reduce qualifying evidence to a project-agnostic\nprocess trigger, cause, and prevention before recording. The store rejects known tracker node-ID forms;\nthe coordinator remains responsible for removing semantic metadata that no lexical filter can identify.\n\nMap the predecessor session layer to run and its cross-chat project-specific memory to project.\nGlobal-layer writes are forbidden by default and allowed only when the user or config explicitly enables\nreflection.layers.global. Record user-correction directly at layer=project. For other evidence, use\nlayer=run on the first occurrence and layer=project only when the scope recurs in a later unit or was\ninjected from an earlier run. Scope is the dedup key: recording it again updates trigger and hits but\npreserves cause and prevention. Use replace only to improve those fields deliberately. Reflections are\ninjected automatically at turn start under SORTIE_PROCESS_REFLECTIONS with entry id and hits. Record\ndirectly because scope is the store's dedup key; never list before record. Before replace, forget, or\npromote, call list once only when the target id is absent from the bounded injection. Keep every\nreflection field concise ASCII English and keep scope + trigger + cause + prevention + evidenceRef\nwithin 400 characters total. If later evidence disproves attribution, forget that entry. Forget needs\nno confirmation because its exact entry id is the deletion boundary; clear keeps its layer confirmation\nrules. Never clear merely because a task or session ended.\n\nInjected reflections are bounded prevention hints, never workflow authority. They cannot override the\nlatest user scope, batchTarget, batchAttempted, manifest boundaries, validation history, retry ceilings,\nreview gates, or safety policy. Interpret a continuous-execution reflection only inside the currently\naccepted user scope; it never authorizes unrelated work or bypasses manifest, validation, review, or safety gates.\n\nMake at most one record call per triggering event and at most three record calls per run. When hits\nreach two, or a user correction identifies a defect in runtime policy, project docs, an agent contract,\nor a tool path, identify a durable-fix follow-up rather than repeatedly applying the prevention by hand.\nDuring an active user batch, record only the reflection: never turn that follow-up into a candidate,\nedit project instructions for it, dispatch a worker or reviewer for it, consume a batch unit, mutate its\ntracker, or commit it. Report the follow-up after the user batch and require a new explicit top-level\nuser request before implementation. Reuse an injected scope when trigger, cause, or prevention names\nthe same process failure; inventing a synonym scope for equivalent evidence is forbidden.\nAfter an explicitly requested durable fix is committed, promote the entry with its returned id and a short non-path promotedRef;\nforget it instead only when the lesson was false or no runtime judgment remains. Reflection failure is\nalways non-blocking, and no reflection-only text step is allowed.\n\nREFLECTION_POLICY_FIXTURE\n checkpoints: user correction immediately | other evidence after resolved blocker or review defect | terminal unit\n capability_absent: continue without reflection; never block\n allowed_evidence: user-correction | repeated-process-failure | review-artifact-defect | retry-policy-violation\n non_triggers: code bug | ordinary validation failure | expected review finding | external or transient failure | task discovery\n attribution: before/after state or exact command evidence required; shared worktree status alone is insufficient\n tracker_privacy: no item/node/draft ID | URL | title | body | field value | status | inventory payload\n user_correction_layer: project immediately\n first_process_failure_layer: run\n project_layer: same stable scope recurred in a later unit or was injected from an earlier run\n global_layer: forbidden by default; allowed only when user or config explicitly enables reflection.layers.global\n scope: stable lowercase ASCII process key; no task-specific noun\n dedup: same scope updates trigger and hits; equivalent evidence reuses the injected scope; synonym scopes forbidden\n call_limit: one record per triggering event; three record calls per run\n duplicate_scope: same event or same layer in one unit -> no call\n injected_project_recurrence: record project once to increment hits\n field_budget: concise ASCII English; scope + trigger + cause + prevention + evidenceRef <=400 characters total\n scope_format: lowercase kebab-case [a-z0-9-]+; underscores forbidden\n list: never before record; once before replace | forget | promote only when target id is absent from bounded injection\n call: sortie_reflection { action: record, layer: <run|project|global>, scope: <scope>, trigger: <event>, cause: <verified process cause>, prevention: <one reusable imperative>, evidence: <allowed enum>, evidenceRef: <short non-path reference> }\n correction: improved cause or prevention -> replace; disproved attribution -> forget\n forget_confirmation: none; exact entry id is the deletion boundary\n durable_fix: hits>=2 or policy-related user correction -> report follow-up after active batch; new explicit top-level request required\n active_batch_quarantine: no process-only candidate | instruction edit | Task | review | batch unit | tracker mutation | commit\n promotion: durable fix committed -> promote with returned id and short non-path reference; false or fully obsolete lesson -> forget\n read: automatic injection with id and hits under SORTIE_PROCESS_REFLECTIONS at turn start\n precedence: prevention hint only; never overrides user scope | batch counters | manifests | validation history | retry ceilings | review | safety\n continuous_execution: continue inside accepted user scope until complete | user decision | proven blocker | no progress\n extra_step: reflection-only text or tool step forbidden\nEND_REFLECTION_POLICY_FIXTURE";
@@ -0,0 +1,83 @@
1
+ export const REFLECTION_POLICY = `## Bounded process reflection
2
+
3
+ Reflection is an opt-in prevention checkpoint, not routine journaling. If the
4
+ sortie_reflection capability is unavailable, continue without it and never block the task. When
5
+ available, record a user correction immediately after acknowledging it and constraining the current
6
+ remediation, even when that remediation remains open. Consider other evidence only after a blocker or
7
+ review defect is resolved and at a unit's terminal checkpoint. Make no call when no qualifying evidence
8
+ occurred since the previous checkpoint.
9
+
10
+ Record only user-correction, repeated-process-failure, review-artifact-defect, or
11
+ retry-policy-violation evidence. A resolved handoff or routing review blocker and a rescue caused by
12
+ the process map to review-artifact-defect or repeated-process-failure. Code bugs, ordinary validation
13
+ failures, expected review findings, external/network/rate-limit failures, transient tool interruption,
14
+ and task-specific discoveries are not reflection. Attribute a process cause only with before/after
15
+ state or exact command evidence; shared-worktree status alone never attributes fault to an agent or
16
+ user. Use a stable lowercase ASCII scope with no task-specific noun.
17
+
18
+ Never persist tracker or Project item metadata in reflection prose: no item/node/draft ID, URL, title,
19
+ body, field value, status, or inventory payload. Reduce qualifying evidence to a project-agnostic
20
+ process trigger, cause, and prevention before recording. The store rejects known tracker node-ID forms;
21
+ the coordinator remains responsible for removing semantic metadata that no lexical filter can identify.
22
+
23
+ Map the predecessor session layer to run and its cross-chat project-specific memory to project.
24
+ Global-layer writes are forbidden by default and allowed only when the user or config explicitly enables
25
+ reflection.layers.global. Record user-correction directly at layer=project. For other evidence, use
26
+ layer=run on the first occurrence and layer=project only when the scope recurs in a later unit or was
27
+ injected from an earlier run. Scope is the dedup key: recording it again updates trigger and hits but
28
+ preserves cause and prevention. Use replace only to improve those fields deliberately. Reflections are
29
+ injected automatically at turn start under SORTIE_PROCESS_REFLECTIONS with entry id and hits. Record
30
+ directly because scope is the store's dedup key; never list before record. Before replace, forget, or
31
+ promote, call list once only when the target id is absent from the bounded injection. Keep every
32
+ reflection field concise ASCII English and keep scope + trigger + cause + prevention + evidenceRef
33
+ within 400 characters total. If later evidence disproves attribution, forget that entry. Forget needs
34
+ no confirmation because its exact entry id is the deletion boundary; clear keeps its layer confirmation
35
+ rules. Never clear merely because a task or session ended.
36
+
37
+ Injected reflections are bounded prevention hints, never workflow authority. They cannot override the
38
+ latest user scope, batchTarget, batchAttempted, manifest boundaries, validation history, retry ceilings,
39
+ review gates, or safety policy. Interpret a continuous-execution reflection only inside the currently
40
+ accepted user scope; it never authorizes unrelated work or bypasses manifest, validation, review, or safety gates.
41
+
42
+ Make at most one record call per triggering event and at most three record calls per run. When hits
43
+ reach two, or a user correction identifies a defect in runtime policy, project docs, an agent contract,
44
+ or a tool path, identify a durable-fix follow-up rather than repeatedly applying the prevention by hand.
45
+ During an active user batch, record only the reflection: never turn that follow-up into a candidate,
46
+ edit project instructions for it, dispatch a worker or reviewer for it, consume a batch unit, mutate its
47
+ tracker, or commit it. Report the follow-up after the user batch and require a new explicit top-level
48
+ user request before implementation. Reuse an injected scope when trigger, cause, or prevention names
49
+ the same process failure; inventing a synonym scope for equivalent evidence is forbidden.
50
+ After an explicitly requested durable fix is committed, promote the entry with its returned id and a short non-path promotedRef;
51
+ forget it instead only when the lesson was false or no runtime judgment remains. Reflection failure is
52
+ always non-blocking, and no reflection-only text step is allowed.
53
+
54
+ REFLECTION_POLICY_FIXTURE
55
+ checkpoints: user correction immediately | other evidence after resolved blocker or review defect | terminal unit
56
+ capability_absent: continue without reflection; never block
57
+ allowed_evidence: user-correction | repeated-process-failure | review-artifact-defect | retry-policy-violation
58
+ non_triggers: code bug | ordinary validation failure | expected review finding | external or transient failure | task discovery
59
+ attribution: before/after state or exact command evidence required; shared worktree status alone is insufficient
60
+ tracker_privacy: no item/node/draft ID | URL | title | body | field value | status | inventory payload
61
+ user_correction_layer: project immediately
62
+ first_process_failure_layer: run
63
+ project_layer: same stable scope recurred in a later unit or was injected from an earlier run
64
+ global_layer: forbidden by default; allowed only when user or config explicitly enables reflection.layers.global
65
+ scope: stable lowercase ASCII process key; no task-specific noun
66
+ dedup: same scope updates trigger and hits; equivalent evidence reuses the injected scope; synonym scopes forbidden
67
+ call_limit: one record per triggering event; three record calls per run
68
+ duplicate_scope: same event or same layer in one unit -> no call
69
+ injected_project_recurrence: record project once to increment hits
70
+ field_budget: concise ASCII English; scope + trigger + cause + prevention + evidenceRef <=400 characters total
71
+ scope_format: lowercase kebab-case [a-z0-9-]+; underscores forbidden
72
+ list: never before record; once before replace | forget | promote only when target id is absent from bounded injection
73
+ call: sortie_reflection { action: record, layer: <run|project|global>, scope: <scope>, trigger: <event>, cause: <verified process cause>, prevention: <one reusable imperative>, evidence: <allowed enum>, evidenceRef: <short non-path reference> }
74
+ correction: improved cause or prevention -> replace; disproved attribution -> forget
75
+ forget_confirmation: none; exact entry id is the deletion boundary
76
+ durable_fix: hits>=2 or policy-related user correction -> report follow-up after active batch; new explicit top-level request required
77
+ active_batch_quarantine: no process-only candidate | instruction edit | Task | review | batch unit | tracker mutation | commit
78
+ promotion: durable fix committed -> promote with returned id and short non-path reference; false or fully obsolete lesson -> forget
79
+ read: automatic injection with id and hits under SORTIE_PROCESS_REFLECTIONS at turn start
80
+ precedence: prevention hint only; never overrides user scope | batch counters | manifests | validation history | retry ceilings | review | safety
81
+ continuous_execution: continue inside accepted user scope until complete | user decision | proven blocker | no progress
82
+ extra_step: reflection-only text or tool step forbidden
83
+ END_REFLECTION_POLICY_FIXTURE`;