ralphy-spec 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.ja.md +38 -15
  3. package/README.ko.md +38 -15
  4. package/README.md +38 -15
  5. package/README.zh.md +38 -15
  6. package/dist/cli/budget.d.ts +3 -0
  7. package/dist/cli/budget.d.ts.map +1 -0
  8. package/dist/cli/budget.js +77 -0
  9. package/dist/cli/budget.js.map +1 -0
  10. package/dist/cli/init.d.ts.map +1 -1
  11. package/dist/cli/init.js +6 -0
  12. package/dist/cli/init.js.map +1 -1
  13. package/dist/cli/run.d.ts.map +1 -1
  14. package/dist/cli/run.js +42 -9
  15. package/dist/cli/run.js.map +1 -1
  16. package/dist/cli/status.d.ts.map +1 -1
  17. package/dist/cli/status.js +31 -0
  18. package/dist/cli/status.js.map +1 -1
  19. package/dist/core/artifacts/budget-writer.d.ts +11 -0
  20. package/dist/core/artifacts/budget-writer.d.ts.map +1 -0
  21. package/dist/core/artifacts/budget-writer.js +28 -0
  22. package/dist/core/artifacts/budget-writer.js.map +1 -0
  23. package/dist/core/artifacts/run-log-writer.d.ts +20 -0
  24. package/dist/core/artifacts/run-log-writer.d.ts.map +1 -0
  25. package/dist/core/artifacts/run-log-writer.js +40 -0
  26. package/dist/core/artifacts/run-log-writer.js.map +1 -0
  27. package/dist/core/artifacts/run-log-writer.test.d.ts +2 -0
  28. package/dist/core/artifacts/run-log-writer.test.d.ts.map +1 -0
  29. package/dist/core/artifacts/run-log-writer.test.js +37 -0
  30. package/dist/core/artifacts/run-log-writer.test.js.map +1 -0
  31. package/dist/core/artifacts/status-writer.d.ts +16 -0
  32. package/dist/core/artifacts/status-writer.d.ts.map +1 -0
  33. package/dist/core/artifacts/status-writer.js +52 -0
  34. package/dist/core/artifacts/status-writer.js.map +1 -0
  35. package/dist/core/artifacts/status-writer.test.d.ts +2 -0
  36. package/dist/core/artifacts/status-writer.test.d.ts.map +1 -0
  37. package/dist/core/artifacts/status-writer.test.js +47 -0
  38. package/dist/core/artifacts/status-writer.test.js.map +1 -0
  39. package/dist/core/artifacts/task-artifacts.d.ts +19 -0
  40. package/dist/core/artifacts/task-artifacts.d.ts.map +1 -0
  41. package/dist/core/artifacts/task-artifacts.js +35 -0
  42. package/dist/core/artifacts/task-artifacts.js.map +1 -0
  43. package/dist/core/artifacts/tasks-writer.d.ts +19 -0
  44. package/dist/core/artifacts/tasks-writer.d.ts.map +1 -0
  45. package/dist/core/artifacts/tasks-writer.js +67 -0
  46. package/dist/core/artifacts/tasks-writer.js.map +1 -0
  47. package/dist/core/artifacts/tasks-writer.test.d.ts +2 -0
  48. package/dist/core/artifacts/tasks-writer.test.d.ts.map +1 -0
  49. package/dist/core/artifacts/tasks-writer.test.js +28 -0
  50. package/dist/core/artifacts/tasks-writer.test.js.map +1 -0
  51. package/dist/core/budgets/errors.d.ts +5 -0
  52. package/dist/core/budgets/errors.d.ts.map +1 -0
  53. package/dist/core/budgets/errors.js +11 -0
  54. package/dist/core/budgets/errors.js.map +1 -0
  55. package/dist/core/engine/constraints.d.ts +16 -0
  56. package/dist/core/engine/constraints.d.ts.map +1 -0
  57. package/dist/core/engine/constraints.js +21 -0
  58. package/dist/core/engine/constraints.js.map +1 -0
  59. package/dist/core/engine/constraints.policy.test.d.ts +2 -0
  60. package/dist/core/engine/constraints.policy.test.d.ts.map +1 -0
  61. package/dist/core/engine/constraints.policy.test.js +85 -0
  62. package/dist/core/engine/constraints.policy.test.js.map +1 -0
  63. package/dist/core/engine/loop.d.ts.map +1 -1
  64. package/dist/core/engine/loop.hardcap.test.d.ts +2 -0
  65. package/dist/core/engine/loop.hardcap.test.d.ts.map +1 -0
  66. package/dist/core/engine/loop.hardcap.test.js +77 -0
  67. package/dist/core/engine/loop.hardcap.test.js.map +1 -0
  68. package/dist/core/engine/loop.js +511 -13
  69. package/dist/core/engine/loop.js.map +1 -1
  70. package/dist/core/memory/persistence.d.ts +9 -0
  71. package/dist/core/memory/persistence.d.ts.map +1 -1
  72. package/dist/core/memory/persistence.js +19 -1
  73. package/dist/core/memory/persistence.js.map +1 -1
  74. package/dist/core/reporting/failure-summary.d.ts +23 -0
  75. package/dist/core/reporting/failure-summary.d.ts.map +1 -0
  76. package/dist/core/reporting/failure-summary.js +63 -0
  77. package/dist/core/reporting/failure-summary.js.map +1 -0
  78. package/dist/core/reporting/failure-summary.test.d.ts +2 -0
  79. package/dist/core/reporting/failure-summary.test.d.ts.map +1 -0
  80. package/dist/core/reporting/failure-summary.test.js +22 -0
  81. package/dist/core/reporting/failure-summary.test.js.map +1 -0
  82. package/dist/core/spec/loader.d.ts.map +1 -1
  83. package/dist/core/spec/loader.js +12 -1
  84. package/dist/core/spec/loader.js.map +1 -1
  85. package/dist/core/spec/schemas.d.ts +857 -0
  86. package/dist/core/spec/schemas.d.ts.map +1 -1
  87. package/dist/core/spec/schemas.js +28 -0
  88. package/dist/core/spec/schemas.js.map +1 -1
  89. package/dist/core/spec/sprint-defaults.d.ts +16 -0
  90. package/dist/core/spec/sprint-defaults.d.ts.map +1 -0
  91. package/dist/core/spec/sprint-defaults.js +55 -0
  92. package/dist/core/spec/sprint-defaults.js.map +1 -0
  93. package/dist/core/spec/sprint-defaults.test.d.ts +2 -0
  94. package/dist/core/spec/sprint-defaults.test.d.ts.map +1 -0
  95. package/dist/core/spec/sprint-defaults.test.js +51 -0
  96. package/dist/core/spec/sprint-defaults.test.js.map +1 -0
  97. package/dist/core/spec/types.d.ts +11 -0
  98. package/dist/core/spec/types.d.ts.map +1 -1
  99. package/dist/core/validators/types.d.ts +1 -1
  100. package/dist/core/validators/types.d.ts.map +1 -1
  101. package/dist/core/workspace/scope-detector.d.ts +13 -0
  102. package/dist/core/workspace/scope-detector.d.ts.map +1 -0
  103. package/dist/core/workspace/scope-detector.js +34 -0
  104. package/dist/core/workspace/scope-detector.js.map +1 -0
  105. package/dist/core/workspace/worktree-mode.d.ts.map +1 -1
  106. package/dist/core/workspace/worktree-mode.js +2 -1
  107. package/dist/core/workspace/worktree-mode.js.map +1 -1
  108. package/dist/index.js +3 -1
  109. package/dist/index.js.map +1 -1
  110. package/dist/templates/claude-code/ralphy-archive.md +1 -1
  111. package/dist/templates/claude-code/ralphy-implement.md +1 -1
  112. package/dist/templates/claude-code/ralphy-plan.md +1 -1
  113. package/dist/templates/claude-code/ralphy-validate.md +1 -1
  114. package/dist/templates/cursor/ralphy-archive.md +1 -1
  115. package/dist/templates/cursor/ralphy-implement.md +1 -1
  116. package/dist/templates/cursor/ralphy-plan.md +1 -1
  117. package/dist/templates/cursor/ralphy-validate.md +1 -1
  118. package/package.json +7 -4
