@exaudeus/workrail 3.41.0 → 3.42.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.
Files changed (39) hide show
  1. package/dist/cli-worktrain.js +40 -11
  2. package/dist/console-ui/assets/{index-CQt4UhPB.js → index-DwfWMKvv.js} +1 -1
  3. package/dist/console-ui/index.html +1 -1
  4. package/dist/context-assembly/deps.d.ts +8 -0
  5. package/dist/context-assembly/deps.js +2 -0
  6. package/dist/context-assembly/index.d.ts +6 -0
  7. package/dist/context-assembly/index.js +50 -0
  8. package/dist/context-assembly/infra.d.ts +3 -0
  9. package/dist/context-assembly/infra.js +154 -0
  10. package/dist/context-assembly/types.d.ts +30 -0
  11. package/dist/context-assembly/types.js +2 -0
  12. package/dist/coordinators/pr-review.d.ts +3 -1
  13. package/dist/coordinators/pr-review.js +25 -4
  14. package/dist/daemon/workflow-runner.js +11 -0
  15. package/dist/domain/execution/state.d.ts +6 -6
  16. package/dist/manifest.json +64 -32
  17. package/dist/mcp/handlers/v2-workflow.d.ts +2 -2
  18. package/dist/mcp/output-schemas.d.ts +234 -234
  19. package/dist/mcp/tools.d.ts +2 -2
  20. package/dist/mcp/v2/tools.d.ts +24 -24
  21. package/dist/v2/durable-core/schemas/artifacts/assessment.d.ts +2 -2
  22. package/dist/v2/durable-core/schemas/artifacts/coordinator-signal.d.ts +2 -2
  23. package/dist/v2/durable-core/schemas/artifacts/loop-control.d.ts +6 -6
  24. package/dist/v2/durable-core/schemas/artifacts/review-verdict.d.ts +6 -6
  25. package/dist/v2/durable-core/schemas/compiled-workflow/index.d.ts +56 -56
  26. package/dist/v2/durable-core/schemas/execution-snapshot/blocked-snapshot.d.ts +83 -83
  27. package/dist/v2/durable-core/schemas/execution-snapshot/execution-snapshot.v1.d.ts +1024 -1024
  28. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +2336 -2336
  29. package/dist/v2/durable-core/schemas/session/dag-topology.d.ts +6 -6
  30. package/dist/v2/durable-core/schemas/session/events.d.ts +339 -339
  31. package/dist/v2/durable-core/schemas/session/gaps.d.ts +30 -30
  32. package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
  33. package/dist/v2/durable-core/schemas/session/outputs.d.ts +8 -8
  34. package/dist/v2/durable-core/schemas/session/validation-event.d.ts +3 -3
  35. package/docs/design/context-assembly-design-candidates.md +199 -0
  36. package/docs/design/context-assembly-implementation-plan.md +211 -0
  37. package/docs/design/context-assembly-review-findings.md +112 -0
  38. package/docs/ideas/backlog.md +64 -0
  39. package/package.json +1 -1
