@atolis-hq/wake 0.1.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/LICENSE +201 -0
- package/README.md +140 -0
- package/dist/src/adapters/claude/claude-runner.js +388 -0
- package/dist/src/adapters/claude/prompt-templates.js +57 -0
- package/dist/src/adapters/codex/codex-runner.js +391 -0
- package/dist/src/adapters/cursor/cursor-runner.js +352 -0
- package/dist/src/adapters/docker/docker-cli.js +97 -0
- package/dist/src/adapters/fake/fake-artifact-verifier.js +14 -0
- package/dist/src/adapters/fake/fake-github-pull-request-activity-source.js +73 -0
- package/dist/src/adapters/fake/fake-resource-index.js +21 -0
- package/dist/src/adapters/fake/fake-runner.js +22 -0
- package/dist/src/adapters/fake/fake-ticketing-system.js +166 -0
- package/dist/src/adapters/fake/fake-workspace-manager.js +27 -0
- package/dist/src/adapters/fs/resource-index.js +84 -0
- package/dist/src/adapters/fs/self-update-ledger.js +17 -0
- package/dist/src/adapters/fs/state-store.js +364 -0
- package/dist/src/adapters/git/git-workspace-manager.js +168 -0
- package/dist/src/adapters/github/github-artifact-verifier.js +38 -0
- package/dist/src/adapters/github/github-auth.js +16 -0
- package/dist/src/adapters/github/github-client.js +100 -0
- package/dist/src/adapters/github/github-issues-work-source.js +410 -0
- package/dist/src/adapters/github/github-pull-request-activity-source.js +263 -0
- package/dist/src/adapters/http/ui-assets.js +302 -0
- package/dist/src/adapters/http/ui-data.js +389 -0
- package/dist/src/adapters/http/ui-server.js +151 -0
- package/dist/src/adapters/runner/cli-command.js +43 -0
- package/dist/src/adapters/runner/prompt-templates.js +76 -0
- package/dist/src/adapters/runner/runner-cli-adapter.js +112 -0
- package/dist/src/adapters/runner/runner-registry.js +141 -0
- package/dist/src/adapters/runner/stage-prompt.js +256 -0
- package/dist/src/adapters/runner/transcripts.js +25 -0
- package/dist/src/cli/correlate-command.js +62 -0
- package/dist/src/cli/init-command.js +11 -0
- package/dist/src/cli/locks-command.js +19 -0
- package/dist/src/cli/sandbox-command.js +191 -0
- package/dist/src/cli/sandbox-logging.js +30 -0
- package/dist/src/cli/sandbox-resume.js +77 -0
- package/dist/src/cli/scaffold-assets.js +173 -0
- package/dist/src/cli/self-update-command.js +158 -0
- package/dist/src/cli/startup-preflight.js +127 -0
- package/dist/src/cli/stop-command.js +42 -0
- package/dist/src/cli/ui-command.js +27 -0
- package/dist/src/config/defaults.js +11 -0
- package/dist/src/config/load-config.js +26 -0
- package/dist/src/core/contracts.js +1 -0
- package/dist/src/core/control-plane.js +127 -0
- package/dist/src/core/lifecycle-service.js +8 -0
- package/dist/src/core/policy-engine.js +200 -0
- package/dist/src/core/projection-updater.js +438 -0
- package/dist/src/core/quota-backoff.js +69 -0
- package/dist/src/core/sink-router.js +85 -0
- package/dist/src/core/tick-runner.js +1347 -0
- package/dist/src/domain/branch-naming.js +14 -0
- package/dist/src/domain/resource-uri.js +38 -0
- package/dist/src/domain/runner-routing.js +108 -0
- package/dist/src/domain/schema.js +685 -0
- package/dist/src/domain/sources.js +16 -0
- package/dist/src/domain/stages.js +39 -0
- package/dist/src/domain/types.js +1 -0
- package/dist/src/domain/workflows.js +83 -0
- package/dist/src/lib/clock.js +5 -0
- package/dist/src/lib/event-log.js +21 -0
- package/dist/src/lib/json-file.js +23 -0
- package/dist/src/lib/lock.js +118 -0
- package/dist/src/lib/paths.js +44 -0
- package/dist/src/lib/work-id.js +19 -0
- package/dist/src/main.js +523 -0
- package/dist/src/version.js +1 -0
- package/docker/Dockerfile +54 -0
- package/docker/entrypoint.sh +75 -0
- package/docker/log-command.sh +107 -0
- package/docker/setup.sh +72 -0
- package/package.json +52 -0
- package/prompts/implement.md +49 -0
- package/prompts/refine.md +44 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { runnerSentinelValues, } from './stages.js';
|
|
6
|
+
import { correlationProvenanceSchema, correlationRelationSchema, correlationRoleSchema, resourceUriSchema, } from './resource-uri.js';
|
|
7
|
+
const isoTimestampSchema = z.string().datetime({ offset: true });
|
|
8
|
+
const identifierSchema = z.string().min(1);
|
|
9
|
+
export const reportedArtifactSchema = z.object({
|
|
10
|
+
kind: z.literal('pr'),
|
|
11
|
+
url: z.string().url(),
|
|
12
|
+
});
|
|
13
|
+
export const wakeArtifactsEnvelopeSchema = z.object({
|
|
14
|
+
artifacts: z.array(reportedArtifactSchema).default([]),
|
|
15
|
+
});
|
|
16
|
+
export const runnerSentinelSchema = z.enum(runnerSentinelValues);
|
|
17
|
+
export const defaultAgentIdentity = 'Wake';
|
|
18
|
+
export const defaultSmokePrompt = `This is ${defaultAgentIdentity}, reply with "hi ${defaultAgentIdentity} only"`;
|
|
19
|
+
const runnerFailureClassSchema = z.enum(['task', 'quota', 'infra']);
|
|
20
|
+
const modelOverridesSchema = z.object({
|
|
21
|
+
default: z.string().optional(),
|
|
22
|
+
}).catchall(z.string()).default({});
|
|
23
|
+
const claudeEffortSchema = z.enum(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
24
|
+
const codexReasoningEffortSchema = z.enum(['low', 'medium', 'high']);
|
|
25
|
+
const cursorModeSchema = z.enum(['ask', 'agent']);
|
|
26
|
+
const claudeRunnerSettingsSchema = z.object({
|
|
27
|
+
command: z.string().default('claude'),
|
|
28
|
+
model: z.string().default('haiku'),
|
|
29
|
+
smokeModel: z.string().default('haiku'),
|
|
30
|
+
sessionName: z.string().default(defaultAgentIdentity),
|
|
31
|
+
remoteControlName: z.string().default(defaultAgentIdentity),
|
|
32
|
+
smokePrompt: z.string().default(defaultSmokePrompt),
|
|
33
|
+
timeoutMs: z.number().int().positive().default(30 * 60 * 1000),
|
|
34
|
+
remoteControl: z.object({
|
|
35
|
+
enabled: z.boolean().default(false),
|
|
36
|
+
}).default({ enabled: false }),
|
|
37
|
+
models: modelOverridesSchema.default({ default: 'haiku', implement: 'claude-sonnet-4-6' }),
|
|
38
|
+
effort: claudeEffortSchema.optional(),
|
|
39
|
+
});
|
|
40
|
+
const codexRunnerSettingsSchema = z.object({
|
|
41
|
+
command: z.string().default('codex'),
|
|
42
|
+
model: z.string().default('gpt-5.5'),
|
|
43
|
+
smokeModel: z.string().default('gpt-5.4-mini'),
|
|
44
|
+
smokePrompt: z.string().default(defaultSmokePrompt),
|
|
45
|
+
timeoutMs: z.number().int().positive().default(30 * 60 * 1000),
|
|
46
|
+
models: modelOverridesSchema.default({ default: 'gpt-5.5', implement: 'gpt-5.5' }),
|
|
47
|
+
reasoningEffort: codexReasoningEffortSchema.optional(),
|
|
48
|
+
});
|
|
49
|
+
const fakeRunnerEntrySchema = z.object({
|
|
50
|
+
kind: z.literal('fake'),
|
|
51
|
+
cli: z.string().default('Fake'),
|
|
52
|
+
});
|
|
53
|
+
const claudeRunnerEntrySchema = claudeRunnerSettingsSchema.extend({
|
|
54
|
+
kind: z.literal('claude'),
|
|
55
|
+
});
|
|
56
|
+
const codexRunnerEntrySchema = codexRunnerSettingsSchema.extend({
|
|
57
|
+
kind: z.literal('codex'),
|
|
58
|
+
});
|
|
59
|
+
const cursorRunnerSettingsSchema = z.object({
|
|
60
|
+
command: z.string().default('cursor'),
|
|
61
|
+
model: z.string().default('composer-2.5'),
|
|
62
|
+
smokeModel: z.string().default('auto'),
|
|
63
|
+
smokePrompt: z.string().default(defaultSmokePrompt),
|
|
64
|
+
timeoutMs: z.number().int().positive().default(30 * 60 * 1000),
|
|
65
|
+
models: modelOverridesSchema.default({ default: 'composer-2.5', implement: 'composer-2.5' }),
|
|
66
|
+
defaultMode: cursorModeSchema.optional(),
|
|
67
|
+
});
|
|
68
|
+
const cursorRunnerEntrySchema = cursorRunnerSettingsSchema.extend({
|
|
69
|
+
kind: z.literal('cursor'),
|
|
70
|
+
});
|
|
71
|
+
const runnerEntrySchema = z.discriminatedUnion('kind', [
|
|
72
|
+
fakeRunnerEntrySchema,
|
|
73
|
+
claudeRunnerEntrySchema,
|
|
74
|
+
codexRunnerEntrySchema,
|
|
75
|
+
cursorRunnerEntrySchema,
|
|
76
|
+
]);
|
|
77
|
+
const stageRouteSchema = z.object({
|
|
78
|
+
action: identifierSchema.optional(),
|
|
79
|
+
tier: z.string().optional(),
|
|
80
|
+
runner: z.string().optional(),
|
|
81
|
+
});
|
|
82
|
+
const runnerRoutingSchema = z.object({
|
|
83
|
+
runnerName: z.string(),
|
|
84
|
+
runnerKind: z.enum(['fake', 'claude', 'codex', 'cursor']),
|
|
85
|
+
tier: z.string().optional(),
|
|
86
|
+
reason: z.string(),
|
|
87
|
+
});
|
|
88
|
+
const sinkEntrySchema = z.object({
|
|
89
|
+
kind: z.string().min(1),
|
|
90
|
+
subscribe: z.array(z.string().min(1)).default([]),
|
|
91
|
+
}).passthrough();
|
|
92
|
+
export const wakeResultEnvelopeSchema = z.object({
|
|
93
|
+
status: runnerSentinelSchema,
|
|
94
|
+
});
|
|
95
|
+
const stageHistoryEntrySchema = z.object({
|
|
96
|
+
stage: identifierSchema,
|
|
97
|
+
changedAt: isoTimestampSchema,
|
|
98
|
+
reason: z.string(),
|
|
99
|
+
});
|
|
100
|
+
const reviewThreadAnchorSchema = z.object({
|
|
101
|
+
path: z.string(),
|
|
102
|
+
line: z.number().int().positive().optional(),
|
|
103
|
+
});
|
|
104
|
+
const commentSnapshotSchema = z.object({
|
|
105
|
+
id: z.string(),
|
|
106
|
+
body: z.string(),
|
|
107
|
+
author: z.object({
|
|
108
|
+
login: z.string(),
|
|
109
|
+
}),
|
|
110
|
+
createdAt: isoTimestampSchema,
|
|
111
|
+
updatedAt: isoTimestampSchema,
|
|
112
|
+
isBotAuthored: z.boolean().default(false),
|
|
113
|
+
// Which correlated surface this comment came from; absent = the originating
|
|
114
|
+
// issue thread, keeping every pre-existing comment valid under this schema.
|
|
115
|
+
resourceUri: resourceUriSchema.optional(),
|
|
116
|
+
reviewThread: reviewThreadAnchorSchema.optional(),
|
|
117
|
+
});
|
|
118
|
+
const issueSnapshotSchema = z.object({
|
|
119
|
+
repo: z.string(),
|
|
120
|
+
number: z.number().int().positive(),
|
|
121
|
+
title: z.string(),
|
|
122
|
+
body: z.string(),
|
|
123
|
+
labels: z.array(z.string()),
|
|
124
|
+
assignees: z.array(z.string()),
|
|
125
|
+
isPullRequest: z.boolean().default(false),
|
|
126
|
+
state: z.enum(['open', 'closed']),
|
|
127
|
+
url: z.string().url(),
|
|
128
|
+
createdAt: isoTimestampSchema,
|
|
129
|
+
updatedAt: isoTimestampSchema,
|
|
130
|
+
});
|
|
131
|
+
export const sourceStateRecordSchema = z.object({
|
|
132
|
+
schemaVersion: z.literal(1),
|
|
133
|
+
source: z.string(),
|
|
134
|
+
key: z.string(),
|
|
135
|
+
lastSuccessfulPollAt: isoTimestampSchema,
|
|
136
|
+
});
|
|
137
|
+
export const eventEnvelopeSourceRefsSchema = z.object({
|
|
138
|
+
repo: z.string().optional(),
|
|
139
|
+
issueNumber: z.number().int().positive().optional(),
|
|
140
|
+
commentId: z.string().optional(),
|
|
141
|
+
reviewId: z.string().optional(),
|
|
142
|
+
runId: z.string().optional(),
|
|
143
|
+
sink: z.string().optional(),
|
|
144
|
+
sourceUrl: z.string().optional(),
|
|
145
|
+
// Per-event provenance: which external resource this one event came from.
|
|
146
|
+
// Item-level ownership lives only in the correlation registry, never here.
|
|
147
|
+
resourceUri: resourceUriSchema.optional(),
|
|
148
|
+
// An adapter-supplied hint: the resourceUri of the resource this one
|
|
149
|
+
// "belongs to" for correlation purposes (e.g. a PR review-thread comment's
|
|
150
|
+
// parent PR). Opaque to core — never parsed, only used as a fallback key
|
|
151
|
+
// for resourceIndex.resolve() when resourceUri itself misses the index.
|
|
152
|
+
// The adapter that emits an event is the only party allowed to know its
|
|
153
|
+
// own locator grammar; this keeps that knowledge out of core/.
|
|
154
|
+
parentResourceUri: resourceUriSchema.optional(),
|
|
155
|
+
});
|
|
156
|
+
/** Event type constants for the correlation registry (ADR 0001 §5). */
|
|
157
|
+
export const WORK_ITEM_CREATED_EVENT = 'wake.workitem.created';
|
|
158
|
+
export const CORRELATION_REGISTERED_EVENT = 'wake.correlation.registered';
|
|
159
|
+
export const CORRELATION_RETRACTED_EVENT = 'wake.correlation.retracted';
|
|
160
|
+
export const CORRELATION_PRIMARY_CONFLICT_EVENT = 'wake.correlation.primary-conflict';
|
|
161
|
+
/**
|
|
162
|
+
* Shared key for events whose resource failed mint qualification (spec D1').
|
|
163
|
+
* Never a real work item: `readIssueState(UNRESOLVED_WORK_ITEM_KEY)` always
|
|
164
|
+
* returns null, so these events are durable and inspectable via the event
|
|
165
|
+
* log but must never materialize a projection or an index entry. Exported
|
|
166
|
+
* from here (rather than only living in tick-runner.ts, which mints these
|
|
167
|
+
* events) so projection-updater.ts's fold can also recognize and skip it —
|
|
168
|
+
* an unqualified event's sourceEventType is still `ticket.upsert` /
|
|
169
|
+
* `fake.issue.upsert` / etc., which the fold would otherwise happily turn
|
|
170
|
+
* into a projection the first time it sees this key with no current record.
|
|
171
|
+
*/
|
|
172
|
+
export const UNRESOLVED_WORK_ITEM_KEY = 'unresolved';
|
|
173
|
+
// The envelope's `workItemKey` already carries the identity; this payload is
|
|
174
|
+
// deliberately empty.
|
|
175
|
+
export const workItemCreatedPayloadSchema = z.object({});
|
|
176
|
+
export const correlationRegisteredPayloadSchema = z.object({
|
|
177
|
+
resourceUri: resourceUriSchema,
|
|
178
|
+
role: correlationRoleSchema,
|
|
179
|
+
relation: correlationRelationSchema,
|
|
180
|
+
provenance: correlationProvenanceSchema,
|
|
181
|
+
registeredBy: z.string().optional(),
|
|
182
|
+
});
|
|
183
|
+
export const correlationRetractedPayloadSchema = z.object({
|
|
184
|
+
resourceUri: resourceUriSchema,
|
|
185
|
+
});
|
|
186
|
+
// Warning event emitted when a second `primary` registration lands on an
|
|
187
|
+
// already-claimed URI (ADR 0001 §6). Fold behavior lives in the projection.
|
|
188
|
+
export const correlationPrimaryConflictPayloadSchema = z.object({
|
|
189
|
+
resourceUri: resourceUriSchema,
|
|
190
|
+
incumbentWorkItemKey: z.string(),
|
|
191
|
+
});
|
|
192
|
+
// Folded shape of a `wake.correlation.registered`/`wake.correlation.retracted`
|
|
193
|
+
// event, one entry per resourceUri currently held by this work item
|
|
194
|
+
// (ADR 0001 §5). `registeredAt` comes from the folding event's `occurredAt`.
|
|
195
|
+
export const correlatedResourceSchema = z.object({
|
|
196
|
+
resourceUri: resourceUriSchema,
|
|
197
|
+
role: correlationRoleSchema,
|
|
198
|
+
relation: correlationRelationSchema,
|
|
199
|
+
provenance: correlationProvenanceSchema,
|
|
200
|
+
registeredBy: z.string().optional(),
|
|
201
|
+
registeredAt: isoTimestampSchema,
|
|
202
|
+
});
|
|
203
|
+
export const eventEnvelopeSchema = z.object({
|
|
204
|
+
schemaVersion: z.literal(1),
|
|
205
|
+
eventId: z.string(),
|
|
206
|
+
workItemKey: z.string(),
|
|
207
|
+
streamScope: z.enum(['global-intake', 'work-item']),
|
|
208
|
+
direction: z.enum(['inbound', 'outbound', 'internal']),
|
|
209
|
+
sourceSystem: z.string(),
|
|
210
|
+
sourceEventType: z.string(),
|
|
211
|
+
sourceRefs: eventEnvelopeSourceRefsSchema,
|
|
212
|
+
occurredAt: isoTimestampSchema,
|
|
213
|
+
ingestedAt: isoTimestampSchema,
|
|
214
|
+
trigger: z.enum(['immediate', 'context-only']),
|
|
215
|
+
payload: z.record(z.string(), z.unknown()),
|
|
216
|
+
raw: z.record(z.string(), z.unknown()).optional(),
|
|
217
|
+
derivedHints: z.record(z.string(), z.unknown()).optional(),
|
|
218
|
+
});
|
|
219
|
+
export const issueStateRecordSchema = z.object({
|
|
220
|
+
schemaVersion: z.literal(1),
|
|
221
|
+
workItemKey: z.string(),
|
|
222
|
+
origin: z.string().optional(),
|
|
223
|
+
issue: issueSnapshotSchema,
|
|
224
|
+
comments: z.array(commentSnapshotSchema).default([]),
|
|
225
|
+
latestComment: commentSnapshotSchema.optional(),
|
|
226
|
+
wake: z.object({
|
|
227
|
+
stage: identifierSchema,
|
|
228
|
+
lastRunId: z.string().optional(),
|
|
229
|
+
sessionId: z.string().optional(),
|
|
230
|
+
sessionCli: z.string().optional(),
|
|
231
|
+
workspacePath: z.string().optional(),
|
|
232
|
+
blockReason: z.string().optional(),
|
|
233
|
+
syncedAt: isoTimestampSchema,
|
|
234
|
+
stageHistory: z.array(stageHistoryEntrySchema),
|
|
235
|
+
recentEventIds: z.array(z.string()).default([]),
|
|
236
|
+
expectedEcho: z.object({
|
|
237
|
+
commentIds: z.array(z.string()).default([]),
|
|
238
|
+
labels: z.array(z.string()).default([]),
|
|
239
|
+
}).default({ commentIds: [], labels: [] }),
|
|
240
|
+
}),
|
|
241
|
+
context: z.record(z.string(), z.unknown()).default({}),
|
|
242
|
+
correlatedResources: z.array(correlatedResourceSchema).default([]),
|
|
243
|
+
});
|
|
244
|
+
const runTokenUsageSchema = z.object({
|
|
245
|
+
inputTokens: z.number().nonnegative(),
|
|
246
|
+
outputTokens: z.number().nonnegative(),
|
|
247
|
+
cacheCreationInputTokens: z.number().nonnegative().optional(),
|
|
248
|
+
cacheReadInputTokens: z.number().nonnegative().optional(),
|
|
249
|
+
costUsd: z.number().nonnegative().optional(),
|
|
250
|
+
turns: z.number().nonnegative().optional(),
|
|
251
|
+
});
|
|
252
|
+
export const runRecordSchema = z.object({
|
|
253
|
+
schemaVersion: z.literal(1),
|
|
254
|
+
runId: z.string(),
|
|
255
|
+
// The work item this run belongs to. Required: run records are Wake-owned
|
|
256
|
+
// state that Wake itself writes, so the work id is always in hand at the
|
|
257
|
+
// write site, and an optional key would lie about runtime while re-admitting
|
|
258
|
+
// the ticket-shaped ambiguity minted identity exists to remove.
|
|
259
|
+
workItemKey: z.string(),
|
|
260
|
+
// Human-readable representation content only — the ticket this run was
|
|
261
|
+
// launched against (same reasoning as the projection's retained `issue`
|
|
262
|
+
// snapshot, spec §9). Never used to find the work item: a transferred issue
|
|
263
|
+
// gets a new repo/number while the work item and its runs persist (spec D3).
|
|
264
|
+
repo: z.string(),
|
|
265
|
+
issueNumber: z.number().int().positive(),
|
|
266
|
+
action: identifierSchema,
|
|
267
|
+
status: z.enum(['running', 'completed', 'awaiting-approval', 'blocked', 'failed', 'superseded']),
|
|
268
|
+
startedAt: isoTimestampSchema,
|
|
269
|
+
finishedAt: isoTimestampSchema.optional(),
|
|
270
|
+
sessionId: z.string().optional(),
|
|
271
|
+
sentinel: runnerSentinelSchema.optional(),
|
|
272
|
+
summary: z.string().optional(),
|
|
273
|
+
routing: runnerRoutingSchema.optional(),
|
|
274
|
+
tokenUsage: runTokenUsageSchema.optional(),
|
|
275
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
276
|
+
});
|
|
277
|
+
const runnerHealthEntrySchema = z.object({
|
|
278
|
+
pausedUntil: isoTimestampSchema.optional(),
|
|
279
|
+
// 'reported' = CLI told us the real reset time; 'estimated' = exponential
|
|
280
|
+
// backoff guess. Only 'estimated' pauses are eligible for an early recovery
|
|
281
|
+
// probe (domain/runner-routing.ts) - a reported time is already trustworthy.
|
|
282
|
+
pausedUntilSource: z.enum(['reported', 'estimated']).optional(),
|
|
283
|
+
failureCount: z.number().int().nonnegative().default(0),
|
|
284
|
+
lastFailureAt: isoTimestampSchema.optional(),
|
|
285
|
+
});
|
|
286
|
+
const workflowWorkspaceSchema = z.enum(['none', 'read-only', 'branch']);
|
|
287
|
+
const workflowStageSchema = stageRouteSchema.extend({
|
|
288
|
+
workspace: workflowWorkspaceSchema,
|
|
289
|
+
onDone: identifierSchema,
|
|
290
|
+
});
|
|
291
|
+
const workflowDefinitionSchema = z.object({
|
|
292
|
+
entryStage: identifierSchema.optional(),
|
|
293
|
+
stages: z.record(identifierSchema, workflowStageSchema),
|
|
294
|
+
});
|
|
295
|
+
function defaultPromptsRoot() {
|
|
296
|
+
const sourceDir = dirname(fileURLToPath(import.meta.url));
|
|
297
|
+
for (const candidate of [
|
|
298
|
+
join(process.cwd(), 'prompts'),
|
|
299
|
+
join(sourceDir, '..', '..', 'prompts'),
|
|
300
|
+
join(sourceDir, '..', '..', '..', 'prompts'),
|
|
301
|
+
]) {
|
|
302
|
+
if (existsSync(candidate)) {
|
|
303
|
+
return candidate;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
let dir = sourceDir;
|
|
307
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
308
|
+
if (existsSync(join(dir, 'package.json'))) {
|
|
309
|
+
return join(dir, 'prompts');
|
|
310
|
+
}
|
|
311
|
+
const parent = dirname(dir);
|
|
312
|
+
if (parent === dir) {
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
dir = parent;
|
|
316
|
+
}
|
|
317
|
+
return join(process.cwd(), 'prompts');
|
|
318
|
+
}
|
|
319
|
+
function promptExists(promptsRoot, stageOrAction) {
|
|
320
|
+
return (existsSync(join(promptsRoot, `${stageOrAction}.md`)) ||
|
|
321
|
+
existsSync(join(promptsRoot, `${stageOrAction}.start.md`)) ||
|
|
322
|
+
existsSync(join(promptsRoot, `${stageOrAction}.resume.md`)));
|
|
323
|
+
}
|
|
324
|
+
function canReachDone(start, stages) {
|
|
325
|
+
const seen = new Set();
|
|
326
|
+
let current = start;
|
|
327
|
+
while (current !== 'done') {
|
|
328
|
+
if (seen.has(current)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
seen.add(current);
|
|
332
|
+
const next = stages[current]?.onDone;
|
|
333
|
+
if (next === undefined) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
current = next;
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
export const ledgerSchema = z.object({
|
|
341
|
+
schemaVersion: z.literal(1),
|
|
342
|
+
// Legacy single-runner pause fields, superseded by `runners` (per-runner
|
|
343
|
+
// health, #67). Kept read-only so ledgers written before the fallback/rotation
|
|
344
|
+
// change still parse; nothing writes them anymore.
|
|
345
|
+
pausedUntil: isoTimestampSchema.optional(),
|
|
346
|
+
quotaFailureCount: z.number().int().nonnegative().optional(),
|
|
347
|
+
lastQuotaFailureAt: isoTimestampSchema.optional(),
|
|
348
|
+
runners: z.record(z.string(), runnerHealthEntrySchema).default({}),
|
|
349
|
+
});
|
|
350
|
+
export const wakeConfigSchema = z.object({
|
|
351
|
+
schemaVersion: z.literal(1).default(1),
|
|
352
|
+
paths: z.object({
|
|
353
|
+
wakeRoot: z.string(),
|
|
354
|
+
promptsRoot: z.string().optional(),
|
|
355
|
+
}),
|
|
356
|
+
sandbox: z.object({
|
|
357
|
+
image: z.string().min(1).default('wake-sandbox'),
|
|
358
|
+
// Base image name (no tag) that `wake sandbox self-update` appends a
|
|
359
|
+
// release tag to, e.g. "wake-sandbox:v0.0.80". `image` above stays the
|
|
360
|
+
// resolved ref that `build`/`up`/`update` actually run.
|
|
361
|
+
imageRepository: z.string().min(1).default('wake-sandbox'),
|
|
362
|
+
containerName: z.string().min(1).default('wake-sandbox'),
|
|
363
|
+
containerMountPath: z.string().min(1).default('/wake'),
|
|
364
|
+
containerHomeMountPath: z.string().min(1).default('/home/wake'),
|
|
365
|
+
start: z.object({
|
|
366
|
+
enabled: z.boolean().default(true),
|
|
367
|
+
}).default({ enabled: true }),
|
|
368
|
+
extraMounts: z.array(z.object({
|
|
369
|
+
source: z.string().min(1),
|
|
370
|
+
target: z.string().min(1),
|
|
371
|
+
readOnly: z.boolean().optional(),
|
|
372
|
+
})).default([]),
|
|
373
|
+
}).default({ image: 'wake-sandbox', imageRepository: 'wake-sandbox', containerName: 'wake-sandbox', containerMountPath: '/wake', containerHomeMountPath: '/home/wake', start: { enabled: true }, extraMounts: [] }),
|
|
374
|
+
dev: z.object({
|
|
375
|
+
repoRoot: z.string().optional(),
|
|
376
|
+
}).default({}),
|
|
377
|
+
scheduler: z.object({
|
|
378
|
+
intervalMs: z.number().int().positive().default(60 * 1000),
|
|
379
|
+
// Cap for the idle-cadence backoff (#81): consecutive idle ticks double the
|
|
380
|
+
// sleep, up to this ceiling, so a quiet repo doesn't poll every intervalMs.
|
|
381
|
+
maxIntervalMs: z.number().int().positive().default(10 * 60 * 1000),
|
|
382
|
+
}).default({ intervalMs: 60 * 1000, maxIntervalMs: 10 * 60 * 1000 }),
|
|
383
|
+
transcripts: z.object({
|
|
384
|
+
enabled: z.boolean().default(false),
|
|
385
|
+
retainAfterWorkspaceCleanup: z.boolean().default(false),
|
|
386
|
+
}).default({ enabled: false, retainAfterWorkspaceCleanup: false }),
|
|
387
|
+
runners: z.record(z.string(), runnerEntrySchema).default({
|
|
388
|
+
fake: { kind: 'fake', cli: 'Fake' },
|
|
389
|
+
'claude-haiku': { kind: 'claude', command: 'claude', model: 'haiku', smokeModel: 'haiku', sessionName: defaultAgentIdentity, remoteControlName: defaultAgentIdentity, smokePrompt: defaultSmokePrompt, timeoutMs: 30 * 60 * 1000, remoteControl: { enabled: false }, models: { default: 'haiku' } },
|
|
390
|
+
'claude-opus': { kind: 'claude', command: 'claude', model: 'claude-opus-4-8', smokeModel: 'haiku', sessionName: defaultAgentIdentity, remoteControlName: defaultAgentIdentity, smokePrompt: defaultSmokePrompt, timeoutMs: 30 * 60 * 1000, remoteControl: { enabled: false }, models: { default: 'claude-opus-4-8' } },
|
|
391
|
+
'codex-mini': { kind: 'codex', command: 'codex', model: 'gpt-5.4-mini', smokeModel: 'gpt-5.4-mini', smokePrompt: defaultSmokePrompt, timeoutMs: 30 * 60 * 1000, models: { default: 'gpt-5.4-mini', implement: 'gpt-5.4-mini' } },
|
|
392
|
+
'codex-flagship': { kind: 'codex', command: 'codex', model: 'gpt-5.5', smokeModel: 'gpt-5.4-mini', smokePrompt: defaultSmokePrompt, timeoutMs: 30 * 60 * 1000, models: { default: 'gpt-5.5', implement: 'gpt-5.5' } },
|
|
393
|
+
'cursor-composer': { kind: 'cursor', command: 'cursor', model: 'composer-2.5', smokeModel: 'auto', smokePrompt: defaultSmokePrompt, timeoutMs: 30 * 60 * 1000, models: { default: 'composer-2.5', implement: 'composer-2.5' } },
|
|
394
|
+
}),
|
|
395
|
+
tiers: z.record(z.string(), z.array(z.string().min(1)).min(1)).default({
|
|
396
|
+
light: ['fake'],
|
|
397
|
+
standard: ['fake'],
|
|
398
|
+
deep: ['fake'],
|
|
399
|
+
}),
|
|
400
|
+
defaultTier: z.string().default('standard'),
|
|
401
|
+
workflows: z.record(identifierSchema, workflowDefinitionSchema).default({
|
|
402
|
+
default: {
|
|
403
|
+
stages: {
|
|
404
|
+
refine: {
|
|
405
|
+
action: 'refine',
|
|
406
|
+
workspace: 'read-only',
|
|
407
|
+
tier: 'light',
|
|
408
|
+
onDone: 'implement',
|
|
409
|
+
},
|
|
410
|
+
implement: {
|
|
411
|
+
action: 'implement',
|
|
412
|
+
workspace: 'branch',
|
|
413
|
+
tier: 'standard',
|
|
414
|
+
onDone: 'done',
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
}),
|
|
419
|
+
stages: z.record(z.string(), stageRouteSchema).default({
|
|
420
|
+
queue: { action: 'refine', tier: 'light' },
|
|
421
|
+
implement: { action: 'implement', tier: 'standard' },
|
|
422
|
+
}),
|
|
423
|
+
ui: z.object({
|
|
424
|
+
enabled: z.boolean().default(false),
|
|
425
|
+
port: z.number().int().positive().default(4317),
|
|
426
|
+
token: z.string().optional(),
|
|
427
|
+
tunnel: z.object({
|
|
428
|
+
enabled: z.boolean().default(false),
|
|
429
|
+
authToken: z.string().optional(),
|
|
430
|
+
}).default({ enabled: false }),
|
|
431
|
+
archiveFreshnessDays: z.number().int().nonnegative().default(5),
|
|
432
|
+
}).default({ enabled: false, port: 4317, tunnel: { enabled: false }, archiveFreshnessDays: 5 }),
|
|
433
|
+
sources: z.object({
|
|
434
|
+
github: z.object({
|
|
435
|
+
enabled: z.boolean().default(false),
|
|
436
|
+
repos: z.array(z.string().min(1)).default([]),
|
|
437
|
+
polling: z.object({
|
|
438
|
+
maxIssuesPerRepo: z.number().int().positive().default(25),
|
|
439
|
+
commentPageSize: z.number().int().positive().default(25),
|
|
440
|
+
lookbackMs: z.number().int().nonnegative().default(60_000),
|
|
441
|
+
}).default({ maxIssuesPerRepo: 25, commentPageSize: 25, lookbackMs: 60_000 }),
|
|
442
|
+
policy: z.object({
|
|
443
|
+
requiredLabels: z.array(z.string()).default([]),
|
|
444
|
+
ignoredLabels: z.array(z.string()).default([]),
|
|
445
|
+
requiredAssignees: z.array(z.string()).default([]),
|
|
446
|
+
}).default({ requiredLabels: [], ignoredLabels: [], requiredAssignees: [] }),
|
|
447
|
+
publication: z.object({
|
|
448
|
+
postStatusComments: z.boolean().default(true),
|
|
449
|
+
activeLabel: z.string().optional(),
|
|
450
|
+
}).default({ postStatusComments: true }),
|
|
451
|
+
pullRequests: z.object({
|
|
452
|
+
enabled: z.boolean().default(false),
|
|
453
|
+
maxPullRequestsPerRepo: z.number().int().positive().default(25),
|
|
454
|
+
commentPageSize: z.number().int().positive().default(25),
|
|
455
|
+
policy: z.object({
|
|
456
|
+
requiredAuthors: z.array(z.string()).default([]),
|
|
457
|
+
}).default({ requiredAuthors: [] }),
|
|
458
|
+
}).default({ enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, policy: { requiredAuthors: [] } }),
|
|
459
|
+
}).default({ enabled: false, repos: [], polling: { maxIssuesPerRepo: 25, commentPageSize: 25, lookbackMs: 60_000 }, policy: { requiredLabels: [], ignoredLabels: [], requiredAssignees: [] }, publication: { postStatusComments: true }, pullRequests: { enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, policy: { requiredAuthors: [] } } }),
|
|
460
|
+
}).default({ github: { enabled: false, repos: [], polling: { maxIssuesPerRepo: 25, commentPageSize: 25, lookbackMs: 60_000 }, policy: { requiredLabels: [], ignoredLabels: [], requiredAssignees: [] }, publication: { postStatusComments: true }, pullRequests: { enabled: false, maxPullRequestsPerRepo: 25, commentPageSize: 25, policy: { requiredAuthors: [] } } } }),
|
|
461
|
+
sinks: z.record(z.string(), sinkEntrySchema).default({}),
|
|
462
|
+
}).superRefine((config, ctx) => {
|
|
463
|
+
const promptsRoot = config.paths.promptsRoot ?? defaultPromptsRoot();
|
|
464
|
+
const workflowEntries = Object.entries(config.workflows);
|
|
465
|
+
if (workflowEntries.length === 0) {
|
|
466
|
+
ctx.addIssue({
|
|
467
|
+
code: z.ZodIssueCode.custom,
|
|
468
|
+
path: ['workflows'],
|
|
469
|
+
message: 'At least one workflow must be configured.',
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
for (const [workflowName, workflow] of workflowEntries) {
|
|
473
|
+
const stageNames = Object.keys(workflow.stages);
|
|
474
|
+
const stageSet = new Set(stageNames);
|
|
475
|
+
if (stageNames.length === 0) {
|
|
476
|
+
ctx.addIssue({
|
|
477
|
+
code: z.ZodIssueCode.custom,
|
|
478
|
+
path: ['workflows', workflowName, 'stages'],
|
|
479
|
+
message: 'Workflow must define at least one runnable stage.',
|
|
480
|
+
});
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
for (const reserved of ['queue', 'done']) {
|
|
484
|
+
if (stageSet.has(reserved)) {
|
|
485
|
+
ctx.addIssue({
|
|
486
|
+
code: z.ZodIssueCode.custom,
|
|
487
|
+
path: ['workflows', workflowName, 'stages', reserved],
|
|
488
|
+
message: `Workflow must not define implicit ${reserved} stage.`,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const actualEntryStage = workflow.entryStage ?? stageNames[0];
|
|
493
|
+
if (workflow.entryStage === 'queue') {
|
|
494
|
+
ctx.addIssue({
|
|
495
|
+
code: z.ZodIssueCode.custom,
|
|
496
|
+
path: ['workflows', workflowName, 'entryStage'],
|
|
497
|
+
message: 'Workflow entryStage must not be queue.',
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
if (actualEntryStage !== undefined && !stageSet.has(actualEntryStage)) {
|
|
501
|
+
ctx.addIssue({
|
|
502
|
+
code: z.ZodIssueCode.custom,
|
|
503
|
+
path: ['workflows', workflowName, 'entryStage'],
|
|
504
|
+
message: `Workflow entryStage "${actualEntryStage}" is not a configured stage.`,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
for (const [stageName, stage] of Object.entries(workflow.stages)) {
|
|
508
|
+
if (stage.onDone === 'queue') {
|
|
509
|
+
ctx.addIssue({
|
|
510
|
+
code: z.ZodIssueCode.custom,
|
|
511
|
+
path: ['workflows', workflowName, 'stages', stageName, 'onDone'],
|
|
512
|
+
message: 'Workflow transitions must not target queue.',
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (stage.onDone !== 'done' && !stageSet.has(stage.onDone)) {
|
|
516
|
+
ctx.addIssue({
|
|
517
|
+
code: z.ZodIssueCode.custom,
|
|
518
|
+
path: ['workflows', workflowName, 'stages', stageName, 'onDone'],
|
|
519
|
+
message: `Workflow transition targets unknown stage "${stage.onDone}".`,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
if (stage.action === undefined && !promptExists(promptsRoot, stageName)) {
|
|
523
|
+
ctx.addIssue({
|
|
524
|
+
code: z.ZodIssueCode.custom,
|
|
525
|
+
path: ['workflows', workflowName, 'stages', stageName, 'action'],
|
|
526
|
+
message: `Workflow stage "${stageName}" omits action but no prompts/${stageName}.md template exists.`,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (actualEntryStage !== undefined && stageSet.has(actualEntryStage) && !canReachDone(actualEntryStage, workflow.stages)) {
|
|
531
|
+
ctx.addIssue({
|
|
532
|
+
code: z.ZodIssueCode.custom,
|
|
533
|
+
path: ['workflows', workflowName],
|
|
534
|
+
message: 'Workflow cannot reach done from its entry stage.',
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
export const claudePrintResultSchema = z.object({
|
|
540
|
+
type: z.string().optional(),
|
|
541
|
+
subtype: z.string().optional(),
|
|
542
|
+
result: z.string(),
|
|
543
|
+
session_id: z.string().optional(),
|
|
544
|
+
total_cost_usd: z.number().optional(),
|
|
545
|
+
num_turns: z.number().optional(),
|
|
546
|
+
usage: z.record(z.string(), z.unknown()).optional(),
|
|
547
|
+
}).passthrough();
|
|
548
|
+
export function parseIssueStateRecord(input) {
|
|
549
|
+
return issueStateRecordSchema.parse(input);
|
|
550
|
+
}
|
|
551
|
+
export function parseRunRecord(input) {
|
|
552
|
+
return runRecordSchema.parse(input);
|
|
553
|
+
}
|
|
554
|
+
export function parseEventEnvelope(input) {
|
|
555
|
+
return eventEnvelopeSchema.parse(input);
|
|
556
|
+
}
|
|
557
|
+
export function parseLedger(input) {
|
|
558
|
+
return ledgerSchema.parse(input);
|
|
559
|
+
}
|
|
560
|
+
export function parseWakeConfig(input) {
|
|
561
|
+
return structuredClone(wakeConfigSchema.parse(input));
|
|
562
|
+
}
|
|
563
|
+
export function parseSourceStateRecord(input) {
|
|
564
|
+
return sourceStateRecordSchema.parse(input);
|
|
565
|
+
}
|
|
566
|
+
export function parseClaudePrintResult(input) {
|
|
567
|
+
return claudePrintResultSchema.parse(input);
|
|
568
|
+
}
|
|
569
|
+
function synthesizeBodyFromEnvelope(envelope) {
|
|
570
|
+
const labels = {
|
|
571
|
+
DONE: 'Run completed.',
|
|
572
|
+
BLOCKED: 'Run blocked — needs input.',
|
|
573
|
+
AWAITING_APPROVAL: 'Ready for approval.',
|
|
574
|
+
FAILED: 'Run failed.',
|
|
575
|
+
};
|
|
576
|
+
return labels[envelope.status] ?? 'Run finished.';
|
|
577
|
+
}
|
|
578
|
+
export function parseRunnerResult(result) {
|
|
579
|
+
const wakeResultFencePattern = /^```(?:wake-result[^\n]*\n|[ \t]*\n[ \t]*wake-result[ \t]*\n)([\s\S]*?)^```[ \t]*$/gm;
|
|
580
|
+
let lastMatch = null;
|
|
581
|
+
let match;
|
|
582
|
+
while ((match = wakeResultFencePattern.exec(result)) !== null) {
|
|
583
|
+
lastMatch = match;
|
|
584
|
+
}
|
|
585
|
+
if (lastMatch !== null) {
|
|
586
|
+
try {
|
|
587
|
+
const rawContent = lastMatch[1] ?? 'null';
|
|
588
|
+
// Claude sometimes places the sentinel keyword inside the fence rather than after
|
|
589
|
+
// the closing fence. Strip a trailing sentinel line so JSON.parse sees only the JSON.
|
|
590
|
+
// The capture group includes the newline before the closing fence, so allow \n? after.
|
|
591
|
+
const jsonContent = rawContent.replace(/\n(?:DONE|BLOCKED|FAILED|AWAITING_APPROVAL)[ \t]*\n?$/, '') || rawContent;
|
|
592
|
+
const parsed = wakeResultEnvelopeSchema.safeParse(JSON.parse(jsonContent));
|
|
593
|
+
if (parsed.success) {
|
|
594
|
+
const proseBody = result.slice(0, lastMatch.index).trim();
|
|
595
|
+
return {
|
|
596
|
+
status: parsed.data.status,
|
|
597
|
+
body: proseBody || synthesizeBodyFromEnvelope(parsed.data),
|
|
598
|
+
envelope: 'structured',
|
|
599
|
+
result: parsed.data,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
// Invalid structured trailers intentionally degrade to last-line parsing.
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const offFenceEnvelopePattern = /^```wake-result[^\n]*\n```[ \t]*\n(\{[^\n]+\})[ \t]*$/gm;
|
|
608
|
+
let offFenceMatch = null;
|
|
609
|
+
while ((match = offFenceEnvelopePattern.exec(result)) !== null) {
|
|
610
|
+
offFenceMatch = match;
|
|
611
|
+
}
|
|
612
|
+
if (offFenceMatch !== null) {
|
|
613
|
+
try {
|
|
614
|
+
const parsed = wakeResultEnvelopeSchema.safeParse(JSON.parse(offFenceMatch[1] ?? 'null'));
|
|
615
|
+
if (parsed.success) {
|
|
616
|
+
return {
|
|
617
|
+
status: parsed.data.status,
|
|
618
|
+
body: result.slice(0, offFenceMatch.index).trim() || synthesizeBodyFromEnvelope(parsed.data),
|
|
619
|
+
envelope: 'structured',
|
|
620
|
+
result: parsed.data,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
// Invalid off-fence trailers intentionally degrade to sentinel parsing.
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const lines = result.split('\n');
|
|
629
|
+
const lastLine = lines
|
|
630
|
+
.map((line) => line.trim())
|
|
631
|
+
// Skip closing code fence lines — they appear as the last non-empty line when the
|
|
632
|
+
// sentinel is embedded inside the fenced block or when a plain ``` fence is used.
|
|
633
|
+
.filter((line) => line.length > 0 && line !== '```')
|
|
634
|
+
.at(-1);
|
|
635
|
+
const normalizedLastLine = lastLine?.replace(/^(?:\*\*|__)(.+)(?:\*\*|__)$/, '$1');
|
|
636
|
+
const parsed = runnerSentinelSchema.safeParse(normalizedLastLine);
|
|
637
|
+
if (!parsed.success) {
|
|
638
|
+
const body = result.trim();
|
|
639
|
+
return {
|
|
640
|
+
status: body.length === 0 ? 'FAILED' : 'BLOCKED',
|
|
641
|
+
body,
|
|
642
|
+
envelope: 'degraded',
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
let removed = false;
|
|
646
|
+
const body = lines
|
|
647
|
+
.slice()
|
|
648
|
+
.reverse()
|
|
649
|
+
.filter((line) => {
|
|
650
|
+
if (!removed && line.trim() === lastLine) {
|
|
651
|
+
removed = true;
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
return true;
|
|
655
|
+
})
|
|
656
|
+
.reverse()
|
|
657
|
+
.join('\n')
|
|
658
|
+
.trim();
|
|
659
|
+
return {
|
|
660
|
+
status: parsed.data,
|
|
661
|
+
body,
|
|
662
|
+
envelope: 'degraded',
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
export function parseRunnerArtifacts(result) {
|
|
666
|
+
const fencePattern = /^```wake-artifacts[^\n]*\n([\s\S]*?)^```[ \t]*$/gm;
|
|
667
|
+
let lastMatch = null;
|
|
668
|
+
let match;
|
|
669
|
+
while ((match = fencePattern.exec(result)) !== null) {
|
|
670
|
+
lastMatch = match;
|
|
671
|
+
}
|
|
672
|
+
if (lastMatch === null) {
|
|
673
|
+
return { artifacts: [] };
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
const parsed = wakeArtifactsEnvelopeSchema.safeParse(JSON.parse(lastMatch[1] ?? '{}'));
|
|
677
|
+
return parsed.success ? parsed.data : { artifacts: [] };
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
return { artifacts: [] };
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
export function parseRunnerResultSentinel(result) {
|
|
684
|
+
return parseRunnerResult(result).status;
|
|
685
|
+
}
|