@@ -12,8 +12,18 @@ const runner_1 = require("../validators/runner");
12
12
  const signatures_1 = require("../validators/signatures");
13
13
  const manager_1 = require("../budgets/manager");
14
14
  const state_1 = require("../budgets/state");
15
+ const errors_1 = require("../budgets/errors");
15
16
  const context_pack_1 = require("./context-pack");
16
17
  const repair_1 = require("./repair");
18
+ const failure_summary_1 = require("../reporting/failure-summary");
19
+ const status_writer_1 = require("../artifacts/status-writer");
20
+ const tasks_writer_1 = require("../artifacts/tasks-writer");
21
+ const budget_writer_1 = require("../artifacts/budget-writer");
22
+ const task_artifacts_1 = require("../artifacts/task-artifacts");
23
+ const run_log_writer_1 = require("../artifacts/run-log-writer");
24
+ const constraints_1 = require("./constraints");
25
+ const scope_detector_1 = require("../workspace/scope-detector");
26
+ const sprint_defaults_1 = require("../spec/sprint-defaults");
17
27
  function createRunId() {
18
28
  return `run_${new Date().toISOString().replace(/[:.]/g, "-")}_${node_crypto_1.default
19
29
  .randomBytes(4)
@@ -24,6 +34,9 @@ class EngineLoop {
24
34
  const runId = createRunId();
25
35
  const persistence = await persistence_1.PersistenceLayer.openForRepo(opts.repoRoot);
26
36
  const ledger = new ledger_1.LedgerLogger(persistence, runId);
37
+ let artifactsEnabled = Boolean(opts.spec.artifacts?.enabled);
38
+ const artifactsRootDir = opts.spec.artifacts?.rootDir;
39
+ const artifactsStatusIcons = opts.spec.artifacts?.statusIcons ?? "emoji";
27
40
  const runBudget = opts.spec.budgets?.run;
28
41
  const budgetState = new state_1.BudgetState({
29
42
  usd: runBudget?.moneyUsd,
@@ -33,7 +46,7 @@ class EngineLoop {
33
46
  : undefined,
34
47
  maxIterations: runBudget?.maxIterationsTotal,
35
48
  });
36
- const budgetManager = new manager_1.BudgetManager(budgetState);
49
+ const runBudgetManager = new manager_1.BudgetManager(budgetState);
37
50
  persistence.createRun({
38
51
  runId,
39
52
  repoRoot: opts.repoRoot,
@@ -41,6 +54,70 @@ class EngineLoop {
41
54
  workspaceMode: opts.workspace.mode,
42
55
  });
43
56
  ledger.event({ kind: "run_started", message: "Run started", data: { runId } });
57
+ const safeArtifact = async (fn) => {
58
+ if (!artifactsEnabled)
59
+ return;
60
+ try {
61
+ await fn();
62
+ }
63
+ catch (e) {
64
+ artifactsEnabled = false;
65
+ ledger.event({
66
+ kind: "artifact_error",
67
+ message: `Artifact write failed; disabling artifacts for this run: ${e?.message ? String(e.message) : String(e)}`,
68
+ });
69
+ }
70
+ };
71
+ await safeArtifact(async () => {
72
+ await (0, status_writer_1.writeStatus)({
73
+ repoRoot: opts.repoRoot,
74
+ rootDir: artifactsRootDir,
75
+ runId,
76
+ backendId: opts.backend.id,
77
+ workspaceMode: opts.workspace.mode,
78
+ phase: "RUN",
79
+ message: "Run started",
80
+ });
81
+ });
82
+ const finalizeArtifacts = async (outcome) => {
83
+ if (!artifactsEnabled)
84
+ return;
85
+ const ledgerEvents = persistence.listLedger({ runId, limit: 5000 });
86
+ await safeArtifact(async () => {
87
+ await (0, tasks_writer_1.writeTasksBoard)({
88
+ repoRoot: opts.repoRoot,
89
+ rootDir: artifactsRootDir,
90
+ runId,
91
+ statusIcons: artifactsStatusIcons,
92
+ specTasks: opts.spec.tasks ?? [],
93
+ rows: persistence.listTasksForRun({ runId }),
94
+ });
95
+ });
96
+ await safeArtifact(async () => {
97
+ await (0, budget_writer_1.writeBudgetReport)({
98
+ repoRoot: opts.repoRoot,
99
+ rootDir: artifactsRootDir,
100
+ runId,
101
+ ledgerEvents,
102
+ });
103
+ });
104
+ await safeArtifact(async () => {
105
+ await (0, run_log_writer_1.writeRunLogOnce)({
106
+ repoRoot: opts.repoRoot,
107
+ rootDir: artifactsRootDir,
108
+ runId,
109
+ outcome: outcome.ok
110
+ ? { ok: true }
111
+ : { ok: false, exitCode: outcome.exitCode, reason: outcome.reason },
112
+ ledgerEvents: ledgerEvents.map((e) => ({
113
+ ts: e.ts,
114
+ kind: e.kind,
115
+ message: e.message,
116
+ taskId: e.taskId,
117
+ })),
118
+ });
119
+ });
120
+ };
44
121
  try {
45
122
  const tasks = opts.spec.tasks ?? [];
46
123
  const dag = (0, dag_1.buildTaskDAG)(tasks);
@@ -52,18 +129,21 @@ class EngineLoop {
52
129
  data: { tasks: planOrder },
53
130
  });
54
131
  persistence.finishRun({ runId, status: "success" });
132
+ await finalizeArtifacts({ ok: true, runId });
55
133
  return { ok: true, runId };
56
134
  }
57
135
  for (const taskId of planOrder) {
58
136
  const task = dag.tasksById.get(taskId) ?? tasks.find((t) => t.id === taskId);
59
137
  if (!task) {
60
138
  persistence.finishRun({ runId, status: "error" });
61
- return {
139
+ const outcome = {
62
140
  ok: false,
63
141
  runId,
64
142
  exitCode: 4,
65
143
  reason: `Unknown task id: ${taskId}`,
66
144
  };
145
+ await finalizeArtifacts(outcome);
146
+ return outcome;
67
147
  }
68
148
  const outcome = await this.runOneTask({
69
149
  runId,
@@ -74,19 +154,23 @@ class EngineLoop {
74
154
  workspace: opts.workspace,
75
155
  persistence,
76
156
  ledger,
77
- budgetManager,
157
+ runBudgetManager,
158
+ artifacts: artifactsEnabled ? { rootDir: artifactsRootDir } : null,
78
159
  });
79
160
  if (!outcome.ok) {
80
161
  persistence.finishRun({
81
162
  runId,
82
163
  status: outcome.exitCode === 2 || outcome.exitCode === 3 ? "stopped" : "error",
83
164
  });
165
+ await finalizeArtifacts(outcome);
84
166
  return outcome;
85
167
  }
86
168
  }
87
169
  ledger.event({ kind: "run_done", message: "All tasks done" });
88
170
  persistence.finishRun({ runId, status: "success" });
89
- return { ok: true, runId };
171
+ const okOutcome = { ok: true, runId };
172
+ await finalizeArtifacts(okOutcome);
173
+ return okOutcome;
90
174
  }
91
175
  catch (err) {
92
176
  ledger.event({
@@ -94,22 +178,78 @@ class EngineLoop {
94
178
  message: err?.message ? String(err.message) : String(err),
95
179
  });
96
180
  persistence.finishRun({ runId, status: "error" });
97
- return {
181
+ const outcome = {
98
182
  ok: false,
99
183
  runId,
100
184
  exitCode: 4,
101
185
  reason: err?.message ? String(err.message) : String(err),
102
186
  };
187
+ await finalizeArtifacts(outcome);
188
+ return outcome;
103
189
  }
104
190
  finally {
105
191
  persistence.close();
106
192
  }
107
193
  }
108
194
  async runOneTask(args) {
109
- const { task, runId, persistence, ledger, budgetManager } = args;
195
+ const { task, runId, persistence, ledger, runBudgetManager } = args;
196
+ let artifacts = args.artifacts;
110
197
  let phase = "PLAN";
111
198
  persistence.upsertTaskState({ runId, taskId: task.id, status: "running", phase });
112
199
  ledger.event({ taskId: task.id, kind: "task_started", message: "Task started" });
200
+ const refreshArtifacts = async () => {
201
+ if (!artifacts)
202
+ return;
203
+ const ledgerEvents = persistence.listLedger({ runId, limit: 5000 });
204
+ try {
205
+ await (0, tasks_writer_1.writeTasksBoard)({
206
+ repoRoot: args.repoRoot,
207
+ rootDir: artifacts.rootDir,
208
+ runId,
209
+ statusIcons: args.spec.artifacts?.statusIcons ?? "emoji",
210
+ specTasks: args.spec.tasks ?? [],
211
+ rows: persistence.listTasksForRun({ runId }),
212
+ });
213
+ await (0, budget_writer_1.writeBudgetReport)({
214
+ repoRoot: args.repoRoot,
215
+ rootDir: artifacts.rootDir,
216
+ runId,
217
+ ledgerEvents,
218
+ });
219
+ }
220
+ catch (e) {
221
+ artifacts = null;
222
+ ledger.event({
223
+ taskId: task.id,
224
+ kind: "artifact_error",
225
+ message: `Artifact write failed; disabling artifacts for this task: ${e?.message ? String(e.message) : String(e)}`,
226
+ });
227
+ }
228
+ };
229
+ await refreshArtifacts();
230
+ if (artifacts) {
231
+ try {
232
+ await (0, status_writer_1.writeStatus)({
233
+ repoRoot: args.repoRoot,
234
+ rootDir: artifacts.rootDir,
235
+ runId,
236
+ backendId: args.backend.id,
237
+ workspaceMode: args.workspace.mode,
238
+ taskId: task.id,
239
+ phase,
240
+ iteration: 0,
241
+ message: "Task started",
242
+ });
243
+ }
244
+ catch (e) {
245
+ artifacts = null;
246
+ ledger.event({
247
+ taskId: task.id,
248
+ kind: "artifact_error",
249
+ message: `STATUS.md write failed; disabling artifacts: ${e?.message ? String(e.message) : String(e)}`,
250
+ });
251
+ }
252
+ }
113
253
  const maxIter = task.budget?.hard?.maxIterations ??
114
254
  args.spec.budgets?.run?.maxIterationsTotal ??
115
255
  12;
@@ -117,15 +257,96 @@ class EngineLoop {
117
257
  await args.workspace.prepare(task.id);
118
258
  const cwd = args.workspace.getWorkingDir(task.id);
119
259
  const taskBudgetConfig = this.toTaskBudgetConfig(task) ?? null;
260
+ const taskBudgetState = new state_1.BudgetState({
261
+ usd: taskBudgetConfig?.hard.usd,
262
+ tokens: taskBudgetConfig?.hard.tokens,
263
+ wallTimeMs: taskBudgetConfig?.hard.timeMinutes !== undefined
264
+ ? taskBudgetConfig.hard.timeMinutes * 60_000
265
+ : undefined,
266
+ maxIterations: taskBudgetConfig?.hard.maxIterations ?? maxIter,
267
+ });
268
+ const taskBudgetManager = new manager_1.BudgetManager(taskBudgetState);
269
+ const checkpointEvery = task.sprint?.intent ? sprint_defaults_1.SPRINT_INTENT_CONSTRAINTS[task.sprint.intent]?.checkpointEveryIterations : undefined;
120
270
  let pendingRepairNotes;
121
271
  let lastValidatorResults = null;
122
272
  let lastIssues = null;
273
+ const blockHardCap = async (iter, reason) => {
274
+ const status = taskBudgetConfig ? taskBudgetManager.getStatus(taskBudgetConfig) : null;
275
+ ledger.event({
276
+ taskId: task.id,
277
+ kind: "hard_cap",
278
+ message: `HARD cap reached: blocking task (preserving workspace)`,
279
+ data: status ?? undefined,
280
+ });
281
+ persistence.upsertTaskState({
282
+ runId,
283
+ taskId: task.id,
284
+ status: "blocked",
285
+ phase: "DIAGNOSE",
286
+ iteration: iter,
287
+ lastError: reason,
288
+ });
289
+ if (artifacts) {
290
+ try {
291
+ await (0, status_writer_1.writeStatus)({
292
+ repoRoot: args.repoRoot,
293
+ rootDir: artifacts.rootDir,
294
+ runId,
295
+ backendId: args.backend.id,
296
+ workspaceMode: args.workspace.mode,
297
+ taskId: task.id,
298
+ phase: "DIAGNOSE",
299
+ iteration: iter,
300
+ tier: "hard",
301
+ budgetStatus: status,
302
+ message: `Blocked: ${reason} (workspace preserved)`,
303
+ });
304
+ }
305
+ catch {
306
+ artifacts = null;
307
+ }
308
+ }
309
+ ledger.event({
310
+ taskId: task.id,
311
+ kind: "failure_summary",
312
+ message: "Failure summary (hard cap)",
313
+ data: {
314
+ markdown: (0, failure_summary_1.buildFailureSummary)({
315
+ runId,
316
+ taskId: task.id,
317
+ reason,
318
+ tier: "hard",
319
+ budgetStatus: status,
320
+ lastIssues: lastIssues?.map((i) => ({
321
+ level: i.level,
322
+ kind: i.kind,
323
+ message: i.message,
324
+ file: i.file,
325
+ })),
326
+ ledgerEvents: persistence.listLedger({ runId, limit: 200 }),
327
+ }),
328
+ },
329
+ });
330
+ await refreshArtifacts();
331
+ return { ok: false, runId, exitCode: 2, reason: "Hard cap" };
332
+ };
123
333
  for (let iter = 1; iter <= maxIter; iter++) {
124
334
  const iterStarted = Date.now();
125
335
  try {
126
- budgetManager.preflightOrThrow({ estimatedUsd: 0, estimatedTokens: 0 });
336
+ // Run-level hard limits
337
+ runBudgetManager.preflightOrThrow({ estimatedUsd: 0, estimatedTokens: 0 });
338
+ // Task-level hard limits (best-effort)
339
+ taskBudgetManager.preflightOrThrow({ estimatedUsd: 0, estimatedTokens: 0 });
340
+ // Hard-cap: do not attempt another backend call once at cap.
341
+ if (taskBudgetConfig && taskBudgetManager.isAtHardCap(taskBudgetConfig)) {
342
+ // Spec requires that "starting another iteration" throws BudgetExhaustedError.
343
+ throw new errors_1.BudgetExhaustedError("Hard cap reached");
344
+ }
127
345
  }
128
346
  catch (e) {
347
+ if (e instanceof errors_1.BudgetExhaustedError) {
348
+ return await blockHardCap(iter, "Hard cap reached");
349
+ }
129
350
  ledger.event({
130
351
  taskId: task.id,
131
352
  kind: "budget_exceeded",
@@ -139,11 +360,33 @@ class EngineLoop {
139
360
  iteration: iter,
140
361
  lastError: "Budget exceeded",
141
362
  });
363
+ ledger.event({
364
+ taskId: task.id,
365
+ kind: "failure_summary",
366
+ message: "Failure summary (budget exceeded)",
367
+ data: {
368
+ markdown: (0, failure_summary_1.buildFailureSummary)({
369
+ runId,
370
+ taskId: task.id,
371
+ reason: "Budget exceeded",
372
+ tier: taskBudgetConfig ? taskBudgetManager.getTier(taskBudgetConfig) : "hard",
373
+ budgetStatus: taskBudgetConfig ? taskBudgetManager.getStatus(taskBudgetConfig) : null,
374
+ lastIssues: lastIssues?.map((i) => ({
375
+ level: i.level,
376
+ kind: i.kind,
377
+ message: i.message,
378
+ file: i.file,
379
+ })),
380
+ ledgerEvents: persistence.listLedger({ runId, limit: 200 }),
381
+ }),
382
+ },
383
+ });
384
+ await refreshArtifacts();
142
385
  return { ok: false, runId, exitCode: 2, reason: "Budget limit" };
143
386
  }
144
- const tier = taskBudgetConfig ? budgetManager.getTier(taskBudgetConfig) : "optimal";
387
+ const tier = taskBudgetConfig ? taskBudgetManager.getTier(taskBudgetConfig) : "optimal";
145
388
  const shouldDegrade = taskBudgetConfig
146
- ? budgetManager.shouldApplyDegrade(taskBudgetConfig)
389
+ ? taskBudgetManager.shouldApplyDegrade(taskBudgetConfig)
147
390
  : false;
148
391
  if (shouldDegrade) {
149
392
  ledger.event({
@@ -180,6 +423,59 @@ class EngineLoop {
180
423
  iteration: iter,
181
424
  });
182
425
  ledger.event({ taskId: task.id, kind: "exec", message: `EXEC iteration ${iter}` });
426
+ if (artifacts) {
427
+ try {
428
+ await (0, status_writer_1.writeStatus)({
429
+ repoRoot: args.repoRoot,
430
+ rootDir: artifacts.rootDir,
431
+ runId,
432
+ backendId: args.backend.id,
433
+ workspaceMode: args.workspace.mode,
434
+ taskId: task.id,
435
+ phase,
436
+ iteration: iter,
437
+ tier,
438
+ budgetStatus: taskBudgetConfig ? taskBudgetManager.getStatus(taskBudgetConfig) : null,
439
+ message: `Executing iteration ${iter}`,
440
+ });
441
+ }
442
+ catch {
443
+ artifacts = null;
444
+ }
445
+ }
446
+ if (artifacts) {
447
+ // Write task artifacts before backend call (best-effort).
448
+ const ctx = lastValidatorResults && lastIssues
449
+ ? (0, context_pack_1.buildContextPack)({
450
+ tier: shouldDegrade ? "warning" : "optimal",
451
+ taskId: task.id,
452
+ validatorResults: lastValidatorResults,
453
+ issues: lastIssues,
454
+ })
455
+ : {
456
+ size: "full",
457
+ text: [`# Context`, ``, `Task: ${task.id}`, `Iteration: ${iter}`, ``].join("\n"),
458
+ };
459
+ try {
460
+ await (0, task_artifacts_1.writeTaskContext)({
461
+ repoRoot: args.repoRoot,
462
+ rootDir: artifacts.rootDir,
463
+ taskId: task.id,
464
+ markdown: ctx.text,
465
+ });
466
+ if (pendingRepairNotes) {
467
+ await (0, task_artifacts_1.writeTaskRepair)({
468
+ repoRoot: args.repoRoot,
469
+ rootDir: artifacts.rootDir,
470
+ taskId: task.id,
471
+ markdown: pendingRepairNotes,
472
+ });
473
+ }
474
+ }
475
+ catch {
476
+ artifacts = null;
477
+ }
478
+ }
183
479
  const backendRes = await args.backend.implement({ cwd, backendId: args.backend.id }, {
184
480
  task,
185
481
  iteration: iter,
@@ -202,9 +498,52 @@ class EngineLoop {
202
498
  kind: "backend_error",
203
499
  message: backendRes.message,
204
500
  });
501
+ await refreshArtifacts();
205
502
  return { ok: false, runId, exitCode: 5, reason: "Backend invocation error" };
206
503
  }
