@exaudeus/workrail 3.73.1 → 3.74.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 (41) hide show
  1. package/dist/cli-worktrain.js +126 -1
  2. package/dist/console-ui/assets/{index-txIYXGHx.js → index-CfU3va8H.js} +1 -1
  3. package/dist/console-ui/index.html +1 -1
  4. package/dist/coordinators/pr-review.d.ts +11 -1
  5. package/dist/coordinators/types.d.ts +15 -0
  6. package/dist/coordinators/types.js +2 -0
  7. package/dist/manifest.json +81 -57
  8. package/dist/mcp/handlers/v2-advance-core/index.d.ts +1 -0
  9. package/dist/mcp/handlers/v2-advance-core/index.js +3 -3
  10. package/dist/mcp/handlers/v2-advance-core/outcome-success.js +4 -18
  11. package/dist/mcp/handlers/v2-advance-events.d.ts +1 -1
  12. package/dist/mcp/handlers/v2-advance-events.js +1 -1
  13. package/dist/mcp/handlers/v2-execution/advance.d.ts +1 -0
  14. package/dist/mcp/handlers/v2-execution/advance.js +3 -3
  15. package/dist/mcp/handlers/v2-execution/continue-advance.d.ts +1 -0
  16. package/dist/mcp/handlers/v2-execution/continue-advance.js +2 -1
  17. package/dist/mcp/handlers/v2-execution/index.js +3 -1
  18. package/dist/mcp/server.js +6 -4
  19. package/dist/mcp/types.d.ts +2 -0
  20. package/dist/trigger/coordinator-deps.js +203 -36
  21. package/dist/trigger/delivery-action.d.ts +1 -0
  22. package/dist/trigger/delivery-action.js +1 -1
  23. package/dist/trigger/delivery-pipeline.d.ts +13 -2
  24. package/dist/trigger/delivery-pipeline.js +58 -3
  25. package/dist/trigger/trigger-router.js +6 -3
  26. package/dist/v2/durable-core/constants.d.ts +1 -0
  27. package/dist/v2/durable-core/constants.js +1 -0
  28. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +202 -0
  29. package/dist/v2/durable-core/schemas/session/events.d.ts +56 -0
  30. package/dist/v2/durable-core/schemas/session/events.js +8 -0
  31. package/dist/v2/infra/local/git-snapshot/index.d.ts +6 -0
  32. package/dist/v2/infra/local/git-snapshot/index.js +39 -0
  33. package/dist/v2/ports/git-snapshot.port.d.ts +10 -0
  34. package/dist/v2/ports/git-snapshot.port.js +9 -0
  35. package/dist/v2/projections/session-metrics.js +17 -2
  36. package/docs/authoring.md +23 -0
  37. package/docs/design/engine-boundary-discovery.md +123 -0
  38. package/docs/design/engine-boundary-review-findings.md +72 -0
  39. package/docs/ideas/backlog.md +129 -48
  40. package/package.json +1 -1
  41. package/spec/authoring-spec.json +36 -1
@@ -2567,6 +2567,62 @@ export declare const SessionContentsV1Schema: z.ZodObject<{
2567
2567
  durationMs?: number | undefined;
2568
2568
  };
2569
2569
  timestampMs: number;
2570
+ }>, z.ZodObject<{
2571
+ v: z.ZodLiteral<1>;
2572
+ eventId: z.ZodString;
2573
+ eventIndex: z.ZodNumber;
2574
+ sessionId: z.ZodString;
2575
+ dedupeKey: z.ZodString;
2576
+ timestampMs: z.ZodNumber;
2577
+ } & {
2578
+ kind: z.ZodLiteral<"delivery_recorded">;
2579
+ scope: z.ZodObject<{
2580
+ runId: z.ZodString;
2581
+ }, "strip", z.ZodTypeAny, {
2582
+ runId: string;
2583
+ }, {
2584
+ runId: string;
2585
+ }>;
2586
+ data: z.ZodObject<{
2587
+ shas: z.ZodArray<z.ZodString, "many">;
2588
+ prUrl: z.ZodOptional<z.ZodString>;
2589
+ }, "strip", z.ZodTypeAny, {
2590
+ shas: string[];
2591
+ prUrl?: string | undefined;
2592
+ }, {
2593
+ shas: string[];
2594
+ prUrl?: string | undefined;
2595
+ }>;
2596
+ }, "strip", z.ZodTypeAny, {
2597
+ kind: "delivery_recorded";
2598
+ v: 1;
2599
+ sessionId: string;
2600
+ eventIndex: number;
2601
+ eventId: string;
2602
+ dedupeKey: string;
2603
+ scope: {
2604
+ runId: string;
2605
+ };
2606
+ data: {
2607
+ shas: string[];
2608
+ prUrl?: string | undefined;
2609
+ };
2610
+ timestampMs: number;
2611
+ }, {
2612
+ kind: "delivery_recorded";
2613
+ v: 1;
2614
+ sessionId: string;
2615
+ eventIndex: number;
2616
+ eventId: string;
2617
+ dedupeKey: string;
2618
+ scope: {
2619
+ runId: string;
2620
+ };
2621
+ data: {
2622
+ shas: string[];
2623
+ prUrl?: string | undefined;
2624
+ };
2625
+ timestampMs: number;
2570
2626
  }>]>, "many">;
