@exaudeus/workrail 3.77.1 → 3.78.0

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.
@@ -95,6 +95,7 @@ export declare const DiscoveryPhaseRecordSchema: z.ZodObject<{
95
95
  path: string;
96
96
  relevance: string;
97
97
  }>, "many">>;
98
+ selectionTier: z.ZodOptional<z.ZodEnum<["strong_recommendation", "provisional_recommendation", "insufficient_signal"]>>;
98
99
  }, "strict", z.ZodTypeAny, {
99
100
  kind: "wr.discovery_handoff";
100
101
  version: 1;
@@ -111,6 +112,7 @@ export declare const DiscoveryPhaseRecordSchema: z.ZodObject<{
111
112
  path: string;
112
113
  relevance: string;
113
114
  }[] | undefined;
115
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
114
116
  }, {
115
117
  kind: "wr.discovery_handoff";
116
118
  version: 1;
@@ -127,6 +129,7 @@ export declare const DiscoveryPhaseRecordSchema: z.ZodObject<{
127
129
  path: string;
128
130
  relevance: string;
129
131
  }[] | undefined;
132
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
130
133
  }>;
131
134
  confidenceBand: z.ZodNullable<z.ZodEnum<["high", "medium", "low"]>>;
132
135
  recapMarkdown: z.ZodNullable<z.ZodString>;
@@ -148,6 +151,7 @@ export declare const DiscoveryPhaseRecordSchema: z.ZodObject<{
148
151
  path: string;
149
152
  relevance: string;
150
153
  }[] | undefined;
154
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
151
155
  };
152
156
  confidenceBand: "low" | "high" | "medium" | null;
153
157
  recapMarkdown: string | null;
@@ -169,6 +173,7 @@ export declare const DiscoveryPhaseRecordSchema: z.ZodObject<{
169
173
  path: string;
170
174
  relevance: string;
171
175
  }[] | undefined;
176
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
172
177
  };
173
178
  confidenceBand: "low" | "high" | "medium" | null;
174
179
  recapMarkdown: string | null;
@@ -210,6 +215,7 @@ export declare const DiscoveryPhaseRecordSchema: z.ZodObject<{
210
215
  path: string;
211
216
  relevance: string;
212
217
  }[] | undefined;
218
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
213
219
  };
214
220
  confidenceBand: "low" | "high" | "medium" | null;
215
221
  recapMarkdown: string | null;
@@ -241,6 +247,7 @@ export declare const DiscoveryPhaseRecordSchema: z.ZodObject<{
241
247
  path: string;
242
248
  relevance: string;
243
249
  }[] | undefined;
250
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
244
251
  };
245
252
  confidenceBand: "low" | "high" | "medium" | null;
246
253
  recapMarkdown: string | null;
@@ -737,6 +744,7 @@ export declare const PipelineRunContextSchema: z.ZodObject<{
737
744
  path: string;
738
745
  relevance: string;
739
746
  }>, "many">>;
747
+ selectionTier: z.ZodOptional<z.ZodEnum<["strong_recommendation", "provisional_recommendation", "insufficient_signal"]>>;
740
748
  }, "strict", z.ZodTypeAny, {
741
749
  kind: "wr.discovery_handoff";
742
750
  version: 1;
@@ -753,6 +761,7 @@ export declare const PipelineRunContextSchema: z.ZodObject<{
753
761
  path: string;
754
762
  relevance: string;
755
763
  }[] | undefined;
764
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
756
765
  }, {
757
766
  kind: "wr.discovery_handoff";
758
767
  version: 1;
@@ -769,6 +778,7 @@ export declare const PipelineRunContextSchema: z.ZodObject<{
769
778
  path: string;
770
779
  relevance: string;
771
780
  }[] | undefined;
781
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
772
782
  }>;
773
783
  confidenceBand: z.ZodNullable<z.ZodEnum<["high", "medium", "low"]>>;
774
784
  recapMarkdown: z.ZodNullable<z.ZodString>;
@@ -790,6 +800,7 @@ export declare const PipelineRunContextSchema: z.ZodObject<{
790
800
  path: string;
791
801
  relevance: string;
792
802
  }[] | undefined;
803
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
793
804
  };
794
805
  confidenceBand: "low" | "high" | "medium" | null;