@@ -4,38 +4,38 @@ export declare const GapReasonSchema: z.ZodDiscriminatedUnion<"category", [z.Zod
4
4
  category: z.ZodLiteral<"user_only_dependency">;
5
5
  detail: z.ZodEnum<["needs_user_secret_or_token", "needs_user_account_access", "needs_user_artifact", "needs_user_choice", "needs_user_approval", "needs_user_environment_action"]>;
6
6
  }, "strip", z.ZodTypeAny, {
7
- detail: "needs_user_secret_or_token" | "needs_user_account_access" | "needs_user_artifact" | "needs_user_choice" | "needs_user_approval" | "needs_user_environment_action";
8
7
  category: "user_only_dependency";
9
- }, {
10
8
  detail: "needs_user_secret_or_token" | "needs_user_account_access" | "needs_user_artifact" | "needs_user_choice" | "needs_user_approval" | "needs_user_environment_action";
9
+ }, {
11
10
  category: "user_only_dependency";
11
+ detail: "needs_user_secret_or_token" | "needs_user_account_access" | "needs_user_artifact" | "needs_user_choice" | "needs_user_approval" | "needs_user_environment_action";
12
12
  }>, z.ZodObject<{
13
13
  category: z.ZodLiteral<"contract_violation">;
14
14
  detail: z.ZodEnum<["missing_required_output", "invalid_required_output", "missing_required_notes", "assessment_followup_required"]>;
15
15
  }, "strip", z.ZodTypeAny, {
16
- detail: "invalid_required_output" | "missing_required_output" | "assessment_followup_required" | "missing_required_notes";
17
16
  category: "contract_violation";
17
+ detail: "missing_required_output" | "invalid_required_output" | "missing_required_notes" | "assessment_followup_required";
18
18
  }, {
19
- detail: "invalid_required_output" | "missing_required_output" | "assessment_followup_required" | "missing_required_notes";
20
19
  category: "contract_violation";
20
+ detail: "missing_required_output" | "invalid_required_output" | "missing_required_notes" | "assessment_followup_required";
21
21
  }>, z.ZodObject<{
22
22
  category: z.ZodLiteral<"capability_missing">;
23
23
  detail: z.ZodEnum<["required_capability_unavailable", "required_capability_unknown"]>;
24
24
  }, "strip", z.ZodTypeAny, {
25
- detail: "required_capability_unknown" | "required_capability_unavailable";
26
25
  category: "capability_missing";
26
+ detail: "required_capability_unavailable" | "required_capability_unknown";
27
27
  }, {
28
- detail: "required_capability_unknown" | "required_capability_unavailable";
29
28
  category: "capability_missing";
29
+ detail: "required_capability_unavailable" | "required_capability_unknown";
30
30
  }>, z.ZodObject<{
31
31
  category: z.ZodLiteral<"unexpected">;
32
32
  detail: z.ZodEnum<["invariant_violation", "storage_corruption_detected", "evaluation_error"]>;
33
33
  }, "strip", z.ZodTypeAny, {
34
- detail: "invariant_violation" | "storage_corruption_detected" | "evaluation_error";
35
34
  category: "unexpected";
36
- }, {
37
35
  detail: "invariant_violation" | "storage_corruption_detected" | "evaluation_error";
36
+ }, {
38
37
  category: "unexpected";
38
+ detail: "invariant_violation" | "storage_corruption_detected" | "evaluation_error";
39
39
  }>]>;
40
40
  export declare const GapResolutionSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
41
41
  kind: z.ZodLiteral<"unresolved">;
@@ -79,38 +79,38 @@ export declare const GapRecordedDataV1Schema: z.ZodObject<{
79
79
  category: z.ZodLiteral<"user_only_dependency">;
80
80
  detail: z.ZodEnum<["needs_user_secret_or_token", "needs_user_account_access", "needs_user_artifact", "needs_user_choice", "needs_user_approval", "needs_user_environment_action"]>;
81
81
  }, "strip", z.ZodTypeAny, {
82
- detail: "needs_user_secret_or_token" | "needs_user_account_access" | "needs_user_artifact" | "needs_user_choice" | "needs_user_approval" | "needs_user_environment_action";
83
82
  category: "user_only_dependency";
84
- }, {
85
83
  detail: "needs_user_secret_or_token" | "needs_user_account_access" | "needs_user_artifact" | "needs_user_choice" | "needs_user_approval" | "needs_user_environment_action";
84
+ }, {
86
85
  category: "user_only_dependency";
86
+ detail: "needs_user_secret_or_token" | "needs_user_account_access" | "needs_user_artifact" | "needs_user_choice" | "needs_user_approval" | "needs_user_environment_action";
87
87
  }>, z.ZodObject<{
88
88
  category: z.ZodLiteral<"contract_violation">;
89
89
  detail: z.ZodEnum<["missing_required_output", "invalid_required_output", "missing_required_notes", "assessment_followup_required"]>;
90
90
  }, "strip", z.ZodTypeAny, {
91
- detail: "invalid_required_output" | "missing_required_output" | "assessment_followup_required" | "missing_required_notes";
92
91
  category: "contract_violation";
92
+ detail: "missing_required_output" | "invalid_required_output" | "missing_required_notes" | "assessment_followup_required";
93
93
  }, {
94
- detail: "invalid_required_output" | "missing_required_output" | "assessment_followup_required" | "missing_required_notes";
95
94
  category: "contract_violation";
95
+ detail: "missing_required_output" | "invalid_required_output" | "missing_required_notes" | "assessment_followup_required";
96
96
  }>, z.ZodObject<{
97
97
  category: z.ZodLiteral<"capability_missing">;
98
98
  detail: z.ZodEnum<["required_capability_unavailable", "required_capability_unknown"]>;
99
99
  }, "strip", z.ZodTypeAny, {
100
- detail: "required_capability_unknown" | "required_capability_unavailable";
101
100
  category: "capability_missing";
101
+ detail: "required_capability_unavailable" | "required_capability_unknown";
102
102
  }, {
103
- detail: "required_capability_unknown" | "required_capability_unavailable";
104
103
  category: "capability_missing";
104
+ detail: "required_capability_unavailable" | "required_capability_unknown";
105
105
  }>, z.ZodObject<{
106
106
  category: z.ZodLiteral<"unexpected">;
107
107
  detail: z.ZodEnum<["invariant_violation", "storage_corruption_detected", "evaluation_error"]>;
108
108
  }, "strip", z.ZodTypeAny, {
109
- detail: "invariant_violation" | "storage_corruption_detected" | "evaluation_error";
110
109
  category: "unexpected";
111
- }, {
112
110
  detail: "invariant_violation" | "storage_corruption_detected" | "evaluation_error";
111
+ }, {
113
112
  category: "unexpected";
113
+ detail: "invariant_violation" | "storage_corruption_detected" | "evaluation_error";
114
114
  }>]>;