2571
2627
  manifest: z.ZodArray<z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
2572
2628
  v: z.ZodLiteral<1>;
@@ -5311,6 +5367,21 @@ export declare const SessionContentsV1Schema: z.ZodObject<{
5311
5367
  durationMs?: number | undefined;
5312
5368
  };
5313
5369
  timestampMs: number;
5370
+ } | {
5371
+ kind: "delivery_recorded";
5372
+ v: 1;
5373
+ sessionId: string;
5374
+ eventIndex: number;
5375
+ eventId: string;
5376
+ dedupeKey: string;
5377
+ scope: {
5378
+ runId: string;
5379
+ };
5380
+ data: {
5381
+ shas: string[];
5382
+ prUrl?: string | undefined;
5383
+ };
5384
+ timestampMs: number;
5314
5385
  })[];
5315
5386
  manifest: ({
5316
5387
  kind: "segment_closed";
@@ -5925,6 +5996,21 @@ export declare const SessionContentsV1Schema: z.ZodObject<{
5925
5996
  durationMs?: number | undefined;
5926
5997
  };
5927
5998
  timestampMs: number;
5999
+ } | {
6000
+ kind: "delivery_recorded";
6001
+ v: 1;
6002
+ sessionId: string;
6003
+ eventIndex: number;
6004
+ eventId: string;
6005
+ dedupeKey: string;
6006
+ scope: {
6007
+ runId: string;
6008
+ };
6009
+ data: {
6010
+ shas: string[];
6011
+ prUrl?: string | undefined;
6012
+ };
6013
+ timestampMs: number;
5928
6014
  })[];
5929
6015
  manifest: ({
5930
6016
  kind: "segment_closed";
@@ -8504,6 +8590,62 @@ export declare const ExportBundleV1Schema: z.ZodObject<{
8504
8590
  durationMs?: number | undefined;
8505
8591
  };
8506
8592
  timestampMs: number;
8593
+ }>, z.ZodObject<{
8594
+ v: z.ZodLiteral<1>;
8595
+ eventId: z.ZodString;
8596
+ eventIndex: z.ZodNumber;
8597
+ sessionId: z.ZodString;
8598
+ dedupeKey: z.ZodString;
8599
+ timestampMs: z.ZodNumber;
8600
+ } & {
8601
+ kind: z.ZodLiteral<"delivery_recorded">;
8602
+ scope: z.ZodObject<{
8603
+ runId: z.ZodString;
8604
+ }, "strip", z.ZodTypeAny, {
8605
+ runId: string;
8606
+ }, {
8607
+ runId: string;
8608
+ }>;
8609
+ data: z.ZodObject<{
8610
+ shas: z.ZodArray<z.ZodString, "many">;
8611
+ prUrl: z.ZodOptional<z.ZodString>;
8612
+ }, "strip", z.ZodTypeAny, {
8613
+ shas: string[];
8614
+ prUrl?: string | undefined;
8615
+ }, {
8616
+ shas: string[];
8617
+ prUrl?: string | undefined;
8618
+ }>;
8619
+ }, "strip", z.ZodTypeAny, {
8620
+ kind: "delivery_recorded";
8621
+ v: 1;
8622
+ sessionId: string;
8623
+ eventIndex: number;
8624
+ eventId: string;
8625
+ dedupeKey: string;
8626
+ scope: {
8627
+ runId: string;
8628
+ };
8629
+ data: {
8630
+ shas: string[];
8631
+ prUrl?: string | undefined;
8632
+ };
8633
+ timestampMs: number;
8634
+ }, {
8635
+ kind: "delivery_recorded";
8636
+ v: 1;
8637
+ sessionId: string;
8638
+ eventIndex: number;
8639
+ eventId: string;
8640
+ dedupeKey: string;
8641
+ scope: {
8642
+ runId: string;
8643
+ };
8644
+ data: {
8645
+ shas: string[];
8646
+ prUrl?: string | undefined;
8647
+ };
8648
+ timestampMs: number;
8507
8649
  }>]>, "many">;