795
806
  recapMarkdown: string | null;
@@ -811,6 +822,7 @@ export declare const PipelineRunContextSchema: z.ZodObject<{
811
822
  path: string;
812
823
  relevance: string;
813
824
  }[] | undefined;
825
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
814
826
  };
815
827
  confidenceBand: "low" | "high" | "medium" | null;
816
828
  recapMarkdown: string | null;
@@ -852,6 +864,7 @@ export declare const PipelineRunContextSchema: z.ZodObject<{
852
864
  path: string;
853
865
  relevance: string;
854
866
  }[] | undefined;
867
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
855
868
  };
856
869
  confidenceBand: "low" | "high" | "medium" | null;
857
870
  recapMarkdown: string | null;
@@ -883,6 +896,7 @@ export declare const PipelineRunContextSchema: z.ZodObject<{
883
896
  path: string;
884
897
  relevance: string;
885
898
  }[] | undefined;
899
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
886
900
  };
887
901
  confidenceBand: "low" | "high" | "medium" | null;
888
902
  recapMarkdown: string | null;
@@ -1412,6 +1426,7 @@ export declare const PipelineRunContextSchema: z.ZodObject<{
1412
1426
  path: string;
1413
1427
  relevance: string;
1414
1428
  }[] | undefined;
1429
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
1415
1430
  };
1416
1431
  confidenceBand: "low" | "high" | "medium" | null;
1417
1432
  recapMarkdown: string | null;
@@ -1525,6 +1540,7 @@ export declare const PipelineRunContextSchema: z.ZodObject<{
1525
1540
  path: string;
1526
1541
  relevance: string;
1527
1542
  }[] | undefined;
1543
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
1528
1544
  };
1529
1545
  confidenceBand: "low" | "high" | "medium" | null;
1530
1546
  recapMarkdown: string | null;
@@ -1645,6 +1661,7 @@ export declare const PipelineRunContextSchema: z.ZodObject<{
1645
1661
  path: string;
1646
1662
  relevance: string;
1647
1663
  }[] | undefined;
1664
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
1648
1665
  };
1649
1666
  confidenceBand: "low" | "high" | "medium" | null;
1650
1667
  recapMarkdown: string | null;
@@ -1766,6 +1783,7 @@ export declare const PipelineRunContextSchema: z.ZodObject<{
1766
1783
  path: string;
1767
1784
  relevance: string;
1768
1785
  }[] | undefined;
1786
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
1769
1787
  };
1770
1788
  confidenceBand: "low" | "high" | "medium" | null;
1771
1789
  recapMarkdown: string | null;
@@ -30,8 +30,8 @@
30
30
  "bytes": 428
31
31
  },
32
32
  "application/services/compiler/ref-registry.js": {
33
- "sha256": "a01cceb5a2084cb6effdd408f2ed35bd1dda45f70eb1498f6456ad57d344f36d",
34
- "bytes": 5305
33
+ "sha256": "683b5b08f827d8d695d8f1417798a260e03554b6b2723889320d645174fc2353",
34
+ "bytes": 5456
35
35
  },
36
36
  "application/services/compiler/resolve-bindings.d.ts": {
37
37
  "sha256": "ef190f200228aedf5d27b3fd38c890c5a0613d3346bf0942ec0a7b51cc167d2e",
@@ -473,16 +473,16 @@
473
473
  "sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
474
474
  "bytes": 8011
475
475
  },
