@voybio/ace-swarm 2.4.8 → 2.4.10
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 +52 -14
- package/README.md +14 -10
- package/assets/.agents/ACE/ACE_coders/AGENTS.md +23 -30
- package/assets/.agents/ACE/ACE_coders/INSTRUCTIONS.md +21 -30
- package/assets/.agents/skills/landing-review-watcher/SKILL.md +1 -1
- package/assets/.agents/skills/learn-from-work/SKILL.md +41 -0
- package/assets/agent-state/ACE_WORKFLOW.md +25 -2
- package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +143 -2
- package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +2 -2
- package/assets/agent-state/MODULES/schemas/SWARM_HANDOFF.schema.json +10 -0
- package/assets/eval-battery/battery.json +1 -1
- package/assets/instructions/ACE_Coder.instructions.md +9 -13
- package/assets/scripts/ace-hook-dispatch.mjs +16 -411
- package/assets/tasks/SWARM_HANDOFF.example_ui_to_coders.json +1 -2
- package/assets/vendor/README.md +9 -0
- package/assets/vendor/vericify-reader.mjs +272 -0
- package/assets/vendor/vericify-reader.provenance.json +10 -0
- package/dist/ace-autonomy.d.ts +58 -0
- package/dist/ace-autonomy.js +211 -6
- package/dist/ace-internal-tools.d.ts +1 -0
- package/dist/ace-internal-tools.js +3 -3
- package/dist/ace-server-instructions.d.ts +3 -3
- package/dist/ace-server-instructions.js +46 -192
- package/dist/cli.js +44 -6
- package/dist/experience-reflector.d.ts +7 -0
- package/dist/experience-reflector.js +39 -0
- package/dist/goal-engine.d.ts +64 -7
- package/dist/goal-engine.js +197 -41
- package/dist/handoff-registry.d.ts +10 -0
- package/dist/handoff-registry.js +59 -0
- package/dist/helpers/artifacts.d.ts +6 -2
- package/dist/helpers/artifacts.js +8 -6
- package/dist/helpers/bootstrap.js +13 -20
- package/dist/helpers/path-utils.js +2 -0
- package/dist/helpers/store-resolution.js +18 -3
- package/dist/helpers/workspace-root.d.ts +2 -0
- package/dist/helpers/workspace-root.js +9 -0
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.js +1 -0
- package/dist/hermes/bridge-protocol.d.ts +1 -1
- package/dist/hermes/session-manager.d.ts +17 -0
- package/dist/hermes/session-manager.js +67 -14
- package/dist/host-goal-engine.d.ts +138 -0
- package/dist/host-goal-engine.js +793 -0
- package/dist/host-goal-status.d.ts +113 -0
- package/dist/host-goal-status.js +175 -0
- package/dist/host-learning.d.ts +68 -0
- package/dist/host-learning.js +115 -0
- package/dist/host-observation.d.ts +12 -0
- package/dist/host-observation.js +91 -0
- package/dist/host-plan-revision.d.ts +14 -0
- package/dist/host-plan-revision.js +101 -0
- package/dist/host-verification-job.d.ts +12 -0
- package/dist/host-verification-job.js +183 -0
- package/dist/host-work-context.d.ts +12 -0
- package/dist/host-work-context.js +8 -0
- package/dist/job-scheduler.d.ts +58 -13
- package/dist/job-scheduler.js +298 -48
- package/dist/job-worker.d.ts +3 -1
- package/dist/job-worker.js +10 -3
- package/dist/kanban.d.ts +4 -1
- package/dist/kanban.js +44 -16
- package/dist/learned-artifacts.d.ts +64 -0
- package/dist/learned-artifacts.js +321 -0
- package/dist/learning-controls.d.ts +142 -0
- package/dist/learning-controls.js +135 -0
- package/dist/learning-input-context.d.ts +9 -0
- package/dist/learning-input-context.js +62 -0
- package/dist/learning-lifecycle.d.ts +10 -0
- package/dist/learning-lifecycle.js +11 -0
- package/dist/learning-overlays.d.ts +3 -0
- package/dist/learning-overlays.js +17 -0
- package/dist/learning-policy.d.ts +40 -0
- package/dist/learning-policy.js +38 -0
- package/dist/learning-records.d.ts +35 -0
- package/dist/learning-records.js +17 -0
- package/dist/learning-scorecard.d.ts +82 -0
- package/dist/learning-scorecard.js +86 -0
- package/dist/learning-serving.d.ts +29 -0
- package/dist/learning-serving.js +134 -0
- package/dist/linked-work.d.ts +9 -0
- package/dist/linked-work.js +28 -0
- package/dist/local-model-runtime.js +1 -1
- package/dist/mcp-command-progress.d.ts +16 -0
- package/dist/mcp-command-progress.js +70 -0
- package/dist/model-bridge.d.ts +14 -13
- package/dist/model-bridge.js +73 -41
- package/dist/orchestrator-supervisor.d.ts +111 -6
- package/dist/orchestrator-supervisor.js +67 -43
- package/dist/plan-proposal.js +3 -3
- package/dist/playbook-curator.d.ts +1 -0
- package/dist/playbook-curator.js +11 -9
- package/dist/process-identity.d.ts +8 -0
- package/dist/process-identity.js +38 -0
- package/dist/progress-command.d.ts +34 -0
- package/dist/progress-command.js +153 -0
- package/dist/prompts.js +15 -15
- package/dist/resources.js +2 -2
- package/dist/runtime-command.d.ts +8 -1
- package/dist/runtime-command.js +48 -7
- package/dist/runtime-executor.js +33 -11
- package/dist/runtime-profile-syntax.d.ts +6 -0
- package/dist/runtime-profile-syntax.js +121 -0
- package/dist/runtime-profile.d.ts +37 -5
- package/dist/runtime-profile.js +36 -129
- package/dist/runtime-tool-specs.js +1 -1
- package/dist/safe-edit.d.ts +7 -3
- package/dist/safe-edit.js +40 -47
- package/dist/schemas.js +4 -2
- package/dist/server.d.ts +2 -16
- package/dist/server.js +14 -20
- package/dist/shared.d.ts +2 -0
- package/dist/shared.js +2 -0
- package/dist/skill-auditor.d.ts +19 -0
- package/dist/skill-auditor.js +32 -2
- package/dist/skill-catalog.d.ts +4 -1
- package/dist/skill-catalog.js +6 -5
- package/dist/store/ace-packed-store.d.ts +5 -1
- package/dist/store/ace-packed-store.js +33 -1
- package/dist/store/bootstrap-store.js +19 -12
- package/dist/store/cache-workspace.d.ts +8 -0
- package/dist/store/cache-workspace.js +94 -27
- package/dist/store/importer.js +11 -20
- package/dist/store/knowledge-bake.d.ts +3 -1
- package/dist/store/knowledge-bake.js +10 -1
- package/dist/store/legacy-vericify-posts.d.ts +8 -0
- package/dist/store/legacy-vericify-posts.js +76 -0
- package/dist/store/materializers/hook-context-materializer.d.ts +2 -1
- package/dist/store/materializers/hook-context-materializer.js +6 -2
- package/dist/store/materializers/host-file-materializer.js +2 -2
- package/dist/store/materializers/projection-manager.d.ts +4 -1
- package/dist/store/materializers/projection-manager.js +10 -3
- package/dist/store/materializers/todo-syncer.d.ts +39 -20
- package/dist/store/materializers/todo-syncer.js +208 -110
- package/dist/store/materializers/vericify-projector.js +15 -26
- package/dist/store/repositories/goal-repository.d.ts +51 -2
- package/dist/store/repositories/goal-repository.js +114 -48
- package/dist/store/repositories/playbook-repository.d.ts +41 -3
- package/dist/store/repositories/playbook-repository.js +61 -5
- package/dist/store/repositories/todo-repository.d.ts +8 -2
- package/dist/store/repositories/todo-repository.js +1 -1
- package/dist/store/repositories/vericify-repository.d.ts +4 -0
- package/dist/store/repositories/vericify-repository.js +46 -0
- package/dist/store/skills-install.d.ts +1 -0
- package/dist/store/skills-install.js +20 -16
- package/dist/store/source-preservation.d.ts +24 -0
- package/dist/store/source-preservation.js +79 -0
- package/dist/store/store-artifacts.js +8 -1
- package/dist/store/test-write-boundary.d.ts +16 -0
- package/dist/store/test-write-boundary.js +175 -0
- package/dist/store/types.d.ts +5 -1
- package/dist/store/write-coordinator.js +16 -0
- package/dist/task-plan-context.d.ts +11 -0
- package/dist/task-plan-context.js +59 -0
- package/dist/todo-state.d.ts +19 -9
- package/dist/todo-state.js +204 -321
- package/dist/tools-agent.d.ts +1 -1
- package/dist/tools-agent.js +38 -62
- package/dist/tools-files.d.ts +0 -3
- package/dist/tools-files.js +21 -11
- package/dist/tools-framework.js +16 -25
- package/dist/tools-goal.d.ts +2 -4
- package/dist/tools-goal.js +119 -16
- package/dist/tools-handoff.d.ts +3 -2
- package/dist/tools-handoff.js +53 -23
- package/dist/tools-lifecycle.js +1 -1
- package/dist/tools-memory.d.ts +0 -3
- package/dist/tools-memory.js +51 -6
- package/dist/tools-scheduler.js +20 -17
- package/dist/tools-todo.js +160 -148
- package/dist/vericify-bridge.d.ts +8 -0
- package/dist/vericify-bridge.js +18 -0
- package/dist/vericify-context.d.ts +4 -0
- package/dist/vericify-context.js +26 -5
- package/dist/vericify-native.d.ts +101 -0
- package/dist/vericify-native.js +221 -0
- package/dist/verification-worker.d.ts +2 -0
- package/dist/verification-worker.js +31 -0
- package/docs/mcp-tool-reference.md +50 -10
- package/docs/troubleshooting.md +29 -0
- package/package.json +19 -6
- package/scripts/dev/run-tests-isolated.mjs +165 -0
- package/scripts/dev/run_hermes_crispr_live.mjs +1 -0
- package/scripts/dev/test-isolation-guard.mjs +20 -0
- package/scripts/dev/test-preload.mjs +153 -0
- package/scripts/dev/test-source-inventory.mjs +31 -0
- package/docs/development.md +0 -46
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
import { renderLearningTask } from './learning-input-context.js';
|
|
2
|
+
import { readLearningPolicy } from './learning-policy.js';
|
|
3
|
+
/** Attached MCP execution over the existing GoalRepository and TaskPlan owners. */
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { scheduleHostVerification, hostVerificationActive } from './host-verification-job.js';
|
|
6
|
+
import { defaultGoalBudgets, newGoalId } from './store/repositories/goal-repository.js';
|
|
7
|
+
import { resolveGoalDeps, checkSuccessContract, verificationCommandText } from './goal-engine.js';
|
|
8
|
+
import { createTaskPlan, amendTaskPlan, deriveTaskPlanStatus } from './orchestrator-supervisor.js';
|
|
9
|
+
import { collectUpstreamOutputs, formatStepTaskForBridge, getReadyTaskSteps, utf8Prefix } from './task-plan-context.js';
|
|
10
|
+
import { resolveCurrentWork, formatCurrentWork } from './ace-autonomy.js';
|
|
11
|
+
import { withStoreWriteCoordinator } from './store/write-coordinator.js';
|
|
12
|
+
import { withWorkspaceRoot } from './helpers.js';
|
|
13
|
+
import { syncLinkedTodoStepSafe } from './todo-state.js';
|
|
14
|
+
import { selectLearning, scoreLearning } from './learning-serving.js';
|
|
15
|
+
import { prepareLearned, reopenLearningCorrection } from './learned-artifacts.js';
|
|
16
|
+
import { appendAttempt, harvestStep } from './host-learning.js';
|
|
17
|
+
import { reviseHostRequirements } from './host-plan-revision.js';
|
|
18
|
+
import { observationAdapter, observationSatisfied, pendingObservation, recoverableObservation, retainObservation, settleHostObservations, observationReadyForChecks } from './host-observation.js';
|
|
19
|
+
export function hostRefusal(root, reason, message, goalId) {
|
|
20
|
+
if (reason === 'follow_up_budget_reached')
|
|
21
|
+
return { ok: false, isError: true, reason_code: reason, message, waiting_on: ['operator:replan_budget'], now: formatCurrentWork(resolveCurrentWork({ workspaceRoot: root, goal_id: goalId }), 'line') };
|
|
22
|
+
return { ok: false, isError: true, reason_code: reason, message,
|
|
23
|
+
now: formatCurrentWork(resolveCurrentWork({ workspaceRoot: root, goal_id: goalId }), 'line'),
|
|
24
|
+
options: [{ tool: 'next_step', args: goalId ? { goal_id: goalId } : {}, why: 'Receive eligible work or its current wait condition.' },
|
|
25
|
+
{ tool: 'recall_context', args: goalId ? { goal_id: goalId } : {}, why: 'Recover current ownership, results and blockers.' }] };
|
|
26
|
+
}
|
|
27
|
+
function canonical(value) {
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
30
|
+
if (value && typeof value === 'object')
|
|
31
|
+
return `{${Object.entries(value).filter(([, v]) => v !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(',')}}`;
|
|
32
|
+
return JSON.stringify(value);
|
|
33
|
+
}
|
|
34
|
+
function planValid(plan) {
|
|
35
|
+
const steps = new Map(plan.steps.map(s => [s.step_id, s]));
|
|
36
|
+
if (steps.size !== plan.steps.length)
|
|
37
|
+
return 'Step ids must be unique.';
|
|
38
|
+
const visited = new Set(), visiting = new Set();
|
|
39
|
+
const visit = (id) => {
|
|
40
|
+
if (visited.has(id))
|
|
41
|
+
return true;
|
|
42
|
+
const step = steps.get(id);
|
|
43
|
+
if (!step || visiting.has(id))
|
|
44
|
+
return false;
|
|
45
|
+
visiting.add(id);
|
|
46
|
+
if (!(step.depends_on ?? []).every(visit))
|
|
47
|
+
return false;
|
|
48
|
+
visiting.delete(id);
|
|
49
|
+
visited.add(id);
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
52
|
+
if (!plan.steps.every(s => s.task.trim() && visit(s.step_id)))
|
|
53
|
+
return 'Every task needs text and an acyclic dependency on existing step ids.';
|
|
54
|
+
}
|
|
55
|
+
function nowLine(root, goalId) { return formatCurrentWork(resolveCurrentWork({ workspaceRoot: root, goal_id: goalId }), 'line'); }
|
|
56
|
+
function hasFinalChecks(goal) {
|
|
57
|
+
return goal.success_contract.verification_commands.length > 0 || goal.success_contract.expected_artifacts.some(a => a.required !== false) || goal.success_contract.required_evidence_refs.length > 0;
|
|
58
|
+
}
|
|
59
|
+
function clearImplicitDeadlineBlock(goal) {
|
|
60
|
+
if (goal.reason_code === 'host_budget_exhausted' && !goal.budgets.host_deadline_explicit) {
|
|
61
|
+
goal.reason_code = undefined;
|
|
62
|
+
if (goal.status === 'blocked')
|
|
63
|
+
goal.status = 'executing';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function planWork(root, goal, plan) {
|
|
67
|
+
return resolveCurrentWork({ workspaceRoot: root, goal_id: goal.goal_id, goal_snapshot: { goal, plan } });
|
|
68
|
+
}
|
|
69
|
+
function assignment(root, goal, plan, step) {
|
|
70
|
+
// Direct dependency summaries are bounded; the full results remain in this plan.
|
|
71
|
+
let remaining = 9000;
|
|
72
|
+
const outputs = collectUpstreamOutputs(plan, step);
|
|
73
|
+
const upstream = outputs.map((o, index) => {
|
|
74
|
+
// Share bytes across every direct dependency; early findings cannot consume
|
|
75
|
+
// the entire budget and erase the most recent prerequisite's summary.
|
|
76
|
+
const text = utf8Prefix(o.result_summary, Math.min(2000, Math.floor(remaining / (outputs.length - index))));
|
|
77
|
+
remaining -= Buffer.byteLength(text);
|
|
78
|
+
return { ...o, result_summary: text.length < o.result_summary.length ? `${text} [full result: ${goal.goal_id}/${o.step_id}]` : text, evidence_refs: o.evidence_refs.slice(0, 8) };
|
|
79
|
+
});
|
|
80
|
+
const work = planWork(root, goal, plan);
|
|
81
|
+
const blockers = [...work.blocked, ...work.waiting].map(item => {
|
|
82
|
+
const reason = item.reason && utf8Prefix(item.reason, 500);
|
|
83
|
+
return { ...item, ...(reason && reason !== item.reason ? { reason: `${reason} [full blocker: ${item.goal_id ?? goal.goal_id}/${item.id}]` } : {}) };
|
|
84
|
+
});
|
|
85
|
+
const failure = step.verification_failure;
|
|
86
|
+
const task = formatStepTaskForBridge({ ...step, task: renderLearningTask(root, goal.goal_id, step), upstream_outputs: upstream });
|
|
87
|
+
return { goal_id: goal.goal_id, step_id: step.step_id, attempt: step.attempt, assignee: step.assignee,
|
|
88
|
+
task_text: task + (failure ? `\nVerification failure (${failure.ref}; full output: ${goal.goal_id}/${step.step_id}):\n${utf8Prefix(failure.output, 2000)}` : ''), learning: readLearningPolicy(root).enabled ? step.served_learning?.text : undefined, role: step.role,
|
|
89
|
+
expected_artifacts: (step.expected_artifacts ?? []).map(a => a.path), required_evidence_refs: step.required_evidence_refs ?? [],
|
|
90
|
+
verify_at_goal_end: goal.success_contract.verification_commands.map(verificationCommandText), open_blockers: blockers,
|
|
91
|
+
now: formatCurrentWork(work, 'line'), observation: step.observation ?? 'unavailable', started_at: step.started_at };
|
|
92
|
+
}
|
|
93
|
+
/** Called only with a fresh plan while the per-goal lease is already held. */
|
|
94
|
+
function beginStepLocked(root, goal, plan, options, now) {
|
|
95
|
+
clearImplicitDeadlineBlock(goal);
|
|
96
|
+
const fail = (reason, message) => hostRefusal(root, reason, message, goal.goal_id);
|
|
97
|
+
const named = options.step_id ? plan.steps.find(s => s.step_id === options.step_id) : undefined;
|
|
98
|
+
if (options.step_id && !named)
|
|
99
|
+
return fail('step_not_found', `Unknown step ${options.step_id}.`);
|
|
100
|
+
if (named?.status === 'failed') {
|
|
101
|
+
const result = fail('step_not_ready', `${named.step_id} failed: ${named.result_summary ?? ''}`);
|
|
102
|
+
result.options = [{ tool: 'resume_goal', args: { goal_id: goal.goal_id }, why: 'After correcting the reported issue, open a new attempt.' }];
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
if (named?.status === 'blocked' && named.waiting_on)
|
|
106
|
+
return { ok: true, goal_id: goal.goal_id, status: goal.status, waiting_on: [named.waiting_on], now: nowLine(root, goal.goal_id), message: named.blocked_reason ?? 'Waiting for the named dependency.' };
|
|
107
|
+
if (named && ['done', 'skipped', 'failed'].includes(named.status))
|
|
108
|
+
return fail('step_not_ready', `${named.step_id} is ${named.status}: ${named.result_summary ?? ''}`);
|
|
109
|
+
if (goal.stop_requested || ['stopped', 'failed', 'done', 'verifying'].includes(goal.status))
|
|
110
|
+
return { ok: true, goal_id: goal.goal_id, status: goal.status, next_ready: [], now: nowLine(root, goal.goal_id), message: goal.result_summary ?? goal.reason_code ?? (goal.status === 'verifying' ? 'Declared checks are pending. If their process was interrupted, resume_goal reruns those checks only.' : undefined), ...(goal.stop_requested || goal.status === 'stopped' ? { waiting_on: ['user:resume'] } : {}), ...(goal.status === 'verifying' ? { options: [{ tool: 'resume_goal', args: { goal_id: goal.goal_id }, why: 'Recover interrupted goal-end verification without rerunning host steps.' }] } : {}) };
|
|
111
|
+
if (goal.reason_code?.startsWith('follow-up budget reached'))
|
|
112
|
+
return fail('follow_up_budget_reached', goal.reason_code);
|
|
113
|
+
const ready = getReadyTaskSteps(plan);
|
|
114
|
+
const step = options.step_id ? plan.steps.find(s => s.step_id === options.step_id) : plan.steps.find(s => s.status === 'running' && s.assignee === options.agent) ?? ready[0];
|
|
115
|
+
if (!step && plan.steps.some(s => ['failed', 'blocked'].includes(s.status) && !s.waiting_on))
|
|
116
|
+
return { ok: true, goal_id: goal.goal_id, status: goal.status, now: nowLine(root, goal.goal_id), message: 'After correcting the reported issue, resume_goal opens a new attempt; it does not rerun accepted or running host work.', options: [{ tool: 'resume_goal', args: { goal_id: goal.goal_id }, why: 'Make corrected failed/blocked work eligible for a new host attempt.' }] };
|
|
117
|
+
if (!step)
|
|
118
|
+
return { ok: true, goal_id: goal.goal_id, status: goal.status, waiting_on: plan.steps.filter(s => !['done', 'skipped'].includes(s.status)).map(s => `${s.step_id}: ${s.waiting_on ?? s.blocked_reason ?? s.status}${s.assignee ? ` (${s.assignee})` : ''}`), now: nowLine(root, goal.goal_id), message: 'No ready step. Resume when its listed owner or dependency makes progress.' };
|
|
119
|
+
if (step.status === 'running' && step.assignee !== options.agent)
|
|
120
|
+
return fail('step_moved', `${step.step_id} is held by ${step.assignee}. Request a handoff or choose another ready step.`);
|
|
121
|
+
if (!(step.status === 'running' && step.assignee === options.agent) && !ready.some(s => s.step_id === step.step_id))
|
|
122
|
+
return fail('step_not_ready', `${step.step_id} is ${step.status}; dependencies: ${(step.depends_on ?? []).join(', ') || 'none'}. ${step.result_summary ?? ''}`);
|
|
123
|
+
if (step.status !== 'running') {
|
|
124
|
+
const started = Date.parse(goal.started_at ?? goal.created_at);
|
|
125
|
+
if (goal.budgets.host_deadline_explicit && now() - started >= goal.budgets.max_wall_clock_ms) {
|
|
126
|
+
goal.status = 'blocked';
|
|
127
|
+
goal.reason_code = 'host_budget_exhausted';
|
|
128
|
+
return { ok: true, goal_id: goal.goal_id, status: 'blocked', reason_code: goal.reason_code,
|
|
129
|
+
waiting_on: ['operator:budget'], message: 'The host assignment budget has expired. Existing outcomes are retained; new work requires an explicitly authorized budget or a new goal.',
|
|
130
|
+
now: formatCurrentWork(planWork(root, goal, plan), 'line') };
|
|
131
|
+
}
|
|
132
|
+
preserveAttempt(step);
|
|
133
|
+
step.status = 'running';
|
|
134
|
+
step.assignee = options.agent;
|
|
135
|
+
step.attempt = (step.attempt ?? 0) + 1;
|
|
136
|
+
step.started_at = new Date(now()).toISOString();
|
|
137
|
+
step.waiting_on = undefined;
|
|
138
|
+
step.blocked_reason = undefined;
|
|
139
|
+
step.result_digest = undefined;
|
|
140
|
+
step.result_response = undefined;
|
|
141
|
+
step.upstream_outputs = collectUpstreamOutputs(plan, step);
|
|
142
|
+
step.served_learning = selectLearning(root, plan, step);
|
|
143
|
+
step.loaded_learning = [];
|
|
144
|
+
step.observation = 'unavailable';
|
|
145
|
+
step.observation_window = { workspace_id: root, session_id: options.session_id, started_at: step.started_at };
|
|
146
|
+
goal.tick_count++;
|
|
147
|
+
goal.current_step_id = step.step_id;
|
|
148
|
+
goal.status = 'executing';
|
|
149
|
+
}
|
|
150
|
+
step.last_ace_call_at = new Date(now()).toISOString();
|
|
151
|
+
return { ok: true, assignment: assignment(root, goal, plan, step), now: nowLine(root, goal.goal_id) };
|
|
152
|
+
}
|
|
153
|
+
function preserveAttempt(step) {
|
|
154
|
+
if (step.attempt && (step.result_digest || step.evidence?.length) && !step.attempt_history?.some(old => old.attempt === step.attempt && old.result_digest === step.result_digest && step.result_digest)) {
|
|
155
|
+
step.attempt_history ??= [];
|
|
156
|
+
step.attempt_history.push({ attempt: step.attempt, assignee: step.assignee, status: step.result_response?.status ?? step.status, result_summary: step.result_summary, evidence: step.evidence, artifacts: step.artifacts, result_digest: step.result_digest, result_response: step.result_response });
|
|
157
|
+
}
|
|
158
|
+
step.evidence = [];
|
|
159
|
+
step.evidence_refs = [];
|
|
160
|
+
step.artifacts = [];
|
|
161
|
+
step.observation_refs = [];
|
|
162
|
+
step.observation_check = undefined;
|
|
163
|
+
step.observation_query = undefined;
|
|
164
|
+
step.observation_coverage = undefined;
|
|
165
|
+
}
|
|
166
|
+
async function syncViews(root, goal, plan) {
|
|
167
|
+
const pending = [];
|
|
168
|
+
for (const step of plan.steps.filter(s => s.todo_id)) {
|
|
169
|
+
try {
|
|
170
|
+
await withWorkspaceRoot(root, () => syncLinkedTodoStepSafe(step.todo_id, { goal_id: goal.goal_id, step_id: step.step_id }, step));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
pending.push(`todo:${step.todo_id}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const step of plan.steps.filter(s => s.handoff_id && s.status === 'done')) {
|
|
177
|
+
try {
|
|
178
|
+
const { syncLinkedHandoffStepSafe } = await import('./handoff-registry.js');
|
|
179
|
+
await withWorkspaceRoot(root, () => syncLinkedHandoffStepSafe(step.handoff_id, step.result_summary ?? 'Step completed'));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
pending.push(`handoff:${step.handoff_id}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return pending;
|
|
186
|
+
}
|
|
187
|
+
export async function startHostGoal(objective, options, deps = {}) {
|
|
188
|
+
const d = resolveGoalDeps(deps);
|
|
189
|
+
const id = options.goal_id ?? newGoalId();
|
|
190
|
+
const iso = new Date(d.now()).toISOString();
|
|
191
|
+
let plan = createTaskPlan({ task: objective, steps: (options.steps ?? []).map(s => ({ role: s.role ?? 'coders', task: s.task, depends_on: s.depends_on, expected_artifacts: s.expected_artifacts?.map(a => typeof a === 'string' ? { path: a, required: true } : { required: true, ...a }), required_evidence_refs: s.required_evidence_refs, required_observation: s.required_observation })) });
|
|
192
|
+
plan.steps.forEach((s, i) => { s.step_id = options.steps[i].id ?? `S${i + 1}`; s.todo_id = options.todo_ids?.[s.step_id]; });
|
|
193
|
+
const invalid = !objective.trim() ? 'Objective must not be empty.' : !plan.steps.length ? 'Host goals require at least one step.' : planValid(plan);
|
|
194
|
+
if (invalid)
|
|
195
|
+
throw new Error(invalid);
|
|
196
|
+
const goal = { goal_id: id, objective, executor: 'host', status: options.plan_only ? 'planning' : 'executing',
|
|
197
|
+
plan_id: plan.plan_id, created_at: iso, updated_at: iso, started_at: iso, tick_count: 0, replan_count: 0, stop_requested: false, escalations: [],
|
|
198
|
+
budgets: { ...defaultGoalBudgets(), ...(options.max_replans !== undefined ? { max_replans: options.max_replans, replan_budget_explicit: true } : {}), ...(options.budget_hours !== undefined ? { max_wall_clock_ms: options.budget_hours * 3600000, host_deadline_explicit: true, deadline_explicit: true } : {}) },
|
|
199
|
+
success_contract: { expected_artifacts: plan.steps.flatMap(s => (s.expected_artifacts ?? []).filter(a => !a.transient)), required_evidence_refs: [], verification_commands: options.verify_commands ?? [], gate_ids: [], uncompilable_criteria: [] } };
|
|
200
|
+
return withStoreWriteCoordinator(d.repo.planPath(id), async () => {
|
|
201
|
+
if (d.repo.getGoal(id))
|
|
202
|
+
throw new Error(`Goal already exists: ${id}`);
|
|
203
|
+
// Linked creation briefly serializes with its existing todo owner. Goals without
|
|
204
|
+
// linked todos never take the pack lease. Plan records settle duplicate links.
|
|
205
|
+
const publish = async () => {
|
|
206
|
+
for (const [stepId, todoId] of Object.entries(options.todo_ids ?? {})) {
|
|
207
|
+
if (!plan.steps.some(s => s.step_id === stepId))
|
|
208
|
+
throw new Error(`Unknown linked step: ${stepId}`);
|
|
209
|
+
const { readStoreJsonSync } = await import('./store/store-snapshot.js');
|
|
210
|
+
const todo = readStoreJsonSync(d.workspaceRoot, `state/todo/${todoId}`);
|
|
211
|
+
if (!todo || todo.step_ref || d.repo.listGoals().some(g => d.repo.getPlan(g.goal_id)?.steps.some(s => s.todo_id === todoId)))
|
|
212
|
+
throw new Error(`Todo missing or already linked: ${todoId}`);
|
|
213
|
+
}
|
|
214
|
+
d.repo.savePlan(id, plan);
|
|
215
|
+
d.repo.saveGoal(goal);
|
|
216
|
+
};
|
|
217
|
+
if (Object.keys(options.todo_ids ?? {}).length) {
|
|
218
|
+
const { getWorkspaceStorePath } = await import('./store/store-snapshot.js');
|
|
219
|
+
await withStoreWriteCoordinator(getWorkspaceStorePath(d.workspaceRoot), publish, { operation_label: 'host_link_creation' });
|
|
220
|
+
}
|
|
221
|
+
else
|
|
222
|
+
await publish();
|
|
223
|
+
const begun = options.plan_only ? undefined : beginStepLocked(d.workspaceRoot, goal, plan, { agent: options.agent ?? 'host', session_id: options.session_id }, d.now);
|
|
224
|
+
plan.status = deriveTaskPlanStatus(plan);
|
|
225
|
+
d.repo.savePlan(id, plan);
|
|
226
|
+
d.repo.saveGoal(goal);
|
|
227
|
+
const pending = await syncViews(d.workspaceRoot, goal, plan);
|
|
228
|
+
return { ...begun, ok: true, goal: d.repo.getGoal(id), assignment: begun?.assignment ? assignment(d.workspaceRoot, goal, plan, plan.steps.find(s => s.step_id === begun.assignment.step_id)) : undefined, now: nowLine(d.workspaceRoot, id), view_repair_pending: pending };
|
|
229
|
+
}, { operation_label: 'host_goal_start' });
|
|
230
|
+
}
|
|
231
|
+
export async function beginStep(goalId, options, deps = {}) {
|
|
232
|
+
const d = resolveGoalDeps(deps);
|
|
233
|
+
return withStoreWriteCoordinator(d.repo.planPath(goalId), async () => {
|
|
234
|
+
const goal = d.repo.getGoal(goalId), plan = d.repo.getPlan(goalId);
|
|
235
|
+
if (!goal || !plan || goal.executor !== 'host')
|
|
236
|
+
return hostRefusal(d.workspaceRoot, 'host_goal_required', 'Choose an existing host-executed goal.', goalId);
|
|
237
|
+
const previousReason = goal.reason_code;
|
|
238
|
+
const result = beginStepLocked(d.workspaceRoot, goal, plan, options, d.now);
|
|
239
|
+
if (result.reason_code === 'host_budget_exhausted' || previousReason !== goal.reason_code)
|
|
240
|
+
d.repo.saveGoal(goal);
|
|
241
|
+
if (result.assignment) {
|
|
242
|
+
plan.steps.find(s => s.step_id === result.assignment.step_id).last_ace_call_at = new Date(d.now()).toISOString();
|
|
243
|
+
plan.status = deriveTaskPlanStatus(plan);
|
|
244
|
+
d.repo.savePlan(goalId, plan);
|
|
245
|
+
d.repo.saveGoal(goal);
|
|
246
|
+
result.view_repair_pending = await syncViews(d.workspaceRoot, goal, plan);
|
|
247
|
+
result.assignment = assignment(d.workspaceRoot, goal, plan, plan.steps.find(s => s.step_id === result.assignment.step_id));
|
|
248
|
+
result.now = result.assignment.now;
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}, { operation_label: 'host_next_step' });
|
|
252
|
+
}
|
|
253
|
+
export function applyHostResultToStep(step, outcome) {
|
|
254
|
+
step.status = outcome.status;
|
|
255
|
+
step.result_summary = outcome.summary;
|
|
256
|
+
step.evidence = [...(step.evidence ?? []).filter(e => e.provenance === 'ace'), ...(outcome.evidence ?? []).map(e => ({ ...e, provenance: 'host' }))];
|
|
257
|
+
step.evidence_refs = [...new Set(step.evidence.map(e => e.ref))];
|
|
258
|
+
step.artifacts = outcome.artifacts ?? [];
|
|
259
|
+
step.waiting_on = outcome.status === 'blocked' ? outcome.waiting_on : undefined;
|
|
260
|
+
step.blocked_reason = outcome.status === 'done' ? undefined : outcome.summary;
|
|
261
|
+
}
|
|
262
|
+
/** Allocate prospective IDs once, then remap duplicates before budget admission. */
|
|
263
|
+
function prepareFollowUps(plan, additions, reserved = []) {
|
|
264
|
+
// Reserve retained pending IDs in the existing allocator, without admitting
|
|
265
|
+
// those obligations or persisting placeholder steps in the execution plan.
|
|
266
|
+
const allocationBase = { ...plan, steps: [...plan.steps, ...reserved.filter(s => !plan.steps.some(p => p.step_id === s.step_id)).map(s => ({ ...s, status: 'planned' }))] };
|
|
267
|
+
const proposed = amendTaskPlan(allocationBase, { append_steps: additions.map(s => ({ ...s, role: s.role ?? 'coders' })) });
|
|
268
|
+
const candidates = proposed.steps.slice(allocationBase.steps.length);
|
|
269
|
+
const aliases = new Map();
|
|
270
|
+
const retained = [];
|
|
271
|
+
const skipped = [];
|
|
272
|
+
for (const candidate of candidates) {
|
|
273
|
+
const duplicate = [...plan.steps, ...retained].find(s => ['planned', 'running', 'blocked'].includes(s.status) && s.task === candidate.task);
|
|
274
|
+
if (duplicate) {
|
|
275
|
+
aliases.set(candidate.step_id, duplicate.step_id);
|
|
276
|
+
skipped.push(candidate.task);
|
|
277
|
+
}
|
|
278
|
+
else
|
|
279
|
+
retained.push(candidate);
|
|
280
|
+
}
|
|
281
|
+
const normalized = retained.map(s => ({ ...s, depends_on: s.depends_on?.map(id => aliases.get(id) ?? id) }));
|
|
282
|
+
const full = { ...proposed, steps: [...plan.steps, ...normalized] };
|
|
283
|
+
const invalid = planValid(full);
|
|
284
|
+
if (invalid)
|
|
285
|
+
throw new Error(invalid);
|
|
286
|
+
return { full, candidates: normalized, skipped };
|
|
287
|
+
}
|
|
288
|
+
function addFollowUps(goal, plan, additions) {
|
|
289
|
+
const { candidates, skipped } = prepareFollowUps(plan, additions, goal.pending_follow_ups);
|
|
290
|
+
const admitted = admitFollowUpsLocked(goal, plan, candidates);
|
|
291
|
+
return { plan: admitted, skipped };
|
|
292
|
+
}
|
|
293
|
+
/** Shared admission path for new follow-ups and explicitly funded pending work. */
|
|
294
|
+
function admitFollowUpsLocked(goal, plan, candidates) {
|
|
295
|
+
const invalid = planValid({ ...plan, steps: [...plan.steps, ...candidates] });
|
|
296
|
+
if (invalid)
|
|
297
|
+
throw new Error(invalid);
|
|
298
|
+
const byId = new Map(candidates.map(s => [s.step_id, s]));
|
|
299
|
+
const admitted = new Set();
|
|
300
|
+
const available = goal.budgets.replan_budget_explicit ? Math.max(0, goal.budgets.max_replans - goal.replan_count) : Number.POSITIVE_INFINITY;
|
|
301
|
+
for (const candidate of candidates) {
|
|
302
|
+
const closure = new Set();
|
|
303
|
+
const visit = (id) => {
|
|
304
|
+
if (admitted.has(id) || closure.has(id) || !byId.has(id))
|
|
305
|
+
return;
|
|
306
|
+
closure.add(id);
|
|
307
|
+
byId.get(id).depends_on?.forEach(visit);
|
|
308
|
+
};
|
|
309
|
+
visit(candidate.step_id);
|
|
310
|
+
if (admitted.size + closure.size <= available)
|
|
311
|
+
closure.forEach(id => admitted.add(id));
|
|
312
|
+
}
|
|
313
|
+
const pending = candidates.filter(s => !admitted.has(s.step_id));
|
|
314
|
+
if (pending.length) {
|
|
315
|
+
goal.pending_follow_ups = [...(goal.pending_follow_ups ?? []), ...pending.map(s => ({ step_id: s.step_id, task: s.task, role: s.role, depends_on: s.depends_on }))];
|
|
316
|
+
goal.status = 'blocked';
|
|
317
|
+
goal.reason_code = `follow-up budget reached: ${pending.map(s => s.task).join('; ')}`;
|
|
318
|
+
}
|
|
319
|
+
goal.replan_count += admitted.size;
|
|
320
|
+
return { ...plan, steps: [...plan.steps, ...candidates.filter(s => admitted.has(s.step_id))] };
|
|
321
|
+
}
|
|
322
|
+
/** The caller holds the existing per-goal coordinator; never changes receipts. */
|
|
323
|
+
export function recoverHostBudgetsLocked(goal, plan, options, now) {
|
|
324
|
+
const hours = options.budget_hours, replans = options.max_replans;
|
|
325
|
+
const fundingPending = Boolean(goal.pending_follow_ups?.length);
|
|
326
|
+
if (hours !== undefined) {
|
|
327
|
+
if (!Number.isFinite(hours * 3600000) || hours <= 0)
|
|
328
|
+
throw new Error('budget_hours must be a finite positive allowance.');
|
|
329
|
+
if (!goal.budgets.host_deadline_explicit)
|
|
330
|
+
throw new Error('Cannot lower an unlimited host budget to a finite deadline through resume.');
|
|
331
|
+
if (hours * 3600000 < goal.budgets.max_wall_clock_ms)
|
|
332
|
+
throw new Error('Cannot lower budget_hours; resume accepts only equal or increased allowances.');
|
|
333
|
+
}
|
|
334
|
+
if (replans !== undefined && (!Number.isSafeInteger(replans) || replans < goal.budgets.max_replans))
|
|
335
|
+
throw new Error('Cannot lower max_replans; supply an equal or increased integer allowance.');
|
|
336
|
+
if (hours !== undefined)
|
|
337
|
+
goal.budgets.max_wall_clock_ms = hours * 3600000;
|
|
338
|
+
if (replans !== undefined) {
|
|
339
|
+
goal.budgets.max_replans = replans;
|
|
340
|
+
goal.budgets.replan_budget_explicit = true;
|
|
341
|
+
}
|
|
342
|
+
if (replans !== undefined && goal.pending_follow_ups?.length) {
|
|
343
|
+
const pending = goal.pending_follow_ups.map(s => ({ ...s, status: 'planned' }));
|
|
344
|
+
goal.pending_follow_ups = [];
|
|
345
|
+
plan = admitFollowUpsLocked(goal, plan, pending);
|
|
346
|
+
}
|
|
347
|
+
if (goal.reason_code?.startsWith('follow-up budget reached')) {
|
|
348
|
+
// A different pending requirement can share this legacy reason prefix.
|
|
349
|
+
// Funding follow-ups must never certify obligations it did not admit.
|
|
350
|
+
if (goal.pending_follow_ups?.length || replans === undefined || !fundingPending)
|
|
351
|
+
return plan;
|
|
352
|
+
goal.reason_code = undefined;
|
|
353
|
+
}
|
|
354
|
+
clearImplicitDeadlineBlock(goal);
|
|
355
|
+
if (goal.budgets.host_deadline_explicit && now - Date.parse(goal.started_at ?? goal.created_at) >= goal.budgets.max_wall_clock_ms) {
|
|
356
|
+
if (goal.status !== 'done') {
|
|
357
|
+
goal.status = 'blocked';
|
|
358
|
+
goal.reason_code = 'host_budget_exhausted';
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else if (goal.reason_code === 'host_budget_exhausted')
|
|
362
|
+
goal.reason_code = undefined;
|
|
363
|
+
return plan;
|
|
364
|
+
}
|
|
365
|
+
export async function completeStep(goalId, stepId, outcome, options, deps = {}) {
|
|
366
|
+
if (!outcome.learned)
|
|
367
|
+
return completeStepInternal(goalId, stepId, outcome, options, deps);
|
|
368
|
+
if (!readLearningPolicy(resolveGoalDeps(deps).workspaceRoot).enabled)
|
|
369
|
+
return completeStepInternal(goalId, stepId, outcome, options, deps, { ok: false, ids: [], errors: ['Learning is disabled; the work outcome and proposed payload are retained, no artifact was activated.'], reason_code: 'learning_disabled' });
|
|
370
|
+
const d = resolveGoalDeps(deps);
|
|
371
|
+
return withStoreWriteCoordinator(`${d.repo.planPath(goalId)}.${createHash('sha256').update(stepId).digest('hex').slice(0, 12)}.${options.attempt}.learning`, async () => {
|
|
372
|
+
const step = d.repo.getPlan(goalId)?.steps.find(s => s.step_id === stepId);
|
|
373
|
+
if (!step || step.assignee !== options.agent || step.attempt !== options.attempt || step.result_digest || step.status !== 'running' || !outcome.summary.trim() || outcome.summary.length > 2000)
|
|
374
|
+
return completeStepInternal(goalId, stepId, outcome, options, deps);
|
|
375
|
+
const qualified = await prepareLearned(d.workspaceRoot, outcome.learned, { goal_id: goalId, step_id: stepId, attempt: options.attempt }, deps);
|
|
376
|
+
return completeStepInternal(goalId, stepId, outcome, options, deps, qualified.result, qualified.publish);
|
|
377
|
+
}, { operation_label: 'learning_qualification' });
|
|
378
|
+
}
|
|
379
|
+
async function completeStepInternal(goalId, stepId, outcome, options, deps = {}, learnedResult, publishLearning) {
|
|
380
|
+
const d = resolveGoalDeps(deps);
|
|
381
|
+
if (!outcome.summary.trim() || outcome.summary.length > 2000 || !['done', 'blocked', 'failed'].includes(outcome.status))
|
|
382
|
+
return hostRefusal(d.workspaceRoot, 'invalid_outcome', 'Supply a status and a summary of 1–2000 characters.', goalId);
|
|
383
|
+
const digest = createHash('sha256').update(canonical([stepId, options.attempt, outcome])).digest('hex');
|
|
384
|
+
let verification;
|
|
385
|
+
// Adapter reads are external and read-only; they never hold the goal's write lease.
|
|
386
|
+
const before = d.repo.getPlan(goalId)?.steps.find(s => s.step_id === stepId);
|
|
387
|
+
const window = before?.observation_window && { ...before.observation_window, ended_at: before.observation_window.ended_at ?? new Date(d.now()).toISOString() };
|
|
388
|
+
const adapter = observationAdapter(d.workspaceRoot, deps);
|
|
389
|
+
let observed = { status: 'unavailable', records: [] };
|
|
390
|
+
if (adapter && window && !before?.result_digest) {
|
|
391
|
+
try {
|
|
392
|
+
observed = await adapter.readWindow(window);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
observed = { status: 'unavailable', records: [], reason_code: 'observation_query_failed' };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const response = await withStoreWriteCoordinator(d.repo.planPath(goalId), async () => {
|
|
399
|
+
const goal = d.repo.getGoal(goalId);
|
|
400
|
+
let plan = d.repo.getPlan(goalId);
|
|
401
|
+
const fail = (reason, message) => hostRefusal(d.workspaceRoot, reason, message, goalId);
|
|
402
|
+
if (!goal || !plan || goal.executor !== 'host')
|
|
403
|
+
return fail('host_goal_required', 'Choose an existing host-executed goal.');
|
|
404
|
+
let step = plan.steps.find(s => s.step_id === stepId);
|
|
405
|
+
if (!step)
|
|
406
|
+
return fail('step_not_found', `Unknown step ${stepId}.`);
|
|
407
|
+
if (step.assignee !== options.agent || step.attempt !== options.attempt)
|
|
408
|
+
return fail('step_moved', `${stepId} is held by ${step.assignee} at attempt ${step.attempt}. Forward partial findings through its handoff.`);
|
|
409
|
+
if (step.result_digest)
|
|
410
|
+
return step.result_digest === digest ? step.result_response : fail('already_completed', `This attempt already recorded: ${step.result_summary}`);
|
|
411
|
+
step.last_ace_call_at = new Date(d.now()).toISOString();
|
|
412
|
+
if (step.status !== 'running')
|
|
413
|
+
return fail('step_moved', `${stepId} is ${step.status}; wait for its owner or accept its handoff.`);
|
|
414
|
+
clearImplicitDeadlineBlock(goal);
|
|
415
|
+
try {
|
|
416
|
+
prepareFollowUps(plan, outcome.follow_ups ?? [], goal.pending_follow_ups);
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
return fail('invalid_follow_up', error instanceof Error ? error.message : String(error));
|
|
420
|
+
}
|
|
421
|
+
if (outcome.requirement_updates?.length) {
|
|
422
|
+
try {
|
|
423
|
+
reviseHostRequirements(plan, outcome.requirement_updates);
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
return fail('invalid_requirement_update', error instanceof Error ? error.message : String(error));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (publishLearning)
|
|
430
|
+
await publishLearning();
|
|
431
|
+
applyHostResultToStep(step, outcome);
|
|
432
|
+
retainObservation(step, observed);
|
|
433
|
+
step.observation_window = window;
|
|
434
|
+
if (outcome.status === 'done') {
|
|
435
|
+
const refs = step.evidence_refs ?? [];
|
|
436
|
+
const unmet = [...(step.expected_artifacts ?? []).filter(a => a.required !== false && !d.artifactExists(a.path, d.workspaceRoot)).map(a => a.path), ...(step.required_evidence_refs ?? []).filter(ref => !refs.includes(ref))];
|
|
437
|
+
const missingObservation = step.required_observation && !observationSatisfied(step, observed.records);
|
|
438
|
+
if (missingObservation && adapter?.supported_requirements && !adapter.supported_requirements.includes(step.required_observation)) {
|
|
439
|
+
const reason = `Observation requirement ${step.required_observation} is unsupported by this adapter: native file attribution does not prove an ACE attempt or a passed command.`;
|
|
440
|
+
step.observation_check = { attempt: step.attempt, status: 'unsupported', deadline_at: new Date(d.now()).toISOString(), wait_ms: 0, poll_ms: 250, reason_code: 'observation_requirement_unsupported' };
|
|
441
|
+
step.status = 'blocked';
|
|
442
|
+
step.waiting_on = 'operator:observation_capability';
|
|
443
|
+
step.blocked_reason = reason;
|
|
444
|
+
}
|
|
445
|
+
else if (missingObservation && adapter?.late_evidence && !unmet.length) {
|
|
446
|
+
const wait = Math.max(0, Math.min(120000, adapter.late_evidence.wait_ms));
|
|
447
|
+
step.observation_check = { attempt: step.attempt, status: 'pending', deadline_at: new Date(d.now() + wait).toISOString(), wait_ms: wait, poll_ms: Math.max(10, adapter.late_evidence.poll_ms ?? 250) };
|
|
448
|
+
step.status = 'blocked';
|
|
449
|
+
step.waiting_on = `observation:${step.step_id}`;
|
|
450
|
+
step.blocked_reason = 'Outcome recorded; waiting for required observation of this original attempt.';
|
|
451
|
+
}
|
|
452
|
+
else if (missingObservation)
|
|
453
|
+
unmet.push(`observed ${step.required_observation}: ${observed.status}`);
|
|
454
|
+
if (unmet.length) {
|
|
455
|
+
step.status = 'blocked';
|
|
456
|
+
step.blocked_reason = `Unmet declared requirements: ${unmet.join(', ')}. Supply the missing evidence, then resume_goal for another attempt.`;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
step.learning_feedback = outcome.feedback;
|
|
460
|
+
if (outcome.learned) {
|
|
461
|
+
step.learning_payload = outcome.learned;
|
|
462
|
+
step.learning_result = learnedResult;
|
|
463
|
+
}
|
|
464
|
+
appendAttempt(step, goal, new Date(d.now()).toISOString());
|
|
465
|
+
const recordedStatus = step.status;
|
|
466
|
+
let reopened = [], pendingRequirements = [];
|
|
467
|
+
if (outcome.requirement_updates?.length) {
|
|
468
|
+
const count = new Set(outcome.requirement_updates.map(update => update.step_id)).size;
|
|
469
|
+
if (goal.budgets.replan_budget_explicit && goal.replan_count + count > goal.budgets.max_replans) {
|
|
470
|
+
goal.status = 'blocked';
|
|
471
|
+
goal.reason_code = `follow-up budget reached: requirement changes ${outcome.requirement_updates.map(u => u.step_id).join(', ')}`;
|
|
472
|
+
pendingRequirements = outcome.requirement_updates;
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
const revised = reviseHostRequirements(plan, outcome.requirement_updates);
|
|
476
|
+
plan = revised.plan;
|
|
477
|
+
reopened = revised.reopened;
|
|
478
|
+
goal.replan_count += count;
|
|
479
|
+
goal.success_contract.expected_artifacts = plan.steps.flatMap(s => (s.expected_artifacts ?? []).filter(a => !a.transient));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const amended = addFollowUps(goal, plan, outcome.follow_ups ?? []);
|
|
483
|
+
plan = amended.plan;
|
|
484
|
+
step = plan.steps.find(s => s.step_id === stepId);
|
|
485
|
+
plan.status = deriveTaskPlanStatus(plan);
|
|
486
|
+
if (!goal.reason_code?.startsWith('follow-up budget reached')) {
|
|
487
|
+
const allDone = plan.steps.every(s => ['done', 'skipped'].includes(s.status));
|
|
488
|
+
if (allDone && goal.reason_code === 'host_budget_exhausted')
|
|
489
|
+
goal.reason_code = undefined;
|
|
490
|
+
goal.status = plan.steps.some(pendingObservation) ? 'verifying' : allDone ? (hasFinalChecks(goal) ? 'verifying' : 'done') : goal.reason_code === 'host_budget_exhausted' ? 'blocked' : getReadyTaskSteps(plan).length || plan.steps.some(s => s.status === 'running') ? 'executing' : 'blocked';
|
|
491
|
+
if (goal.status === 'verifying')
|
|
492
|
+
goal.verification_attempt = (goal.verification_attempt ?? 0) + 1;
|
|
493
|
+
if (goal.status === 'done') {
|
|
494
|
+
goal.verified = false;
|
|
495
|
+
goal.result_summary = 'done (not machine-verified)';
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (goal.stop_requested)
|
|
499
|
+
goal.status = 'stopped';
|
|
500
|
+
const begun = options.continue && goal.status === 'executing' ? beginStepLocked(d.workspaceRoot, goal, plan, { agent: options.agent }, d.now) : undefined;
|
|
501
|
+
plan.status = deriveTaskPlanStatus(plan);
|
|
502
|
+
const pending = plan.steps.flatMap(s => [s.todo_id ? `todo:${s.todo_id}` : '', s.handoff_id && s.status === 'done' ? `handoff:${s.handoff_id}` : ''].filter(Boolean));
|
|
503
|
+
const result = { ok: true, goal_id: goalId, step_id: stepId, status: recordedStatus, summary: outcome.summary,
|
|
504
|
+
goal_status: goal.status,
|
|
505
|
+
now: formatCurrentWork(planWork(d.workspaceRoot, goal, plan), 'line'), next_ready: getReadyTaskSteps(plan).map(s => s.step_id),
|
|
506
|
+
...(begun?.assignment ? { assignment: assignment(d.workspaceRoot, goal, plan, plan.steps.find(s => s.step_id === begun.assignment.step_id)) } : {}),
|
|
507
|
+
...(pending.length ? { view_repair_pending: [...new Set(pending)] } : {}), ...(amended.skipped.length ? { skipped_follow_ups: amended.skipped } : {}),
|
|
508
|
+
...(goal.status === 'verifying' ? { verification: 'pending' } : {}) };
|
|
509
|
+
if (learnedResult)
|
|
510
|
+
result.learning = learnedResult;
|
|
511
|
+
if (reopened.length)
|
|
512
|
+
result.reopened = reopened;
|
|
513
|
+
if (pendingRequirements.length)
|
|
514
|
+
result.requirement_updates_pending = pendingRequirements;
|
|
515
|
+
if (goal.status === 'blocked')
|
|
516
|
+
result.message = goal.reason_code ?? step.blocked_reason;
|
|
517
|
+
if (goal.status === 'verifying') {
|
|
518
|
+
result.message = 'Step outcome accepted. Goal verification is pending in this immutable receipt; read compact status to confirm completion or receive the corrective next action.';
|
|
519
|
+
result.options = [{ tool: 'get_goal_status', args: { goal_id: goalId, view: 'compact', agent: options.agent }, why: 'Confirm final acceptance and receive the next action without loading full execution history.' }];
|
|
520
|
+
}
|
|
521
|
+
if (goal.stop_requested) {
|
|
522
|
+
result.waiting_on = ['user:resume'];
|
|
523
|
+
result.message = 'Stopped at the user request. Resume only when requested.';
|
|
524
|
+
}
|
|
525
|
+
else if (goal.reason_code?.startsWith('follow-up budget reached'))
|
|
526
|
+
result.waiting_on = ['operator:replan_budget'];
|
|
527
|
+
else if (goal.reason_code === 'host_budget_exhausted')
|
|
528
|
+
result.waiting_on = ['operator:budget'];
|
|
529
|
+
else if (['failed', 'blocked'].includes(recordedStatus)) {
|
|
530
|
+
if (step.waiting_on)
|
|
531
|
+
result.waiting_on = [step.waiting_on];
|
|
532
|
+
else if (!result.assignment)
|
|
533
|
+
result.options = [{ tool: 'resume_goal', args: { goal_id: goalId }, why: 'After correcting the reported issue, open a new attempt without rerunning accepted or running work.' }];
|
|
534
|
+
}
|
|
535
|
+
// Commit the outcome, successor and replay response together BEFORE touching views.
|
|
536
|
+
// A crash during a view write leaves a usable receipt, never an unfinishable attempt.
|
|
537
|
+
const receipt = reopened.includes(stepId) ? step.attempt_history.at(-1) : step;
|
|
538
|
+
receipt.result_digest = digest;
|
|
539
|
+
receipt.result_response = result;
|
|
540
|
+
// A completion that itself revises its dependencies remains replayable for
|
|
541
|
+
// this attempt. Older receipts of other reopened steps stay history only.
|
|
542
|
+
if (receipt !== step) {
|
|
543
|
+
step.assignee = options.agent;
|
|
544
|
+
step.result_digest = digest;
|
|
545
|
+
step.result_response = result;
|
|
546
|
+
}
|
|
547
|
+
d.repo.savePlan(goalId, plan);
|
|
548
|
+
d.repo.saveGoal(goal);
|
|
549
|
+
const failedViews = await syncViews(d.workspaceRoot, goal, plan);
|
|
550
|
+
if (failedViews.length)
|
|
551
|
+
result.view_repair_pending = failedViews;
|
|
552
|
+
else
|
|
553
|
+
delete result.view_repair_pending;
|
|
554
|
+
const accepted = JSON.parse(JSON.stringify(result));
|
|
555
|
+
receipt.result_response = accepted;
|
|
556
|
+
if (receipt !== step)
|
|
557
|
+
step.result_response = accepted;
|
|
558
|
+
d.repo.savePlan(goalId, plan);
|
|
559
|
+
if (goal.status === 'verifying')
|
|
560
|
+
verification = { goal, planDigest: verificationSignature(plan) };
|
|
561
|
+
return accepted;
|
|
562
|
+
}, { operation_label: 'host_complete_step' });
|
|
563
|
+
if (response.ok && readLearningPolicy(d.workspaceRoot).enabled) {
|
|
564
|
+
const goal = d.repo.getGoal(goalId), step = d.repo.getPlan(goalId)?.steps.find(s => s.step_id === stepId);
|
|
565
|
+
if (goal && step && !pendingObservation(step)) {
|
|
566
|
+
await scoreLearning(d.workspaceRoot, goal, step, outcome);
|
|
567
|
+
await harvestStep(d.workspaceRoot, goal, step);
|
|
568
|
+
const { advanceLearning } = await import('./learning-controls.js');
|
|
569
|
+
await advanceLearning(d.workspaceRoot);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (response.ok && learnedResult && !learnedResult.ok && goalId === 'G-learning' && readLearningPolicy(d.workspaceRoot).enabled)
|
|
573
|
+
await reopenLearningCorrection(d.workspaceRoot, learnedResult.errors);
|
|
574
|
+
if (verification)
|
|
575
|
+
await scheduleHostVerification(goalId, verification, deps);
|
|
576
|
+
return response;
|
|
577
|
+
}
|
|
578
|
+
export function verificationSignature(plan) {
|
|
579
|
+
return canonical(plan.steps.map(s => [s.step_id, s.status, s.attempt, s.task, s.depends_on, s.expected_artifacts, s.required_evidence_refs]));
|
|
580
|
+
}
|
|
581
|
+
export async function finishHostVerification(goalId, snapshot, deps) {
|
|
582
|
+
const d = resolveGoalDeps(deps);
|
|
583
|
+
if (d.repo.getPlan(goalId)?.steps.some(pendingObservation)) {
|
|
584
|
+
if (!await settleHostObservations(goalId, snapshot.goal.verification_attempt, deps))
|
|
585
|
+
return;
|
|
586
|
+
const continued = await withStoreWriteCoordinator(d.repo.planPath(goalId), async () => {
|
|
587
|
+
const current = d.repo.getGoal(goalId), plan = d.repo.getPlan(goalId);
|
|
588
|
+
if (!current || !plan || current.stop_requested || current.status !== 'verifying' || current.verification_attempt !== snapshot.goal.verification_attempt)
|
|
589
|
+
return false;
|
|
590
|
+
if (!observationReadyForChecks(plan)) {
|
|
591
|
+
current.status = 'executing';
|
|
592
|
+
current.reason_code = undefined;
|
|
593
|
+
if (current.verification_run)
|
|
594
|
+
current.verification_run.status = 'done';
|
|
595
|
+
d.repo.saveGoal(current);
|
|
596
|
+
await syncViews(d.workspaceRoot, current, plan);
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
snapshot = { goal: current, planDigest: verificationSignature(plan) };
|
|
600
|
+
return true;
|
|
601
|
+
}, { operation_label: 'host_observation_continue' });
|
|
602
|
+
if (!continued)
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const commands = [];
|
|
606
|
+
const check = await checkSuccessContract({ ...d, runVerification: async (command, root, options) => {
|
|
607
|
+
let result;
|
|
608
|
+
try {
|
|
609
|
+
result = await d.runVerification(command, root, options);
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
result = { ok: false, output: error instanceof Error ? error.message : String(error) };
|
|
613
|
+
}
|
|
614
|
+
commands.push({ command, ok: result.ok, output: result.output.slice(-12000) });
|
|
615
|
+
return result;
|
|
616
|
+
} }, snapshot.goal);
|
|
617
|
+
if (check.uncheckable && !snapshot.goal.success_contract.verification_commands.length)
|
|
618
|
+
check.ok = true;
|
|
619
|
+
await withStoreWriteCoordinator(d.repo.planPath(goalId), async () => {
|
|
620
|
+
const goal = d.repo.getGoal(goalId);
|
|
621
|
+
let plan = d.repo.getPlan(goalId);
|
|
622
|
+
if (!goal || !plan || goal.verification_attempt !== snapshot.goal.verification_attempt)
|
|
623
|
+
return;
|
|
624
|
+
if (goal.verification_run)
|
|
625
|
+
goal.verification_run.status = check.ok ? 'done' : 'failed';
|
|
626
|
+
if (goal.stop_requested) {
|
|
627
|
+
goal.status = 'stopped';
|
|
628
|
+
d.repo.saveGoal(goal);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (goal.status !== 'verifying')
|
|
632
|
+
return;
|
|
633
|
+
if (canonical(snapshot.goal.success_contract) !== canonical(goal.success_contract) || snapshot.planDigest !== verificationSignature(plan) || !plan.steps.every(s => ['done', 'skipped'].includes(s.status))) {
|
|
634
|
+
goal.status = 'executing';
|
|
635
|
+
d.repo.saveGoal(goal);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
goal.verification_results = commands;
|
|
639
|
+
goal.final_contract_check = { ok: check.ok, missing_artifacts: check.missing_artifacts, missing_evidence_refs: check.missing_evidence_refs, failed_commands: check.failed_commands };
|
|
640
|
+
if (check.ok) {
|
|
641
|
+
goal.status = 'done';
|
|
642
|
+
goal.verified = goal.success_contract.verification_commands.length > 0;
|
|
643
|
+
goal.result_summary = goal.verified ? 'done (machine-verified)' : 'done (not machine-verified)';
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
const signature = canonical({ commands: check.failure_signatures, artifacts: check.missing_artifacts, evidence: check.missing_evidence_refs });
|
|
647
|
+
const previous = goal.failure_repetition;
|
|
648
|
+
goal.failure_repetition = { signature, count: previous?.signature === signature ? previous.count + 1 : 1 };
|
|
649
|
+
if (goal.failure_repetition.count >= 3) {
|
|
650
|
+
goal.status = 'blocked';
|
|
651
|
+
goal.reason_code = 'repeated_failure';
|
|
652
|
+
goal.result_summary = 'The same final acceptance failure repeated three times without progress. Inspect the retained failure before resuming.';
|
|
653
|
+
d.repo.saveGoal(goal);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const additions = [...check.failed_commands.map(cmd => ({ task: `Make \`${cmd}\` pass` })), ...check.missing_artifacts.map(path => ({ task: `Produce missing artifact ${path}` })), ...check.missing_evidence_refs.map(ref => ({ task: `Produce missing evidence ${ref}` }))];
|
|
657
|
+
const oldIds = new Set(plan.steps.map(s => s.step_id));
|
|
658
|
+
plan = addFollowUps(goal, plan, additions).plan;
|
|
659
|
+
for (const follow of plan.steps.filter(s => !oldIds.has(s.step_id))) {
|
|
660
|
+
const command = commands.find(c => !c.ok && follow.task === `Make \`${c.command}\` pass`);
|
|
661
|
+
if (command) {
|
|
662
|
+
follow.result_summary = command.output;
|
|
663
|
+
follow.evidence_refs = [`goal:${goalId}:verification:${command.command}`];
|
|
664
|
+
follow.verification_failure = { command: command.command, output: command.output, ref: follow.evidence_refs[0] };
|
|
665
|
+
const status = follow.status;
|
|
666
|
+
follow.status = 'failed';
|
|
667
|
+
follow.attempt = 0;
|
|
668
|
+
appendAttempt(follow, goal, new Date(d.now()).toISOString(), [{ command: command.command, cwd: d.workspaceRoot, ok: false }]);
|
|
669
|
+
follow.status = status;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (!goal.reason_code?.startsWith('follow-up budget reached'))
|
|
673
|
+
goal.status = 'executing';
|
|
674
|
+
}
|
|
675
|
+
plan.status = deriveTaskPlanStatus(plan);
|
|
676
|
+
d.repo.savePlan(goalId, plan);
|
|
677
|
+
d.repo.saveGoal(goal);
|
|
678
|
+
}, { operation_label: 'host_verify_result' });
|
|
679
|
+
}
|
|
680
|
+
/** Explicit restart recovery reruns only the declared checks, never host work. */
|
|
681
|
+
export async function retryHostVerification(goalId, deps = {}) {
|
|
682
|
+
const d = resolveGoalDeps(deps);
|
|
683
|
+
if (hostVerificationActive(d.workspaceRoot, goalId))
|
|
684
|
+
return { goal: d.repo.getGoal(goalId), plan: d.repo.getPlan(goalId) };
|
|
685
|
+
const snapshot = await withStoreWriteCoordinator(d.repo.planPath(goalId), async () => {
|
|
686
|
+
const goal = d.repo.getGoal(goalId), plan = d.repo.getPlan(goalId);
|
|
687
|
+
if (!goal || !plan || goal.executor !== 'host' || goal.status === 'done' || goal.pending_follow_ups?.length || goal.reason_code?.startsWith('follow-up budget reached') || (!plan.steps.every(s => ['done', 'skipped'].includes(s.status)) && !plan.steps.some(recoverableObservation)))
|
|
688
|
+
return;
|
|
689
|
+
const adapter = observationAdapter(d.workspaceRoot, deps);
|
|
690
|
+
for (const step of plan.steps.filter(recoverableObservation)) {
|
|
691
|
+
if (adapter?.supported_requirements && !adapter.supported_requirements.includes(step.required_observation))
|
|
692
|
+
return;
|
|
693
|
+
const check = step.observation_check;
|
|
694
|
+
check.status = 'pending';
|
|
695
|
+
check.deadline_at = new Date(d.now() + check.wait_ms).toISOString();
|
|
696
|
+
step.waiting_on = `observation:${step.step_id}`;
|
|
697
|
+
step.blocked_reason = 'Retrying evidence collection only; the original edit attempt is retained.';
|
|
698
|
+
}
|
|
699
|
+
goal.stop_requested = false;
|
|
700
|
+
goal.status = 'verifying';
|
|
701
|
+
goal.verification_attempt = (goal.verification_attempt ?? 0) + 1;
|
|
702
|
+
d.repo.saveGoal(goal);
|
|
703
|
+
goal.reason_code = undefined;
|
|
704
|
+
d.repo.savePlan(goalId, plan);
|
|
705
|
+
d.repo.saveGoal(goal);
|
|
706
|
+
return { goal, planDigest: verificationSignature(plan) };
|
|
707
|
+
}, { operation_label: 'host_retry_verification' });
|
|
708
|
+
if (snapshot)
|
|
709
|
+
await scheduleHostVerification(goalId, snapshot, deps);
|
|
710
|
+
return { goal: d.repo.getGoal(goalId), plan: d.repo.getPlan(goalId) };
|
|
711
|
+
}
|
|
712
|
+
/** Attach only ACE's own result to the exact attempt active when its tool ran. */
|
|
713
|
+
export async function recordHostContact(binding) {
|
|
714
|
+
if (!binding?.goal_id || !binding.step_id)
|
|
715
|
+
return;
|
|
716
|
+
const d = resolveGoalDeps();
|
|
717
|
+
await withStoreWriteCoordinator(d.repo.planPath(binding.goal_id), async () => {
|
|
718
|
+
const plan = d.repo.getPlan(binding.goal_id);
|
|
719
|
+
const step = plan?.steps.find(s => s.step_id === binding.step_id);
|
|
720
|
+
if (!plan || !step || step.status !== 'running' || step.assignee !== binding.agent || step.attempt !== binding.attempt)
|
|
721
|
+
return;
|
|
722
|
+
step.last_ace_call_at = new Date(d.now()).toISOString();
|
|
723
|
+
d.repo.savePlan(binding.goal_id, plan);
|
|
724
|
+
}, { operation_label: 'host_assignee_contact' });
|
|
725
|
+
}
|
|
726
|
+
export async function recordHostToolEvidence(tool, result, binding) {
|
|
727
|
+
if (!binding?.goal_id || !binding.step_id)
|
|
728
|
+
return;
|
|
729
|
+
const d = resolveGoalDeps();
|
|
730
|
+
await withStoreWriteCoordinator(d.repo.planPath(binding.goal_id), async () => {
|
|
731
|
+
const plan = d.repo.getPlan(binding.goal_id);
|
|
732
|
+
const step = plan?.steps.find(s => s.step_id === binding.step_id);
|
|
733
|
+
if (!plan || !step || step.status !== 'running' || step.attempt !== binding.attempt || step.assignee !== binding.agent)
|
|
734
|
+
return;
|
|
735
|
+
step.last_ace_call_at = new Date(d.now()).toISOString();
|
|
736
|
+
const data = result;
|
|
737
|
+
const detail = JSON.stringify(data.structuredContent ?? data.content ?? result);
|
|
738
|
+
step.evidence ??= [];
|
|
739
|
+
step.evidence.push({ ref: `ace:${tool}:${createHash('sha256').update(detail).digest('hex')}`, kind: ['run_tests', 'execute_gates'].includes(tool) ? 'command' : 'diff', provenance: 'ace', detail: detail.slice(-12000), ok: data.structuredContent?.ok ?? (data.isError ? false : undefined) });
|
|
740
|
+
step.evidence_refs = [...new Set(step.evidence.map(e => e.ref))];
|
|
741
|
+
d.repo.savePlan(binding.goal_id, plan);
|
|
742
|
+
}, { operation_label: 'host_ace_tool_result' });
|
|
743
|
+
}
|
|
744
|
+
/** Goal -> packed view is the sole lock order. No store callback acquires a goal lease. */
|
|
745
|
+
export async function withLinkedHandoff(ref, input, writeView) {
|
|
746
|
+
const d = resolveGoalDeps();
|
|
747
|
+
return withStoreWriteCoordinator(d.repo.planPath(ref.goal_id), async () => {
|
|
748
|
+
const goal = d.repo.getGoal(ref.goal_id), plan = d.repo.getPlan(ref.goal_id);
|
|
749
|
+
const step = plan?.steps.find(s => s.step_id === ref.step_id);
|
|
750
|
+
if (!goal || goal.executor !== 'host' || !plan || !step)
|
|
751
|
+
throw new Error('Linked host step missing');
|
|
752
|
+
if (input.status === 'open' && !(step.handoff_id === input.id && step.waiting_on === `handoff:${input.id}`) && (step.status !== 'running' || step.assignee !== input.actor))
|
|
753
|
+
throw new Error(`Step is held by ${step.assignee}; cannot hand off another participant's work.`);
|
|
754
|
+
if (input.status !== 'open' && step.handoff_id !== input.id)
|
|
755
|
+
throw new Error('Handoff no longer owns this step');
|
|
756
|
+
if (['done', 'skipped'].includes(step.status))
|
|
757
|
+
throw new Error('Completed steps cannot be transferred');
|
|
758
|
+
const result = await writeView();
|
|
759
|
+
if (result.ok === false)
|
|
760
|
+
return result;
|
|
761
|
+
step.handoff_id = input.id;
|
|
762
|
+
if (input.status === 'accepted') {
|
|
763
|
+
if (step.assignee !== input.to || step.status !== 'running') {
|
|
764
|
+
preserveAttempt(step);
|
|
765
|
+
step.assignee = input.to;
|
|
766
|
+
step.attempt = (step.attempt ?? 0) + 1;
|
|
767
|
+
step.status = 'running';
|
|
768
|
+
step.started_at = new Date(d.now()).toISOString();
|
|
769
|
+
step.result_digest = undefined;
|
|
770
|
+
step.result_response = undefined;
|
|
771
|
+
step.evidence = [];
|
|
772
|
+
step.evidence_refs = [];
|
|
773
|
+
step.observation = 'unavailable';
|
|
774
|
+
step.served_learning = selectLearning(d.workspaceRoot, plan, step);
|
|
775
|
+
step.loaded_learning = [];
|
|
776
|
+
step.observation_window = { workspace_id: d.workspaceRoot, started_at: step.started_at };
|
|
777
|
+
}
|
|
778
|
+
step.waiting_on = undefined;
|
|
779
|
+
step.blocked_reason = undefined;
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
step.status = 'blocked';
|
|
783
|
+
step.waiting_on = `handoff:${input.id}`;
|
|
784
|
+
}
|
|
785
|
+
plan.status = deriveTaskPlanStatus(plan);
|
|
786
|
+
goal.status = input.status === 'accepted' ? 'executing' : 'blocked';
|
|
787
|
+
d.repo.savePlan(ref.goal_id, plan);
|
|
788
|
+
d.repo.saveGoal(goal);
|
|
789
|
+
await syncViews(d.workspaceRoot, goal, plan);
|
|
790
|
+
return result;
|
|
791
|
+
}, { operation_label: 'host_handoff_transfer' });
|
|
792
|
+
}
|
|
793
|
+
//# sourceMappingURL=host-goal-engine.js.map
|