8508
8650
  manifest: z.ZodArray<z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
8509
8651
  v: z.ZodLiteral<1>;
@@ -11248,6 +11390,21 @@ export declare const ExportBundleV1Schema: z.ZodObject<{
11248
11390
  durationMs?: number | undefined;
11249
11391
  };
11250
11392
  timestampMs: number;
11393
+ } | {
11394
+ kind: "delivery_recorded";
11395
+ v: 1;
11396
+ sessionId: string;
11397
+ eventIndex: number;
11398
+ eventId: string;
11399
+ dedupeKey: string;
11400
+ scope: {
11401
+ runId: string;
11402
+ };
11403
+ data: {
11404
+ shas: string[];
11405
+ prUrl?: string | undefined;
11406
+ };
11407
+ timestampMs: number;
11251
11408
  })[];
11252
11409
  manifest: ({
11253
11410
  kind: "segment_closed";
@@ -11862,6 +12019,21 @@ export declare const ExportBundleV1Schema: z.ZodObject<{
11862
12019
  durationMs?: number | undefined;
11863
12020
  };
11864
12021
  timestampMs: number;
12022
+ } | {
12023
+ kind: "delivery_recorded";
12024
+ v: 1;
12025
+ sessionId: string;
12026
+ eventIndex: number;
12027
+ eventId: string;
12028
+ dedupeKey: string;
12029
+ scope: {
12030
+ runId: string;
12031
+ };
12032
+ data: {
12033
+ shas: string[];
12034
+ prUrl?: string | undefined;
12035
+ };
12036
+ timestampMs: number;
11865
12037
  })[];
11866
12038
  manifest: ({
11867
12039
  kind: "segment_closed";
@@ -12490,6 +12662,21 @@ export declare const ExportBundleV1Schema: z.ZodObject<{
12490
12662
  durationMs?: number | undefined;
12491
12663
  };
12492
12664
  timestampMs: number;
12665
+ } | {
12666
+ kind: "delivery_recorded";
12667
+ v: 1;
12668
+ sessionId: string;
12669
+ eventIndex: number;
12670
+ eventId: string;
12671
+ dedupeKey: string;
12672
+ scope: {
12673
+ runId: string;
12674
+ };
12675
+ data: {
12676
+ shas: string[];
12677
+ prUrl?: string | undefined;
12678
+ };
12679
+ timestampMs: number;
12493
12680
  })[];
12494
12681
  manifest: ({
12495
12682
  kind: "segment_closed";
@@ -13121,6 +13308,21 @@ export declare const ExportBundleV1Schema: z.ZodObject<{
13121
13308
  durationMs?: number | undefined;
13122
13309
  };
13123
13310
  timestampMs: number;
13311
+ } | {
13312
+ kind: "delivery_recorded";
13313
+ v: 1;
13314
+ sessionId: string;
13315
+ eventIndex: number;
13316
+ eventId: string;
13317
+ dedupeKey: string;
13318
+ scope: {
13319
+ runId: string;
13320
+ };
13321
+ data: {
13322
+ shas: string[];
13323
+ prUrl?: string | undefined;
13324
+ };
13325
+ timestampMs: number;
13124
13326
  })[];
13125
13327
  manifest: ({
13126
13328
  kind: "segment_closed";
@@ -2555,5 +2555,61 @@ export declare const DomainEventV1Schema: z.ZodDiscriminatedUnion<"kind", [z.Zod
2555
2555
  durationMs?: number | undefined;
2556
2556
  };
2557
2557
  timestampMs: number;
2558
+ }>, z.ZodObject<{
2559
+ v: z.ZodLiteral<1>;
2560
+ eventId: z.ZodString;
2561
+ eventIndex: z.ZodNumber;
2562
+ sessionId: z.ZodString;
2563
+ dedupeKey: z.ZodString;
2564
+ timestampMs: z.ZodNumber;
2565
+ } & {
2566
+ kind: z.ZodLiteral<"delivery_recorded">;
2567
+ scope: z.ZodObject<{
2568
+ runId: z.ZodString;
2569
+ }, "strip", z.ZodTypeAny, {
2570
+ runId: string;
2571
+ }, {
2572
+ runId: string;
2573
+ }>;
2574
+ data: z.ZodObject<{
2575
+ shas: z.ZodArray<z.ZodString, "many">;
2576
+ prUrl: z.ZodOptional<z.ZodString>;
2577
+ }, "strip", z.ZodTypeAny, {
2578
+ shas: string[];
2579
+ prUrl?: string | undefined;
2580
+ }, {
2581
+ shas: string[];
2582
+ prUrl?: string | undefined;
2583
+ }>;
2584
+ }, "strip", z.ZodTypeAny, {
2585
+ kind: "delivery_recorded";
2586
+ v: 1;
2587
+ sessionId: string;
2588
+ eventIndex: number;
2589
+ eventId: string;
2590
+ dedupeKey: string;
2591
+ scope: {
2592
+ runId: string;
2593
+ };
2594
+ data: {
2595
+ shas: string[];
2596
+ prUrl?: string | undefined;
2597
+ };
2598
+ timestampMs: number;
2599
+ }, {
2600
+ kind: "delivery_recorded";
2601
+ v: 1;
2602
+ sessionId: string;
2603
+ eventIndex: number;
2604
+ eventId: string;
2605
+ dedupeKey: string;
2606
+ scope: {
2607
+ runId: string;
2608
+ };
2609
+ data: {
2610
+ shas: string[];
2611
+ prUrl?: string | undefined;
2612
+ };
2613
+ timestampMs: number;
2558
2614
  }>]>;