476
- "console-ui/assets/index-BooFww1c.js": {
477
- "sha256": "74a7000918d3c20decbab9563481fd738471cbbbfe0e13a57ac443283ee4c410",
478
- "bytes": 767524
476
+ "console-ui/assets/index-CtQZQTW-.js": {
477
+ "sha256": "59916cf0f65c7edf6cc75dc2b56c091c70cd2b871882d3d0e446e2f4a0025e18",
478
+ "bytes": 768377
479
479
  },
480
480
  "console-ui/assets/index-DHrKiMCf.css": {
481
481
  "sha256": "40290b50e21ee7e82433efe13b1aa31c1ea608bd057a5c4e324982f284bc928b",
482
482
  "bytes": 60673
483
483
  },
484
484
  "console-ui/index.html": {
485
- "sha256": "34573fa44dba81dda52ce425f674ebbee9283bd1ec4ab405dd8ef20f8a0d1760",
485
+ "sha256": "77f3b5e6131c8d6bcb34a8c2a6be1a202ebbb1b03e28edfbc102b243eddcbaa2",
486
486
  "bytes": 417
487
487
  },
488
488
  "console/standalone-console.d.ts": {
@@ -582,8 +582,8 @@
582
582
  "bytes": 1198
583
583
  },
584
584
  "coordinators/pipeline-run-context.d.ts": {
585
- "sha256": "8fc743a1458e4d7a0971c968d51749d55a3bc114b286efe6b2030eb062809589",
586
- "bytes": 68469
585
+ "sha256": "cb3cb3fde2851472e6769e3934808675175e003e7f8a56579b7807b4e780d9a5",
586
+ "bytes": 70743
587
587
  },
588
588
  "coordinators/pipeline-run-context.js": {
589
589
  "sha256": "5489319764a0dbd1b037521e014784fab518c4ff4f9137e045129e6845793e55",
@@ -2630,12 +2630,12 @@
2630
2630
  "bytes": 1242
2631
2631
  },
2632
2632
  "v2/durable-core/schemas/artifacts/discovery-handoff.d.ts": {
2633
- "sha256": "36b933327a0aed2b767834c4567c7bc51209f9f39e844f3a2503ddccc70ec93e",
2634
- "bytes": 2323
2633
+ "sha256": "425c0b1fcb69311e0e42f06359fecb8d935128ea35a53e081d93e43f5ff9d8fb",
2634
+ "bytes": 2672
2635
2635
  },
2636
2636
  "v2/durable-core/schemas/artifacts/discovery-handoff.js": {
2637
- "sha256": "2c8e2f51df1691a34e64bfb4ecfbfd8b4447ca961824612c366f9a30092bfe9b",
2638
- "bytes": 1624
2637
+ "sha256": "e76c73a9028a4106aec647e7d9862cae66b9eb36066a46cf4f4634e9d7f7b7d6",
2638
+ "bytes": 1748
2639
2639
  },
2640
2640
  "v2/durable-core/schemas/artifacts/index.d.ts": {
2641
2641
  "sha256": "016e3d46d2eac61e12caf851f8b9d46512b2a3a186bbab7d672127f7f48eb168",
@@ -3474,12 +3474,12 @@
3474
3474
  "bytes": 1701
3475
3475
  },
3476
3476
  "v2/usecases/console-service.js": {
3477
- "sha256": "46c126cc1edee0e672629ec546bc80dcd0700044d89867277dfb285277fff206",
3478
- "bytes": 40380
3477
+ "sha256": "be260ca0de913b08f1693a61ac8106d61449f999ce7f9e2c1127d7351bc5238e",
3478
+ "bytes": 40956
3479
3479
  },
3480
3480
  "v2/usecases/console-types.d.ts": {
3481
- "sha256": "854975a0735590ac2981c1c4b5b93a0518eb97493567e88af4ffa101e596195f",
3482
- "bytes": 8094
3481
+ "sha256": "385e4c316de5b3884532d14ba77a0a06e88b91873cb346bf339723aaf01c5d93",
3482
+ "bytes": 8184
3483
3483
  },
3484
3484
  "v2/usecases/console-types.js": {
3485
3485
  "sha256": "d43aa81f5bc89faa359e0f97c814ba25155591ff078fbb9bfd40f8c7c9683230",
@@ -28,6 +28,7 @@ export declare const DiscoveryHandoffArtifactV1Schema: z.ZodObject<{
28
28
  path: string;
29
29
  relevance: string;
30
30
  }>, "many">>;
31
+ selectionTier: z.ZodOptional<z.ZodEnum<["strong_recommendation", "provisional_recommendation", "insufficient_signal"]>>;
31
32
  }, "strict", z.ZodTypeAny, {
32
33
  kind: "wr.discovery_handoff";
33
34
  version: 1;
@@ -44,6 +45,7 @@ export declare const DiscoveryHandoffArtifactV1Schema: z.ZodObject<{
44
45
  path: string;
45
46
  relevance: string;
46
47
  }[] | undefined;
48
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
47
49
  }, {
48
50
  kind: "wr.discovery_handoff";
49
51
  version: 1;
@@ -60,6 +62,7 @@ export declare const DiscoveryHandoffArtifactV1Schema: z.ZodObject<{
60
62
  path: string;
61
63
  relevance: string;
62
64
  }[] | undefined;
65
+ selectionTier?: "strong_recommendation" | "provisional_recommendation" | "insufficient_signal" | undefined;
63
66
  }>;
64
67
  export type DiscoveryHandoffArtifactV1 = z.infer<typeof DiscoveryHandoffArtifactV1Schema>;
65
68
  export declare function isDiscoveryHandoffArtifact(artifact: unknown): artifact is {
@@ -22,6 +22,7 @@ exports.DiscoveryHandoffArtifactV1Schema = zod_1.z
22
22
  path: zod_1.z.string().min(1).max(300),
23
23
  relevance: zod_1.z.string().min(1).max(150),
24
24
  })).max(10).optional(),
