@voybio/ace-swarm 2.4.4 → 2.4.7
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 +69 -0
- package/README.md +72 -532
- package/assets/eval-battery/battery.json +85 -0
- package/assets/eval-battery/fixtures/edit-target.js +3 -0
- package/assets/scripts/render-mcp-configs.sh +12 -0
- package/dist/ace-context.d.ts +18 -0
- package/dist/ace-context.js +161 -16
- package/dist/ace-server-instructions.js +41 -12
- package/dist/bridge-trace.d.ts +17 -0
- package/dist/bridge-trace.js +23 -0
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +623 -11
- package/dist/discovery-runtime-wrappers.d.ts +16 -1
- package/dist/discovery-runtime-wrappers.js +65 -9
- package/dist/eval-harness.d.ts +110 -0
- package/dist/eval-harness.js +254 -0
- package/dist/experience-reflector.d.ts +13 -0
- package/dist/experience-reflector.js +62 -0
- package/dist/goal-engine.d.ts +155 -0
- package/dist/goal-engine.js +795 -0
- package/dist/helpers/store-resolution.js +40 -39
- package/dist/hermes/session-manager.d.ts +10 -0
- package/dist/hermes/session-manager.js +130 -27
- package/dist/job-scheduler.d.ts +58 -0
- package/dist/job-scheduler.js +202 -2
- package/dist/job-worker.d.ts +87 -0
- package/dist/job-worker.js +182 -0
- package/dist/local-model-policy.d.ts +8 -2
- package/dist/local-model-policy.js +31 -17
- package/dist/local-model-runtime.d.ts +6 -0
- package/dist/local-model-runtime.js +130 -2
- package/dist/measured-classing.d.ts +66 -0
- package/dist/measured-classing.js +165 -0
- package/dist/model-bridge.d.ts +45 -0
- package/dist/model-bridge.js +502 -44
- package/dist/orchestrator-supervisor.js +1 -1
- package/dist/plan-proposal.d.ts +7 -0
- package/dist/plan-proposal.js +74 -4
- package/dist/playbook-adaptation.d.ts +37 -0
- package/dist/playbook-adaptation.js +159 -0
- package/dist/playbook-curator.d.ts +16 -0
- package/dist/playbook-curator.js +106 -0
- package/dist/playbook-llm-reflector.d.ts +26 -0
- package/dist/playbook-llm-reflector.js +157 -0
- package/dist/public-surface.d.ts +2 -0
- package/dist/public-surface.js +26 -14
- package/dist/safe-edit.d.ts +20 -0
- package/dist/safe-edit.js +355 -78
- package/dist/server.d.ts +11 -2
- package/dist/server.js +55 -5
- package/dist/shared.d.ts +12 -0
- package/dist/shared.js +13 -1
- package/dist/source-mutation-coordinator.d.ts +54 -0
- package/dist/source-mutation-coordinator.js +259 -0
- package/dist/status-events.d.ts +4 -1
- package/dist/status-events.js +41 -6
- package/dist/store/ace-packed-store.d.ts +28 -9
- package/dist/store/ace-packed-store.js +324 -57
- package/dist/store/bgzf.d.ts +4 -0
- package/dist/store/bgzf.js +94 -0
- package/dist/store/bootstrap-store.d.ts +4 -1
- package/dist/store/bootstrap-store.js +57 -3
- package/dist/store/cache-workspace.d.ts +8 -0
- package/dist/store/cache-workspace.js +67 -1
- package/dist/store/crc32.d.ts +2 -0
- package/dist/store/crc32.js +16 -0
- package/dist/store/fsck.d.ts +16 -0
- package/dist/store/fsck.js +240 -0
- package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
- package/dist/store/materializers/context-snapshot-materializer.js +4 -1
- package/dist/store/materializers/projection-manager.d.ts +7 -0
- package/dist/store/materializers/projection-manager.js +37 -0
- package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
- package/dist/store/repositories/goal-repository.d.ts +112 -0
- package/dist/store/repositories/goal-repository.js +157 -0
- package/dist/store/repositories/playbook-repository.d.ts +107 -0
- package/dist/store/repositories/playbook-repository.js +314 -0
- package/dist/store/store-artifacts.js +44 -2
- package/dist/store/store-authority-audit.js +1 -0
- package/dist/store/store-cli.d.ts +30 -0
- package/dist/store/store-cli.js +242 -0
- package/dist/store/store-snapshot.d.ts +12 -0
- package/dist/store/store-snapshot.js +74 -11
- package/dist/store/types.d.ts +1 -0
- package/dist/store/write-coordinator.d.ts +7 -0
- package/dist/store/write-coordinator.js +11 -0
- package/dist/todo-state.d.ts +21 -0
- package/dist/todo-state.js +150 -53
- package/dist/tools-agent.js +86 -22
- package/dist/tools-discovery.js +5 -3
- package/dist/tools-files.js +39 -18
- package/dist/tools-framework.js +31 -3
- package/dist/tools-goal.d.ts +11 -0
- package/dist/tools-goal.js +108 -0
- package/dist/tools-handoff.js +7 -11
- package/dist/tools-lifecycle.js +2 -0
- package/dist/tools-scheduler.js +44 -1
- package/dist/tools-skills.js +3 -2
- package/dist/tools-todo.js +64 -3
- package/dist/tools.js +2 -0
- package/dist/tui/chat.js +4 -0
- package/dist/tui/index.js +4 -1
- package/dist/tui/ollama.d.ts +2 -0
- package/dist/tui/openai-compatible.d.ts +14 -0
- package/dist/tui/openai-compatible.js +15 -2
- package/dist/tui/provider-discovery.d.ts +1 -0
- package/dist/tui/provider-discovery.js +2 -2
- package/docs/cli-reference.md +127 -0
- package/docs/development.md +46 -0
- package/docs/evaluation-safety.md +27 -0
- package/docs/experience-playbook.md +35 -0
- package/docs/hermes.md +129 -0
- package/docs/library-api.md +31 -0
- package/docs/mcp-tool-reference.md +290 -0
- package/docs/provider-security.md +85 -0
- package/docs/state-model.md +113 -0
- package/docs/troubleshooting.md +95 -0
- package/docs/tui-operator-guide.md +75 -0
- package/package.json +3 -1
- package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACE Goal Engine (WS-F).
|
|
3
|
+
*
|
|
4
|
+
* `startGoal("<objective>")` compiles a plan with ACE's own planner, then a
|
|
5
|
+
* self-paced driver loop (`runGoal`) executes it step-by-step under evidence-
|
|
6
|
+
* gated completion. A goal can leave `executing` only through one of four
|
|
7
|
+
* control-flow paths — evidence-gated success, budget exhaustion, operator
|
|
8
|
+
* stop, or an escalated blocker — so "declaring success by prose" is
|
|
9
|
+
* structurally impossible (there is no code path from a model message to
|
|
10
|
+
* `status: "done"`).
|
|
11
|
+
*
|
|
12
|
+
* That guarantee rests on the success contract carrying at least one
|
|
13
|
+
* machine-checkable obligation. A contract with none is refused at `startGoal`
|
|
14
|
+
* and again at verify time with `contract_uncheckable`, never satisfied
|
|
15
|
+
* vacuously — see {@link enforceableObligationCount}.
|
|
16
|
+
*
|
|
17
|
+
* This composes existing organs rather than forking a second orchestrator:
|
|
18
|
+
* - planning: proposePlan → validatePlan (blocking) → createTaskPlan
|
|
19
|
+
* - execution: getReadyTaskSteps / applyBridgeResultToStep / amendTaskPlan
|
|
20
|
+
* (the supervisor's own step primitives), one step per tick
|
|
21
|
+
* - completion: the same artifact-existence check the ModelBridge false-
|
|
22
|
+
* completion gate uses, run against the compiled success contract
|
|
23
|
+
*
|
|
24
|
+
* The single new mechanism is the goal record + tick/budget policy. All side
|
|
25
|
+
* effects (spawning a step, running a verification command, checking an
|
|
26
|
+
* artifact, sleeping, posting) are injectable so the engine is driven
|
|
27
|
+
* hermetically in tests with a stub bridge.
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync } from "node:fs";
|
|
30
|
+
import { isAbsolute, join } from "node:path";
|
|
31
|
+
import { amendTaskPlan, applyBridgeResultToStep, createTaskPlan, deriveTaskPlanStatus, getReadyTaskSteps, markTaskStepStatus, } from "./orchestrator-supervisor.js";
|
|
32
|
+
import { proposePlan, validatePlan } from "./plan-proposal.js";
|
|
33
|
+
import { resolveWorkspaceRoot } from "./helpers.js";
|
|
34
|
+
import { appendVericifyProcessPostSafe } from "./vericify-bridge.js";
|
|
35
|
+
import { appendStatusEventSafe } from "./status-events.js";
|
|
36
|
+
import { GoalRepository, defaultGoalBudgets, newGoalId, } from "./store/repositories/goal-repository.js";
|
|
37
|
+
import { PlaybookRepository } from "./store/repositories/playbook-repository.js";
|
|
38
|
+
// ── Defaults for the injectable seams ────────────────────────────────────────
|
|
39
|
+
function defaultArtifactExists(path, workspaceRoot) {
|
|
40
|
+
if (!path || path.trim().length === 0)
|
|
41
|
+
return false;
|
|
42
|
+
const clean = path.split("#")[0].trim();
|
|
43
|
+
const abs = isAbsolute(clean) ? clean : join(workspaceRoot, clean);
|
|
44
|
+
return existsSync(abs);
|
|
45
|
+
}
|
|
46
|
+
async function defaultRunVerification(command, workspaceRoot) {
|
|
47
|
+
const { exec } = await import("node:child_process");
|
|
48
|
+
return await new Promise((resolve) => {
|
|
49
|
+
exec(command, { cwd: workspaceRoot, timeout: 10 * 60 * 1000, maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
50
|
+
resolve({
|
|
51
|
+
ok: !error,
|
|
52
|
+
output: `${stdout ?? ""}${stderr ? `\n${stderr}` : ""}`.trim(),
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function defaultSpawnStep(step, _plan, _goal) {
|
|
58
|
+
const { runLocalModelTask } = await import("./local-model-runtime.js");
|
|
59
|
+
const outcome = await runLocalModelTask({
|
|
60
|
+
task: step.task,
|
|
61
|
+
role: step.role,
|
|
62
|
+
toolScope: step.tool_scope,
|
|
63
|
+
});
|
|
64
|
+
return outcome.result;
|
|
65
|
+
}
|
|
66
|
+
function defaultSleep(ms) {
|
|
67
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
68
|
+
}
|
|
69
|
+
function resolveDeps(deps = {}) {
|
|
70
|
+
const workspaceRoot = deps.workspaceRoot ?? resolveWorkspaceRoot();
|
|
71
|
+
const repo = new GoalRepository(workspaceRoot);
|
|
72
|
+
return {
|
|
73
|
+
workspaceRoot,
|
|
74
|
+
repo,
|
|
75
|
+
now: deps.now ?? (() => Date.now()),
|
|
76
|
+
spawnStep: deps.spawnStep ?? defaultSpawnStep,
|
|
77
|
+
compilePlan: deps.compilePlan ??
|
|
78
|
+
(async (objective) => {
|
|
79
|
+
const proposal = await proposePlan(objective);
|
|
80
|
+
const verdict = await validatePlan({ proposal });
|
|
81
|
+
return { proposal, verdict };
|
|
82
|
+
}),
|
|
83
|
+
runVerification: deps.runVerification ?? defaultRunVerification,
|
|
84
|
+
artifactExists: deps.artifactExists ?? defaultArtifactExists,
|
|
85
|
+
sleep: deps.sleep ?? defaultSleep,
|
|
86
|
+
emitStatusEvent: deps.emitStatusEvent ??
|
|
87
|
+
(async (input) => {
|
|
88
|
+
await appendStatusEventSafe({
|
|
89
|
+
source_module: "goal-engine",
|
|
90
|
+
event_type: `GOAL_${input.status.toUpperCase()}`,
|
|
91
|
+
status: "in_progress",
|
|
92
|
+
summary: input.summary,
|
|
93
|
+
payload: input.payload,
|
|
94
|
+
});
|
|
95
|
+
}),
|
|
96
|
+
appendProcessPost: deps.appendProcessPost ??
|
|
97
|
+
(async (input) => {
|
|
98
|
+
const kind = input.kind === "progress" || input.kind === "completion" || input.kind === "blocker"
|
|
99
|
+
? input.kind
|
|
100
|
+
: "blocker";
|
|
101
|
+
await appendVericifyProcessPostSafe({
|
|
102
|
+
run_id: "workspace:current",
|
|
103
|
+
agent_id: "ace-goal-engine",
|
|
104
|
+
kind: kind === "completion" ? "completion" : kind === "progress" ? "progress" : "blocker",
|
|
105
|
+
summary: input.summary,
|
|
106
|
+
blocker_category: kind === "blocker" ? input.reason_code : undefined,
|
|
107
|
+
evidence_refs: input.evidence_refs,
|
|
108
|
+
});
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// ── Success-contract compilation ─────────────────────────────────────────────
|
|
113
|
+
const CRITERION_VERIFICATION_HINT = /^\s*verify\s*:\s*(.+)$/i;
|
|
114
|
+
const OBJECTIVE_VERIFICATION_HINT = /(?:^|[.!?]\s+)verify\s*:\s*(.+)$/i;
|
|
115
|
+
/**
|
|
116
|
+
* Extract explicitly delimited shell directives. Planner-authored criteria must
|
|
117
|
+
* devote the whole line to `verify:`. Objectives additionally support the
|
|
118
|
+
* documented trailing sentence form, `Ship it. verify: npm test`. In particular,
|
|
119
|
+
* ordinary prose containing `verification:` is never executable.
|
|
120
|
+
*/
|
|
121
|
+
function extractVerificationCommands(text, mode) {
|
|
122
|
+
const found = [];
|
|
123
|
+
const hint = mode === "criterion" ? CRITERION_VERIFICATION_HINT : OBJECTIVE_VERIFICATION_HINT;
|
|
124
|
+
for (const line of text.split(/\r?\n/)) {
|
|
125
|
+
const match = hint.exec(line);
|
|
126
|
+
const command = match?.[1]?.trim().replace(/\.$/, "").trim();
|
|
127
|
+
if (command)
|
|
128
|
+
found.push(command);
|
|
129
|
+
}
|
|
130
|
+
return found;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* An evidence ref is only machine-checkable when it names a filesystem path, and
|
|
134
|
+
* only a path separator proves that. A bare `foo.patch` or `v1.2` matches an
|
|
135
|
+
* "ends in an extension" test but names a hash or a version, so requiring it to
|
|
136
|
+
* exist yields a contract that `startGoal` accepts and `checkSuccessContract` can
|
|
137
|
+
* never satisfy. Those route to `uncompilable_criteria` for the qa step instead.
|
|
138
|
+
*
|
|
139
|
+
* Note the rest of the codebase treats `required_evidence_refs` as *substrings*
|
|
140
|
+
* matched against emitted evidence (tools-agent.ts `missingEvidence`); this engine
|
|
141
|
+
* is the only consumer that resolves them against the filesystem.
|
|
142
|
+
*/
|
|
143
|
+
function isPathLikeEvidenceRef(ref) {
|
|
144
|
+
const clean = (ref.split("#")[0] ?? "").trim();
|
|
145
|
+
return clean.length > 0 && clean.includes("/");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The number of machine-checkable obligations in a contract. Zero means the
|
|
149
|
+
* only thing standing between the model and `status: "done"` is the model's own
|
|
150
|
+
* assertion — which is exactly what this engine exists to prevent.
|
|
151
|
+
*
|
|
152
|
+
* `gate_ids` is excluded: nothing populates it yet (see goal-repository.ts).
|
|
153
|
+
*/
|
|
154
|
+
export function enforceableObligationCount(contract) {
|
|
155
|
+
return (contract.expected_artifacts.filter((a) => a.required !== false).length +
|
|
156
|
+
contract.required_evidence_refs.length +
|
|
157
|
+
contract.verification_commands.length);
|
|
158
|
+
}
|
|
159
|
+
function compileSuccessContract(proposal, input = {}) {
|
|
160
|
+
const expected = new Map();
|
|
161
|
+
const evidence = new Set();
|
|
162
|
+
const commands = new Set();
|
|
163
|
+
const uncompilable = [];
|
|
164
|
+
for (const step of proposal.steps) {
|
|
165
|
+
for (const artifact of step.expected_artifacts ?? []) {
|
|
166
|
+
if (!artifact.path)
|
|
167
|
+
continue;
|
|
168
|
+
// Scaffolded paths were invented by the deterministic planner to satisfy
|
|
169
|
+
// validatePlan, not promised by anyone. `agent-state/SPEC_CONTRACT.json` is
|
|
170
|
+
// shared across every goal in a workspace, so admitting it as an obligation
|
|
171
|
+
// lets one goal's leftover file certify the next goal's prose. Keep it in
|
|
172
|
+
// the plan, keep it out of the contract.
|
|
173
|
+
if (artifact.scaffolded)
|
|
174
|
+
continue;
|
|
175
|
+
expected.set(artifact.path, {
|
|
176
|
+
path: artifact.path,
|
|
177
|
+
required: artifact.required !== false,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
for (const ref of step.required_evidence_refs ?? []) {
|
|
181
|
+
if (!ref)
|
|
182
|
+
continue;
|
|
183
|
+
// A ref like "reviewer sign-off" is not a path; enforcing it via
|
|
184
|
+
// artifactExists would fail forever. Adjudicate it in the qa step instead.
|
|
185
|
+
if (isPathLikeEvidenceRef(ref))
|
|
186
|
+
evidence.add(ref);
|
|
187
|
+
else
|
|
188
|
+
uncompilable.push(`evidence: ${ref}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Success criteria and the objective both carry `verify: <cmd>` directives;
|
|
192
|
+
// everything else is uncompilable and routed to a qa step. The objective is the
|
|
193
|
+
// only channel a CLI user has, so it must be read: `ace goal start "Ship it.
|
|
194
|
+
// verify: npm test"`.
|
|
195
|
+
for (const criterion of proposal.success_criteria ?? []) {
|
|
196
|
+
const found = extractVerificationCommands(criterion, "criterion");
|
|
197
|
+
if (found.length > 0)
|
|
198
|
+
for (const command of found)
|
|
199
|
+
commands.add(command);
|
|
200
|
+
else
|
|
201
|
+
uncompilable.push(criterion);
|
|
202
|
+
}
|
|
203
|
+
for (const command of extractVerificationCommands(input.objective ?? "", "objective")) {
|
|
204
|
+
commands.add(command);
|
|
205
|
+
}
|
|
206
|
+
for (const command of input.verify_commands ?? []) {
|
|
207
|
+
const trimmed = command.trim();
|
|
208
|
+
if (trimmed)
|
|
209
|
+
commands.add(trimmed);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
expected_artifacts: Array.from(expected.values()),
|
|
213
|
+
required_evidence_refs: Array.from(evidence),
|
|
214
|
+
verification_commands: Array.from(commands),
|
|
215
|
+
gate_ids: [],
|
|
216
|
+
uncompilable_criteria: uncompilable,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function proposalToPlanSteps(proposal) {
|
|
220
|
+
return proposal.steps.map((s) => ({
|
|
221
|
+
role: s.role,
|
|
222
|
+
task: s.task,
|
|
223
|
+
depends_on: s.depends_on && s.depends_on.length > 0 ? s.depends_on : undefined,
|
|
224
|
+
tool_scope: s.tool_scope && s.tool_scope.length > 0 ? s.tool_scope : undefined,
|
|
225
|
+
expected_output_class: s.expected_output_class,
|
|
226
|
+
expected_artifacts: s.expected_artifacts,
|
|
227
|
+
allowed_tools: s.allowed_tools,
|
|
228
|
+
forbidden_patterns: s.forbidden_patterns,
|
|
229
|
+
required_evidence_refs: s.required_evidence_refs,
|
|
230
|
+
structural_edit_plan_required: s.structural_edit_plan_required,
|
|
231
|
+
structural_edit_waiver: s.structural_edit_waiver,
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
// ── Escalation helper ────────────────────────────────────────────────────────
|
|
235
|
+
async function escalate(d, goal, kind, reasonCode, summary) {
|
|
236
|
+
const escalation = {
|
|
237
|
+
at: new Date(d.now()).toISOString(),
|
|
238
|
+
kind,
|
|
239
|
+
reason_code: reasonCode,
|
|
240
|
+
summary,
|
|
241
|
+
};
|
|
242
|
+
goal.escalations.push(escalation);
|
|
243
|
+
await d.emitStatusEvent({
|
|
244
|
+
status: kind === "run_complete" ? "done" : "blocked",
|
|
245
|
+
summary: `[${goal.goal_id}] ${summary}`,
|
|
246
|
+
payload: { goal_id: goal.goal_id, kind, reason_code: reasonCode },
|
|
247
|
+
});
|
|
248
|
+
await d.appendProcessPost({
|
|
249
|
+
kind: kind === "run_complete" ? "completion" : "blocker",
|
|
250
|
+
summary: `Goal ${goal.goal_id}: ${summary}`,
|
|
251
|
+
reason_code: reasonCode,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
export async function startGoal(objective, options = {}, deps = {}) {
|
|
255
|
+
const d = resolveDeps(deps);
|
|
256
|
+
const trimmed = objective.trim();
|
|
257
|
+
// `typeof NaN === "number"`, and NaN silently disables a budget in memory
|
|
258
|
+
// (`elapsed >= NaN` is false) while serialising to `null`, which makes the same
|
|
259
|
+
// budget fire immediately after the record is reloaded. Neither is a budget.
|
|
260
|
+
const budgets = {
|
|
261
|
+
...defaultGoalBudgets(),
|
|
262
|
+
...(Number.isFinite(options.budget_hours)
|
|
263
|
+
? { max_wall_clock_ms: Math.max(1, options.budget_hours) * 60 * 60 * 1000 }
|
|
264
|
+
: {}),
|
|
265
|
+
...(Number.isFinite(options.max_ticks) ? { max_ticks: options.max_ticks } : {}),
|
|
266
|
+
...(Number.isFinite(options.max_replans) ? { max_replans: options.max_replans } : {}),
|
|
267
|
+
};
|
|
268
|
+
const iso = new Date(d.now()).toISOString();
|
|
269
|
+
let goal = {
|
|
270
|
+
goal_id: options.goal_id ?? newGoalId(),
|
|
271
|
+
objective: trimmed,
|
|
272
|
+
status: "planning",
|
|
273
|
+
success_contract: {
|
|
274
|
+
expected_artifacts: [],
|
|
275
|
+
required_evidence_refs: [],
|
|
276
|
+
verification_commands: [],
|
|
277
|
+
gate_ids: [],
|
|
278
|
+
uncompilable_criteria: [],
|
|
279
|
+
},
|
|
280
|
+
budgets,
|
|
281
|
+
created_at: iso,
|
|
282
|
+
updated_at: iso,
|
|
283
|
+
tick_count: 0,
|
|
284
|
+
replan_count: 0,
|
|
285
|
+
stop_requested: false,
|
|
286
|
+
escalations: [],
|
|
287
|
+
};
|
|
288
|
+
if (trimmed.length === 0) {
|
|
289
|
+
goal.status = "failed";
|
|
290
|
+
goal.reason_code = "empty_objective";
|
|
291
|
+
goal = d.repo.saveGoal(goal);
|
|
292
|
+
return { ok: false, goal, reason_code: "empty_objective" };
|
|
293
|
+
}
|
|
294
|
+
goal = d.repo.saveGoal(goal);
|
|
295
|
+
// Compile + validate the plan. A plan that fails the quality gate never starts.
|
|
296
|
+
const { proposal, verdict } = await d.compilePlan(trimmed);
|
|
297
|
+
if (!verdict.ok) {
|
|
298
|
+
goal.status = "blocked";
|
|
299
|
+
goal.reason_code = "plan_rejected";
|
|
300
|
+
goal = d.repo.saveGoal(goal);
|
|
301
|
+
await escalate(d, goal, "approval_required", "plan_rejected", `plan rejected by quality gate: ${verdict.blocking_findings.join("; ")}`);
|
|
302
|
+
d.repo.saveGoal(goal);
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
goal,
|
|
306
|
+
reason_code: "plan_rejected",
|
|
307
|
+
blocking_findings: verdict.blocking_findings,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const contract = compileSuccessContract(proposal, {
|
|
311
|
+
objective: trimmed,
|
|
312
|
+
verify_commands: options.verify_commands,
|
|
313
|
+
});
|
|
314
|
+
// Fail fast: a goal that could only ever be completed by model assertion must
|
|
315
|
+
// never reach `executing`. Blocking here beats burning ten steps of model
|
|
316
|
+
// budget first.
|
|
317
|
+
if (enforceableObligationCount(contract) === 0) {
|
|
318
|
+
goal.status = "blocked";
|
|
319
|
+
goal.reason_code = "contract_uncheckable";
|
|
320
|
+
goal.success_contract = contract;
|
|
321
|
+
goal = d.repo.saveGoal(goal);
|
|
322
|
+
await escalate(d, goal, "approval_required", "contract_uncheckable", "compiled plan declares no required artifacts, no path-like evidence refs, and no " +
|
|
323
|
+
"`verify: <command>` success criteria — the goal could only ever be completed by " +
|
|
324
|
+
"model assertion. Append `verify: <shell command>` to the objective, or pass " +
|
|
325
|
+
"`--verify <shell command>`.");
|
|
326
|
+
d.repo.saveGoal(goal);
|
|
327
|
+
return { ok: false, goal, reason_code: "contract_uncheckable" };
|
|
328
|
+
}
|
|
329
|
+
let plan = createTaskPlan({
|
|
330
|
+
task: trimmed,
|
|
331
|
+
steps: proposalToPlanSteps(proposal),
|
|
332
|
+
execution_mode: "sequential",
|
|
333
|
+
});
|
|
334
|
+
// L3: uncompilable success criteria are never silently dropped — append a
|
|
335
|
+
// terminal qa review step that adjudicates them, gated after all other work.
|
|
336
|
+
if (contract.uncompilable_criteria.length > 0) {
|
|
337
|
+
const priorIds = plan.steps.map((s) => s.step_id);
|
|
338
|
+
plan = amendTaskPlan(plan, {
|
|
339
|
+
append_steps: [
|
|
340
|
+
{
|
|
341
|
+
role: "qa",
|
|
342
|
+
task: "Review and confirm the following success criteria that could not be " +
|
|
343
|
+
`machine-verified:\n- ${contract.uncompilable_criteria.join("\n- ")}`,
|
|
344
|
+
depends_on: priorIds,
|
|
345
|
+
expected_output_class: "qa_verdict",
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
goal.plan_id = plan.plan_id;
|
|
351
|
+
goal.success_contract = contract;
|
|
352
|
+
d.repo.savePlan(goal.goal_id, plan);
|
|
353
|
+
if (options.plan_only) {
|
|
354
|
+
goal.status = "planning";
|
|
355
|
+
goal.result_summary = `Plan compiled (${plan.steps.length} steps); execution deferred (plan_only).`;
|
|
356
|
+
goal = d.repo.saveGoal(goal);
|
|
357
|
+
return { ok: true, goal };
|
|
358
|
+
}
|
|
359
|
+
goal.status = "executing";
|
|
360
|
+
goal.started_at = iso;
|
|
361
|
+
goal.last_heartbeat_at = iso;
|
|
362
|
+
goal = d.repo.saveGoal(goal);
|
|
363
|
+
return { ok: true, goal };
|
|
364
|
+
}
|
|
365
|
+
export async function tickGoal(goalId, deps = {}) {
|
|
366
|
+
const d = resolveDeps(deps);
|
|
367
|
+
const goal = requireGoal(d, goalId);
|
|
368
|
+
const plan = requirePlan(d, goal);
|
|
369
|
+
const ready = getReadyTaskSteps(plan);
|
|
370
|
+
if (ready.length === 0) {
|
|
371
|
+
return { goal, plan, progressed: false };
|
|
372
|
+
}
|
|
373
|
+
const step = ready[0];
|
|
374
|
+
// Mark running and persist *before* the spawn so a crash mid-step leaves a
|
|
375
|
+
// `running` step that resumeGoal can detect and reset.
|
|
376
|
+
let nextPlan = markTaskStepStatus(plan, step.step_id, "running");
|
|
377
|
+
goal.current_step_id = step.step_id;
|
|
378
|
+
goal.tick_count += 1;
|
|
379
|
+
goal.last_heartbeat_at = new Date(d.now()).toISOString();
|
|
380
|
+
d.repo.savePlan(goalId, nextPlan);
|
|
381
|
+
d.repo.saveGoal(goal);
|
|
382
|
+
await d.emitStatusEvent({
|
|
383
|
+
status: "step_started",
|
|
384
|
+
summary: `tick ${goal.tick_count}: running ${step.step_id} (${step.role})`,
|
|
385
|
+
payload: { goal_id: goalId, step_id: step.step_id, role: step.role },
|
|
386
|
+
});
|
|
387
|
+
let result;
|
|
388
|
+
try {
|
|
389
|
+
result = await d.spawnStep(step, nextPlan, goal);
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
result = {
|
|
393
|
+
bridge_id: `goal-${goalId}`,
|
|
394
|
+
role: step.role,
|
|
395
|
+
status: "failed",
|
|
396
|
+
summary: err instanceof Error ? err.message : String(err),
|
|
397
|
+
turns: 0,
|
|
398
|
+
tool_calls: [],
|
|
399
|
+
child_results: [],
|
|
400
|
+
reason_code: "spawn_threw",
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
nextPlan = applyBridgeResultToStep(nextPlan, step.step_id, result);
|
|
404
|
+
const finalStep = nextPlan.steps.find((s) => s.step_id === step.step_id);
|
|
405
|
+
// Re-hydrate so any field mutated *during* the spawn (e.g. an operator
|
|
406
|
+
// stop_requested via stopGoal) is preserved rather than clobbered by this
|
|
407
|
+
// tick's stale in-memory snapshot.
|
|
408
|
+
const latestGoal = d.repo.getGoal(goalId) ?? goal;
|
|
409
|
+
latestGoal.current_step_id = step.step_id;
|
|
410
|
+
latestGoal.last_heartbeat_at = new Date(d.now()).toISOString();
|
|
411
|
+
d.repo.savePlan(goalId, nextPlan);
|
|
412
|
+
d.repo.saveGoal(latestGoal);
|
|
413
|
+
await d.appendProcessPost({
|
|
414
|
+
kind: "progress",
|
|
415
|
+
summary: `Goal ${goalId} tick ${latestGoal.tick_count}: ${step.step_id} → ${finalStep?.status ?? "unknown"} (${result.summary})`,
|
|
416
|
+
evidence_refs: finalStep?.evidence_refs,
|
|
417
|
+
});
|
|
418
|
+
return {
|
|
419
|
+
goal: latestGoal,
|
|
420
|
+
plan: nextPlan,
|
|
421
|
+
progressed: true,
|
|
422
|
+
step_id: step.step_id,
|
|
423
|
+
step_status: finalStep?.status,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
export async function checkSuccessContract(d, goal) {
|
|
427
|
+
const contract = goal.success_contract;
|
|
428
|
+
// A contract with nothing to check cannot certify anything. Refuse rather
|
|
429
|
+
// than pass vacuously — the whole point of the engine (see module docstring).
|
|
430
|
+
if (enforceableObligationCount(contract) === 0) {
|
|
431
|
+
return {
|
|
432
|
+
ok: false,
|
|
433
|
+
missing_artifacts: [],
|
|
434
|
+
missing_evidence_refs: [],
|
|
435
|
+
failed_commands: [],
|
|
436
|
+
uncheckable: true,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const missing = [];
|
|
440
|
+
for (const artifact of contract.expected_artifacts) {
|
|
441
|
+
if (artifact.required === false)
|
|
442
|
+
continue;
|
|
443
|
+
if (!d.artifactExists(artifact.path, d.workspaceRoot)) {
|
|
444
|
+
missing.push(artifact.path);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const missingEvidence = [];
|
|
448
|
+
for (const ref of contract.required_evidence_refs) {
|
|
449
|
+
if (!d.artifactExists(ref, d.workspaceRoot))
|
|
450
|
+
missingEvidence.push(ref);
|
|
451
|
+
}
|
|
452
|
+
const failed = [];
|
|
453
|
+
for (const command of contract.verification_commands) {
|
|
454
|
+
const outcome = await d.runVerification(command, d.workspaceRoot);
|
|
455
|
+
if (!outcome.ok)
|
|
456
|
+
failed.push(command);
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
ok: missing.length === 0 && missingEvidence.length === 0 && failed.length === 0,
|
|
460
|
+
missing_artifacts: missing,
|
|
461
|
+
missing_evidence_refs: missingEvidence,
|
|
462
|
+
failed_commands: failed,
|
|
463
|
+
uncheckable: false,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const BACKOFF_START_MS = 5_000;
|
|
467
|
+
const BACKOFF_CAP_MS = 60_000;
|
|
468
|
+
function playbookEnabled() {
|
|
469
|
+
const raw = String(process.env.ACE_PLAYBOOK ?? "").trim().toLowerCase();
|
|
470
|
+
return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
|
|
471
|
+
}
|
|
472
|
+
function playbookRetryNote(workspaceRoot, reasonCode) {
|
|
473
|
+
if (!playbookEnabled() || !reasonCode)
|
|
474
|
+
return undefined;
|
|
475
|
+
try {
|
|
476
|
+
const count = new PlaybookRepository(workspaceRoot)
|
|
477
|
+
.listBullets({ status: "active" })
|
|
478
|
+
.filter((bullet) => bullet.scope.reason_code === reasonCode).length;
|
|
479
|
+
if (count === 0)
|
|
480
|
+
return undefined;
|
|
481
|
+
return `Playbook: ${count} lessons for reason ${reasonCode} are active in your context`;
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
export async function runGoal(goalId, deps = {}) {
|
|
488
|
+
const d = resolveDeps(deps);
|
|
489
|
+
let goal = requireGoal(d, goalId);
|
|
490
|
+
if (goal.status !== "executing" && goal.status !== "verifying") {
|
|
491
|
+
return { goal, plan: d.repo.getPlan(goalId) };
|
|
492
|
+
}
|
|
493
|
+
let backoff = BACKOFF_START_MS;
|
|
494
|
+
// Bounded by max_ticks; the loop can only exit through an explicit terminal
|
|
495
|
+
// transition below, never by a model asserting completion.
|
|
496
|
+
for (;;) {
|
|
497
|
+
goal = requireGoal(d, goalId);
|
|
498
|
+
// 1. Operator stop.
|
|
499
|
+
if (goal.stop_requested) {
|
|
500
|
+
goal.status = "stopped";
|
|
501
|
+
goal.result_summary = "Stopped by operator.";
|
|
502
|
+
goal = d.repo.saveGoal(goal);
|
|
503
|
+
await escalate(d, goal, "run_error", "operator_stop", "stopped by operator");
|
|
504
|
+
d.repo.saveGoal(goal);
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
// 2. Budget enforcement.
|
|
508
|
+
const budgetReason = budgetExhausted(d, goal);
|
|
509
|
+
if (budgetReason) {
|
|
510
|
+
const budgetCode = `goal_budget_exhausted:${budgetReason}`;
|
|
511
|
+
goal.status = "blocked";
|
|
512
|
+
goal.reason_code = budgetCode;
|
|
513
|
+
goal = d.repo.saveGoal(goal);
|
|
514
|
+
await escalate(d, goal, "budget_blocked", budgetCode, `budget exhausted (${budgetReason})`);
|
|
515
|
+
d.repo.saveGoal(goal);
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
const plan = requirePlan(d, goal);
|
|
519
|
+
const status = deriveTaskPlanStatus(plan);
|
|
520
|
+
// 3. Plan finished all steps → verify against the success contract.
|
|
521
|
+
if (status === "done") {
|
|
522
|
+
goal.status = "verifying";
|
|
523
|
+
goal = d.repo.saveGoal(goal);
|
|
524
|
+
const check = await checkSuccessContract(d, goal);
|
|
525
|
+
// Defense in depth: a hand-edited or pre-fix goal record can carry a
|
|
526
|
+
// vacuous contract past startGoal. Never certify it on prose alone.
|
|
527
|
+
if (check.uncheckable) {
|
|
528
|
+
goal.status = "blocked";
|
|
529
|
+
goal.reason_code = "contract_uncheckable";
|
|
530
|
+
goal = d.repo.saveGoal(goal);
|
|
531
|
+
await escalate(d, goal, "approval_required", "contract_uncheckable", `all plan steps completed but the success contract has zero enforceable obligations ` +
|
|
532
|
+
`(${goal.success_contract.uncompilable_criteria.length} uncompilable criteria); ` +
|
|
533
|
+
`refusing to mark done on model assertion alone`);
|
|
534
|
+
d.repo.saveGoal(goal);
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
if (check.ok) {
|
|
538
|
+
const doneSummary = `Success contract satisfied (${goal.success_contract.expected_artifacts.length} artifacts, ${goal.success_contract.verification_commands.length} commands).`;
|
|
539
|
+
goal.status = "done";
|
|
540
|
+
goal.result_summary = doneSummary;
|
|
541
|
+
goal = d.repo.saveGoal(goal);
|
|
542
|
+
await escalate(d, goal, "run_complete", "success", doneSummary);
|
|
543
|
+
d.repo.saveGoal(goal);
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
// Missing evidence → synthesize a remediation step (counts as a replan).
|
|
547
|
+
if (goal.replan_count >= goal.budgets.max_replans) {
|
|
548
|
+
const replanCode = "goal_budget_exhausted:max_replans";
|
|
549
|
+
goal.status = "blocked";
|
|
550
|
+
goal.reason_code = replanCode;
|
|
551
|
+
goal = d.repo.saveGoal(goal);
|
|
552
|
+
await escalate(d, goal, "gate_failed", replanCode, `success contract unmet and replans exhausted (missing: ${[
|
|
553
|
+
...check.missing_artifacts,
|
|
554
|
+
...check.missing_evidence_refs,
|
|
555
|
+
...check.failed_commands,
|
|
556
|
+
].join(", ")})`);
|
|
557
|
+
d.repo.saveGoal(goal);
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
const remediated = amendTaskPlan(plan, {
|
|
561
|
+
append_steps: [
|
|
562
|
+
{
|
|
563
|
+
role: "coders",
|
|
564
|
+
task: "The goal success contract is unmet. Produce the missing evidence.\n" +
|
|
565
|
+
(check.missing_artifacts.length > 0
|
|
566
|
+
? `Missing artifacts: ${check.missing_artifacts.join(", ")}\n`
|
|
567
|
+
: "") +
|
|
568
|
+
(check.missing_evidence_refs.length > 0
|
|
569
|
+
? `Missing evidence refs: ${check.missing_evidence_refs.join(", ")}\n`
|
|
570
|
+
: "") +
|
|
571
|
+
(check.failed_commands.length > 0
|
|
572
|
+
? `Failing verification commands: ${check.failed_commands.join("; ")}`
|
|
573
|
+
: ""),
|
|
574
|
+
expected_artifacts: [
|
|
575
|
+
...check.missing_artifacts,
|
|
576
|
+
...check.missing_evidence_refs,
|
|
577
|
+
].map((path) => ({ path, required: true })),
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
});
|
|
581
|
+
goal.replan_count += 1;
|
|
582
|
+
goal.status = "executing";
|
|
583
|
+
d.repo.savePlan(goalId, remediated);
|
|
584
|
+
goal = d.repo.saveGoal(goal);
|
|
585
|
+
await d.appendProcessPost({
|
|
586
|
+
kind: "progress",
|
|
587
|
+
summary: `Goal ${goalId}: success contract unmet, synthesized remediation step (replan ${goal.replan_count}/${goal.budgets.max_replans}).`,
|
|
588
|
+
});
|
|
589
|
+
backoff = BACKOFF_START_MS;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
// 4. Plan blocked/failed → classify and either replan or park.
|
|
593
|
+
if (status === "blocked" || status === "failed") {
|
|
594
|
+
if (goal.replan_count >= goal.budgets.max_replans) {
|
|
595
|
+
goal.status = "blocked";
|
|
596
|
+
goal.reason_code = "plan_blocked";
|
|
597
|
+
goal = d.repo.saveGoal(goal);
|
|
598
|
+
await escalate(d, goal, "blocker", "plan_blocked", `plan ${status}; replans exhausted (${blockedReasons(plan)})`);
|
|
599
|
+
d.repo.saveGoal(goal);
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
// Reset blocked/failed steps to planned for one more attempt (a replan).
|
|
603
|
+
const retryNoteByStep = new Map(plan.steps
|
|
604
|
+
.filter((s) => s.status === "blocked" || s.status === "failed")
|
|
605
|
+
.map((s) => [
|
|
606
|
+
s.step_id,
|
|
607
|
+
playbookRetryNote(d.workspaceRoot, s.blocked_reason ?? s.result_summary ?? undefined),
|
|
608
|
+
]));
|
|
609
|
+
const retried = amendTaskPlan(plan, {
|
|
610
|
+
update_steps: plan.steps
|
|
611
|
+
.filter((s) => s.status === "blocked" || s.status === "failed")
|
|
612
|
+
.map((s) => ({
|
|
613
|
+
step_id: s.step_id,
|
|
614
|
+
patch: {
|
|
615
|
+
status: "planned",
|
|
616
|
+
blocked_reason: undefined,
|
|
617
|
+
upstream_outputs: [
|
|
618
|
+
...(s.upstream_outputs ?? []),
|
|
619
|
+
{
|
|
620
|
+
step_id: s.step_id,
|
|
621
|
+
result_summary: `retry after ${s.status}: ${s.blocked_reason ?? s.result_summary ?? ""}`,
|
|
622
|
+
evidence_refs: [],
|
|
623
|
+
},
|
|
624
|
+
...(retryNoteByStep.get(s.step_id)
|
|
625
|
+
? [{
|
|
626
|
+
step_id: s.step_id,
|
|
627
|
+
result_summary: retryNoteByStep.get(s.step_id),
|
|
628
|
+
evidence_refs: [],
|
|
629
|
+
}]
|
|
630
|
+
: []),
|
|
631
|
+
],
|
|
632
|
+
},
|
|
633
|
+
})),
|
|
634
|
+
});
|
|
635
|
+
goal.replan_count += 1;
|
|
636
|
+
goal = d.repo.saveGoal(goal);
|
|
637
|
+
d.repo.savePlan(goalId, retried);
|
|
638
|
+
await d.appendProcessPost({
|
|
639
|
+
kind: "progress",
|
|
640
|
+
summary: `Goal ${goalId}: plan ${status}, retrying blocked steps (replan ${goal.replan_count}/${goal.budgets.max_replans}).`,
|
|
641
|
+
});
|
|
642
|
+
backoff = BACKOFF_START_MS;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
// 5. Ready work exists → execute one step.
|
|
646
|
+
const tick = await tickGoal(goalId, deps);
|
|
647
|
+
if (tick.progressed) {
|
|
648
|
+
backoff = BACKOFF_START_MS;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
// 6. No ready step and not terminal. If nothing is in flight either, no future
|
|
652
|
+
// event can make a step ready — the plan's dependency graph is unsatisfiable
|
|
653
|
+
// and backing off would spin until the wall-clock budget expires (or forever,
|
|
654
|
+
// since `tickGoal` does not advance `tick_count` when it cannot progress).
|
|
655
|
+
if (!plan.steps.some((step) => step.status === "running")) {
|
|
656
|
+
goal.status = "blocked";
|
|
657
|
+
goal.reason_code = "plan_deadlocked";
|
|
658
|
+
goal = d.repo.saveGoal(goal);
|
|
659
|
+
await escalate(d, goal, "blocker", "plan_deadlocked", `no step is ready and none is running; the plan cannot progress ` +
|
|
660
|
+
`(unresolved: ${plan.steps.filter((s) => s.status === "planned").map((s) => s.step_id).join(", ") || "none"})`);
|
|
661
|
+
d.repo.saveGoal(goal);
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
// A step is still running (an engine restarted mid-step). Back off, but charge
|
|
665
|
+
// the wait against max_ticks so an in-flight step cannot stall the goal forever.
|
|
666
|
+
goal.tick_count += 1;
|
|
667
|
+
goal = d.repo.saveGoal(goal);
|
|
668
|
+
await d.sleep(backoff);
|
|
669
|
+
backoff = Math.min(backoff * 2, BACKOFF_CAP_MS);
|
|
670
|
+
}
|
|
671
|
+
return { goal, plan: d.repo.getPlan(goalId) };
|
|
672
|
+
}
|
|
673
|
+
// ── resumeGoal / stopGoal / status ───────────────────────────────────────────
|
|
674
|
+
export async function resumeGoal(goalId, deps = {}) {
|
|
675
|
+
const d = resolveDeps(deps);
|
|
676
|
+
const goal = requireGoal(d, goalId);
|
|
677
|
+
const plan = d.repo.getPlan(goalId);
|
|
678
|
+
if (plan) {
|
|
679
|
+
// A step left `running` means the engine died mid-step — reset it to planned
|
|
680
|
+
// (the supervisor's own revisit pattern) so it re-executes on resume.
|
|
681
|
+
const stuck = plan.steps.filter((s) => s.status === "running");
|
|
682
|
+
if (stuck.length > 0) {
|
|
683
|
+
const reset = amendTaskPlan(plan, {
|
|
684
|
+
update_steps: stuck.map((s) => ({
|
|
685
|
+
step_id: s.step_id,
|
|
686
|
+
patch: {
|
|
687
|
+
status: "planned",
|
|
688
|
+
upstream_outputs: [
|
|
689
|
+
...(s.upstream_outputs ?? []),
|
|
690
|
+
{ step_id: s.step_id, result_summary: "reset after engine restart mid-step", evidence_refs: [] },
|
|
691
|
+
],
|
|
692
|
+
},
|
|
693
|
+
})),
|
|
694
|
+
});
|
|
695
|
+
d.repo.savePlan(goalId, reset);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (goal.status === "blocked" || goal.status === "stopped") {
|
|
699
|
+
// A `contract_uncheckable` block is not a transient failure — nothing about
|
|
700
|
+
// re-running changes the contract. Force the operator to restart the goal.
|
|
701
|
+
if (goal.reason_code === "contract_uncheckable") {
|
|
702
|
+
throw new GoalNotResumableError(goalId, `Goal ${goalId} is blocked on an uncheckable success contract and cannot be resumed. ` +
|
|
703
|
+
`Start a new goal whose objective declares an artifact or a \`verify: <command>\` criterion.`);
|
|
704
|
+
}
|
|
705
|
+
goal.status = "executing";
|
|
706
|
+
goal.stop_requested = false;
|
|
707
|
+
goal.reason_code = undefined;
|
|
708
|
+
d.repo.saveGoal(goal);
|
|
709
|
+
}
|
|
710
|
+
// `--plan-only` parks at `planning` with a compiled plan. Resuming is the
|
|
711
|
+
// documented way to execute it later; without this, the plan is unreachable.
|
|
712
|
+
if (goal.status === "planning") {
|
|
713
|
+
if (!plan)
|
|
714
|
+
throw new Error(`Goal ${goalId} has no compiled plan to resume.`);
|
|
715
|
+
const iso = new Date(d.now()).toISOString();
|
|
716
|
+
goal.status = "executing";
|
|
717
|
+
goal.stop_requested = false;
|
|
718
|
+
goal.started_at = goal.started_at ?? iso;
|
|
719
|
+
goal.last_heartbeat_at = iso;
|
|
720
|
+
d.repo.saveGoal(goal);
|
|
721
|
+
}
|
|
722
|
+
return runGoal(goalId, deps);
|
|
723
|
+
}
|
|
724
|
+
export function stopGoal(goalId, deps = {}) {
|
|
725
|
+
const d = resolveDeps(deps);
|
|
726
|
+
const goal = requireGoal(d, goalId);
|
|
727
|
+
goal.stop_requested = true;
|
|
728
|
+
return d.repo.saveGoal(goal);
|
|
729
|
+
}
|
|
730
|
+
export function getGoalStatus(goalId, deps = {}) {
|
|
731
|
+
const d = resolveDeps(deps);
|
|
732
|
+
const goal = d.repo.getGoal(goalId);
|
|
733
|
+
if (!goal)
|
|
734
|
+
return undefined;
|
|
735
|
+
const plan = d.repo.getPlan(goalId);
|
|
736
|
+
return {
|
|
737
|
+
goal,
|
|
738
|
+
plan,
|
|
739
|
+
ready_steps: plan ? getReadyTaskSteps(plan).map((s) => s.step_id) : [],
|
|
740
|
+
plan_status: plan ? deriveTaskPlanStatus(plan) : undefined,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
export function listGoals(filter = {}, deps = {}) {
|
|
744
|
+
const d = resolveDeps(deps);
|
|
745
|
+
return d.repo.listGoals(filter);
|
|
746
|
+
}
|
|
747
|
+
// ── internal helpers ─────────────────────────────────────────────────────────
|
|
748
|
+
export class UnknownGoalError extends Error {
|
|
749
|
+
goal_id;
|
|
750
|
+
name = "UnknownGoalError";
|
|
751
|
+
constructor(goal_id) {
|
|
752
|
+
super(`Unknown goal_id: ${goal_id}`);
|
|
753
|
+
this.goal_id = goal_id;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/** A goal whose state cannot be revived by re-running it. Reported, not thrown, at the CLI. */
|
|
757
|
+
export class GoalNotResumableError extends Error {
|
|
758
|
+
goal_id;
|
|
759
|
+
message;
|
|
760
|
+
name = "GoalNotResumableError";
|
|
761
|
+
constructor(goal_id, message) {
|
|
762
|
+
super(message);
|
|
763
|
+
this.goal_id = goal_id;
|
|
764
|
+
this.message = message;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function requireGoal(d, goalId) {
|
|
768
|
+
const goal = d.repo.getGoal(goalId);
|
|
769
|
+
if (!goal)
|
|
770
|
+
throw new UnknownGoalError(goalId);
|
|
771
|
+
return goal;
|
|
772
|
+
}
|
|
773
|
+
function requirePlan(d, goal) {
|
|
774
|
+
const plan = goal.plan_id ? d.repo.getPlan(goal.goal_id) : undefined;
|
|
775
|
+
if (!plan)
|
|
776
|
+
throw new Error(`Goal ${goal.goal_id} has no persisted plan.`);
|
|
777
|
+
return plan;
|
|
778
|
+
}
|
|
779
|
+
function budgetExhausted(d, goal) {
|
|
780
|
+
if (goal.tick_count >= goal.budgets.max_ticks)
|
|
781
|
+
return "max_ticks";
|
|
782
|
+
if (goal.started_at) {
|
|
783
|
+
const elapsed = d.now() - Date.parse(goal.started_at);
|
|
784
|
+
if (elapsed >= goal.budgets.max_wall_clock_ms)
|
|
785
|
+
return "max_wall_clock";
|
|
786
|
+
}
|
|
787
|
+
return undefined;
|
|
788
|
+
}
|
|
789
|
+
function blockedReasons(plan) {
|
|
790
|
+
return plan.steps
|
|
791
|
+
.filter((s) => s.status === "blocked" || s.status === "failed")
|
|
792
|
+
.map((s) => `${s.step_id}:${s.blocked_reason ?? s.result_summary ?? s.status}`)
|
|
793
|
+
.join(", ");
|
|
794
|
+
}
|
|
795
|
+
//# sourceMappingURL=goal-engine.js.map
|