2559
2615
  export type DomainEventV1 = z.infer<typeof DomainEventV1Schema>;
@@ -274,4 +274,12 @@ exports.DomainEventV1Schema = zod_1.z.discriminatedUnion('kind', [
274
274
  }
275
275
  }),
276
276
  }),
277
+ exports.DomainEventEnvelopeV1Schema.extend({
278
+ kind: zod_1.z.literal('delivery_recorded'),
279
+ scope: zod_1.z.object({ runId: zod_1.z.string().min(1) }),
280
+ data: zod_1.z.object({
281
+ shas: zod_1.z.array(zod_1.z.string()),
282
+ prUrl: zod_1.z.string().optional(),
283
+ }),
284
+ }),
277
285
  ]);
@@ -0,0 +1,6 @@
1
+ import type { GitEndSnapshot, GitSnapshotPortV2 } from '../../../ports/git-snapshot.port.js';
2
+ export declare class LocalGitSnapshotV2 implements GitSnapshotPortV2 {
3
+ resolveEndSnapshot(repoRoot: string | null, startSha: string | null): Promise<GitEndSnapshot>;
4
+ private resolveEndSha;
5
+ private resolveCommitRange;
6
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalGitSnapshotV2 = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_util_1 = require("node:util");
6
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
7
+ const GIT_TIMEOUT_MS = 2000;
8
+ class LocalGitSnapshotV2 {
9
+ async resolveEndSnapshot(repoRoot, startSha) {
10
+ if (!repoRoot)
11
+ return { endSha: null, commitShas: [] };
12
+ const [endSha, commitShas] = await Promise.all([
13
+ this.resolveEndSha(repoRoot),
14
+ this.resolveCommitRange(repoRoot, startSha),
15
+ ]);
16
+ return { endSha, commitShas };
17
+ }
18
+ async resolveEndSha(repoRoot) {
19
+ try {
20
+ const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, timeout: GIT_TIMEOUT_MS });
21
+ return stdout.trim() || null;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ async resolveCommitRange(repoRoot, startSha) {
28
+ if (!startSha)
29
+ return [];
30
+ try {
31
+ const { stdout } = await execFileAsync('git', ['log', '--no-merges', '--first-parent', `${startSha}..HEAD`, '--format=%H'], { cwd: repoRoot, timeout: GIT_TIMEOUT_MS });
32
+ return stdout.trim().split('\n').filter(s => s.length > 0);
33
+ }
34
+ catch {
35
+ return [];
36
+ }
37
+ }
38
+ }
39
+ exports.LocalGitSnapshotV2 = LocalGitSnapshotV2;
@@ -0,0 +1,10 @@
1
+ export interface GitEndSnapshot {
2
+ readonly endSha: string | null;
3
+ readonly commitShas: readonly string[];
4
+ }
5
+ export interface GitSnapshotPortV2 {
6
+ resolveEndSnapshot(repoRoot: string | null, startSha: string | null): Promise<GitEndSnapshot>;
7
+ }
8
+ export declare class NullGitSnapshotV2 implements GitSnapshotPortV2 {
9
+ resolveEndSnapshot(): Promise<GitEndSnapshot>;
10
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NullGitSnapshotV2 = void 0;
4
+ class NullGitSnapshotV2 {
5
+ async resolveEndSnapshot() {
6
+ return { endSha: null, commitShas: [] };
7
+ }
8
+ }
9
+ exports.NullGitSnapshotV2 = NullGitSnapshotV2;
@@ -52,6 +52,18 @@ function projectSessionMetricsV2(events) {
52
52
  }
53
53
  }
54
54
  }
