@exaudeus/workrail 3.32.0 → 3.34.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.
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/worktrain-await.js +11 -9
- package/dist/cli/commands/worktrain-daemon-install.d.ts +35 -0
- package/dist/cli/commands/worktrain-daemon-install.js +291 -0
- package/dist/cli/commands/worktrain-daemon.d.ts +31 -0
- package/dist/cli/commands/worktrain-daemon.js +272 -0
- package/dist/cli/commands/worktrain-spawn.js +11 -9
- package/dist/cli-worktrain.js +488 -0
- package/dist/cli.js +1 -22
- package/dist/console/standalone-console.d.ts +28 -0
- package/dist/console/standalone-console.js +142 -0
- package/dist/{console/assets/index-Cb_LO718.js → console-ui/assets/index-C1JXnwZS.js} +1 -1
- package/dist/{console → console-ui}/index.html +1 -1
- package/dist/daemon/agent-loop.d.ts +27 -0
- package/dist/daemon/agent-loop.js +39 -1
- package/dist/daemon/daemon-events.d.ts +63 -1
- package/dist/daemon/workflow-runner.d.ts +3 -2
- package/dist/daemon/workflow-runner.js +285 -46
- package/dist/infrastructure/session/HttpServer.js +133 -34
- package/dist/manifest.json +136 -104
- package/dist/mcp/handlers/v2-error-mapping.d.ts +3 -0
- package/dist/mcp/handlers/v2-error-mapping.js +2 -0
- package/dist/mcp/handlers/v2-execution/advance.js +25 -0
- package/dist/mcp/handlers/v2-execution/continue-advance.js +7 -0
- package/dist/mcp/output-schemas.d.ts +30 -30
- package/dist/mcp/transports/fatal-exit.js +4 -0
- package/dist/mcp/transports/http-entry.js +0 -5
- package/dist/mcp/transports/stdio-entry.js +24 -12
- package/dist/mcp/v2/tools.d.ts +4 -4
- package/dist/mcp-server.d.ts +0 -2
- package/dist/mcp-server.js +1 -42
- package/dist/trigger/adapters/github-poller.d.ts +44 -0
- package/dist/trigger/adapters/github-poller.js +190 -0
- package/dist/trigger/adapters/gitlab-poller.d.ts +27 -0
- package/dist/trigger/adapters/gitlab-poller.js +81 -0
- package/dist/trigger/index.d.ts +4 -1
- package/dist/trigger/index.js +5 -1
- package/dist/trigger/polled-event-store.d.ts +22 -0
- package/dist/trigger/polled-event-store.js +173 -0
- package/dist/trigger/polling-scheduler.d.ts +20 -0
- package/dist/trigger/polling-scheduler.js +249 -0
- package/dist/trigger/trigger-listener.d.ts +3 -0
- package/dist/trigger/trigger-listener.js +47 -3
- package/dist/trigger/trigger-store.js +114 -33
- package/dist/trigger/types.d.ts +17 -1
- package/dist/v2/durable-core/domain/observation-builder.d.ts +3 -0
- package/dist/v2/durable-core/domain/observation-builder.js +2 -2
- package/dist/v2/durable-core/domain/prompt-renderer.d.ts +2 -1
- package/dist/v2/durable-core/domain/prompt-renderer.js +10 -0
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +224 -224
- package/dist/v2/durable-core/schemas/session/events.d.ts +42 -42
- package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
- package/dist/v2/durable-core/schemas/session/validation-event.d.ts +2 -2
- package/dist/v2/durable-core/tokens/payloads.d.ts +52 -52
- package/dist/v2/usecases/console-routes.js +3 -3
- package/dist/v2/usecases/console-service.js +185 -10
- package/dist/v2/usecases/console-types.d.ts +8 -0
- package/docs/design/bridge-removal-pr-a-candidates.md +115 -0
- package/docs/design/bridge-removal-pr-a-design-review.md +79 -0
- package/docs/design/bridge-removal-pr-a-implementation-plan.md +203 -0
- package/docs/design/daemon-conversation-logging-plan.md +98 -0
- package/docs/design/daemon-conversation-logging-review.md +55 -0
- package/docs/design/daemon-conversation-logging.md +129 -0
- package/docs/design/github-polling-adapter-design-candidates.md +226 -0
- package/docs/design/github-polling-adapter-design-review-findings.md +131 -0
- package/docs/design/github-polling-adapter-implementation-plan.md +284 -0
- package/docs/design/implementation_plan.md +192 -0
- package/docs/design/workflow-id-validation-at-startup.md +146 -0
- package/docs/design/workflow-id-validation-design-review.md +87 -0
- package/docs/design/workflow-id-validation-implementation-plan.md +185 -0
- package/docs/design/worktrain-system-prompt-report-issue-candidates.md +135 -0
- package/docs/design/worktrain-system-prompt-report-issue-design-review.md +73 -0
- package/docs/discovery/design-candidates.md +180 -0
- package/docs/discovery/design-review-findings.md +110 -0
- package/docs/discovery/wr-discovery-goal-reframing.md +303 -0
- package/docs/ideas/backlog.md +627 -0
- package/package.json +1 -1
- package/workflows/architecture-scalability-audit.json +1 -1
- package/workflows/bug-investigation.agentic.v2.json +3 -3
- package/workflows/coding-task-workflow-agentic.json +32 -32
- package/workflows/coding-task-workflow-agentic.lean.v2.json +1 -1
- package/workflows/coding-task-workflow-agentic.v2.json +7 -7
- package/workflows/mr-review-workflow.agentic.v2.json +21 -12
- package/workflows/personal-learning-materials-creation-branched.json +2 -2
- package/workflows/production-readiness-audit.json +1 -1
- package/workflows/relocation-workflow-us.json +2 -2
- package/workflows/ui-ux-design-workflow.json +14 -14
- package/workflows/workflow-for-workflows.json +3 -3
- package/workflows/workflow-for-workflows.v2.json +2 -2
- package/workflows/wr.discovery.json +59 -8
- package/dist/mcp/transports/bridge-entry.d.ts +0 -102
- package/dist/mcp/transports/bridge-entry.js +0 -454
- package/dist/mcp/transports/bridge-events.d.ts +0 -51
- package/dist/mcp/transports/bridge-events.js +0 -24
- package/dist/mcp/transports/primary-tombstone.d.ts +0 -21
- package/dist/mcp/transports/primary-tombstone.js +0 -51
- /package/dist/{console → console-ui}/assets/index-8dh0Psu-.css +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Design Review: Workflow ID Validation at Daemon Startup
|
|
2
|
+
|
|
3
|
+
**Design under review:** Candidate A -- injectable `getWorkflowByIdFn` on `StartTriggerListenerOptions`
|
|
4
|
+
**Date:** 2026-04-16
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Tradeoff Review
|
|
9
|
+
|
|
10
|
+
| Tradeoff | Acceptable? | Condition that breaks it |
|
|
11
|
+
|----------|-------------|--------------------------|
|
|
12
|
+
| Validation silently skipped when fn not provided | Yes | A new production call site added without the fn |
|
|
13
|
+
| New option field (API surface) | Yes | Interface is internal, non-breaking |
|
|
14
|
+
|
|
15
|
+
Hidden assumption: single production call site for `startTriggerListener`. True today.
|
|
16
|
+
|
|
17
|
+
**Mitigation added:** Log message when fn not provided, making the skip visible in startup logs.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Failure Mode Review
|
|
22
|
+
|
|
23
|
+
| Failure Mode | Handled? | Action Required |
|
|
24
|
+
|--------------|----------|-----------------|
|
|
25
|
+
| FM1: getWorkflowByIdFn throws/rejects | NOT YET | Add try/catch around each fn call; warn+skip on error |
|
|
26
|
+
| FM2: transient workflow unavailability | Acceptable | warn+skip behavior is correct here |
|
|
27
|
+
| FM3: Map mutation during iteration | NOT YET | Collect unknowns in first pass, delete in second pass |
|
|
28
|
+
| FM4: ctx.workflowService undefined in production | Needs guard | Use optional chaining `?.` in default fn |
|
|
29
|
+
|
|
30
|
+
**Highest-risk:** FM1. An unhandled rejection would crash `startTriggerListener`. Must fix.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Runner-Up / Simpler Alternative Review
|
|
35
|
+
|
|
36
|
+
- Runner-up (Candidate B, ctx direct): no elements to borrow. Testability loss outweighs API surface saving.
|
|
37
|
+
- No simpler variant satisfies both testability and correctness requirements.
|
|
38
|
+
- Candidate A is already minimum viable for the acceptance criteria.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Philosophy Alignment
|
|
43
|
+
|
|
44
|
+
**Satisfied:** Dependency injection, validate at boundaries, errors are data, YAGNI, surface information.
|
|
45
|
+
**Under tension (acceptable):**
|
|
46
|
+
- "Make illegal states unrepresentable" -- TriggerDefinition can still hold invalid workflowIds. Compile-time enforcement would require two-phase types; over-engineering for a Small task.
|
|
47
|
+
- "Immutability by default" -- triggerIndex Map is mutated, but mutation is local (created and modified within `startTriggerListener`, not shared until passed to TriggerRouter).
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Findings
|
|
52
|
+
|
|
53
|
+
**ORANGE -- FM1: Unhandled rejection from getWorkflowByIdFn**
|
|
54
|
+
The current design has no try/catch around the fn call. An I/O error in the workflow storage lookup would propagate as an unhandled rejection and crash `startTriggerListener`. Fix: wrap each `await getWorkflowByIdFn(trigger.workflowId)` in try/catch; on error, log warning and skip that trigger (same policy as other validation failures).
|
|
55
|
+
|
|
56
|
+
**YELLOW -- FM3: Two-pass Map deletion**
|
|
57
|
+
Must not delete from `triggerIndex` while iterating it. Fix: collect unknown IDs in an array during the loop, then delete in a second pass.
|
|
58
|
+
|
|
59
|
+
**YELLOW -- FM4: ctx.workflowService guard**
|
|
60
|
+
The default fn production expression `ctx.workflowService.getWorkflowById(id).then(...)` will throw if `workflowService` is undefined. Fix: use optional chaining `ctx.workflowService?.getWorkflowById(id).then(w => w !== null) ?? true` (treat unavailable service as "found" -- skip validation rather than crash).
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Recommended Revisions
|
|
65
|
+
|
|
66
|
+
1. **Required:** Add try/catch in the validation loop; treat fn errors as warn+skip.
|
|
67
|
+
2. **Required:** Collect unknowns first, delete after iteration.
|
|
68
|
+
3. **Required:** Use optional chaining for the default production fn.
|
|
69
|
+
4. **Nice-to-have:** Log `[TriggerListener] workflowId validation skipped (no resolver provided)` when fn is absent, for observability.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Residual Concerns
|
|
74
|
+
|
|
75
|
+
- The "silent skip when fn not provided" is acceptable but relies on a naming convention (option field) to communicate intent. Future callers won't get a compile-time reminder to provide the fn. This is a documentation concern, not a correctness concern.
|
|
76
|
+
- `onComplete.workflowId` is not validated by this design. Out of scope for this task; should be a follow-up if `onComplete` usage grows.
|
|
77
|
+
- No RED findings. All issues are fixable at implementation time with minor code additions.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Pass 2 Findings (incremental)
|
|
82
|
+
|
|
83
|
+
No new RED or ORANGE findings. Design revisions from pass 1 are sufficient.
|
|
84
|
+
|
|
85
|
+
**New observation (YELLOW):** `onComplete.workflowId` (secondary workflow for completion hooks) is not validated. Accepted as out of scope -- add a comment in the implementation noting this limitation.
|
|
86
|
+
|
|
87
|
+
**Performance:** Sequential validation of N triggers is acceptable at expected trigger counts (1-10). No action needed.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Implementation Plan: Workflow ID Validation at Daemon Startup
|
|
2
|
+
|
|
3
|
+
**Status:** Ready to implement
|
|
4
|
+
**Branch:** `fix/workflow-id-validation-at-startup`
|
|
5
|
+
**Date:** 2026-04-16
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Problem Statement
|
|
10
|
+
|
|
11
|
+
When a user writes an incorrect `workflowId` in `triggers.yml` (e.g., `coding-task-workflow-agentic.lean.v2` instead of `coding-task-workflow-agentic`), the daemon starts successfully, accepts webhooks, but every dispatch silently fails with `workflow_not_found`. The error only appears in logs during actual webhook events -- not at startup. This is a silent-failure bug.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 2. Acceptance Criteria
|
|
16
|
+
|
|
17
|
+
- [x] At daemon startup, after loading and indexing triggers, validate that each trigger's `workflowId` resolves to a known workflow
|
|
18
|
+
- [x] Triggers with unknown `workflowId` are logged with a clear warning (naming the triggerId and the bad workflowId) and removed from the active index
|
|
19
|
+
- [x] Triggers with valid `workflowId` start normally
|
|
20
|
+
- [x] If `getWorkflowByIdFn` throws or rejects, that trigger is also warned+skipped (not a daemon crash)
|
|
21
|
+
- [x] Existing behavior when `getWorkflowByIdFn` is not provided: validation is skipped (backward compat, logged)
|
|
22
|
+
- [x] Existing tests continue to pass without modification
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 3. Non-Goals
|
|
27
|
+
|
|
28
|
+
- No hard-fail policy (daemon does not refuse to start; it starts with fewer triggers)
|
|
29
|
+
- No validation of `onComplete.workflowId` (secondary workflow ID -- follow-up ticket)
|
|
30
|
+
- No changes to `trigger-store.ts` or `TriggerDefinition` type
|
|
31
|
+
- No re-validation on webhook arrival
|
|
32
|
+
- No dynamic reload / hot-reload of trigger config
|
|
33
|
+
- No change to `trigger-router.ts` (it already handles `workflow_not_found` at dispatch)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 4. Philosophy-Driven Constraints
|
|
38
|
+
|
|
39
|
+
- **Dependency injection**: `getWorkflowByIdFn` must be injectable -- no direct `ctx.workflowService` access
|
|
40
|
+
- **Validate at boundaries**: validation runs at `startTriggerListener` (startup boundary), not inside routing
|
|
41
|
+
- **Errors are data**: validation failures are warnings + skip, not thrown exceptions
|
|
42
|
+
- **Document why**: implementation must include WHY comments on the key decisions
|
|
43
|
+
- **Warn+skip over hard-fail**: consistent with `loadTriggerConfig` existing behavior
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 5. Invariants
|
|
48
|
+
|
|
49
|
+
1. The `triggerIndex` passed to `TriggerRouter` contains ONLY triggers whose `workflowId` was confirmed to exist (when `getWorkflowByIdFn` is provided)
|
|
50
|
+
2. The validation loop MUST NOT mutate `triggerIndex` during iteration (collect unknowns first, delete after)
|
|
51
|
+
3. A `getWorkflowByIdFn` rejection/throw MUST NOT propagate -- it is caught, the trigger is warned+skipped
|
|
52
|
+
4. When `getWorkflowByIdFn` is absent, validation is skipped entirely (backward compat) and a log message says so
|
|
53
|
+
5. `DefaultWorkflowService.getWorkflowById` delegates directly to storage (no compilation cache interference) -- validation results are authoritative
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 6. Selected Approach
|
|
58
|
+
|
|
59
|
+
**Add `getWorkflowByIdFn?: (id: string) => Promise<boolean>` to `StartTriggerListenerOptions`.**
|
|
60
|
+
|
|
61
|
+
In `startTriggerListener`, after `buildTriggerIndex()` returns ok, if `getWorkflowByIdFn` is provided:
|
|
62
|
+
1. Iterate `triggerIndex` entries (read-only pass), collect unknown workflowIds
|
|
63
|
+
2. For each, try `await getWorkflowByIdFn(trigger.workflowId)` -- catch rejection, treat as false
|
|
64
|
+
3. Collect trigger IDs where result is false or threw
|
|
65
|
+
4. After iteration: delete collected IDs from `triggerIndex`, log warnings
|
|
66
|
+
5. Log summary if any were skipped
|
|
67
|
+
|
|
68
|
+
Production default (not on the option -- called inline): `async (id) => (await ctx.workflowService?.getWorkflowById(id)) !== null`.
|
|
69
|
+
|
|
70
|
+
**Runner-up:** Candidate B (ctx direct with null guard) -- lost because `FAKE_CTX = {} as V2ToolContext` makes the behavior untestable.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 7. Vertical Slices
|
|
75
|
+
|
|
76
|
+
### Slice 1 -- Core validation logic in `startTriggerListener`
|
|
77
|
+
|
|
78
|
+
**Files:** `src/trigger/trigger-listener.ts`
|
|
79
|
+
|
|
80
|
+
**Changes:**
|
|
81
|
+
- Add `getWorkflowByIdFn?: (id: string) => Promise<boolean>` to `StartTriggerListenerOptions`
|
|
82
|
+
- After `buildTriggerIndex()` returns ok (before `new TriggerRouter`): add validation block
|
|
83
|
+
- Validation block logic:
|
|
84
|
+
```
|
|
85
|
+
if (getWorkflowByIdFn) {
|
|
86
|
+
const unknownTriggerIds: string[] = []
|
|
87
|
+
for (const [triggerId, trigger] of triggerIndex) {
|
|
88
|
+
let found: boolean
|
|
89
|
+
try {
|
|
90
|
+
found = await getWorkflowByIdFn(trigger.workflowId)
|
|
91
|
+
} catch (e) {
|
|
92
|
+
found = false
|
|
93
|
+
console.warn(`[TriggerListener] Error validating workflowId '${trigger.workflowId}' for trigger '${triggerId}': ${e}`)
|
|
94
|
+
}
|
|
95
|
+
if (!found) {
|
|
96
|
+
unknownTriggerIds.push(triggerId)
|
|
97
|
+
console.warn(`[TriggerListener] Skipping trigger '${triggerId}': workflowId '${trigger.workflowId}' not found`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const id of unknownTriggerIds) { triggerIndex.delete(id) }
|
|
101
|
+
if (unknownTriggerIds.length > 0) {
|
|
102
|
+
console.warn(`[TriggerListener] Skipped ${unknownTriggerIds.length} trigger(s) with unknown workflowId(s)`)
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
console.log(`[TriggerListener] workflowId validation skipped (no resolver provided)`)
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
- Add production default invocation: pass `async (id) => (await ctx.workflowService?.getWorkflowById(id)) !== null` as the default when option not provided
|
|
109
|
+
|
|
110
|
+
**Acceptance criterion:** Unknown workflowId triggers are not present in the index passed to `TriggerRouter`.
|
|
111
|
+
|
|
112
|
+
### Slice 2 -- Tests in `trigger-router.test.ts`
|
|
113
|
+
|
|
114
|
+
**Files:** `tests/unit/trigger-router.test.ts`
|
|
115
|
+
|
|
116
|
+
**New test cases (in a new describe block `startTriggerListener workflowId validation`):**
|
|
117
|
+
1. Triggers with unknown workflowId are warned and skipped (index excludes them, server starts)
|
|
118
|
+
2. Triggers with valid workflowId are kept in the index
|
|
119
|
+
3. When `getWorkflowByIdFn` is not provided, validation is skipped and all triggers are kept
|
|
120
|
+
4. When `getWorkflowByIdFn` rejects, that trigger is warned and skipped (daemon doesn't crash)
|
|
121
|
+
5. Mix: some valid, some invalid -- only valid triggers remain
|
|
122
|
+
|
|
123
|
+
**Acceptance criterion:** All 5 test cases pass.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 8. Test Design
|
|
128
|
+
|
|
129
|
+
**Pattern to follow:** `startTriggerListener` tests in `trigger-router.test.ts` (~line 432). Same structure:
|
|
130
|
+
- Use `tmpPath()` for workspacePath
|
|
131
|
+
- `env: { WORKRAIL_TRIGGERS_ENABLED: 'true' }`
|
|
132
|
+
- `port: 0` for OS-assigned port
|
|
133
|
+
- `runWorkflowFn: vi.fn()`
|
|
134
|
+
- `workspaces: {}` to skip workspace config loading
|
|
135
|
+
|
|
136
|
+
**Fixtures needed:**
|
|
137
|
+
- A minimal `triggers.yml` with two triggers: one with valid workflowId, one with invalid
|
|
138
|
+
- `getWorkflowByIdFn` stub: `vi.fn().mockImplementation(async (id: string) => id === 'coding-task-workflow-agentic')`
|
|
139
|
+
|
|
140
|
+
**Note:** Tests write real `triggers.yml` files to `tmpPath()` directories (pattern established in existing tests). Check how existing `startTriggerListener` tests set up workspace directories.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 9. Risk Register
|
|
145
|
+
|
|
146
|
+
| Risk | Likelihood | Impact | Mitigation |
|
|
147
|
+
|------|-----------|--------|-----------|
|
|
148
|
+
| `ctx.workflowService` undefined in production | Low | Medium | Optional chaining `?.` in default fn |
|
|
149
|
+
| FM1: getWorkflowByIdFn throws | Low-Medium | High | try/catch per call, warn+skip |
|
|
150
|
+
| FM3: Map mutation during iteration | Low (easy to avoid) | Medium | Two-pass (collect then delete) |
|
|
151
|
+
| False positive (valid workflow not found due to I/O error) | Low | Low | Same as FM1 -- warn+skip, operator can restart |
|
|
152
|
+
| `onComplete.workflowId` still silent-fails | Medium | Low | Accepted, documented, follow-up ticket |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 10. PR Packaging
|
|
157
|
+
|
|
158
|
+
**Single PR.** Small task, all changes in 2 files. Branch: `fix/workflow-id-validation-at-startup`.
|
|
159
|
+
|
|
160
|
+
PR title: `fix(trigger): warn and skip triggers with unknown workflowId at startup`
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 11. Philosophy Alignment Per Slice
|
|
165
|
+
|
|
166
|
+
| Principle | Slice 1 | Slice 2 |
|
|
167
|
+
|-----------|---------|---------|
|
|
168
|
+
| Dependency injection for boundaries | Satisfied -- fn injectable | Satisfied -- tests inject stub |
|
|
169
|
+
| Validate at boundaries | Satisfied -- startup boundary | N/A |
|
|
170
|
+
| Errors are data | Satisfied -- warn+skip, no throw | Satisfied -- tests verify no crash |
|
|
171
|
+
| Document why | Satisfied -- WHY comments required | N/A |
|
|
172
|
+
| Warn+skip over hard-fail | Satisfied | Verified by tests |
|
|
173
|
+
| Immutability by default | Tension -- triggerIndex mutated, but local scope | N/A |
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 12. Follow-up Tickets
|
|
178
|
+
|
|
179
|
+
- `onComplete.workflowId` validation (secondary workflow IDs in completion hooks)
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
**unresolvedUnknownCount:** 0
|
|
184
|
+
**planConfidenceBand:** High
|
|
185
|
+
**estimatedPRCount:** 1
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# WorkTrain: System Prompt Preamble + report_issue Tool -- Design Candidates
|
|
2
|
+
|
|
3
|
+
## Problem Understanding
|
|
4
|
+
|
|
5
|
+
### Core Tensions
|
|
6
|
+
|
|
7
|
+
1. **Preamble richness vs. existing test assertions** -- The current `buildSystemPrompt()` preamble is ~15 lines (identity, tools, execution contract, session state). The new spec replaces it with ~55 lines covering oracle hierarchy, self-directed reasoning, workflow-as-contract, silent failure, and tools-as-hands. The existing tests assert specific strings (`'You are WorkRail Auto'`, `'## Your tools'`, `'## Execution contract'`) must be present. The new content must include those strings or the tests break.
|
|
8
|
+
|
|
9
|
+
2. **report_issue write durability vs. fire-and-forget contract** -- The JSONL file must be written reliably enough for a future coordinator to read it, but a disk-full or permission error must never interrupt the agent session. Resolution: same void+catch pattern as `DaemonEventEmitter`.
|
|
10
|
+
|
|
11
|
+
3. **Purity of buildSystemPrompt vs. tool name references in prose** -- The new preamble mentions `report_issue` by name in the "silent failure" section. If the tool name ever changes, the system prompt text becomes stale. This is an accepted documentation-drift risk; tool names are stable identifiers.
|
|
12
|
+
|
|
13
|
+
4. **YAGNI (no coordinator yet) vs. extractability (coordinator coming soon)** -- The auto-fix coordinator that will read the issue JSONL doesn't exist. Building a dedicated `IssueStore` class now is speculative. However, the file write must still work for the coordinator when it arrives.
|
|
14
|
+
|
|
15
|
+
### Likely Seam
|
|
16
|
+
|
|
17
|
+
- **Preamble:** `buildSystemPrompt()` lines 1086-1108 in `src/daemon/workflow-runner.ts`. This is the correct seam -- pure function, all callers go through it.
|
|
18
|
+
- **report_issue tool:** The tools array in `runWorkflow()` at lines 1318-1323. New tool factory `makeReportIssueTool()` slots in here.
|
|
19
|
+
- **DaemonEvent union:** `src/daemon/daemon-events.ts` -- add `IssueReportedEvent` + extend union.
|
|
20
|
+
|
|
21
|
+
### What Makes This Hard
|
|
22
|
+
|
|
23
|
+
- Existing test assertions constrain the new preamble content -- `'## Your tools'` and `'## Execution contract'` must survive the rewrite.
|
|
24
|
+
- `report_issue.execute()` must be fire-and-forget for the JSONL write. Junior devs would `await` it directly and let I/O errors propagate.
|
|
25
|
+
- `IssueReportedEvent` fields must be typed as literal union strings (not `string`), otherwise illegal kind/severity values are representable at compile time.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Philosophy Constraints
|
|
30
|
+
|
|
31
|
+
From `/Users/etienneb/CLAUDE.md` and `~/.workrail/daemon-soul.md`:
|
|
32
|
+
|
|
33
|
+
- **Exhaustiveness everywhere** -- `DaemonEvent` must remain a discriminated union; new variant must follow the pattern
|
|
34
|
+
- **Make illegal states unrepresentable** -- `kind` and `severity` on IssueReportedEvent must be literal unions, not strings
|
|
35
|
+
- **YAGNI with discipline** -- don't build IssueStore class until the coordinator needs it
|
|
36
|
+
- **Observability as a constraint** -- fire-and-forget writes must never block correctness
|
|
37
|
+
- **Document 'why' not 'what'** -- JSDoc on BASE_SYSTEM_PROMPT explaining why it's a constant
|
|
38
|
+
- **Immutability by default** -- all DaemonEvent interfaces use `readonly` fields
|
|
39
|
+
- **Functional core, imperative shell** -- buildSystemPrompt is pure; JSONL write is at the shell boundary
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Impact Surface
|
|
44
|
+
|
|
45
|
+
- `tests/unit/workflow-runner-system-prompt.test.ts` -- 3 assertions on preamble content that must pass after rewrite
|
|
46
|
+
- `tests/unit/daemon-events.test.ts` -- existing event tests; adding a new event kind must not break exhaustiveness checks
|
|
47
|
+
- Any future TypeScript code that does `switch (event.kind)` on `DaemonEvent` -- must handle `'issue_reported'` or get a compile error (desired)
|
|
48
|
+
- `runWorkflow()` tools array -- adding `makeReportIssueTool` here; `sessionId` and `emitter` are already in scope
|
|
49
|
+
- The `BASE_SYSTEM_PROMPT` constant mentions the tool by name -- documentation drift if tool is renamed later
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Candidates
|
|
54
|
+
|
|
55
|
+
### Part 1: New Preamble
|
|
56
|
+
|
|
57
|
+
#### Candidate A -- Inline replacement (no constant)
|
|
58
|
+
- **Summary:** Replace the 4-item preamble lines directly in the `lines` array inside `buildSystemPrompt()`.
|
|
59
|
+
- **Tensions resolved:** Existing tests still pass (required strings included). **Accepted:** Preamble not readable in isolation; harder to document with JSDoc.
|
|
60
|
+
- **Boundary:** Inside `buildSystemPrompt()`, same as today.
|
|
61
|
+
- **Failure mode:** If session state tag position shifts, tests break. Manageable.
|
|
62
|
+
- **Repo pattern:** Departs -- existing code has no named constant but the soul-template.ts extraction is a precedent.
|
|
63
|
+
- **Gains:** No indirection. **Losses:** Not visible as a document; can't be verified without calling buildSystemPrompt.
|
|
64
|
+
- **Scope:** Too narrow.
|
|
65
|
+
|
|
66
|
+
#### Candidate B -- Named `BASE_SYSTEM_PROMPT` constant (recommended)
|
|
67
|
+
- **Summary:** Extract the static preamble into `export const BASE_SYSTEM_PROMPT: string` defined above `buildSystemPrompt()`. The function uses it as the first element of the lines array. JSDoc explains why it's a constant vs inline.
|
|
68
|
+
- **Tensions resolved:** Preamble visible as a document; existing test assertions pass; honors 'Document why not what'.
|
|
69
|
+
- **Accepted:** Slight indirection; tool name drift risk (prose mentions `report_issue`).
|
|
70
|
+
- **Boundary:** Module-scoped constant, not exported (callers use `buildSystemPrompt`). Dynamic content (session state, soul, workspace) remains in the function.
|
|
71
|
+
- **Failure mode:** If a future author edits `BASE_SYSTEM_PROMPT` and removes `'## Your tools'` or `'## Execution contract'`, tests catch it immediately.
|
|
72
|
+
- **Repo pattern:** Follows soul-template.ts precedent (constant for stable content, function for dynamic assembly).
|
|
73
|
+
- **Gains:** Readable, documentable, testable in isolation. **Losses:** None significant.
|
|
74
|
+
- **Scope:** Best-fit.
|
|
75
|
+
- **Philosophy:** Honors 'Immutability by default' (const), 'Document why not what' (JSDoc), 'Determinism' (pure function unchanged).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### Part 2: report_issue Tool
|
|
80
|
+
|
|
81
|
+
#### Candidate A -- Inline tool factory with private async helper (recommended)
|
|
82
|
+
- **Summary:** `makeReportIssueTool(sessionId: string, emitter?: DaemonEventEmitter): AgentTool` -- custom inline JSON schema (no schemas param), `execute()` fires a void Promise for the JSONL write (same pattern as DaemonEventEmitter), emits `issue_reported` event, returns string confirmation. A module-level private `appendIssueAsync()` function handles the actual fs writes to keep execute() clean.
|
|
83
|
+
- **Tensions resolved:** Fire-and-forget (never blocks), type-safe kind/severity, YAGNI (no IssueStore class), natural tool-factory shape.
|
|
84
|
+
- **Accepted:** Less isolated unit-testability for the JSONL write path (no dirOverride). Extractable later.
|
|
85
|
+
- **Boundary:** Tool factory in `workflow-runner.ts` alongside make*Tool siblings. Private helper in same file.
|
|
86
|
+
- **Failure mode:** JSONL write fails silently. Accepted -- same contract as DaemonEventEmitter.
|
|
87
|
+
- **Repo pattern:** Follows tool factory pattern exactly (name, description, inputSchema, label, execute).
|
|
88
|
+
- **Gains:** Simple, correct, consistent with existing code. **Losses:** JSONL write not unit-testable in isolation today.
|
|
89
|
+
- **Scope:** Best-fit.
|
|
90
|
+
- **Philosophy:** Honors 'YAGNI with discipline', 'Exhaustiveness everywhere' (literal unions), 'Make illegal states unrepresentable', 'Observability as a constraint'.
|
|
91
|
+
|
|
92
|
+
#### Candidate B -- Dedicated `IssueStore` class (parallel to DaemonEventEmitter)
|
|
93
|
+
- **Summary:** Extract a class `IssueStore` with `append(sessionId, issue)` and `dirOverride` for tests. Injected into `makeReportIssueTool`. Separate file or same file.
|
|
94
|
+
- **Tensions resolved:** Full unit-testability with dirOverride, separation of concerns.
|
|
95
|
+
- **Accepted:** Over-engineering for current scope (coordinator doesn't exist). YAGNI violated.
|
|
96
|
+
- **Boundary:** New abstraction layer. Only one caller today.
|
|
97
|
+
- **Failure mode:** Premature abstraction; adds complexity with no current benefit.
|
|
98
|
+
- **Repo pattern:** Matches DaemonEventEmitter exactly -- but DaemonEventEmitter was built when multiple callers needed it immediately.
|
|
99
|
+
- **Gains:** Testable in isolation, clean interface for future coordinator. **Losses:** Speculative complexity today.
|
|
100
|
+
- **Scope:** Too broad (no coordinator in this PR).
|
|
101
|
+
- **Philosophy:** Conflicts with 'YAGNI with discipline'. Honors 'Prefer fakes over mocks'.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Comparison and Recommendation
|
|
106
|
+
|
|
107
|
+
### Part 1
|
|
108
|
+
Candidate B (BASE_SYSTEM_PROMPT constant) dominates on every axis. Not a close call.
|
|
109
|
+
|
|
110
|
+
### Part 2
|
|
111
|
+
Candidate A (inline tool factory) wins on YAGNI. The only real loss is JSONL write isolation in tests -- acceptable since the write is purely observational (fire-and-forget), not correctness-affecting.
|
|
112
|
+
|
|
113
|
+
**If the auto-fix coordinator were being built in this PR**, Candidate B would be justified. It isn't.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Self-Critique
|
|
118
|
+
|
|
119
|
+
### Part 1 strongest counter-argument
|
|
120
|
+
'The constant is 55 lines and clutters the file.' Counter: it's in a clearly labeled `## System prompt` section. A 55-line constant is readable. The alternative (55 lines of array items with string literals) is worse. Not a real objection.
|
|
121
|
+
|
|
122
|
+
### Part 2 strongest counter-argument
|
|
123
|
+
'DaemonEventEmitter is a class -- to be consistent, IssueStore should also be a class.' True in the abstract. But DaemonEventEmitter was built when the event stream was a first-class concern. Issue recording is secondary observability for a future feature. The consistency argument applies when there are multiple callers, not one.
|
|
124
|
+
|
|
125
|
+
### Pivot conditions
|
|
126
|
+
- **Part 2:** If the coordinator PR is planned for this sprint, extract IssueStore now to save rework later.
|
|
127
|
+
- **Part 1:** If the BASE_SYSTEM_PROMPT constant is exported and used in tests directly, add it to the test file's import.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Open Questions for the Main Agent
|
|
132
|
+
|
|
133
|
+
1. Should `BASE_SYSTEM_PROMPT` be exported (for tests to import directly) or left module-private? Current test assertions only check the output of `buildSystemPrompt()`, so private is sufficient.
|
|
134
|
+
2. Should `IssueReportedEvent` include `continueToken` (for coordinator to resume)? The spec says optional -- include it as `readonly continueToken?: string`.
|
|
135
|
+
3. The new preamble's `## Your tools` section should list `report_issue` -- does it also list `Bash`, `Read`, `Write`, `continue_workflow`? Yes, for completeness.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# WorkTrain: System Prompt Preamble + report_issue Tool -- Design Review Findings
|
|
2
|
+
|
|
3
|
+
## Tradeoff Review
|
|
4
|
+
|
|
5
|
+
| Tradeoff | Assessment |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Tool name drift: preamble prose mentions `report_issue` | Acceptable. Prose is guidance, not tool registration. Agent uses the tools array. |
|
|
8
|
+
| JSONL write not unit-testable in isolation | **Resolved** by adding `issuesDirOverride?: string` to `makeReportIssueTool`. |
|
|
9
|
+
| No IssueStore class (YAGNI) | Acceptable. Coordinator doesn't exist in this PR. Extractable later. |
|
|
10
|
+
| Fatal severity doesn't abort the loop | Accepted limitation. Tool returns instructional text. Agent is told to stop. Recording the issue is the priority. |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Failure Mode Review
|
|
15
|
+
|
|
16
|
+
| Failure Mode | Handling | Risk |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| Missing issues dir | `mkdir recursive` before appendFile (DaemonEventEmitter pattern) | Low |
|
|
19
|
+
| Preamble loses required test strings | Caught immediately by existing vitest assertions | Low (self-healing) |
|
|
20
|
+
| Agent ignores fatal severity and continues | Instructional return value. Can't force stop from inside tool without violating 'errors are data'. | Medium (accepted) |
|
|
21
|
+
| sessionId is process-local UUID not server ID | Consistent with all other tools in this file. Coordinator correlates via sessionId. | Low |
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Runner-Up / Simpler Alternative Review
|
|
26
|
+
|
|
27
|
+
**Part 1:** Inline preamble (runner-up) offers no advantages over the named constant. Candidate B dominates.
|
|
28
|
+
|
|
29
|
+
**Part 2:** IssueStore class (runner-up) offers `dirOverride` for isolated testing. A hybrid was adopted: add `issuesDirOverride?: string` parameter to `makeReportIssueTool` without extracting a full class. This resolves the testability weakness at negligible complexity cost.
|
|
30
|
+
|
|
31
|
+
**Simpler alternative (no dirOverride):** Technically satisfies acceptance criteria. Rejected because it violates 'prefer fakes over mocks' and departs from the established DaemonEventEmitter precedent for no benefit.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Philosophy Alignment
|
|
36
|
+
|
|
37
|
+
| Principle | Status |
|
|
38
|
+
|---|---|
|
|
39
|
+
| Immutability by default | Satisfied: `const BASE_SYSTEM_PROMPT`; readonly IssueReportedEvent fields |
|
|
40
|
+
| Make illegal states unrepresentable | Satisfied: `kind` and `severity` are literal union types |
|
|
41
|
+
| Exhaustiveness everywhere | Satisfied: new DaemonEvent variant triggers TS compile error on unhandled switch |
|
|
42
|
+
| Errors are data | Satisfied: report_issue returns string confirmation, never throws |
|
|
43
|
+
| YAGNI with discipline | Satisfied: no IssueStore class |
|
|
44
|
+
| Observability as a constraint | Satisfied: fire-and-forget, never blocks correctness |
|
|
45
|
+
| Prefer fakes over mocks | Satisfied (after hybrid): issuesDirOverride allows temp-dir testing |
|
|
46
|
+
| Document why not what | Satisfied: JSDoc on BASE_SYSTEM_PROMPT constant; WHY comments on void+catch |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Findings
|
|
51
|
+
|
|
52
|
+
### Yellow (low-risk, note for implementer)
|
|
53
|
+
|
|
54
|
+
**Y1:** `BASE_SYSTEM_PROMPT` must include `'You are WorkRail Auto'`, `'## Your tools'`, and `'## Execution contract'` to preserve existing test assertions. The implementer must verify these strings survive the rewrite verbatim.
|
|
55
|
+
|
|
56
|
+
**Y2:** The new preamble's `## Your tools` section should list all 5 tools: `continue_workflow`, `Bash`, `Read`, `Write`, `report_issue`. If any tool is added/removed in the future, the preamble prose becomes stale. Accepted documentation-drift risk.
|
|
57
|
+
|
|
58
|
+
**Y3:** The `issuesDirOverride` parameter changes the signature of `makeReportIssueTool`. Wire it through `runWorkflow()` with `undefined` (production path). A test for the file write path should be added to `tests/unit/`.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Recommended Revisions
|
|
63
|
+
|
|
64
|
+
1. **Add `issuesDirOverride?: string` to `makeReportIssueTool`** -- adopt the hybrid over inline-only. (Already decided above.)
|
|
65
|
+
2. **Use `void appendIssueAsync(...).catch(() => {})` idiom** -- identical to DaemonEventEmitter._append; don't await.
|
|
66
|
+
3. **IssueReportedEvent fields:** Use `readonly` on all fields; `issueKind` not `kind` for the payload (since `kind` is the discriminant `'issue_reported'`). Wait -- looking at the spec: the tool's input schema field is `kind` (the issue type), but the event discriminant is also `kind: 'issue_reported'`. Rename the payload field to `issueKind` in `IssueReportedEvent` to avoid shadowing, while keeping the JSON input schema field as `kind` (per spec).
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Residual Concerns
|
|
71
|
+
|
|
72
|
+
- **Fatal severity + agent continuation:** The design cannot enforce stopping. This is a known limitation of the instruction-based approach. The coordinator will need to detect "fatal issue recorded, then agent continued for N more steps" and flag it. Out of scope for this PR.
|
|
73
|
+
- **Issue file format:** The spec says 'JSON line' but doesn't specify the schema. The implementer should include all input fields plus `ts: Date.now()` and `sessionId` for correlation -- consistent with how daemon events are written.
|