115
115
  summary: z.ZodString;
116
116
  resolution: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
@@ -149,28 +149,28 @@ export declare const GapRecordedDataV1Schema: z.ZodObject<{
149
149
  outputId: string;
150
150
  }>]>, "many">>;
151
151
  }, "strip", z.ZodTypeAny, {
152
- summary: string;
152
+ gapId: string;
153
+ severity: "info" | "warning" | "critical";
153
154
  reason: {
154
- detail: "needs_user_secret_or_token" | "needs_user_account_access" | "needs_user_artifact" | "needs_user_choice" | "needs_user_approval" | "needs_user_environment_action";
155
155
  category: "user_only_dependency";
156
+ detail: "needs_user_secret_or_token" | "needs_user_account_access" | "needs_user_artifact" | "needs_user_choice" | "needs_user_approval" | "needs_user_environment_action";
156
157
  } | {
157
- detail: "invalid_required_output" | "missing_required_output" | "assessment_followup_required" | "missing_required_notes";
158
158
  category: "contract_violation";
159
+ detail: "missing_required_output" | "invalid_required_output" | "missing_required_notes" | "assessment_followup_required";
159
160
  } | {
160
- detail: "required_capability_unknown" | "required_capability_unavailable";
161
161
  category: "capability_missing";
162
+ detail: "required_capability_unavailable" | "required_capability_unknown";
162
163
  } | {
163
- detail: "invariant_violation" | "storage_corruption_detected" | "evaluation_error";
164
164
  category: "unexpected";
165
+ detail: "invariant_violation" | "storage_corruption_detected" | "evaluation_error";
165
166
  };
166
- severity: "critical" | "info" | "warning";
167
+ summary: string;
167
168
  resolution: {
168
169
  kind: "unresolved";
169
170
  } | {
170
171
  kind: "resolves";
171
172
  resolvesGapId: string;
172
173
  };
173
- gapId: string;
174
174
  evidenceRefs?: ({
175
175
  kind: "event";
176
176
  eventId: string;
@@ -179,28 +179,28 @@ export declare const GapRecordedDataV1Schema: z.ZodObject<{
179
179
  outputId: string;
180
180
  })[] | undefined;
181
181
  }, {
182
- summary: string;
182
+ gapId: string;
183
+ severity: "info" | "warning" | "critical";
183
184
  reason: {
184
- detail: "needs_user_secret_or_token" | "needs_user_account_access" | "needs_user_artifact" | "needs_user_choice" | "needs_user_approval" | "needs_user_environment_action";
185
185
  category: "user_only_dependency";
186
+ detail: "needs_user_secret_or_token" | "needs_user_account_access" | "needs_user_artifact" | "needs_user_choice" | "needs_user_approval" | "needs_user_environment_action";
186
187
  } | {
187
- detail: "invalid_required_output" | "missing_required_output" | "assessment_followup_required" | "missing_required_notes";
188
188
  category: "contract_violation";
189
+ detail: "missing_required_output" | "invalid_required_output" | "missing_required_notes" | "assessment_followup_required";
189
190
  } | {
190
- detail: "required_capability_unknown" | "required_capability_unavailable";
191
191
  category: "capability_missing";
192
+ detail: "required_capability_unavailable" | "required_capability_unknown";
192
193
  } | {
193
- detail: "invariant_violation" | "storage_corruption_detected" | "evaluation_error";
194
194
  category: "unexpected";
195
+ detail: "invariant_violation" | "storage_corruption_detected" | "evaluation_error";
195
196
  };
196
- severity: "critical" | "info" | "warning";
197
+ summary: string;
197
198
  resolution: {
198
199
  kind: "unresolved";
199
200
  } | {
200
201
  kind: "resolves";
201
202
  resolvesGapId: string;
202
203
  };
203
- gapId: string;
204
204
  evidenceRefs?: ({
205
205
  kind: "event";
206
206
  eventId: string;
@@ -11,23 +11,23 @@ export declare const ManifestRecordV1Schema: z.ZodDiscriminatedUnion<"kind", [z.
11
11
  bytes: z.ZodNumber;
12
12
  }, "strip", z.ZodTypeAny, {
13
13
  kind: "segment_closed";
14
- sessionId: string;
15
14
  v: 1;
16
- sha256: string;
17
15
  manifestIndex: number;
16
+ sessionId: string;
18
17
  firstEventIndex: number;
19
18
  lastEventIndex: number;
20
19
  segmentRelPath: string;
20
+ sha256: string;
21
21
  bytes: number;
22
22
  }, {
23
23
  kind: "segment_closed";
24
- sessionId: string;
25
24
  v: 1;
26
- sha256: string;
27
25
  manifestIndex: number;
26
+ sessionId: string;
28
27
  firstEventIndex: number;
29
28
  lastEventIndex: number;
30
29
  segmentRelPath: string;
30
+ sha256: string;
31
31
  bytes: number;
32
32
  }>, z.ZodObject<{
33
33
  v: z.ZodLiteral<1>;
@@ -39,17 +39,17 @@ export declare const ManifestRecordV1Schema: z.ZodDiscriminatedUnion<"kind", [z.
39
39
  createdByEventId: z.ZodString;
40
40
  }, "strip", z.ZodTypeAny, {
41
41
  kind: "snapshot_pinned";
42
- sessionId: string;
43
42
  v: 1;
44
43
  manifestIndex: number;
44
+ sessionId: string;
45
45
  eventIndex: number;
46
46
  snapshotRef: string;
47
47
  createdByEventId: string;
48
48
  }, {
49
49
  kind: "snapshot_pinned";
50
- sessionId: string;
51
50
  v: 1;
52
51
  manifestIndex: number;
52
+ sessionId: string;
53
53
  eventIndex: number;
54
54
  snapshotRef: string;
55
55
  createdByEventId: string;
@@ -90,6 +90,8 @@ export declare const NodeOutputAppendedDataV1Schema: z.ZodEffects<z.ZodObject<{
90
90
  content?: unknown;
91
91
  }>]>;
92
92
  }, "strip", z.ZodTypeAny, {
93
+ outputId: string;
94
+ outputChannel: "recap" | "artifact";
93
95
  payload: {
94
96
  payloadKind: "notes";
95
97
  notesMarkdown: string;
@@ -100,10 +102,10 @@ export declare const NodeOutputAppendedDataV1Schema: z.ZodEffects<z.ZodObject<{
100
102
  byteLength: number;
101
103
  content?: unknown;
102
104
  };
103
- outputId: string;
104
- outputChannel: "recap" | "artifact";
105
105
  supersedesOutputId?: string | undefined;
106
106
  }, {
107
+ outputId: string;
108
+ outputChannel: "recap" | "artifact";
107
109
  payload: {
108
110
  payloadKind: "notes";
109
111
  notesMarkdown: string;
@@ -114,10 +116,10 @@ export declare const NodeOutputAppendedDataV1Schema: z.ZodEffects<z.ZodObject<{
114
116
  byteLength: number;
115
117
  content?: unknown;
116
118
  };
117
- outputId: string;
118
- outputChannel: "recap" | "artifact";
119
119
  supersedesOutputId?: string | undefined;
120
120
  }>, {
121
+ outputId: string;
122
+ outputChannel: "recap" | "artifact";
121
123
  payload: {
122
124
  payloadKind: "notes";
123
125
  notesMarkdown: string;
@@ -128,10 +130,10 @@ export declare const NodeOutputAppendedDataV1Schema: z.ZodEffects<z.ZodObject<{
128
130
  byteLength: number;
129
131
  content?: unknown;
130
132
  };
131
- outputId: string;
132
- outputChannel: "recap" | "artifact";
133
133
  supersedesOutputId?: string | undefined;
134
134
  }, {
135
+ outputId: string;
136
+ outputChannel: "recap" | "artifact";
135
137
  payload: {
136
138
  payloadKind: "notes";
137
139
  notesMarkdown: string;
@@ -142,7 +144,5 @@ export declare const NodeOutputAppendedDataV1Schema: z.ZodEffects<z.ZodObject<{
142
144
  byteLength: number;
143
145
  content?: unknown;
144
146
  };
145
- outputId: string;
146
- outputChannel: "recap" | "artifact";
147
147
  supersedesOutputId?: string | undefined;
148
148
  }>;
@@ -46,23 +46,23 @@ export declare const ValidationPerformedDataV1Schema: z.ZodObject<{
46
46
  suggestions: readonly string[];
47
47
  }>;
48
48
  }, "strict", z.ZodTypeAny, {
49
+ validationId: string;
50
+ attemptId: string;
49
51
  contractRef: string;
50
52
  result: {
51
53
  issues: readonly string[];
52
54
  valid: boolean;
53
55
  suggestions: readonly string[];
54
56
  };
57
+ }, {
55
58
  validationId: string;
56
59
  attemptId: string;
57
- }, {
58
60
  contractRef: string;
59
61
  result: {
60
62
  issues: readonly string[];
61
63
  valid: boolean;
62
64
  suggestions: readonly string[];
63
65
  };
64
- validationId: string;
65
- attemptId: string;
66
66
  }>;
67
67
  export type ValidationPerformedDataV1 = z.infer<typeof ValidationPerformedDataV1Schema>;
68
68
  export type ValidationPerformedResultV1 = z.infer<typeof ValidationPerformedResultV1Schema>;
@@ -0,0 +1,199 @@
1
+ # Design Candidates: Context Assembly Layer v1
2
+
3
+ *Generated during coding-task-workflow-agentic session | 2026-04-19*
4
+
5
+ ---
6
+
7
+ ## Problem Understanding
8
+
9
+ ### Core Tensions
10
+
11
+ 1. **Clean DI vs. pragmatic inline construction**: `LocalSessionSummaryProviderV2` is
12
+ wired through the full DI container in the MCP server process. The coordinator runs
13
+ as a CLI in a separate process. We cannot share the DI container. The coordinator
14
+ composition root (`src/cli-worktrain.ts`) must construct the provider inline.
15
+
16
+ 2. **Neverthrow ResultAsync vs. custom Result**: Storage layer uses neverthrow. New
17
+ `src/context-assembly/` uses the repo's custom `Result<T,E>` (`kind: 'ok'|'err'`).
18
+ The bridge must happen at the `infra.ts` boundary.
19
+
20
+ 3. **Partial failure semantics**: Each source (gitDiff, priorNotes) must fail
21
+ independently without aborting context assembly. `Promise.all()` is safe because
22
+ each source always resolves to a `Result` (never throws) when implemented correctly.
23
+
24
+ 4. **Backward compatibility vs. interface evolution**: Adding `contextAssembler?` as
25
+ optional to `CoordinatorDeps` and optional 4th arg to `spawnSession` keeps all
26
+ existing tests and callers working without modification.
27
+
28
+ ### Likely Seam
29
+
30
+ The real seam is `CoordinatorDeps.spawnSession`. The coordinator calls this to dispatch
31
+ a session. Context bundle assembly belongs immediately before this call.
32
+
33
+ ### What Makes This Hard
34
+
35
+ - `LocalSessionEventLogStoreV2` requires `FileSystemPortV2` (a composite of 5 sub-ports
36
+ including write/fd ops). But `load()` only calls `readFileUtf8`. All write methods
37
+ can be stubs for read-only use, but this is a partial lie against the interface contract.
38
+ - `LocalDirectoryListingV2` takes `DirectoryListingOpsPortV2` (separate, simpler) --
39
+ NOT the full `FileSystemPortV2`. These are separate ports.
40
+ - Two Result type systems (neverthrow vs custom) must coexist without leaking across
41
+ the `infra.ts` boundary.
42
+ - `Sha256PortV2` is injected into `LocalSessionEventLogStoreV2` but only called in
43
+ `append()`, not `load()`. Safe to stub.
44
+
45
+ ---
46
+
47
+ ## Philosophy Constraints
48
+
49
+ From `CLAUDE.md` (system-wide agent instructions):
50
+
51
+ - **Errors are data**: Use `Result<T,E>` values. No throwing for expected failures.
52
+ - **DI for boundaries**: Inject I/O effects; keep core logic testable.
53
+ - **Prefer fakes over mocks**: Tests use realistic substitutes (no `vi.mock()`).
54
+ - **YAGNI with discipline**: No speculative abstractions.
55
+ - **Architectural fixes over patches**: Reuse canonical parsing logic.
56
+ - **Immutability by default**: `readonly` on all fields.
57
+
58
+ **Potential conflict**: Creating write stubs on `FileSystemPortV2` is pragmatic but
59
+ technically lies about the contract. The pitch (R1 mitigation) explicitly endorses this
60
+ approach. Acceptable given the inline-construction pattern is already acknowledged as
61
+ a special case.
62
+
63
+ ---
64
+
65
+ ## Impact Surface
66
+
67
+ - `src/coordinators/pr-review.ts`: `CoordinatorDeps.spawnSession` (add optional 4th arg),
68
+ `contextAssembler?` optional field. Backward-compatible.
69
+ - `src/daemon/workflow-runner.ts`: `buildSystemPrompt()` reads `trigger.context['assembledContextSummary']`.
70
+ No signature change needed -- `trigger.context` already typed as `Readonly<Record<string,unknown>>`.
71
+ - `src/cli-worktrain.ts`: `spawnSession` impl forwards 4th arg; `contextAssembler` wired.
72
+ - All existing tests that construct `CoordinatorDeps` fakes: safe (optional fields).
73
+ - No changes to `src/mcp/` or `src/v2/durable-core/`.
74
+
75
+ ---
76
+
77
+ ## Candidates
78
+
79
+ ### Candidate A: Real local infra classes with minimal read-only shims (SELECTED)
80
+
81
+ **Summary**: Construct `LocalDataDirV2`, `LocalDirectoryListingV2`, and
82
+ `LocalSessionEventLogStoreV2` directly inside `infra.ts`. Create inline adapters:
83
+ - `DirectoryListingOpsPortV2`: wraps `fs.promises.readdir` + `stat` with `okAsync/errAsync`
84
+ - `FileSystemPortV2`: only `readFileUtf8` is real; write/fd methods throw 'context-assembly: write ops not supported'
85
+ - `Sha256PortV2`: stub that throws (never called during `load()`)
86
+
87
+ **Tensions resolved**: R1 (DI wiring scope). Uses battle-tested session loading and
88
+ projection code. Neverthrow/custom Result bridge stays at `infra.ts` boundary only.
89
+
90
+ **Tensions accepted**: Write stubs on `FileSystemPortV2` are a partial lie.
91
+
92
+ **Boundary solved at**: `src/context-assembly/infra.ts` -- coordinator composition root.
93
+
94
+ **Why this boundary is best-fit**: The coordinator is already a composition root
95
+ (`src/cli-worktrain.ts` wires all real deps). `infra.ts` is a thin adapter layer that
96
+ belongs exactly here.
97
+
98
+ **Failure mode**: If `LocalSessionEventLogStoreV2.load()` ever internally calls a write
99
+ method (unlikely but possible in future refactors), `infra.ts` would throw at runtime
100
+ rather than returning `err()`. This would be caught immediately by tests.
101
+
102
+ **Repo-pattern relationship**: Follows existing local infra pattern exactly.
103
+ `LocalDataDirV2`, `LocalDirectoryListingV2`, `LocalSessionEventLogStoreV2` are already
104
+ the canonical local implementations.
105
+
106
+ **Gains**: Reuses battle-tested store loading and projection code. No risk of format
107
+ divergence if session file structure changes.
108
+
109
+ **Losses**: Write stubs feel slightly dishonest. Minor complexity in `infra.ts`.
110
+
111
+ **Scope judgment**: Best-fit. Stays within `src/context-assembly/` with no v2 changes.
112
+
113
+ **Philosophy fit**: Honors DI for boundaries (composition root pattern), errors-as-data
114
+ (Result bridge), architectural fixes over patches (reuses canonical code). Minor
115
+ tension with YAGNI (write stubs) and interface honesty.
116
+
117
+ ---
118
+
119
+ ### Candidate B: Read session files directly with fs.promises
120
+
121
+ **Summary**: Skip the port hierarchy. Read `manifest.jsonl` and `events/*.jsonl` directly
122
+ using `fs.promises.readFile`. Parse manually or reuse just the schema validators.
123
+
124
+ **Tensions resolved**: R1 completely -- no DI wiring at all.
125
+
126
+ **Tensions accepted**: Duplicates session reading logic. Any change to session file
127
+ format requires updates in two places.
128
+
129
+ **Failure mode**: Silent breakage if session file format changes.
130
+
131
+ **Repo-pattern relationship**: Departs from established pattern (re-implements existing logic).
132
+
133
+ **Scope judgment**: Too narrow -- creates a fragile parallel implementation.
134
+
135
+ **Philosophy fit**: Conflicts with 'architectural fixes over patches'.
136
+
137
+ ---
138
+
139
+ ### Candidate C: Add createLocalSessionSummaryProvider() factory to v2 module
140
+
141
+ **Summary**: Add a `createLocalSessionSummaryProvider()` factory function to
142
+ `src/v2/infra/local/session-summary-provider/index.ts` that wires real deps and
143
+ returns a `SessionSummaryProviderPortV2`. Import and call from `infra.ts`.
144
+
145
+ **Tensions resolved**: Cleanest DI wiring.
146
+
147
+ **Tensions accepted**: Modifies an existing v2 infra module.
148
+
149
+ **Failure mode**: Entangles CLI-coordinator concerns into the storage layer.
150
+
151
+ **Scope judgment**: Slightly too broad -- modifies v2 module with no functional benefit
152
+ over Candidate A.
153
+
154
+ ---
155
+
156
+ ## Comparison and Recommendation
157
+
158
+ **Recommendation: Candidate A**
159
+
160
+ All candidates agree on the high-level approach (4 new files, 3 modified files, exact
161
+ pitch interfaces). The only real design decision is `infra.ts` implementation.
162
+
163
+ Candidate A wins because:
164
+ 1. It reuses battle-tested code for session reading and health projection
165
+ 2. It stays within `src/context-assembly/` -- no v2 module changes
166
+ 3. The write stubs are safe: `load()` provably does not call write methods
167
+ 4. The pitch explicitly endorses this approach as R1 mitigation
168
+
169
+ ---
170
+
171
+ ## Self-Critique
172
+
173
+ **Strongest counter-argument**: The write stubs on `FileSystemPortV2` create a silent
174
+ contract violation. If a future `load()` implementation adds a write call internally,
175
+ the error would surface at runtime (throw) rather than compile-time.
176
+
177
+ **Narrower option that lost**: Candidate B (raw fs.promises). Lost because it duplicates
178
+ canonical parsing logic and diverges from existing patterns.
179
+
180
+ **Broader option and evidence required**: Candidate C would be justified if multiple
181
+ other CLI commands needed access to session summaries -- adding a factory to the v2
182
+ module would be worth the scope increase. No evidence of this need in v1.
183
+
184
+ **Assumption that would invalidate this design**: If `LocalSessionEventLogStoreV2.load()`
185
+ internally calls any of the stubbed write methods (currently provably false by code
186
+ inspection of `loadImpl()`, `loadValidatedPrefixImpl()`, `readManifestOrEmpty()`).
187
+
188
+ ---
189
+
190
+ ## Open Questions for Main Agent
191
+
192
+ 1. Verify: does `Sha256PortV2` have a simple interface that can be satisfied with a
193
+ 1-line `node:crypto` implementation? (If so, use real impl rather than throw stub.)
194
+ 2. Verify: does the `FileSystemPortV2` shim need anything beyond `readFileUtf8` for
195
+ the `load()` path? (Current analysis: no -- `readManifestOrEmpty` and segment loading
196
+ both use `readFileUtf8` only.)
197
+ 3. The re-review spawn (line ~1265 in pr-review.ts) should forward `spawnContext` from
198
+ the outer scope per the pitch. Confirm this is the correct approach (vs. reassembling
199
+ context for re-reviews).