55
+ let deliveryShas = [];
56
+ for (const e of events) {
57
+ if (e.kind !== constants_js_1.EVENT_KIND.DELIVERY_RECORDED)
58
+ continue;
59
+ if (e.scope?.runId !== runCompletedRunId)
60
+ continue;
61
+ const shasRaw = e.data.shas;
62
+ if (Array.isArray(shasRaw)) {
63
+ deliveryShas = shasRaw.filter((s) => typeof s === 'string');
64
+ }
65
+ break;
66
+ }
55
67
  const commitShasRaw = metricsContext['metrics_commit_shas'];
56
68
  const metricCommitShas = [];
57
69
  if (Array.isArray(commitShasRaw)) {
@@ -60,19 +72,22 @@ function projectSessionMetricsV2(events) {
60
72
  metricCommitShas.push(sha);
61
73
  }
62
74
  }
63
- const finalAgentCommitShas = metricCommitShas.length > 0 ? metricCommitShas : agentCommitShas;
75
+ const finalAgentCommitShas = deliveryShas.length > 0 ? deliveryShas :
76
+ metricCommitShas.length > 0 ? metricCommitShas :
77
+ agentCommitShas;
64
78
  const filesChangedRaw = metricsContext['metrics_files_changed'];
65
79
  const filesChanged = typeof filesChangedRaw === 'number' && Number.isFinite(filesChangedRaw) ? filesChangedRaw : null;
66
80
  const linesAddedRaw = metricsContext['metrics_lines_added'];
67
81
  const linesAdded = typeof linesAddedRaw === 'number' && Number.isFinite(linesAddedRaw) ? linesAddedRaw : null;
68
82
  const linesRemovedRaw = metricsContext['metrics_lines_removed'];
69
83
  const linesRemoved = typeof linesRemovedRaw === 'number' && Number.isFinite(linesRemovedRaw) ? linesRemovedRaw : null;