25
+ selectionTier: zod_1.z.enum(['strong_recommendation', 'provisional_recommendation', 'insufficient_signal']).optional(),
25
26
  })
26
27
  .strict();
27
28
  function isDiscoveryHandoffArtifact(artifact) {
@@ -736,6 +736,7 @@ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, w
736
736
  const sessionHealth = health.isOk() && health.value.kind === 'healthy' ? 'healthy' : 'corrupt';
737
737
  const sortedEventsRes = (0, sorted_event_log_js_1.asSortedEventLog)(events);
738
738
  const sessionTitle = sortedEventsRes.isOk() ? deriveSessionTitle(sortedEventsRes.value) : null;
739
+ const runContextRes = sortedEventsRes.isOk() ? (0, run_context_js_1.projectRunContextV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
739
740
  const detailTriggerSource = (() => {
740
741
  if (sortedEventsRes.isOk()) {
741
742
  for (const e of sortedEventsRes.value) {
@@ -744,13 +745,23 @@ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, w
744
745
  }
745
746
  }
746
747
  }
747
- const contextRes = sortedEventsRes.isOk() ? (0, run_context_js_1.projectRunContextV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
748
- const isAutonomous = contextRes.isOk() && Object.values(contextRes.value.byRunId).some((runCtx) => runCtx.context['is_autonomous'] === 'true');
748
+ const isAutonomous = runContextRes.isOk() && Object.values(runContextRes.value.byRunId).some((runCtx) => runCtx.context['is_autonomous'] === 'true');
749
749
  return isAutonomous ? 'daemon' : 'mcp';
750
750
  })();
751
+ const injectedContext = (() => {
752
+ if (!runContextRes.isOk())
753
+ return undefined;
754
+ for (const runCtx of Object.values(runContextRes.value.byRunId)) {
755
+ const summary = runCtx.context['assembledContextSummary'];
756
+ if (typeof summary === 'string' && summary.trim().length > 0) {
757
+ return { assembledContextSummary: summary };
758
+ }
759
+ }
760
+ return undefined;
761
+ })();
751
762
  const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
752
763
  if (dagRes.isErr()) {
753
- return { sessionId, sessionTitle, health: sessionHealth, runs: [], metrics: null, repoRoot: null, triggerSource: detailTriggerSource };
764
+ return { sessionId, sessionTitle, health: sessionHealth, runs: [], metrics: null, repoRoot: null, triggerSource: detailTriggerSource, ...(injectedContext !== undefined ? { injectedContext } : {}) };
754
765
  }
755
766
  const statusRes = sortedEventsRes.isOk() ? (0, run_status_signals_js_1.projectRunStatusSignalsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
756
767
  const gapsRes = sortedEventsRes.isOk() ? (0, gaps_js_1.projectGapsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
@@ -819,7 +830,7 @@ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, w
819
830
  skippedSteps: skippedStepsMap[run.runId] ?? [],
820
831
  };
821
832
  });
822
- return { sessionId, sessionTitle, health: sessionHealth, runs, metrics: null, repoRoot: null, triggerSource: detailTriggerSource };
833
+ return { sessionId, sessionTitle, health: sessionHealth, runs, metrics: null, repoRoot: null, triggerSource: detailTriggerSource, ...(injectedContext !== undefined ? { injectedContext } : {}) };
823
834
  }