504
+ // Record backend usage if provided (best-effort; some backends may not estimate).
505
+ if (backendRes.estimatedUsd !== undefined || backendRes.estimatedTokens !== undefined) {
506
+ runBudgetManager.recordBackendUsage({
507
+ usd: backendRes.estimatedUsd,
508
+ tokens: backendRes.estimatedTokens,
509
+ });
510
+ taskBudgetManager.recordBackendUsage({
511
+ usd: backendRes.estimatedUsd,
512
+ tokens: backendRes.estimatedTokens,
513
+ });
514
+ ledger.event({
515
+ taskId: task.id,
516
+ kind: "backend_usage",
517
+ message: "Backend usage recorded",
518
+ data: {
519
+ usd: backendRes.estimatedUsd ?? 0,
520
+ tokens: backendRes.estimatedTokens ?? 0,
521
+ backendId: args.backend.id,
522
+ phase: "EXEC",
523
+ },
524
+ });
525
+ }
207
526
  phase = "VALIDATE";
527
+ if (artifacts) {
528
+ try {
529
+ await (0, status_writer_1.writeStatus)({
530
+ repoRoot: args.repoRoot,
531
+ rootDir: artifacts.rootDir,
532
+ runId,
533
+ backendId: args.backend.id,
534
+ workspaceMode: args.workspace.mode,
535
+ taskId: task.id,
536
+ phase,
537
+ iteration: iter,
538
+ tier,
539
+ budgetStatus: taskBudgetConfig ? taskBudgetManager.getStatus(taskBudgetConfig) : null,
540
+ message: "Running validators",
541
+ });
542
+ }
543
+ catch {
544
+ artifacts = null;
545
+ }
546
+ }
208
547
  const validators = this.resolveValidators(args.spec, task).map((v) => ({
209
548
  ...v,
210
549
  timeoutMs: v.timeoutMs ?? (args.spec.budgets?.limits?.commandTimeoutSeconds ?? 900) * 1000,
@@ -228,7 +567,7 @@ class EngineLoop {
228
567
  : r.issues;
229
568
  return issues.map((i) => ({
230
569
  ...i,
231
- raw: i.raw ?? { validatorId: id },
570
+ raw: (i.raw ?? { validatorId: id }),
232
571
  }));
233
572
  });
234
573
  // Contract enforcement after EXEC
@@ -244,6 +583,30 @@ class EngineLoop {
244
583
  })));