84
+ const finalCaptureConfidence = deliveryShas.length > 0 ? 'high' : captureConfidence;
70
85
  return {
71
86
  startGitSha,
72
87
  endGitSha,
73
88
  gitBranch,
74
89
  agentCommitShas: finalAgentCommitShas,
75
- captureConfidence,
90
+ captureConfidence: finalCaptureConfidence,
76
91
  durationMs,
77
92
  outcome,
78
93
  prNumbers,
package/docs/authoring.md CHANGED
@@ -761,6 +761,28 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
761
761
 
762
762
 
763
763
  ## Artifacts and planning surfaces
764
+ ### coordinator-result-artifact-schema
765
+ - **Level**: required
766
+ - **Status**: active
767
+ - **Scope**: artifact.coordinator-result
768
+ - **Rule**: When a workflow step signals coordinator phase completion, emit a `wr.coordinator_result` artifact with exactly 4 fields: `outcome` (enum: success|failed|timed_out|await_degraded), `summary` (string), `sessionId` (string), `error` (string|null). No additional fields allowed.
769
+ - **Why**: Coordinators read this artifact to determine whether to proceed, retry, or escalate. Extra fields pollute the schema boundary and break forward compatibility. The 4-field constraint is a hard limit, not a guideline.
770
+ - **Enforced by**: advisory
771
+
772
+ **Checks**
773
+ - Exactly 4 fields present: outcome, summary, sessionId, error.
774
+ - outcome is one of: success, failed, timed_out, await_degraded.
775
+ - error is string|null -- null when outcome is success, non-null string when outcome is failed.
776
+ - No workflow-specific fields (prUrl, branchName, commitSha, etc.) in wr.coordinator_result. Those belong in workflow-specific artifacts.
777
+
778
+ **Anti-patterns**
779
+ - Adding prUrl, branchName, or commitSha to wr.coordinator_result
780
+ - Using a free-form notes string instead of the typed outcome enum
781
+ - Omitting sessionId (required for coordinator tracing and console parent-child display)
782
+
783
+ **Source refs**
784
+ - `src/coordinators/types.ts` (runtime) — ChildSessionResult discriminated union -- the runtime type that wr.coordinator_result maps to.
785
+
764
786
  ### artifact-canonicality
765
787
  - **Level**: recommended
766
788
  - **Status**: active
@@ -913,6 +935,7 @@ Canonical current rules for authoring good WorkRail workflows. workflow.schema.j
913
935
  - `artifact.plan`: Implementation-planning artifacts
914
936
  - `artifact.spec`: Behavior/specification artifacts
915
937
  - `artifact.verification`: Verification or handoff artifacts
938
+ - `artifact.coordinator-result`: wr.coordinator_result artifact emitted by coordinator-phase workflows to signal phase completion to the coordinator
916
939
  - `delegation.context-packet`: Structured context passed to subagents
917
940
  - `delegation.result-envelope`: Structured result shape returned by subagents
918
941
  - `legacy.patterns`: Older authoring patterns that should now be discouraged or avoided
@@ -0,0 +1,123 @@
1
+ # Engine Boundary Discovery: SHA Tracking and Layer Architecture
2
+
3
+ **Date:** 2026-04-30
4
+ **Status:** Discovery complete -- recommendation: A now (PR #903), C next
5
+ **Goal:** Determine the right architecture for commit SHA tracking and clarify engine/coordinator/delivery layer boundaries
6
+
7
+ ---
8
+
9
+ ## Problem Understanding
10
+
11
+ ### The Real Problem
12
+ The engine emits `run_completed` at step T. The delivery pipeline commits to git at step T+1. There is no mechanism to communicate back from T+1 to T. Agent self-reporting of `metrics_commit_shas` was the broken workaround.
13
+
14
+ ### Core Tensions
15
+ 1. **Information completeness vs engine purity**: commits only exist after delivery, which is after `run_completed`
16
+ 2. **YAGNI vs architectural completeness**: MCP sessions may not need SHA tracking right now
17
+ 3. **Port purity vs pragmatism**: `outcome-success.ts` already calls `execFile` directly for `endGitSha` -- known violation
18
+ 4. **Atomic delivery vs distributed write-back**: appending after `run_completed` requires the session gate to remain open
19
+
20
+ ### What Makes This Hard
21
+ The gap is temporal. Information that belongs to session T becomes available at T+1. The event log is append-only but NOT closed after `run_completed` -- `ExecutionSessionGateV2` gates on `healthy` vs `corrupt`, not on completion state. Post-completion appends ARE valid.
22
+
23
+ ---
24
+
25
+ ## Architecture Contract (what exists, what's missing)
26
+
27
+ ### Engine owns (src/v2/durable-core/ + src/mcp/handlers/):
28
+ - Session lifecycle (start, advance, checkpoint, resume)
29
+ - HMAC token protocol -- cryptographic enforcement
30
+ - Append-only event log with crash-safe writes
31
+ - Port abstractions for all I/O (no direct fs/git/network calls in engine)
32
+ - Projections: reads event log, produces typed views
33
+ - **Records observations it can make directly**: git_branch, git_head_sha (via WorkspaceContextResolverPortV2 at START); endGitSha (via execFile direct call at END -- known port violation)
34
+
35
+ ### Coordinator/delivery layer owns (src/trigger/ + src/daemon/):
36
+ - Git commits, pushes, PR creation
37
+ - Delivery pipeline stages
38
+ - SHA extraction from `git commit` output
39
+ - Proof records, pipeline orchestration (future)
40
+ - Agent loop, context injection, crash recovery
41
+
42
+ ### The gap:
43
+ No mechanism for the delivery layer to record its observations (what it committed) back into the session event log.
44
+
45
+ ---
46
+
47
+ ## Philosophy Constraints
48
+
49
+ - **Architectural fixes over patches**: always-empty is a patch; extending the port is the fix
50
+ - **Dependency injection for boundaries**: git I/O must be behind a port in the engine layer
51
+ - **Make illegal states unrepresentable**: `captureConfidence: 'high'` with `agentCommitShas: []` is already schema-enforced as invalid
52
+ - **Validate at boundaries, trust inside**: SHA derivation must happen at the git boundary, never from agent memory
53
+ - **YAGNI with discipline**: don't build write-back infrastructure until coordinators actually consume it
54
+
55
+ ---
56
+
57
+ ## Candidates
58
+
59
+ ### A: Always-empty (current PR #903)
60
+ Engine emits `agentCommitShas: []` and `captureConfidence: 'none'` always. Console shows no SHAs. Coordinator scripts derive SHAs from `git log startSha..endSha` themselves.
61
+
62
+ - **Tensions resolved**: engine purity, illegal states, YAGNI
63
+ - **Tensions accepted**: information completeness, worktree-after-cleanup risk
64
+ - **Failure mode**: worktree cleaned up before coordinator script runs `git log`
65
+ - **Scope**: best-fit immediate fix; patch not architectural fix
66
+ - **Philosophy**: honors YAGNI, make-illegal-states-unrepresentable. Conflicts with architectural-fixes-over-patches.
67
+
68
+ ### B: Delivery write-back via DeliveryStage
69
+ New 5th DeliveryStage appends `observation_recorded` event with `git_commit_shas` to session event log after successful `git commit`. Requires injecting `SessionEventLogAppendStorePortV2` into `DeliveryPipeline`.
70
+
71
+ - **Tensions resolved**: information completeness (daemon sessions), crash safety (stage before sidecar delete)
72
+ - **Tensions accepted**: delivery-action API change (new session store dependency); MCP sessions still get no SHAs
73
+ - **Failure mode**: append fails silently if session health degraded (must be best-effort, never abort delivery)
74
+ - **Scope**: best-fit for daemon/autoCommit sessions; accepted gap for MCP
75
+ - **Philosophy**: honors architectural-fixes-over-patches, dependency-injection. Mild YAGNI tension.
76
+
77
+ ### C: Extend WorkspaceContextResolverPortV2 with end-state (port-correct fix)
78
+ Add `resolveEndState(repoRoot, startSha) -> { endSha, commitShas }` to the workspace anchor port (or create `GitSnapshotPortV2`). Inject into `outcome-success.ts`. Runs `git rev-parse HEAD` + `git log startSha..HEAD --format=%H` in parallel. `run_completed` event gets `commitShas: string[]` field.
79
+
80
+ - **Tensions resolved**: engine purity (eliminates execFile violation), information completeness (all session types), MCP + daemon covered
81
+ - **Tensions accepted**: run_completed schema gets new field; old sessions project `commitShas: []` (non-breaking)
82
+ - **Failure mode**: `git log startSha..HEAD` may include merge commits or upstream advances; use `--no-merges --first-parent` to scope
83
+ - **Scope**: slightly broader than needed (touches port contract) but correct -- each piece is load-bearing
84
+ - **Philosophy**: honors architectural-fixes-over-patches, dependency-injection-for-boundaries, make-illegal-states-unrepresentable.
85
+
86
+ ### D: Delivery sidecar store (reframe -- REJECTED)
87
+ Delivery writes `delivery-result-<sessionId>.json` sidecar. Session-metrics reads from both event log and sidecar. REJECTED: violates single-source-of-truth principle for the event log. Two truth sources for SHA data is architecturally unsound for this codebase.
88
+
89
+ ---
90
+
91
+ ## Comparison and Recommendation
92
+
93
+ **Recommendation: A now (PR #903 as-is), C next.**
94
+
95
+ C is the architectural ideal because:
96
+ 1. Solves both problems simultaneously: SHA gap AND the existing execFile violation in outcome-success.ts
97
+ 2. Works for ALL session types (MCP + daemon) without special-casing
98
+ 3. Follows the port pattern the codebase is designed around
99
+ 4. run_completed is the correct home -- startGitSha is already there, endGitSha is already there, commitShas belongs alongside them
100
+
101
+ B is viable but inferior: adds a session store dependency to the delivery layer (which currently has none), only covers daemon/autoCommit sessions, and has a semantic question (delivery artifacts vs session truth).
102
+
103
+ A is correct as the IMMEDIATE state. PR #903 should merge. It makes the illegal state (high confidence + empty SHAs) impossible, and clears the field for C.
104
+
105
+ **The original question -- do we need to split the engine? -- is NO.** The current architecture is sound. The fix is a port extension, not a structural split.
106
+
107
+ ---
108
+
109
+ ## Self-Critique
110
+
111
+ **Strongest counter-argument against C**: run_completed schema migration. Existing stored events lack `commitShas`. Rebuttal: session-metrics projection already handles absent fields (defaults to []). Adding `commitShas?: string[]` is non-breaking additive.
112
+
113
+ **What would tip to B**: if daemon sessions with autoCommit dominate and MCP sessions never need SHAs. Currently autoCommit is daemon-only, so B's coverage gap is acceptable. But if MCP sessions start running in worktrees with autoCommit, B becomes wrong.
114
+
115
+ **Invalidating assumption**: if `git log startSha..HEAD` includes upstream merge commits, SHAs from unrelated PRs land in the session's record. Mitigation: `--no-merges --first-parent` in the port implementation.
116
+
117
+ ---
118
+
119
+ ## Open Questions
120
+
121
+ 1. Should `GitSnapshotPortV2` be a new port or an extension of `WorkspaceContextResolverPortV2`? New port has cleaner separation; extension avoids proliferating ports.
122
+ 2. What is the right git log filter for commit SHAs: `--no-merges`, `--first-parent`, both? Depends on whether merge commits from squash-merge PRs should be included.
123
+ 3. Should Candidate C be implemented now (before PR #903 merges) or after? After is safer -- PR #903 is already in CI, touching outcome-success.ts again would create a dirty merge.
@@ -0,0 +1,72 @@
1
+ # Engine Boundary Discovery: Design Review Findings
2
+
3
+ **Date:** 2026-04-30
4
+ **Decision:** A now (PR #903), C next (GitSnapshotPortV2)
5
+ **Confidence:** HIGH
6
+
7
+ ---
8
+
9
+ ## Tradeoff Review
10
+
11
+ | Tradeoff | Acceptable? | Invalidating condition |
12
+ |---|---|---|
13
+ | Console shows empty SHAs until C is built | Yes -- empty is honest, no current consumer needs populated SHAs | Proof records feature built before C |
14
+ | MCP sessions get no SHAs under A-only | Yes -- human can check git; MCP is not the autonomous use case | MCP sessions start requiring attribution for proof records |
15
+ | run_completed schema gets optional field | Yes -- non-breaking additive change; old sessions project [] | Schema versioning feature enforces strict field presence |
16
+
17
+ All tradeoffs acceptable under current conditions.
18
+
19
+ ---
20
+
21
+ ## Failure Mode Review
22
+
23
+ | Failure Mode | Coverage | Risk |
24
+ |---|---|---|
25
+ | git log includes upstream merge commits | Mitigate with `--no-merges --first-parent` in port implementation | MEDIUM -- must be in the implementation spec |
26
+ | outcome-success.ts partial failure (endGitSha or commitShas fails) | Promise.all + best-effort; both degrade to null/[] independently | LOW -- same behavior as current resolveEndGitSha |
27
+
28
+ **Highest-risk**: FM1. Must specify `--no-merges --first-parent` explicitly in Candidate C implementation.
29
+
30
+ ---
31
+
32
+ ## Runner-up / Simpler Alternative Review
33
+
34
+ - B (delivery write-back): no elements worth borrowing. Its coupling (session store injected into delivery layer) is the problem C avoids.
35
+ - Simpler C variant (fix execFile violation only, skip commitShas): would require a second port extension later. Marginal cost of doing both at once is low. Not simpler enough to justify the incompleteness.
36
+ - No hybrid warranted. A-then-C sequencing is correct.
37
+
38
+ ---
39
+
40
+ ## Philosophy Alignment
41
+
42
+ **Satisfied**: make-illegal-states-unrepresentable, validate-at-boundaries, errors-as-data, dependency-injection-for-boundaries, architectural-fixes-over-patches.
43
+
44
+ **Under acceptable tension**: YAGNI (building port before proof records consume it -- justified by dual purpose: also fixes execFile violation). Immutability (appending to completed session -- design-correct, session gate confirms).
45
+
46
+ ---
47
+
48
+ ## Findings
49
+
50
+ ### YELLOW: git log filter not specified
51
+ The implementation of Candidate C's `resolveCommitShaRange` must use `--no-merges --first-parent` to avoid including upstream merge commits. This is not currently specified anywhere. Without it, sessions on branches with merge commits from main would record incorrect SHAs.
52
+ **Action**: add explicit filter spec to the Candidate C implementation ticket.
53
+
54
+ ### YELLOW: No forcing function to build C after A merges
55
+ PR #903 will merge. C is the architectural fix. Without a ticket, C may never get built and the console SHA display stays empty indefinitely.
56
+ **Action**: create a GitHub issue for Candidate C immediately after PR #903 merges.
57
+
58
+ ---
59
+
60
+ ## Recommended Revisions to Selected Design
61
+
62
+ 1. Before implementing C: specify `--no-merges --first-parent` as the required git log filter in the issue description
63
+ 2. New port name: `GitSnapshotPortV2` is cleaner than extending `WorkspaceContextResolverPortV2` -- keeps the end-state capture concern separate from the start-state anchor concern
64
+ 3. New method signature: `resolveEndSnapshot(repoRoot: string, startSha: string): Promise<{ endSha: string | null, commitShas: string[] }>`
65
+ 4. In `outcome-success.ts`: replace `resolveEndGitSha` with `resolveEndSnapshot` -- single port call that returns both `endGitSha` and `commitShas` in parallel
66
+
67
+ ---
68
+
69
+ ## Residual Concerns
70
+
71
+ - **Architecture contract not written**: the meta-fix (documenting what the engine layer owns vs coordinator/delivery) has not been done. This is the most important long-term outcome from this discovery. Without it, future contributors will make the same boundary violations. Should be added to `docs/design/v2-core-design-locks.md`.
72
+ - **C has open design question**: `GitSnapshotPortV2` vs extension of `WorkspaceContextResolverPortV2`. Both work. `GitSnapshotPortV2` is slightly cleaner (separate port, separate concern). Decision can be made at implementation time.