ralphy-spec 0.2.0 → 0.3.1
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/CHANGELOG.md +37 -0
- package/README.ja.md +38 -15
- package/README.ko.md +38 -15
- package/README.md +47 -15
- package/README.zh.md +38 -15
- package/dist/cli/budget.d.ts +3 -0
- package/dist/cli/budget.d.ts.map +1 -0
- package/dist/cli/budget.js +77 -0
- package/dist/cli/budget.js.map +1 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +6 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +42 -9
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +31 -0
- package/dist/cli/status.js.map +1 -1
- package/dist/core/artifacts/budget-writer.d.ts +11 -0
- package/dist/core/artifacts/budget-writer.d.ts.map +1 -0
- package/dist/core/artifacts/budget-writer.js +28 -0
- package/dist/core/artifacts/budget-writer.js.map +1 -0
- package/dist/core/artifacts/run-log-writer.d.ts +20 -0
- package/dist/core/artifacts/run-log-writer.d.ts.map +1 -0
- package/dist/core/artifacts/run-log-writer.js +40 -0
- package/dist/core/artifacts/run-log-writer.js.map +1 -0
- package/dist/core/artifacts/run-log-writer.test.d.ts +2 -0
- package/dist/core/artifacts/run-log-writer.test.d.ts.map +1 -0
- package/dist/core/artifacts/run-log-writer.test.js +37 -0
- package/dist/core/artifacts/run-log-writer.test.js.map +1 -0
- package/dist/core/artifacts/status-writer.d.ts +16 -0
- package/dist/core/artifacts/status-writer.d.ts.map +1 -0
- package/dist/core/artifacts/status-writer.js +52 -0
- package/dist/core/artifacts/status-writer.js.map +1 -0
- package/dist/core/artifacts/status-writer.test.d.ts +2 -0
- package/dist/core/artifacts/status-writer.test.d.ts.map +1 -0
- package/dist/core/artifacts/status-writer.test.js +47 -0
- package/dist/core/artifacts/status-writer.test.js.map +1 -0
- package/dist/core/artifacts/task-artifacts.d.ts +19 -0
- package/dist/core/artifacts/task-artifacts.d.ts.map +1 -0
- package/dist/core/artifacts/task-artifacts.js +35 -0
- package/dist/core/artifacts/task-artifacts.js.map +1 -0
- package/dist/core/artifacts/tasks-writer.d.ts +19 -0
- package/dist/core/artifacts/tasks-writer.d.ts.map +1 -0
- package/dist/core/artifacts/tasks-writer.js +67 -0
- package/dist/core/artifacts/tasks-writer.js.map +1 -0
- package/dist/core/artifacts/tasks-writer.test.d.ts +2 -0
- package/dist/core/artifacts/tasks-writer.test.d.ts.map +1 -0
- package/dist/core/artifacts/tasks-writer.test.js +28 -0
- package/dist/core/artifacts/tasks-writer.test.js.map +1 -0
- package/dist/core/backends/cursor.d.ts.map +1 -1
- package/dist/core/backends/cursor.js +31 -8
- package/dist/core/backends/cursor.js.map +1 -1
- package/dist/core/budgets/errors.d.ts +5 -0
- package/dist/core/budgets/errors.d.ts.map +1 -0
- package/dist/core/budgets/errors.js +11 -0
- package/dist/core/budgets/errors.js.map +1 -0
- package/dist/core/engine/constraints.d.ts +16 -0
- package/dist/core/engine/constraints.d.ts.map +1 -0
- package/dist/core/engine/constraints.js +21 -0
- package/dist/core/engine/constraints.js.map +1 -0
- package/dist/core/engine/constraints.policy.test.d.ts +2 -0
- package/dist/core/engine/constraints.policy.test.d.ts.map +1 -0
- package/dist/core/engine/constraints.policy.test.js +85 -0
- package/dist/core/engine/constraints.policy.test.js.map +1 -0
- package/dist/core/engine/loop.d.ts.map +1 -1
- package/dist/core/engine/loop.hardcap.test.d.ts +2 -0
- package/dist/core/engine/loop.hardcap.test.d.ts.map +1 -0
- package/dist/core/engine/loop.hardcap.test.js +77 -0
- package/dist/core/engine/loop.hardcap.test.js.map +1 -0
- package/dist/core/engine/loop.js +511 -13
- package/dist/core/engine/loop.js.map +1 -1
- package/dist/core/memory/persistence.d.ts +9 -0
- package/dist/core/memory/persistence.d.ts.map +1 -1
- package/dist/core/memory/persistence.js +19 -1
- package/dist/core/memory/persistence.js.map +1 -1
- package/dist/core/reporting/failure-summary.d.ts +23 -0
- package/dist/core/reporting/failure-summary.d.ts.map +1 -0
- package/dist/core/reporting/failure-summary.js +63 -0
- package/dist/core/reporting/failure-summary.js.map +1 -0
- package/dist/core/reporting/failure-summary.test.d.ts +2 -0
- package/dist/core/reporting/failure-summary.test.d.ts.map +1 -0
- package/dist/core/reporting/failure-summary.test.js +22 -0
- package/dist/core/reporting/failure-summary.test.js.map +1 -0
- package/dist/core/spec/loader.d.ts.map +1 -1
- package/dist/core/spec/loader.js +12 -1
- package/dist/core/spec/loader.js.map +1 -1
- package/dist/core/spec/schemas.d.ts +857 -0
- package/dist/core/spec/schemas.d.ts.map +1 -1
- package/dist/core/spec/schemas.js +28 -0
- package/dist/core/spec/schemas.js.map +1 -1
- package/dist/core/spec/sprint-defaults.d.ts +16 -0
- package/dist/core/spec/sprint-defaults.d.ts.map +1 -0
- package/dist/core/spec/sprint-defaults.js +55 -0
- package/dist/core/spec/sprint-defaults.js.map +1 -0
- package/dist/core/spec/sprint-defaults.test.d.ts +2 -0
- package/dist/core/spec/sprint-defaults.test.d.ts.map +1 -0
- package/dist/core/spec/sprint-defaults.test.js +51 -0
- package/dist/core/spec/sprint-defaults.test.js.map +1 -0
- package/dist/core/spec/types.d.ts +11 -0
- package/dist/core/spec/types.d.ts.map +1 -1
- package/dist/core/validators/types.d.ts +1 -1
- package/dist/core/validators/types.d.ts.map +1 -1
- package/dist/core/workspace/scope-detector.d.ts +13 -0
- package/dist/core/workspace/scope-detector.d.ts.map +1 -0
- package/dist/core/workspace/scope-detector.js +34 -0
- package/dist/core/workspace/scope-detector.js.map +1 -0
- package/dist/core/workspace/worktree-mode.d.ts.map +1 -1
- package/dist/core/workspace/worktree-mode.js +2 -1
- package/dist/core/workspace/worktree-mode.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/templates/claude-code/ralphy-archive.md +1 -1
- package/dist/templates/claude-code/ralphy-implement.md +1 -1
- package/dist/templates/claude-code/ralphy-plan.md +1 -1
- package/dist/templates/claude-code/ralphy-validate.md +1 -1
- package/dist/templates/cursor/ralphy-archive.md +1 -1
- package/dist/templates/cursor/ralphy-implement.md +1 -1
- package/dist/templates/cursor/ralphy-plan.md +1 -1
- package/dist/templates/cursor/ralphy-validate.md +1 -1
- package/package.json +7 -4
package/dist/core/engine/loop.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 ?
|
|
387
|
+
const tier = taskBudgetConfig ? taskBudgetManager.getTier(taskBudgetConfig) : "optimal";
|
|
145
388
|
const shouldDegrade = taskBudgetConfig
|
|
146
|
-
?
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
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) {
|