245
584
  }
246
585
  }
586
+ // Sprint constraints & scope detection (best-effort heuristics).
587
+ const changedFiles = await args.workspace.getChangedFiles(task.id);
588
+ const scopePolicy = args.spec.policies?.scopeGuard ?? "warn";
589
+ const scopeLevel = scopePolicy === "block" ? "error" : scopePolicy === "warn" ? "warning" : "warning";
590
+ if (scopePolicy !== "off") {
591
+ for (const v of (0, constraints_1.enforceSprintConstraints)({ task, changedFiles })) {
592
+ allIssues.push({
593
+ kind: "scope_violation",
594
+ level: scopeLevel,
595
+ message: v.message,
596
+ file: v.file,
597
+ raw: v.raw,
598
+ });
599
+ }
600
+ for (const v of (0, scope_detector_1.detectScopeViolations)({ task, changedFiles })) {
601
+ allIssues.push({
602
+ kind: "scope_violation",
603
+ level: scopeLevel,
604
+ message: v.message,
605
+ file: v.file,
606
+ raw: v,
607
+ });
608
+ }
609
+ }
247
610
  const ok = Object.values(results).every((r) => r.ok) && allIssues.every((i) => i.level !== "error");
248
611
  ledger.event({
249
612
  taskId: task.id,
@@ -263,6 +626,8 @@ class EngineLoop {
263
626
  if (ok) {
264
627
  phase = "CHECKPOINT";
265
628
  await args.workspace.checkpoint(task.id, "Task completed");
629
+ await args.workspace.merge(task.id);
630
+ await args.workspace.cleanup(task.id);
266
631
  persistence.upsertTaskState({
267
632
  runId,
268
633
  taskId: task.id,
@@ -271,7 +636,36 @@ class EngineLoop {
271
636
  iteration: iter,
272
637
  });
273
638
  ledger.event({ taskId: task.id, kind: "task_done", message: "Task done" });
274
- budgetManager.recordIteration(Date.now() - iterStarted);
639
+ if (artifacts) {
640
+ try {
641
+ await (0, status_writer_1.writeStatus)({
642
+ repoRoot: args.repoRoot,
643
+ rootDir: artifacts.rootDir,
644
+ runId,
645
+ backendId: args.backend.id,
646
+ workspaceMode: args.workspace.mode,
647
+ taskId: task.id,
648
+ phase: "DONE",
649
+ iteration: iter,
650
+ tier,
651
+ budgetStatus: taskBudgetConfig ? taskBudgetManager.getStatus(taskBudgetConfig) : null,
652
+ message: "Task done",
653
+ });
654
+ }
655
+ catch {
656
+ artifacts = null;
657
+ }
658
+ }
659
+ const wallTimeMs = Date.now() - iterStarted;
660
+ runBudgetManager.recordIteration(wallTimeMs);
661
+ taskBudgetManager.recordIteration(wallTimeMs);
662
+ ledger.event({
663
+ taskId: task.id,
664
+ kind: "iteration_complete",
665
+ message: "Iteration complete",
666
+ data: { wallTimeMs, phase: "DONE" },
667
+ });
668
+ await refreshArtifacts();
275
669
  return { ok: true, runId };
276
670
  }
277
671
  // Save failure context for next iteration.
@@ -287,29 +681,132 @@ class EngineLoop {
287
681
  iteration: iter,
288
682
  lastError: "Validation failed",
289
683
  });
684
+ if (artifacts) {
685
+ try {
686
+ await (0, status_writer_1.writeStatus)({
687
+ repoRoot: args.repoRoot,
688
+ rootDir: artifacts.rootDir,
689
+ runId,
690
+ backendId: args.backend.id,
691
+ workspaceMode: args.workspace.mode,
692
+ taskId: task.id,
693
+ phase,
694
+ iteration: iter,
695
+ tier,
696
+ budgetStatus: taskBudgetConfig ? taskBudgetManager.getStatus(taskBudgetConfig) : null,
697
+ message: "Validation failed",
698
+ });
699
+ }
700
+ catch {
701
+ artifacts = null;
702
+ }
703
+ }
290
704
  if (stuck) {
291
705
  ledger.event({
292
706
  taskId: task.id,
293
707
  kind: "stuck",
294
708
  message: "Stuck detected (same issues repeated)",
295
709
  });
710
+ await refreshArtifacts();
296
711
  return { ok: false, runId, exitCode: 3, reason: "Stuck / max iterations" };
297
712
  }
298
713
  phase = "REPAIR";
299
- const tierAfter = taskBudgetConfig ? budgetManager.getTier(taskBudgetConfig) : "optimal";
714
+ const tierAfter = taskBudgetConfig ? taskBudgetManager.getTier(taskBudgetConfig) : "optimal";
300
715
  const repairNotes = (0, repair_1.buildRepairNotes)({
301
716
  tier: tierAfter === "warning" ? "warning" : "optimal",
302
717
  issues: allIssues,
303
718
  });
304
719
  ledger.event({ taskId: task.id, kind: "repair", message: "Retrying (repair loop)" });
720
+ if (artifacts) {
721
+ try {
722
+ await (0, status_writer_1.writeStatus)({
723
+ repoRoot: args.repoRoot,
724
+ rootDir: artifacts.rootDir,
725
+ runId,
726
+ backendId: args.backend.id,
727
+ workspaceMode: args.workspace.mode,
728
+ taskId: task.id,
729
+ phase,
730
+ iteration: iter,
731
+ tier: tierAfter === "warning" ? "warning" : "optimal",
732
+ budgetStatus: taskBudgetConfig ? taskBudgetManager.getStatus(taskBudgetConfig) : null,
733
+ message: "Repair loop",
734
+ });
735
+ }
736
+ catch {
737
+ artifacts = null;
738
+ }
739
+ }
305
740
  pendingRepairNotes = repairNotes;
306
- budgetManager.recordIteration(Date.now() - iterStarted);
741
+ const wallTimeMs = Date.now() - iterStarted;
742
+ runBudgetManager.recordIteration(wallTimeMs);
743
+ taskBudgetManager.recordIteration(wallTimeMs);
744
+ ledger.event({
745
+ taskId: task.id,
746
+ kind: "iteration_complete",
747
+ message: "Iteration complete",
748
+ data: { wallTimeMs, phase },
749
+ });
750
+ // Intent-based checkpointing (best-effort). Only enabled for worktree mode to avoid committing
751
+ // partial changes directly to the main working directory.
752
+ if (checkpointEvery &&
753
+ args.workspace.mode === "worktree" &&
754
+ iter % checkpointEvery === 0) {
755
+ await args.workspace.checkpoint(task.id, `Checkpoint iteration ${iter}`);
756
+ ledger.event({
757
+ taskId: task.id,
758
+ kind: "checkpoint",
759
+ message: `Checkpoint created (every ${checkpointEvery} iterations)`,
760
+ });
761
+ }
762
+ await refreshArtifacts();
307
763
  }
308
764
  ledger.event({
309
765
  taskId: task.id,
310
766
  kind: "max_iterations",
311
767
  message: `Max iterations reached (${maxIter})`,
312
768
  });
769
+ const treatAsHardCap = Boolean(taskBudgetConfig) && Boolean(taskBudgetManager.isAtHardCap(taskBudgetConfig));
770
+ if (treatAsHardCap) {
771
+ const status = taskBudgetManager.getStatus(taskBudgetConfig);
772
+ persistence.upsertTaskState({
773
+ runId,
774
+ taskId: task.id,
775
+ status: "blocked",
776
+ phase: "DIAGNOSE",
777
+ iteration: maxIter,
778
+ lastError: "Hard cap reached",
779
+ });
780
+ ledger.event({
781
+ taskId: task.id,
782
+ kind: "hard_cap",
783
+ message: "HARD cap reached (max iterations): blocking task (preserving workspace)",
784
+ data: status,
785
+ });
786
+ ledger.event({
787
+ taskId: task.id,
788
+ kind: "failure_summary",
789
+ message: "Failure summary (hard cap / max iterations)",
790
+ data: {
791
+ markdown: (0, failure_summary_1.buildFailureSummary)({
792
+ runId,
793
+ taskId: task.id,
794
+ reason: "Hard cap reached (max iterations)",
795
+ tier: "hard",
796
+ budgetStatus: status,
797
+ lastIssues: lastIssues?.map((i) => ({
798
+ level: i.level,
799
+ kind: i.kind,
800
+ message: i.message,
801
+ file: i.file,
802
+ })),
803
+ ledgerEvents: persistence.listLedger({ runId, limit: 200 }),
804
+ }),
805
+ },
806
+ });
807
+ await refreshArtifacts();
808
+ return { ok: false, runId, exitCode: 2, reason: "Hard cap" };
809
+ }
313
810
  persistence.upsertTaskState({
314
811
  runId,
315
812
  taskId: task.id,
@@ -318,6 +815,7 @@ class EngineLoop {
318
815
  iteration: maxIter,
319
816
  lastError: "Max iterations reached",
320
817
  });
818
+ await refreshArtifacts();
321
819
  return { ok: false, runId, exitCode: 3, reason: "Max iterations reached" };
322
820
  }
323
821
  toTaskBudgetConfig(task) {