824
835
  function deriveRunStatus(isBlocked, hasUnresolvedCriticalGaps, isComplete) {
825
836
  if (isBlocked)
@@ -97,6 +97,9 @@ export interface ConsoleSessionDetail {
97
97
  readonly metrics: SessionMetricsV2 | null;
98
98
  readonly repoRoot: string | null;
99
99
  readonly triggerSource: 'daemon' | 'mcp';
100
+ readonly injectedContext?: {
101
+ readonly assembledContextSummary: string;
102
+ };
100
103
  }
101
104
  export type ConsoleValidationOutcome = 'pass' | 'fail';
102
105
  export interface ConsoleValidationResult {
@@ -194,46 +194,27 @@ The delivery pipeline was extracted into `delivery-pipeline.ts` with explicit st
194
194
 
195
195
  ### Context injection bugs: double-injection, byte-slice truncation, workspaceRules[0] drop (Apr 30, 2026)
196
196
 
197
- **Status: idea** | Priority: high
197
+ **Status: done** | Shipped in PR #946 (fix/etienneb/context-injection-bugs, auto-merge enabled)
198
198
 
199
199
  **Score: 13** | Cor:3 Cap:1 Eff:3 Lev:3 Con:3 | Blocked: no
200
200
 
201
- Three active bugs in the context injection pipeline that waste tokens, produce incorrect truncation, and silently discard workspace context. Confirmed by codebase audit (Apr 30, 2026).
202
-
203
- 1. **Double-injection (`session-context.ts:117-119`):** `trigger.context` is JSON-serialized in full into the initial user message. Since coordinators write `assembledContextSummary` *into* `trigger.context`, the assembled context appears twice -- once in the system prompt (8KB cap applied) and once in the initial user message (uncapped). These diverge when the content exceeds 8KB.
204
-
205
- 2. **Byte-slice truncation (`system-prompt.ts:200-202`):** `assembledContextSummary` is truncated by raw byte index (`ctxStr.slice(0, 8192)`), which splits mid-sentence, mid-section, and can produce malformed UTF-8. The section-aware `buildBudgetedOutput()` pattern already exists in `src/coordinators/context-assembly.ts` and handles this correctly.
206
-
207
- 3. **`workspaceRules[0]` silent drop (`session-context.ts:106`):** `ContextBundle.workspaceRules` is typed as `ContextRule[]` but only `[0]` is consumed. All additional workspace context rules are silently dropped. The type implies per-file rules are supported; the consumer silently ignores them.
208
-
209
- **Also in scope:** introduce `WorkflowContextSlots` typed fields on `WorkflowTrigger` (or a companion type) for system-managed context fields (`assembledContextSummary`, `priorSessionNotes`, `gitDiffStat`). This eliminates the stringly-typed `trigger.context['assembledContextSummary']` access pattern and is a prerequisite for the universal enricher (see next item). Scope Phase 0 changes to consumption sites only (`buildSystemPrompt`, `buildSessionContext`); coordinator write sites migrate in Phase 1.
210
-
211
- **Done looks like:** no `trigger.context` JSON dump in `initialPrompt`; `assembledContextSummary` truncated at section boundaries; all `workspaceRules` entries injected; `WorkflowContextSlots` typed fields replace stringly-typed access in consumption sites.
201
+ All three bugs fixed. `WorkflowContextSlots` typed interface + `extractContextSlots()` introduced in `src/daemon/types.ts`. `buildSystemPrompt` refactored to pipeline of pure section functions. `truncateToByteLimit` uses Buffer/surrogate-safe walk-back.
212
202
 
213
203
  ---
214
204
 
215
205
  ### Universal context enricher for all session entry points (Apr 30, 2026)
216
206
 
217
- **Status: idea** | Priority: high
218
-
219
- **Score: 11** | Cor:1 Cap:3 Eff:2 Lev:3 Con:2 | Blocked: yes (needs context injection bugs fixed first)
220
-
221
- Today 4 of 6 session entry points receive zero assembled context: raw webhook triggers, direct dispatch, `spawn_agent` children, and crash-recovered sessions never get cross-session notes or git diff state. Only coordinator-spawned sessions (via `pr-review.ts` or the adaptive pipeline) get assembled context -- and even then only through opt-in coordinator logic, not structural injection.
207
+ **Status: done** | Shipped in PR #947 (feat/etienneb/workflow-enricher, auto-merge enabled, depends on #946)
222
208
 
223
- There is no single layer that all dispatch paths share where assembly can run universally. Coordinators that care must call assembly explicitly; everything else gets nothing. This means every new entry point or coordinator is another opportunity to forget assembly.
209
+ **Score: 11** | Cor:1 Cap:3 Eff:2 Lev:3 Con:2 | Blocked: no
224
210
 
225
- **Design (from Apr 30 discovery):** A `WorkflowEnricher` service injected into `runWorkflow()` that fires for root sessions only (`spawnDepth === 0`). Provides prior workspace session notes (max 3, newest-first, workspace-scoped) and `git diff HEAD~1 --stat` to all entry points. Injected via `WorkflowContextSlots` typed fields (see context injection bugs item). When a coordinator has already set `assembledContextSummary`, the enricher skips prior-notes injection (coordinator's richer context takes precedence) but still provides git diff stat if absent.
211
+ `WorkflowEnricher` service in `src/daemon/workflow-enricher.ts`. Fires for root sessions (`spawnDepth === 0`) inside `runWorkflow()` before `buildPreAgentSession()`. `PriorNotesPolicy` discriminated type controls notes injection. 1s timeout with partial fallback on `listRecentSessions`. `EnricherResult` threaded as typed value through call chain -- trigger never mutated. All 6 entry points covered.
226
212
 
227
- **Critical gate:** before this ships, run a pilot test -- one session with `assembledContextSummary` injected, inspect turn-1 reasoning for citation. If agents don't reference pre-loaded context, the investment in universal enrichment adds tokens without improving outcomes.
228
-
229
- **Things to hash out:**
230
- - Where exactly does the enricher inject: inside `runWorkflow()` before `buildPreAgentSession()`, or inside `buildPreAgentSession()` itself? The latter is cleaner but changes the pre-agent phase boundary.
231
- - `listRecentSessions` must have a 1s wall-clock timeout with partial-result fallback. Without it, large session stores silently slow all session startups. This is a spec requirement, not optional.
232
- - `spawn_agent` children don't get enriched (they'd trigger redundant assembly for deeply nested trees). Is there a case where children should optionally enrich? Candidate: an `inheritParentContext: boolean` flag in the `spawn_agent` tool schema.
213
+ **Pilot test gate still pending:** before declaring full success, verify agents reference prior notes in turn-1 reasoning in at least one real session.
233
214
 
234
215
  ---
235
216
 
236
- ### MemoryStore: indexed session history and mid-session query_memory tool (Apr 30, 2026)
217
+ ### MemoryStore: indexed session history as a coordinator and enricher dependency (Apr 30, 2026)
237
218
 
238
219
  **Status: idea** | Priority: medium
239
220
 
@@ -241,16 +222,17 @@ There is no single layer that all dispatch paths share where assembly can run un
241
222
 
242
223
  The session event log is rich -- it records goals, step notes, artifacts, delivered commits, git state, and phase handoffs. But querying it requires a full directory scan and per-session event projection on every call. `LocalSessionSummaryProviderV2` does this today and is used in exactly one place (the PR-review coordinator). Every other consumer either skips it or re-implements a slower version.
243
224
 
244
- There is no mid-session memory query capability at all. An agent mid-session cannot ask "what did we decide about this module last week" and get an answer from persistent memory -- it can only use what was pre-loaded at session start.
225
+ **Design:** A `MemoryStore` port backed by `~/.workrail/memory.db` (SQLite, WAL mode), indexed by `finalizeSession()` as fire-and-forget after each session completes. Replaces the current full directory scan with an indexed query -- O(log n + k) for "recent sessions for this workspace" instead of O(n) full scan. Query kinds v1: `recent_sessions` (workspace-scoped, indexed on `(workspace_hash, completed_at DESC)`), `sessions_by_goal_keywords` (requires full-text index or O(n) scan). Consumed by the WorkflowEnricher and coordinator pre-dispatch paths, not by agents directly.
245
226
 
246
- **Design (from Apr 30 discovery):** A `MemoryStore` port backed by `~/.workrail/memory.db` (SQLite, WAL mode) indexed by `finalizeSession()` as fire-and-forget after each session completes. Query kinds v1: `recent_sessions` (by workspace path hash), `sessions_by_goal_keywords`. A `query_memory` tool added to the daemon tool set. Replaces the slow `listRecentSessions` scan in the universal enricher.
227
+ **Why not a mid-session agent tool:** context assembly belongs in the layer that dispatches the session -- the coordinator and enricher know what workspace they're spawning into and can assemble context deterministically before the first turn. Leaving retrieval to the agent requires the LLM to make a judgment call about its own context needs mid-session, burns turns, and produces inconsistent results. If an agent needs something that wasn't pre-loaded, that's a gap in the assembly step, not a signal to give agents a retrieval tool.
247
228
 
248
- Phase 2b (separate): index phase artifacts via a new `phase_artifact_appended` session event kind -- bridges the current PipelineRunContext silo into the session event log so phase artifacts are queryable alongside session notes. Requires engine schema review before implementation.
229
+ Phase 2b (separate): index phase artifacts via a new `phase_artifact_appended` session event kind -- bridges the PipelineRunContext silo into the session event log. Requires engine schema review.
249
230
 
250
231
  **Things to hash out:**
251
- - SQLite native compilation may fail in some deployment environments (Docker, Alpine Linux). Mitigation: use `@sqlite.org/sqlite-wasm` (pure WASM) or make `MemoryStore` fully optional -- daemon works without it, just no indexed queries.
252
- - `phase_artifact_appended` event schema change is the highest-risk part of Phase 2b. Should it reuse the existing artifact channel with a new content type, or be a new event kind? Each has different backward-compatibility implications.
253
- - Should `query_memory` be a general-purpose tool or typed with specific query kinds? A typed discriminated union prevents agents from inventing unsupported query shapes.
232
+ - SQLite native compilation may fail in some environments (Docker, Alpine). Mitigation: `@sqlite.org/sqlite-wasm` (pure WASM) or make MemoryStore fully optional -- daemon works without it, enricher falls back to the slow scan.
233
+ - `sessions_by_goal_keywords` without a full-text index is still O(n). Is keyword search needed in v1, or is recency-scoped `recent_sessions` sufficient to start?
234
+ - `phase_artifact_appended` schema change: new event kind vs reuse existing artifact channel with new content type. Different backward-compatibility implications -- needs engine team input before Phase 2b starts.
235
+ - **The ideal vs achievable tension:** ideally all context is assembled before the first turn and the agent never has to fetch more. Whether that's achievable depends on whether the relevant context is predictable from the trigger payload. For structured tasks (PR review, known issue) it usually is. For open-ended discovery or tasks with ambiguous scope, the needed context only becomes clear as the agent reads code -- you can't fully front-load it. One candidate: a context-gathering sub-agent spawned before the main session that reads the workspace and returns a structured context bundle to the coordinator, which then assembles it into the main session's pre-load. This has its own issues: it adds latency (a full extra session before the real work starts), risks gathering the wrong things (the sub-agent doesn't know what the main agent will need), and may just push the "what context do I need?" judgment to an earlier LLM call rather than eliminating it. Worth tracking as a design direction before deciding whether to invest in mid-session retrieval infrastructure at all.
254
236
 
255
237
  ---
256
238
 
@@ -273,6 +255,35 @@ Today, validating this requires manually reading raw session transcripts, which
273
255
 
274
256
  ---
275
257
 
258
+ ### Operator preference memory: WorkTrain learns and retains operator-specific preferences (Apr 30, 2026)
259
+
260
+ **Status: idea** | Priority: medium
261
+
262
+ **Score: 9** | Cor:1 Cap:2 Eff:2 Lev:2 Con:2 | Blocked: no
263
+
264
+ WorkTrain runs fully autonomously but has no persistent memory of operator preferences -- things like "always squash before merging", "don't open PRs without a linked issue", "prefer functional patterns in new files", or "this workspace uses tabs not spaces." Every session starts from the same generic `daemon-soul.md` baseline. Preferences discovered or stated in one session don't carry forward.
265
+
266
+ Claude Code solves this for human-in-the-loop sessions via its memory system (feedback, user, project entries written by the AI mid-conversation). WorkTrain needs an equivalent, but the mechanism is fundamentally different because: (a) there is no human watching the session to correct or confirm, and (b) opening up an interactive channel into an autonomous pipeline introduces risk that has to be carefully scoped.
267
+
268
+ Candidate input mechanisms (not mutually exclusive):
269
+
270
+ 1. **MR/PR review comments** -- when a human reviewer requests changes or comments on a WorkTrain PR, that signal is authoritative feedback. WorkTrain already monitors PRs post-review (see backlog entry on root cause analysis). Extracting preference-relevant comments ("always add a test for this pattern", "don't use this API directly") and persisting them is a natural extension.
271
+
272
+ 2. **`worktrain tell`** -- the existing CLI command queues a message to the daemon. Could be extended to a `worktrain remember "..."` variant that writes directly to a workspace-scoped preferences store, bypassing the session queue entirely.
273
+
274
+ 3. **Explicit preference file** -- a `~/.workrail/operator-preferences.md` (or per-workspace variant) that the operator edits directly, injected into every session alongside `daemon-soul.md`. Lower friction than building a learning mechanism; higher friction than automatic inference.
275
+
276
+ 4. **Inferred from repeated corrections** -- if WorkTrain makes the same kind of mistake N times across sessions (same type of review finding, same escalation reason), automatically surface a draft preference for operator approval before persisting.
277
+
278
+ **Things to hash out:**
279
+ - What is the storage format -- append-only structured log, a single evolving markdown file, or a SQLite table? The answer affects how preferences are queried and how conflicts between preferences are resolved.
280
+ - How does a persisted preference get *removed or updated*? Stale preferences can be worse than none -- "always use library X" becomes harmful when X is deprecated.
281
+ - What is the trust model for inferred preferences vs explicitly stated ones? A preference extracted from a PR comment should carry different weight than one inferred from repeated behavior.
282
+ - Does this interact with `daemon-soul.md`? Soul covers behavioral philosophy; preferences cover workspace/operator-specific constraints. They're different concerns but both end up in the system prompt -- precedence and load order matter.
283
+ - The fully-closed-pipeline concern is real: mechanisms 1 and 4 operate without human intervention during sessions, which is the correct design. Mechanism 2 requires the operator to pull a lever (acceptable). Mechanism 3 is fully manual (always safe). Any mechanism that *pauses a session mid-run to ask a question* would break the autonomous contract and should not be explored here.
284
+
285
+ ---
286
+
276
287
  ### Per-run retrospective: structured learning from pipeline outcomes (Apr 30, 2026)
277
288
 
278
289
  **Status: idea** | Priority: medium
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/workrail",
3
- "version": "3.77.1",
3
+ "version": "3.78.0",
4
4
  "description": "Step-by-step workflow enforcement for AI agents via MCP",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,8 +34,8 @@
34
34
  {
35
35
  "id": "step-break-claim",
36
36
  "title": "Step 2: Find the Strongest Counter-Argument",
37
- "prompt": "Find the strongest case against the current claim.\n\nChallenge it by asking:\n- What is the strongest counter-argument or competing explanation?\n- What evidence could be interpreted differently?\n- What hidden assumption is carrying too much weight?\n- What would a sharp skeptic say first?\n\nOptimize for the single strongest attack, not a long list of weak objections.",
38
- "agentRole": "You are a sharp skeptic trying to overturn the current favorite with the strongest available attack.",
37
+ "prompt": "Find the strongest case against the current claim.\n\nChallenge it by asking:\n- What is the strongest counter-argument or competing explanation?\n- What evidence could be interpreted differently?\n- What hidden assumption is carrying too much weight?\n- What would a sharp skeptic say first?\n- Construct the strongest possible case for a different answer: what would it look like if the current claim is wrong and a competing explanation is right?\n\nOptimize for the single strongest attack, not a long list of weak objections.",
38
+ "agentRole": "You are a sharp skeptic trying to overturn the current favorite with the strongest available attack -- including constructing the strongest case for an alternative answer, not just finding flaws in the current one.",
39
39
  "requireConfirmation": false
40
40
  },
41
41
  {