Repository navigation
Expand file tree
/
Copy pathgoal.program.ts
More file actions
425 lines (354 loc) · 15.5 KB
/
Copy pathgoal.program.ts
File metadata and controls
425 lines (354 loc) · 15.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
**
* @name goal
* @description Persistent Codex-style goal: derive and store a requirements string, execute work, verify every requirement independently plus the objective as a whole, and repeat until satisfied or the same blocker recurs three consecutive turns.
*/
const RequirementItem = z.object({
id: z.string(),
title: z.string(),
body: z.string(),
status: z.enum(["pending", "in_progress", "complete", "blocked"]),
evidence: z.array(z.string()).default([]),
notes: z.array(z.string()).default([]),
})
const RubricItem = z.object({
id: z.string(),
body: z.string(),
satisfied: z.boolean().default(false),
evidence: z.array(z.string()).default([]),
})
const Finding = z.object({
title: z.string(),
body: z.string(),
confidence_score: z.number().min(0).max(1),
priority: z.enum(["P0", "P1", "P2", "P3"]),
code_location: z
.object({
absolute_file_path: z.string(),
line_range: z.object({ start: z.number(), end: z.number() }),
})
.optional(),
})
const PlannerOutput = z.object({
requirementsString: z.string(),
requirements: z.array(RequirementItem),
rubricItems: z.array(RubricItem).default([]),
summary: z.string(),
})
const WorkerOutput = z.object({
status: z.enum(["worked", "blocked"]),
requirementStatus: z.enum(["pending", "in_progress", "complete", "blocked"]),
summary: z.string(),
evidence: z.array(z.string()).default([]),
notes: z.array(z.string()).default([]),
blocker: z.string().default(""),
findings: z.array(Finding).default([]),
})
const RequirementCheck = z.object({
requirementId: z.string(),
passed: z.boolean(),
evidence: z.array(z.string()).default([]),
gaps: z.array(z.string()).default([]),
summary: z.string(),
})
const ObjectiveCheck = z.object({
passed: z.boolean(),
evidence: z.array(z.string()).default([]),
gaps: z.array(z.string()).default([]),
summary: z.string(),
})
const VerifierOutput = z.object({
verdict: z.enum(["satisfied", "unsatisfied", "blocked"]),
requirementChecks: z.array(RequirementCheck),
objectiveCheck: ObjectiveCheck,
proofs: z.array(z.string()).default([]),
gaps: z.array(z.string()).default([]),
blocker: z.string().default(""),
findings: z.array(Finding).default([]),
summary: z.string(),
})
const asJson = (value: unknown) => JSON.stringify(value, null, 2)
const nowIso = () => new Date().toISOString()
const slugify = (value: string) => {
const slug = value
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 48)
return slug || \`goal-\${Date.now()}\`
}
const safeId = (value: string) => value.replace(/[^a-zA-Z0-9:_-]+/g, "-").slice(0, 180)
const normalizeInputObjective = (input: any) => {
if (typeof input === "string") return input.trim()
return String(input?.objective ?? input?.goal ?? "").trim()
}
const normalizeStringArray = (value: unknown) => Array.isArray(value) ? value.filter((x) => typeof x === "string" && x.trim()).map((x) => x.trim()) : []
const initialRubricItems = (items: string[]) => items.map((body, index) => ({
id: \`rubric-\${index + 1}\`,
body,
satisfied: false,
evidence: [] as string[],
}))
const plannerPrompt = (state: any) => \`You are the planner for a persistent goal loop.
Your task is to compute the stable verification target before execution begins.
Create:
1. A requirements string in concise Markdown.
2. A requirement item array matching that string.
3. Rubric items sufficient to verify the objective, preserving any caller-provided rubric items.
Derive requirements from:
- the objective,
- caller rubric items,
- explicit named deliverables,
- explicit tests/commands/gates/invariants,
- referenced files/plans/specs/issues when the objective names them,
- constraints already visible from the current repository context.
Rules:
- Do not mark requirements complete during planning.
- Each requirement must be independently verifiable.
- Keep the requirements string stable, concrete, and suitable for use as the goal's stored rubric.
- Include objective-level verification expectations in the requirements string, but do not replace the original objective; the verifier will check both.
Objective:
\${state.objective}
Base branch: \${state.baseBranch}
Repository root: \${state.repoRoot}
Caller rubric items:
\${state.rubricItems.length ? state.rubricItems.map((item: any, index: number) => \`\${index + 1}. \${item.body}\`).join("\n") : "none"}
Current state:
\${asJson(state)}\`
const workerPrompt = (state: any, requirementIndex: number) => {
const requirement = state.requirements[requirementIndex]
return \`You are the worker for a persistent goal loop.
Work only on the assigned requirement for this turn, while preserving the original objective.
Iteration: \${state.iteration}
Repository root: \${state.repoRoot}
Base branch: \${state.baseBranch}
Original objective:
\${state.objective}
Stored requirements string:
\${state.requirementsString}
Assigned requirement at requirements[\${requirementIndex}]:
\${asJson(requirement)}
Latest verification gaps to address where relevant:
\${state.latestGaps.length ? state.latestGaps.map((gap: string) => \`- \${gap}\`).join("\n") : "none"}
Instructions:
- Inspect and change the codebase as needed to satisfy the assigned requirement.
- Prefer targeted, idiomatic changes.
- Run focused verification where practical.
- Return concrete evidence: files changed, commands run, test results, or reasoning tied to observed state.
- If blocked, return status "blocked" and an exact blocker string. The same blocker recurring three consecutive turns stops the goal.
- If you discover correctness findings, return them as structured findings.
- Do not claim the whole goal is complete; only report this requirement's progress.\`
}
const verifierPrompt = (state: any) => \`You are the verifier for a persistent goal loop.
You must perform two independent audits:
1. Requirement-by-requirement audit:
- Check every stored requirement independently.
- For each requirement, decide passed true/false.
- Provide concrete evidence for passed requirements.
- Provide specific actionable gaps for failed requirements.
2. Objective-level audit:
- Check the completed work against the original objective as a whole.
- This catches missing or incorrectly-derived requirements.
- The goal is not satisfied unless the objective-level check also passes.
Completion rule:
- verdict = "satisfied" only if every requirement passes AND the objective-level check passes AND there are no P0/P1 blocking findings.
- verdict = "unsatisfied" if more work can address the gaps.
- verdict = "blocked" only if a concrete blocker prevents progress.
If the objective-level check fails because the stored requirements are incomplete, put those missing objective-level requirements in objectiveCheck.gaps as concrete actionable statements. The program will append them to the stored requirements for the next iteration.
Original objective:
\${state.objective}
Stored requirements string:
\${state.requirementsString}
Current requirements:
\${asJson(state.requirements)}
Rubric items:
\${asJson(state.rubricItems)}
Progress notes:
\${asJson(state.progressNotes)}
Existing findings:
\${asJson(state.findings)}
Latest gaps:
\${asJson(state.latestGaps)}\`
const appendNote = (s: any, source: string, note: string, extra: any = {}) => {
s.progressNotes.push({ at: nowIso(), source, note, ...extra })
}
const recordBlocker = (s: any, blocker: string) => {
const text = blocker.trim()
if (!text) {
s.audits.blocked.consecutiveTurns = 0
return
}
if (s.audits.blocked.blocker === text) s.audits.blocked.consecutiveTurns += 1
else {
s.audits.blocked.blocker = text
s.audits.blocked.consecutiveTurns = 1
}
appendNote(s, "program", \`Observed blocker (\${s.audits.blocked.consecutiveTurns}/3): \${text}\`, { blocker: text })
}
const isBlocked = (s: any) => s.audits.blocked.consecutiveTurns >= 3
const nextObjectiveGapRequirementId = (iteration: number, index: number) => \`objective-gap-\${iteration}-\${index + 1}\`
const appendObjectiveGapRequirements = (s: any, gaps: string[], iteration: number) => {
const existingBodies = new Set(s.requirements.map((req: any) => req.body.trim().toLowerCase()))
const added: string[] = []
gaps.forEach((gap, index) => {
const body = gap.trim()
if (!body) return
const key = body.toLowerCase()
if (existingBodies.has(key)) return
const id = nextObjectiveGapRequirementId(iteration, index)
s.requirements.push({
id,
title: \`Objective gap: \${body.slice(0, 80)}\`,
body,
status: "pending",
evidence: [],
notes: ["Added from objective-level verification gap."],
})
existingBodies.add(key)
added.push(\`- \${body}\`)
})
if (added.length) {
s.requirementsString = \`\${s.requirementsString}\n\n## Objective-level gaps added after verification \${iteration}\n\${added.join("\n")}\`
appendNote(s, "program", \`Added \${added.length} objective-level verification gap(s) to stored requirements.\`)
}
}
const applyRequirementChecks = (s: any, checks: any[]) => {
const byId = new Map(checks.map((check: any) => [check.requirementId, check]))
s.requirements = s.requirements.map((req: any) => {
const check = byId.get(req.id)
if (!check) return { ...req, status: "pending", notes: [...req.notes, "Verifier did not return a check for this requirement."] }
const evidence = Array.from(new Set([...(req.evidence ?? []), ...(check.evidence ?? [])]))
const notes = [...(req.notes ?? []), check.summary, ...(check.gaps ?? []).map((gap: string) => \`Gap: \${gap}\`)]
return { ...req, status: check.passed ? "complete" : "pending", evidence, notes }
})
}
const hasBlockingFindings = (findings: any[]) => findings.some((finding) => finding.priority === "P0" || finding.priority === "P1")
program(async (ctx) => {
const input: any = ctx.input ?? {}
const objective = normalizeInputObjective(input)
if (!objective) throw new Error("goal requires a non-empty objective")
const repoRoot = typeof input.repoRoot === "string" && input.repoRoot.trim() ? input.repoRoot.trim() : "/Users/k/workspace/randomlabs"
const baseBranch = typeof input.baseBranch === "string" && input.baseBranch.trim() ? input.baseBranch.trim() : "dev"
const callerRubric = normalizeStringArray(input.rubric)
const runPrefix = safeId(\`goal:\${slugify(objective)}:\${ctx.info.workflowId}\`)
const s = state({
threadId: ctx.info.workflowId,
objective,
requirementsString: "",
status: "active",
iteration: 0,
repoRoot,
baseBranch,
rubricItems: initialRubricItems(callerRubric),
requirements: [] as any[],
latestGaps: [] as string[],
progressNotes: [
{
at: ctx.startedAt,
source: "program",
note: "Goal created; planner will derive a stored requirements string before execution.",
},
],
findings: [] as any[],
audits: {
completion: {
verdict: "pending",
requirementChecks: [] as any[],
objectiveCheck: { passed: false, evidence: [] as string[], gaps: [] as string[], summary: "pending" },
proofs: [] as string[],
gaps: [] as string[],
},
blocked: {
consecutiveTurns: 0,
blocker: null as string | null,
blocked: false,
},
},
})
const planner = await run(\`\${runPrefix}:planner\`, {
type: "general",
maxSteps: 40,
prompt: plannerPrompt(snapshot(s)),
output: PlannerOutput,
})
s.requirementsString = planner.requirementsString
s.requirements = planner.requirements
s.rubricItems = planner.rubricItems.length ? planner.rubricItems : s.rubricItems
appendNote(s, "planner", planner.summary)
for (let iteration = 1; ; iteration++) {
s.iteration = iteration
appendNote(s, "program", \`Starting iteration \${iteration}.\`)
await checkpoint({ name: "goal.iteration", message: \`Starting goal iteration \${iteration}\`, data: { iteration } })
const count = snapshot(s).requirements.length
for (let index = 0; index < count; index++) {
const snap = snapshot(s)
const requirement = snap.requirements[index]
if (!requirement || requirement.status === "complete") continue
s.requirements[index].status = "in_progress"
appendNote(s, "program", \`Iteration \${iteration}: working on \${requirement.id}: \${requirement.title}\`)
const worked = await run(\`\${runPrefix}:worker\`, {
type: "general",
maxSteps: 100,
prompt: workerPrompt(snapshot(s), index),
output: WorkerOutput,
})
s.requirements[index].status = worked.requirementStatus
s.requirements[index].evidence = Array.from(new Set([...(s.requirements[index].evidence ?? []), ...worked.evidence]))
s.requirements[index].notes.push(worked.summary, ...worked.notes)
s.findings.push(...worked.findings)
appendNote(s, "worker", worked.summary, worked.blocker ? { blocker: worked.blocker } : {})
recordBlocker(s, worked.status === "blocked" ? worked.blocker : "")
if (isBlocked(s)) {
s.audits.blocked.blocked = true
s.status = "blocked"
return snapshot(s)
}
}
s.audits.completion.verdict = "pending"
const verification = await run(\`\${runPrefix}:verifier\`, {
type: "general",
maxSteps: 70,
prompt: verifierPrompt(snapshot(s)),
output: VerifierOutput,
})
s.audits.completion = {
verdict: verification.verdict,
requirementChecks: verification.requirementChecks,
objectiveCheck: verification.objectiveCheck,
proofs: verification.proofs,
gaps: verification.gaps,
}
s.findings.push(...verification.findings)
applyRequirementChecks(s, verification.requirementChecks)
const allRequirementsPassed = snapshot(s).requirements.every((req: any) => req.status === "complete")
const blockingFindings = hasBlockingFindings(snapshot(s).findings)
const objectivePassed = verification.objectiveCheck.passed
const allPassed = allRequirementsPassed && objectivePassed && !blockingFindings && verification.verdict === "satisfied"
appendNote(
s,
"verifier",
\`\${verification.summary} Requirement checks passed: \${verification.requirementChecks.filter((check: any) => check.passed).length}/\${snapshot(s).requirements.length}. Objective check: \${objectivePassed ? "passed" : "failed"}.\`,
verification.blocker ? { blocker: verification.blocker } : {},
)
recordBlocker(s, verification.verdict === "blocked" ? verification.blocker : "")
if (isBlocked(s)) {
s.audits.blocked.blocked = true
s.status = "blocked"
return snapshot(s)
}
if (allPassed) {
s.status = "complete"
appendNote(s, "program", "Goal complete: all stored requirements passed independently and the objective-level check passed.")
return snapshot(s)
}
const requirementGaps = verification.requirementChecks.flatMap((check: any) => check.passed ? [] : check.gaps)
const objectiveGaps = verification.objectiveCheck.passed ? [] : verification.objectiveCheck.gaps
const findingGaps = blockingFindings ? snapshot(s).findings.filter((finding: any) => finding.priority === "P0" || finding.priority === "P1").map((finding: any) => \`\${finding.priority}: \${finding.title} — \${finding.body}\`) : []
s.latestGaps = [...verification.gaps, ...requirementGaps, ...objectiveGaps, ...findingGaps]
appendObjectiveGapRequirements(s, objectiveGaps, iteration)
appendNote(
s,
"program",
\`Iteration \${iteration} unsatisfied; continuing. Gaps: \${s.latestGaps.length ? s.latestGaps.join("; ") : "none recorded"}\`,
)
}
})