@voybio/ace-swarm 0.2.5 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -1
- package/README.md +21 -13
- package/assets/.agents/ACE/agent-qa/instructions.md +11 -0
- package/assets/agent-state/EVIDENCE_LOG.md +1 -1
- package/assets/agent-state/MODULES/roles/capability-framework.json +41 -0
- package/assets/agent-state/MODULES/roles/capability-git.json +33 -0
- package/assets/agent-state/MODULES/roles/capability-safety.json +37 -0
- package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +21 -0
- package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +43 -0
- package/assets/agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json +43 -0
- package/assets/agent-state/MODULES/schemas/WORKSPACE_SESSION_REGISTRY.schema.json +11 -0
- package/assets/agent-state/STATUS.md +2 -2
- package/assets/agent-state/runtime-tool-specs.json +70 -2
- package/assets/instructions/ACE_Coder.instructions.md +13 -0
- package/assets/instructions/ACE_UI.instructions.md +11 -0
- package/assets/scripts/ace-hook-dispatch.mjs +70 -6
- package/assets/scripts/render-mcp-configs.sh +19 -5
- package/dist/ace-context.js +91 -11
- package/dist/ace-internal-tools.d.ts +3 -1
- package/dist/ace-internal-tools.js +10 -2
- package/dist/ace-server-instructions.js +3 -3
- package/dist/ace-state-resolver.js +5 -3
- package/dist/agent-runtime/role-adapters.d.ts +18 -1
- package/dist/agent-runtime/role-adapters.js +49 -5
- package/dist/astgrep-index.d.ts +57 -1
- package/dist/astgrep-index.js +140 -4
- package/dist/cli.js +232 -35
- package/dist/discovery-runtime-wrappers.d.ts +108 -0
- package/dist/discovery-runtime-wrappers.js +615 -0
- package/dist/handoff-registry.js +5 -5
- package/dist/helpers/artifacts.d.ts +19 -0
- package/dist/helpers/artifacts.js +152 -0
- package/dist/helpers/bootstrap.d.ts +24 -0
- package/dist/helpers/bootstrap.js +894 -0
- package/dist/helpers/constants.d.ts +53 -0
- package/dist/helpers/constants.js +295 -0
- package/dist/helpers/drift.d.ts +13 -0
- package/dist/helpers/drift.js +45 -0
- package/dist/helpers/path-utils.d.ts +24 -0
- package/dist/helpers/path-utils.js +123 -0
- package/dist/helpers/store-resolution.d.ts +19 -0
- package/dist/helpers/store-resolution.js +305 -0
- package/dist/helpers/workspace-root.d.ts +3 -0
- package/dist/helpers/workspace-root.js +80 -0
- package/dist/helpers.d.ts +8 -125
- package/dist/helpers.js +8 -1768
- package/dist/job-scheduler.js +33 -7
- package/dist/json-sanitizer.d.ts +16 -0
- package/dist/json-sanitizer.js +26 -0
- package/dist/local-model-policy.d.ts +27 -0
- package/dist/local-model-policy.js +84 -0
- package/dist/local-model-runtime.d.ts +6 -0
- package/dist/local-model-runtime.js +33 -21
- package/dist/model-bridge.d.ts +13 -1
- package/dist/model-bridge.js +410 -23
- package/dist/orchestrator-supervisor.d.ts +56 -0
- package/dist/orchestrator-supervisor.js +179 -1
- package/dist/plan-proposal.d.ts +115 -0
- package/dist/plan-proposal.js +1073 -0
- package/dist/run-ledger.js +3 -3
- package/dist/runtime-command.d.ts +8 -0
- package/dist/runtime-command.js +38 -6
- package/dist/runtime-executor.d.ts +20 -1
- package/dist/runtime-executor.js +737 -172
- package/dist/runtime-profile.d.ts +32 -0
- package/dist/runtime-profile.js +89 -13
- package/dist/runtime-tool-specs.d.ts +39 -0
- package/dist/runtime-tool-specs.js +144 -28
- package/dist/safe-edit.d.ts +7 -0
- package/dist/safe-edit.js +163 -37
- package/dist/schemas.js +48 -1
- package/dist/server.js +51 -0
- package/dist/shared.d.ts +3 -2
- package/dist/shared.js +2 -0
- package/dist/status-events.js +9 -6
- package/dist/store/ace-packed-store.d.ts +3 -2
- package/dist/store/ace-packed-store.js +188 -110
- package/dist/store/bootstrap-store.d.ts +2 -1
- package/dist/store/bootstrap-store.js +102 -83
- package/dist/store/cache-workspace.js +11 -5
- package/dist/store/materializers/context-snapshot-materializer.js +6 -2
- package/dist/store/materializers/hook-context-materializer.d.ts +6 -9
- package/dist/store/materializers/hook-context-materializer.js +11 -21
- package/dist/store/materializers/host-file-materializer.js +6 -0
- package/dist/store/materializers/projection-manager.d.ts +0 -1
- package/dist/store/materializers/projection-manager.js +5 -13
- package/dist/store/materializers/scheduler-projection-materializer.js +1 -1
- package/dist/store/materializers/vericify-projector.d.ts +7 -7
- package/dist/store/materializers/vericify-projector.js +11 -11
- package/dist/store/repositories/local-model-runtime-repository.d.ts +120 -3
- package/dist/store/repositories/local-model-runtime-repository.js +242 -6
- package/dist/store/repositories/vericify-repository.d.ts +1 -1
- package/dist/store/skills-install.d.ts +4 -0
- package/dist/store/skills-install.js +21 -12
- package/dist/store/state-reader.d.ts +2 -0
- package/dist/store/state-reader.js +20 -0
- package/dist/store/store-artifacts.d.ts +7 -0
- package/dist/store/store-artifacts.js +27 -1
- package/dist/store/store-authority-audit.d.ts +18 -1
- package/dist/store/store-authority-audit.js +115 -5
- package/dist/store/store-snapshot.d.ts +3 -0
- package/dist/store/store-snapshot.js +22 -2
- package/dist/store/workspace-store-paths.d.ts +39 -0
- package/dist/store/workspace-store-paths.js +94 -0
- package/dist/store/write-coordinator.d.ts +65 -0
- package/dist/store/write-coordinator.js +386 -0
- package/dist/todo-state.js +5 -5
- package/dist/tools-agent.d.ts +20 -0
- package/dist/tools-agent.js +789 -25
- package/dist/tools-discovery.js +136 -1
- package/dist/tools-files.d.ts +7 -0
- package/dist/tools-files.js +1002 -11
- package/dist/tools-framework.js +105 -66
- package/dist/tools-handoff.js +2 -2
- package/dist/tools-lifecycle.js +4 -4
- package/dist/tools-memory.js +6 -6
- package/dist/tools-todo.js +2 -2
- package/dist/tracker-adapters.d.ts +1 -1
- package/dist/tracker-adapters.js +13 -18
- package/dist/tracker-sync.js +5 -3
- package/dist/tui/agent-runner.js +3 -1
- package/dist/tui/chat.js +103 -7
- package/dist/tui/dashboard.d.ts +1 -0
- package/dist/tui/dashboard.js +43 -0
- package/dist/tui/index.js +10 -1
- package/dist/tui/layout.d.ts +20 -0
- package/dist/tui/layout.js +31 -1
- package/dist/tui/local-model-contract.d.ts +6 -2
- package/dist/tui/local-model-contract.js +16 -3
- package/dist/tui/ollama.d.ts +8 -1
- package/dist/tui/ollama.js +53 -12
- package/dist/tui/openai-compatible.d.ts +13 -0
- package/dist/tui/openai-compatible.js +305 -5
- package/dist/tui/provider-discovery.d.ts +1 -0
- package/dist/tui/provider-discovery.js +35 -11
- package/dist/vericify-bridge.d.ts +6 -1
- package/dist/vericify-bridge.js +27 -3
- package/dist/workspace-manager.d.ts +30 -3
- package/dist/workspace-manager.js +257 -27
- package/package.json +1 -2
- package/dist/internal-tool-runtime.d.ts +0 -21
- package/dist/internal-tool-runtime.js +0 -136
- package/dist/store/workspace-snapshot.d.ts +0 -26
- package/dist/store/workspace-snapshot.js +0 -107
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workstream J — Planner contract: propose_plan, validate_plan, loadAcceptanceTraceContract.
|
|
3
|
+
* Extracted to its own module so both tools-agent.ts and runtime-executor.ts can import without
|
|
4
|
+
* a circular dependency (tools-agent already imports from runtime-executor).
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { listAceInternalToolCatalog } from "./ace-internal-tools.js";
|
|
8
|
+
import { runLocalModelTask } from "./local-model-runtime.js";
|
|
9
|
+
import { listRuntimeToolSpecs } from "./runtime-tool-specs.js";
|
|
10
|
+
import { resolveWorkspaceRoot, safeRead, safeWriteAsync } from "./helpers.js";
|
|
11
|
+
import { appendVericifyProcessPostSafe } from "./vericify-bridge.js";
|
|
12
|
+
import { withLocalModelRuntimeRepository } from "./store/repositories/local-model-runtime-repository.js";
|
|
13
|
+
// ── In-memory proposal cache (plan_id → ProposePlanResult) ────────────────
|
|
14
|
+
const _proposalCache = new Map();
|
|
15
|
+
function storeProposal(result) {
|
|
16
|
+
_proposalCache.set(result.plan_id, result);
|
|
17
|
+
}
|
|
18
|
+
export function lookupProposal(plan_id) {
|
|
19
|
+
return _proposalCache.get(plan_id);
|
|
20
|
+
}
|
|
21
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
22
|
+
export function inferExpectedOutputClass(step) {
|
|
23
|
+
if (step.role === "qa" || step.role === "release")
|
|
24
|
+
return "qa_verdict";
|
|
25
|
+
if ((step.tool_scope ?? []).some((tool) => tool.includes("astgrep") || tool.includes("structural_edit"))) {
|
|
26
|
+
return "structural_edit_plan";
|
|
27
|
+
}
|
|
28
|
+
if ((step.tool_scope ?? []).some((tool) => tool.includes("write") || tool.includes("safe_edit"))) {
|
|
29
|
+
return "code_artifact";
|
|
30
|
+
}
|
|
31
|
+
if ((step.tool_scope ?? []).length > 0)
|
|
32
|
+
return "tool_envelope";
|
|
33
|
+
return "plain_text_plan";
|
|
34
|
+
}
|
|
35
|
+
function enrichPlanProposalStep(step) {
|
|
36
|
+
return {
|
|
37
|
+
...step,
|
|
38
|
+
expected_output_class: step.expected_output_class ?? inferExpectedOutputClass(step),
|
|
39
|
+
allowed_tools: step.allowed_tools ?? step.tool_scope,
|
|
40
|
+
structural_edit_plan_required: step.structural_edit_plan_required ??
|
|
41
|
+
(inferExpectedOutputClass(step) === "structural_edit_plan" ? true : undefined),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function enrichContractStep(step) {
|
|
45
|
+
return {
|
|
46
|
+
...step,
|
|
47
|
+
expected_output_class: step.expected_output_class ?? inferExpectedOutputClass(step),
|
|
48
|
+
allowed_tools: step.allowed_tools ?? step.tool_scope,
|
|
49
|
+
structural_edit_plan_required: step.structural_edit_plan_required ??
|
|
50
|
+
(inferExpectedOutputClass(step) === "structural_edit_plan" ? true : undefined),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function scaffoldArtifactExpectations(step) {
|
|
54
|
+
const expected = inferExpectedOutputClass(step);
|
|
55
|
+
if (expected !== "code_artifact" && expected !== "structural_edit_plan")
|
|
56
|
+
return undefined;
|
|
57
|
+
if (step.role === "spec") {
|
|
58
|
+
return [{ path: "agent-state/SPEC_CONTRACT.json", required: false, evidence_ref_kind: "artifact" }];
|
|
59
|
+
}
|
|
60
|
+
if (step.role === "docs") {
|
|
61
|
+
return [{ path: "agent-state", required: false, evidence_ref_kind: "artifact" }];
|
|
62
|
+
}
|
|
63
|
+
return [{ path: "workspace-artifact", required: false, evidence_ref_kind: "artifact" }];
|
|
64
|
+
}
|
|
65
|
+
function isCodeMutatingStep(step) {
|
|
66
|
+
if (step.role === "coders" || step.role === "builder" || step.role === "astgrep")
|
|
67
|
+
return true;
|
|
68
|
+
if (step.expected_output_class === "code_artifact" || step.expected_output_class === "structural_edit_plan")
|
|
69
|
+
return true;
|
|
70
|
+
return (step.tool_scope ?? []).some((tool) => /write|edit|rewrite|safe_edit|astgrep|structural/i.test(tool));
|
|
71
|
+
}
|
|
72
|
+
function hasStructuralEditWaiver(step) {
|
|
73
|
+
const waiver = step.structural_edit_waiver;
|
|
74
|
+
return Boolean(waiver?.reason?.trim() && waiver?.evidence_ref?.trim());
|
|
75
|
+
}
|
|
76
|
+
function isPlanProposal(value) {
|
|
77
|
+
if (!value || typeof value !== "object")
|
|
78
|
+
return false;
|
|
79
|
+
const v = value;
|
|
80
|
+
return (typeof v.intent_summary === "string" &&
|
|
81
|
+
Array.isArray(v.success_criteria) &&
|
|
82
|
+
Array.isArray(v.steps));
|
|
83
|
+
}
|
|
84
|
+
function extractJsonBlock(text) {
|
|
85
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
86
|
+
if (fenced?.[1])
|
|
87
|
+
return fenced[1].trim();
|
|
88
|
+
const bare = text.match(/(\{[\s\S]*\})/);
|
|
89
|
+
if (bare?.[1])
|
|
90
|
+
return bare[1].trim();
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
function getAvailableToolNames() {
|
|
94
|
+
try {
|
|
95
|
+
const names = new Set();
|
|
96
|
+
for (const tool of listAceInternalToolCatalog()) {
|
|
97
|
+
names.add(tool.name);
|
|
98
|
+
}
|
|
99
|
+
for (const tool of listRuntimeToolSpecs()) {
|
|
100
|
+
names.add(tool.name);
|
|
101
|
+
}
|
|
102
|
+
return names.size > 0 ? names : undefined;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function normalizeTaskText(task) {
|
|
109
|
+
return task.replace(/\s+/g, " ").trim();
|
|
110
|
+
}
|
|
111
|
+
const CLAUSE_HINTS = [
|
|
112
|
+
"research",
|
|
113
|
+
"investigate",
|
|
114
|
+
"analyze",
|
|
115
|
+
"analyse",
|
|
116
|
+
"audit",
|
|
117
|
+
"inspect",
|
|
118
|
+
"diagnose",
|
|
119
|
+
"dissect",
|
|
120
|
+
"study",
|
|
121
|
+
"discover",
|
|
122
|
+
"explore",
|
|
123
|
+
"understand",
|
|
124
|
+
"spec",
|
|
125
|
+
"design",
|
|
126
|
+
"plan",
|
|
127
|
+
"contract",
|
|
128
|
+
"define",
|
|
129
|
+
"implement",
|
|
130
|
+
"build",
|
|
131
|
+
"create",
|
|
132
|
+
"add",
|
|
133
|
+
"fix",
|
|
134
|
+
"patch",
|
|
135
|
+
"refactor",
|
|
136
|
+
"rewrite",
|
|
137
|
+
"migrate",
|
|
138
|
+
"update",
|
|
139
|
+
"validate",
|
|
140
|
+
"verify",
|
|
141
|
+
"test",
|
|
142
|
+
"review",
|
|
143
|
+
"check",
|
|
144
|
+
"ship",
|
|
145
|
+
"release",
|
|
146
|
+
"deploy",
|
|
147
|
+
"publish",
|
|
148
|
+
"merge",
|
|
149
|
+
"document",
|
|
150
|
+
"docs",
|
|
151
|
+
"security",
|
|
152
|
+
"hardening",
|
|
153
|
+
"observe",
|
|
154
|
+
"orchestrate",
|
|
155
|
+
"handoff",
|
|
156
|
+
"coordinate",
|
|
157
|
+
];
|
|
158
|
+
function countClauseHints(text) {
|
|
159
|
+
const lower = text.toLowerCase();
|
|
160
|
+
return CLAUSE_HINTS.reduce((count, hint) => {
|
|
161
|
+
const pattern = new RegExp(`\\b${hint.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
|
162
|
+
return count + (pattern.test(lower) ? 1 : 0);
|
|
163
|
+
}, 0);
|
|
164
|
+
}
|
|
165
|
+
function splitTaskClauses(task) {
|
|
166
|
+
const normalized = normalizeTaskText(task);
|
|
167
|
+
if (!normalized)
|
|
168
|
+
return [];
|
|
169
|
+
const initialSegments = normalized
|
|
170
|
+
.split(/[\n;]+/)
|
|
171
|
+
.flatMap((segment) => segment.split(/(?<=[.!?])\s+/))
|
|
172
|
+
.map((segment) => segment.trim())
|
|
173
|
+
.filter((segment) => segment.length > 0);
|
|
174
|
+
const clauses = [];
|
|
175
|
+
const segments = initialSegments.length > 0 ? initialSegments : [normalized];
|
|
176
|
+
for (const segment of segments) {
|
|
177
|
+
if (segment.length > 180 && countClauseHints(segment) >= 2) {
|
|
178
|
+
const conjunctive = segment
|
|
179
|
+
.split(/\s+(?:and|then|plus|while|after)\s+/i)
|
|
180
|
+
.map((piece) => piece.trim().replace(/^[,;:]+|[,;:]+$/g, ""))
|
|
181
|
+
.filter((piece) => piece.length > 0);
|
|
182
|
+
if (conjunctive.length > 1) {
|
|
183
|
+
clauses.push(...conjunctive);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (segment.includes(",")) {
|
|
188
|
+
const commaParts = segment
|
|
189
|
+
.split(/\s*,\s*/)
|
|
190
|
+
.map((piece) => piece.trim())
|
|
191
|
+
.filter((piece) => piece.length > 0);
|
|
192
|
+
if (commaParts.length > 1 && commaParts.some((piece) => piece.length > 12)) {
|
|
193
|
+
clauses.push(...commaParts);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
clauses.push(segment);
|
|
198
|
+
}
|
|
199
|
+
return Array.from(new Set(clauses.map((clause) => clause.trim()).filter(Boolean)));
|
|
200
|
+
}
|
|
201
|
+
function classifyClauseKind(clause) {
|
|
202
|
+
const text = clause.toLowerCase();
|
|
203
|
+
if (/\b(research|investigate|analy[sz]e|audit|inspect|diagnose|dissect|study|discover|explore|understand|triage|map)\b/.test(text)) {
|
|
204
|
+
return "discovery";
|
|
205
|
+
}
|
|
206
|
+
if (/\b(spec|design|plan|architect|contract|define|interface|requirements?|shape)\b/.test(text)) {
|
|
207
|
+
return "spec";
|
|
208
|
+
}
|
|
209
|
+
if (/\b(verify|validate|test|qa|check|confirm|review|prove)\b/.test(text)) {
|
|
210
|
+
return "verification";
|
|
211
|
+
}
|
|
212
|
+
if (/\b(release|ship|deploy|publish|merge|promote|rollout|launch)\b/.test(text)) {
|
|
213
|
+
return "release";
|
|
214
|
+
}
|
|
215
|
+
if (/\b(document|docs|documentation|readme|guide|onboard|changelog)\b/.test(text)) {
|
|
216
|
+
return "docs";
|
|
217
|
+
}
|
|
218
|
+
if (/\b(security|hardening|privacy|threat|vulnerab|auth|risk)\b/.test(text)) {
|
|
219
|
+
return "security";
|
|
220
|
+
}
|
|
221
|
+
if (/\b(ops|orchestrate|coordinate|handoff|schedule|route|dispatch)\b/.test(text)) {
|
|
222
|
+
return "ops";
|
|
223
|
+
}
|
|
224
|
+
return "implementation";
|
|
225
|
+
}
|
|
226
|
+
function roleForClauseKind(kind) {
|
|
227
|
+
switch (kind) {
|
|
228
|
+
case "discovery":
|
|
229
|
+
return "research";
|
|
230
|
+
case "spec":
|
|
231
|
+
return "spec";
|
|
232
|
+
case "verification":
|
|
233
|
+
return "qa";
|
|
234
|
+
case "release":
|
|
235
|
+
return "release";
|
|
236
|
+
case "docs":
|
|
237
|
+
return "docs";
|
|
238
|
+
case "security":
|
|
239
|
+
return "security";
|
|
240
|
+
case "ops":
|
|
241
|
+
return "orchestrator";
|
|
242
|
+
case "implementation":
|
|
243
|
+
default:
|
|
244
|
+
return "coders";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function isShipBoundaryTask(task) {
|
|
248
|
+
return /\b(ship|release|promot(?:e|ion)|land|merge)\b/i.test(task);
|
|
249
|
+
}
|
|
250
|
+
function toolScopeForClauseKind(kind, shipBoundary) {
|
|
251
|
+
switch (kind) {
|
|
252
|
+
case "discovery":
|
|
253
|
+
return [
|
|
254
|
+
"recall_context",
|
|
255
|
+
"read_workspace_file",
|
|
256
|
+
"build_continuity_packet",
|
|
257
|
+
"validate_framework",
|
|
258
|
+
];
|
|
259
|
+
case "spec":
|
|
260
|
+
return ["read_workspace_file", "write_workspace_file", "execute_gates", "validate_framework"];
|
|
261
|
+
case "verification":
|
|
262
|
+
return shipBoundary
|
|
263
|
+
? ["run_tests", "execute_gates", "git_diff", "git_status"]
|
|
264
|
+
: ["run_tests", "execute_gates", "git_diff"];
|
|
265
|
+
case "release":
|
|
266
|
+
return ["git_status", "git_diff", "execute_gates", "validate_framework"];
|
|
267
|
+
case "docs":
|
|
268
|
+
return ["read_workspace_file", "write_workspace_file", "git_diff", "git_status"];
|
|
269
|
+
case "security":
|
|
270
|
+
return ["execute_gates", "validate_framework", "read_workspace_file", "git_diff"];
|
|
271
|
+
case "ops":
|
|
272
|
+
return ["recall_context", "build_continuity_packet", "execute_gates", "validate_framework"];
|
|
273
|
+
case "implementation":
|
|
274
|
+
default:
|
|
275
|
+
return [
|
|
276
|
+
"outline_file",
|
|
277
|
+
"astgrep_query",
|
|
278
|
+
"astgrep_locate",
|
|
279
|
+
"read_file_lines",
|
|
280
|
+
"compile_structural_edit",
|
|
281
|
+
"preview_structural_edit",
|
|
282
|
+
"astgrep_rewrite",
|
|
283
|
+
"safe_edit_file",
|
|
284
|
+
"run_tests",
|
|
285
|
+
"git_diff",
|
|
286
|
+
"git_status",
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function buildAcceptanceCriteriaForStep(input) {
|
|
291
|
+
const { kind, clause, task, index, total, shipBoundary } = input;
|
|
292
|
+
switch (kind) {
|
|
293
|
+
case "discovery":
|
|
294
|
+
return [
|
|
295
|
+
"The objective, constraints, and dependencies are enumerated.",
|
|
296
|
+
"Known unknowns are explicit before downstream work starts.",
|
|
297
|
+
"Existing workspace state is reviewed before planning further passes.",
|
|
298
|
+
];
|
|
299
|
+
case "spec":
|
|
300
|
+
return [
|
|
301
|
+
"The work is broken into explicit phases with dependencies.",
|
|
302
|
+
"Each downstream pass has a clear success criterion.",
|
|
303
|
+
"Stop conditions are written before implementation begins.",
|
|
304
|
+
];
|
|
305
|
+
case "verification":
|
|
306
|
+
return [
|
|
307
|
+
"The results map back to the original intent.",
|
|
308
|
+
"Any residual gaps or failures are explicit.",
|
|
309
|
+
"Evidence exists for the verification verdict.",
|
|
310
|
+
];
|
|
311
|
+
case "release":
|
|
312
|
+
return [
|
|
313
|
+
"The ship decision is evidence-backed.",
|
|
314
|
+
"Residual risks are explicit before the handoff closes.",
|
|
315
|
+
"The release gate references the validated work rather than assumptions.",
|
|
316
|
+
];
|
|
317
|
+
case "docs":
|
|
318
|
+
return [
|
|
319
|
+
"User-facing explanation matches the work performed.",
|
|
320
|
+
"Documentation references the actual artifact state.",
|
|
321
|
+
"Open questions are called out rather than hidden.",
|
|
322
|
+
];
|
|
323
|
+
case "security":
|
|
324
|
+
return [
|
|
325
|
+
"Security risks are assessed against the current change set.",
|
|
326
|
+
"Any blockers are explicit and reviewable.",
|
|
327
|
+
"Ship readiness remains grounded in evidence.",
|
|
328
|
+
];
|
|
329
|
+
case "ops":
|
|
330
|
+
return [
|
|
331
|
+
"Dependencies and handoffs are ordered.",
|
|
332
|
+
"The execution route is explicit.",
|
|
333
|
+
"Circuit-breaker points are clear before the next pass starts.",
|
|
334
|
+
];
|
|
335
|
+
case "implementation":
|
|
336
|
+
default:
|
|
337
|
+
return [
|
|
338
|
+
`Complete clause ${index + 1}/${total}: ${clause}`,
|
|
339
|
+
"Produce a reviewable artifact or diff for the clause.",
|
|
340
|
+
shipBoundary
|
|
341
|
+
? "Leave ship-relevant evidence available for downstream review."
|
|
342
|
+
: "Leave evidence available for downstream verification.",
|
|
343
|
+
];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function buildStopConditionsForStep(input) {
|
|
347
|
+
const { kind, clause, task, shipBoundary } = input;
|
|
348
|
+
switch (kind) {
|
|
349
|
+
case "discovery":
|
|
350
|
+
return [
|
|
351
|
+
"Stop when the objective can be restated with constraints, unknowns, and dependencies.",
|
|
352
|
+
];
|
|
353
|
+
case "spec":
|
|
354
|
+
return [
|
|
355
|
+
"Stop when the plan has a sequenced set of work packages and explicit acceptance gates.",
|
|
356
|
+
];
|
|
357
|
+
case "verification":
|
|
358
|
+
return [
|
|
359
|
+
"Stop when the result is clearly accepted, rejected, or blocked with evidence.",
|
|
360
|
+
];
|
|
361
|
+
case "release":
|
|
362
|
+
return [
|
|
363
|
+
"Stop when the ship decision is explicit and evidence-backed.",
|
|
364
|
+
];
|
|
365
|
+
case "docs":
|
|
366
|
+
return [
|
|
367
|
+
"Stop when the documentation reflects the actual deliverable and no major gap remains.",
|
|
368
|
+
];
|
|
369
|
+
case "security":
|
|
370
|
+
return [
|
|
371
|
+
"Stop when security risks are either cleared or isolated as explicit blockers.",
|
|
372
|
+
];
|
|
373
|
+
case "ops":
|
|
374
|
+
return [
|
|
375
|
+
"Stop when the handoff path and dependency order are explicit enough to execute.",
|
|
376
|
+
];
|
|
377
|
+
case "implementation":
|
|
378
|
+
default:
|
|
379
|
+
return [
|
|
380
|
+
shipBoundary
|
|
381
|
+
? `Stop when clause work for "${clause}" is complete and ready for review in the ship path.`
|
|
382
|
+
: `Stop when clause work for "${clause}" is complete and ready for verification.`,
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function buildSuccessCriteria(task, clauses, shipBoundary) {
|
|
387
|
+
const criteria = [
|
|
388
|
+
"The objective is broken into explicit phases.",
|
|
389
|
+
"Each phase names a role, tool scope, acceptance criteria, and stop condition.",
|
|
390
|
+
"Verification closes the loop before handoff.",
|
|
391
|
+
];
|
|
392
|
+
if (clauses.length > 1) {
|
|
393
|
+
criteria.push(`All ${clauses.length} goal clauses are addressed in order.`);
|
|
394
|
+
}
|
|
395
|
+
if (shipBoundary) {
|
|
396
|
+
criteria.push("Ship readiness is explicitly confirmed before release.");
|
|
397
|
+
}
|
|
398
|
+
if (task.trim().length > 0) {
|
|
399
|
+
criteria.push(`The compiled plan still traces back to the original goal: ${task}.`);
|
|
400
|
+
}
|
|
401
|
+
return criteria;
|
|
402
|
+
}
|
|
403
|
+
function buildDeterministicScaffoldStepTask(input) {
|
|
404
|
+
const { kind, clause, task, index, total, shipBoundary } = input;
|
|
405
|
+
switch (kind) {
|
|
406
|
+
case "discovery":
|
|
407
|
+
return `Phase 3.${index + 1} - Discovery: map constraints, unknowns, and dependencies for ${clause}`;
|
|
408
|
+
case "spec":
|
|
409
|
+
return `Phase 3.${index + 1} - Planning: translate ${clause} into sequenced work with explicit gates`;
|
|
410
|
+
case "verification":
|
|
411
|
+
return `Phase 3.${index + 1} - Verification: validate the compiled goal against ${clause || task}`;
|
|
412
|
+
case "release":
|
|
413
|
+
return `Phase 3.${index + 1} - Release: finalize the ship decision for ${task}`;
|
|
414
|
+
case "docs":
|
|
415
|
+
return `Phase 3.${index + 1} - Documentation: align user-facing explanation with ${clause}`;
|
|
416
|
+
case "security":
|
|
417
|
+
return `Phase 3.${index + 1} - Security: assess risks for ${clause}`;
|
|
418
|
+
case "ops":
|
|
419
|
+
return `Phase 3.${index + 1} - Coordination: turn ${clause} into an executable handoff path`;
|
|
420
|
+
case "implementation":
|
|
421
|
+
default:
|
|
422
|
+
return shipBoundary
|
|
423
|
+
? `Phase 3.${index + 1} - Implementation: deliver ${clause} and keep evidence for ship review`
|
|
424
|
+
: `Phase 3.${index + 1} - Implementation: deliver ${clause} and keep evidence for verification`;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function compileGoalScaffold(task, plan_id) {
|
|
428
|
+
const normalizedTask = normalizeTaskText(task) || "Clarify the objective";
|
|
429
|
+
const clauses = splitTaskClauses(normalizedTask);
|
|
430
|
+
const shipBoundary = isShipBoundaryTask(normalizedTask);
|
|
431
|
+
const scaffoldSteps = [];
|
|
432
|
+
const discoveryId = "goal-discovery";
|
|
433
|
+
scaffoldSteps.push({
|
|
434
|
+
id: discoveryId,
|
|
435
|
+
role: "research",
|
|
436
|
+
task: `Phase 1 - Discovery: map constraints, unknowns, and dependencies for ${normalizedTask}`,
|
|
437
|
+
depends_on_ids: [],
|
|
438
|
+
tool_scope: toolScopeForClauseKind("discovery", shipBoundary),
|
|
439
|
+
acceptance_criteria: buildAcceptanceCriteriaForStep({
|
|
440
|
+
kind: "discovery",
|
|
441
|
+
clause: normalizedTask,
|
|
442
|
+
task: normalizedTask,
|
|
443
|
+
index: 0,
|
|
444
|
+
total: Math.max(clauses.length, 1),
|
|
445
|
+
shipBoundary,
|
|
446
|
+
}),
|
|
447
|
+
stop_condition: buildStopConditionsForStep({
|
|
448
|
+
kind: "discovery",
|
|
449
|
+
clause: normalizedTask,
|
|
450
|
+
task: normalizedTask,
|
|
451
|
+
shipBoundary,
|
|
452
|
+
}),
|
|
453
|
+
});
|
|
454
|
+
const specId = "goal-spec";
|
|
455
|
+
scaffoldSteps.push({
|
|
456
|
+
id: specId,
|
|
457
|
+
role: "spec",
|
|
458
|
+
task: `Phase 2 - Planning: turn the objective into sequenced work with explicit gates for ${normalizedTask}`,
|
|
459
|
+
depends_on_ids: [discoveryId],
|
|
460
|
+
tool_scope: toolScopeForClauseKind("spec", shipBoundary),
|
|
461
|
+
acceptance_criteria: buildAcceptanceCriteriaForStep({
|
|
462
|
+
kind: "spec",
|
|
463
|
+
clause: normalizedTask,
|
|
464
|
+
task: normalizedTask,
|
|
465
|
+
index: 0,
|
|
466
|
+
total: Math.max(clauses.length, 1),
|
|
467
|
+
shipBoundary,
|
|
468
|
+
}),
|
|
469
|
+
stop_condition: buildStopConditionsForStep({
|
|
470
|
+
kind: "spec",
|
|
471
|
+
clause: normalizedTask,
|
|
472
|
+
task: normalizedTask,
|
|
473
|
+
shipBoundary,
|
|
474
|
+
}),
|
|
475
|
+
});
|
|
476
|
+
const executionStepIds = [];
|
|
477
|
+
const executionClauses = clauses.length > 0 ? clauses : [normalizedTask];
|
|
478
|
+
for (const [index, clause] of executionClauses.entries()) {
|
|
479
|
+
const rawKind = classifyClauseKind(clause);
|
|
480
|
+
const kind = rawKind === "release" && shipBoundary && executionClauses.length === 1 && normalizeTaskText(clause) === normalizedTask
|
|
481
|
+
? "implementation"
|
|
482
|
+
: rawKind;
|
|
483
|
+
const role = roleForClauseKind(kind);
|
|
484
|
+
const stepId = `goal-${role}-${index + 1}`;
|
|
485
|
+
const dependency = index === 0 ? specId : executionStepIds[index - 1];
|
|
486
|
+
scaffoldSteps.push({
|
|
487
|
+
id: stepId,
|
|
488
|
+
role,
|
|
489
|
+
task: buildDeterministicScaffoldStepTask({
|
|
490
|
+
kind,
|
|
491
|
+
clause,
|
|
492
|
+
task: normalizedTask,
|
|
493
|
+
index,
|
|
494
|
+
total: executionClauses.length,
|
|
495
|
+
shipBoundary,
|
|
496
|
+
}),
|
|
497
|
+
depends_on_ids: [dependency],
|
|
498
|
+
tool_scope: toolScopeForClauseKind(kind, shipBoundary),
|
|
499
|
+
acceptance_criteria: buildAcceptanceCriteriaForStep({
|
|
500
|
+
kind,
|
|
501
|
+
clause,
|
|
502
|
+
task: normalizedTask,
|
|
503
|
+
index,
|
|
504
|
+
total: executionClauses.length,
|
|
505
|
+
shipBoundary,
|
|
506
|
+
}),
|
|
507
|
+
stop_condition: buildStopConditionsForStep({
|
|
508
|
+
kind,
|
|
509
|
+
clause,
|
|
510
|
+
task: normalizedTask,
|
|
511
|
+
shipBoundary,
|
|
512
|
+
}),
|
|
513
|
+
});
|
|
514
|
+
executionStepIds.push(stepId);
|
|
515
|
+
}
|
|
516
|
+
const verificationDependsOn = executionStepIds.length > 0 ? [...executionStepIds] : [specId];
|
|
517
|
+
if (shipBoundary) {
|
|
518
|
+
const reviewId = "goal-skeptic-1";
|
|
519
|
+
const securityId = "goal-security-1";
|
|
520
|
+
const qaId = "goal-qa-1";
|
|
521
|
+
scaffoldSteps.push({
|
|
522
|
+
id: reviewId,
|
|
523
|
+
role: "skeptic",
|
|
524
|
+
task: `Phase 4 - Review: assess ship readiness for ${normalizedTask}`,
|
|
525
|
+
depends_on_ids: verificationDependsOn,
|
|
526
|
+
tool_scope: toolScopeForClauseKind("verification", shipBoundary),
|
|
527
|
+
acceptance_criteria: buildAcceptanceCriteriaForStep({
|
|
528
|
+
kind: "verification",
|
|
529
|
+
clause: normalizedTask,
|
|
530
|
+
task: normalizedTask,
|
|
531
|
+
index: executionClauses.length,
|
|
532
|
+
total: executionClauses.length + 3,
|
|
533
|
+
shipBoundary,
|
|
534
|
+
}),
|
|
535
|
+
stop_condition: buildStopConditionsForStep({
|
|
536
|
+
kind: "verification",
|
|
537
|
+
clause: normalizedTask,
|
|
538
|
+
task: normalizedTask,
|
|
539
|
+
shipBoundary,
|
|
540
|
+
}),
|
|
541
|
+
});
|
|
542
|
+
scaffoldSteps.push({
|
|
543
|
+
id: securityId,
|
|
544
|
+
role: "security",
|
|
545
|
+
task: `Phase 5 - Security: assess security readiness for ${normalizedTask}`,
|
|
546
|
+
depends_on_ids: verificationDependsOn,
|
|
547
|
+
tool_scope: toolScopeForClauseKind("security", shipBoundary),
|
|
548
|
+
acceptance_criteria: buildAcceptanceCriteriaForStep({
|
|
549
|
+
kind: "security",
|
|
550
|
+
clause: normalizedTask,
|
|
551
|
+
task: normalizedTask,
|
|
552
|
+
index: executionClauses.length + 1,
|
|
553
|
+
total: executionClauses.length + 3,
|
|
554
|
+
shipBoundary,
|
|
555
|
+
}),
|
|
556
|
+
stop_condition: buildStopConditionsForStep({
|
|
557
|
+
kind: "security",
|
|
558
|
+
clause: normalizedTask,
|
|
559
|
+
task: normalizedTask,
|
|
560
|
+
shipBoundary,
|
|
561
|
+
}),
|
|
562
|
+
});
|
|
563
|
+
scaffoldSteps.push({
|
|
564
|
+
id: qaId,
|
|
565
|
+
role: "qa",
|
|
566
|
+
task: `Phase 6 - QA: validate ship readiness for ${normalizedTask}`,
|
|
567
|
+
depends_on_ids: verificationDependsOn,
|
|
568
|
+
tool_scope: toolScopeForClauseKind("verification", shipBoundary),
|
|
569
|
+
acceptance_criteria: buildAcceptanceCriteriaForStep({
|
|
570
|
+
kind: "verification",
|
|
571
|
+
clause: normalizedTask,
|
|
572
|
+
task: normalizedTask,
|
|
573
|
+
index: executionClauses.length + 2,
|
|
574
|
+
total: executionClauses.length + 3,
|
|
575
|
+
shipBoundary,
|
|
576
|
+
}),
|
|
577
|
+
stop_condition: buildStopConditionsForStep({
|
|
578
|
+
kind: "verification",
|
|
579
|
+
clause: normalizedTask,
|
|
580
|
+
task: normalizedTask,
|
|
581
|
+
shipBoundary,
|
|
582
|
+
}),
|
|
583
|
+
});
|
|
584
|
+
scaffoldSteps.push({
|
|
585
|
+
id: "goal-release-1",
|
|
586
|
+
role: "release",
|
|
587
|
+
task: `Phase 7 - Release: finalize the ship decision for ${normalizedTask}`,
|
|
588
|
+
depends_on_ids: [reviewId, securityId, qaId],
|
|
589
|
+
tool_scope: toolScopeForClauseKind("release", shipBoundary),
|
|
590
|
+
acceptance_criteria: buildAcceptanceCriteriaForStep({
|
|
591
|
+
kind: "release",
|
|
592
|
+
clause: normalizedTask,
|
|
593
|
+
task: normalizedTask,
|
|
594
|
+
index: executionClauses.length + 3,
|
|
595
|
+
total: executionClauses.length + 4,
|
|
596
|
+
shipBoundary,
|
|
597
|
+
}),
|
|
598
|
+
stop_condition: buildStopConditionsForStep({
|
|
599
|
+
kind: "release",
|
|
600
|
+
clause: normalizedTask,
|
|
601
|
+
task: normalizedTask,
|
|
602
|
+
shipBoundary,
|
|
603
|
+
}),
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
scaffoldSteps.push({
|
|
608
|
+
id: "goal-qa-1",
|
|
609
|
+
role: "qa",
|
|
610
|
+
task: `Phase 4 - Verification: validate the compiled goal against ${normalizedTask}`,
|
|
611
|
+
depends_on_ids: verificationDependsOn,
|
|
612
|
+
tool_scope: toolScopeForClauseKind("verification", shipBoundary),
|
|
613
|
+
acceptance_criteria: buildAcceptanceCriteriaForStep({
|
|
614
|
+
kind: "verification",
|
|
615
|
+
clause: normalizedTask,
|
|
616
|
+
task: normalizedTask,
|
|
617
|
+
index: executionClauses.length,
|
|
618
|
+
total: executionClauses.length + 1,
|
|
619
|
+
shipBoundary,
|
|
620
|
+
}),
|
|
621
|
+
stop_condition: buildStopConditionsForStep({
|
|
622
|
+
kind: "verification",
|
|
623
|
+
clause: normalizedTask,
|
|
624
|
+
task: normalizedTask,
|
|
625
|
+
shipBoundary,
|
|
626
|
+
}),
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const publicStepLabels = new Map();
|
|
630
|
+
scaffoldSteps.forEach((step, index) => {
|
|
631
|
+
publicStepLabels.set(step.id, `step-${index + 1}`);
|
|
632
|
+
});
|
|
633
|
+
const steps = scaffoldSteps.map(({ id: _id, depends_on_ids: dependsOnIds, ...step }) => ({
|
|
634
|
+
role: step.role,
|
|
635
|
+
task: step.task,
|
|
636
|
+
depends_on: dependsOnIds.length > 0
|
|
637
|
+
? dependsOnIds.map((dependency) => publicStepLabels.get(dependency) ?? dependency)
|
|
638
|
+
: undefined,
|
|
639
|
+
tool_scope: step.tool_scope,
|
|
640
|
+
acceptance_criteria: step.acceptance_criteria,
|
|
641
|
+
stop_condition: step.stop_condition,
|
|
642
|
+
expected_artifacts: scaffoldArtifactExpectations(step),
|
|
643
|
+
structural_edit_plan_required: inferExpectedOutputClass(step) === "code_artifact" ||
|
|
644
|
+
inferExpectedOutputClass(step) === "structural_edit_plan"
|
|
645
|
+
? true
|
|
646
|
+
: undefined,
|
|
647
|
+
}));
|
|
648
|
+
const result = {
|
|
649
|
+
plan_id,
|
|
650
|
+
status: "planning",
|
|
651
|
+
intent_summary: `Deterministic goal compiler scaffold for: ${normalizedTask}`,
|
|
652
|
+
success_criteria: buildSuccessCriteria(normalizedTask, clauses.length > 0 ? clauses : [normalizedTask], shipBoundary),
|
|
653
|
+
steps,
|
|
654
|
+
plan_source: "deterministic_fallback",
|
|
655
|
+
};
|
|
656
|
+
storeProposal(result);
|
|
657
|
+
return result;
|
|
658
|
+
}
|
|
659
|
+
// ── Planner model call ─────────────────────────────────────────────────────
|
|
660
|
+
async function callPlannerModel(task, scaffold) {
|
|
661
|
+
const plannerTask = [
|
|
662
|
+
`You are ACE-Planner. Refine the deterministic scaffold into the strongest valid JSON PlanProposal for the following task.`,
|
|
663
|
+
``,
|
|
664
|
+
`Task: ${task}`,
|
|
665
|
+
``,
|
|
666
|
+
`Deterministic scaffold to preserve or improve (do not shrink coverage, remove verification, or drop stop conditions):`,
|
|
667
|
+
JSON.stringify({
|
|
668
|
+
intent_summary: scaffold.intent_summary,
|
|
669
|
+
success_criteria: scaffold.success_criteria,
|
|
670
|
+
steps: scaffold.steps,
|
|
671
|
+
}, null, 2),
|
|
672
|
+
``,
|
|
673
|
+
`Return ONLY valid JSON matching this shape (no prose before or after):`,
|
|
674
|
+
`{`,
|
|
675
|
+
` "intent_summary": "<one paragraph summary of intent>",`,
|
|
676
|
+
` "success_criteria": ["<criterion>", ...],`,
|
|
677
|
+
` "steps": [`,
|
|
678
|
+
` {`,
|
|
679
|
+
` "role": "<ace-role>",`,
|
|
680
|
+
` "task": "<task directive>",`,
|
|
681
|
+
` "depends_on": ["<step-N>"],`,
|
|
682
|
+
` "tool_scope": ["<tool-name>"],`,
|
|
683
|
+
` "acceptance_criteria": ["<criterion>"],`,
|
|
684
|
+
` "stop_condition": ["<stop condition>"],`,
|
|
685
|
+
` "expected_output_class": "plain_text_plan|tool_envelope|code_artifact|structural_edit_plan|qa_verdict",`,
|
|
686
|
+
` "expected_artifacts": [{"path":"<relative artifact path>","required":true,"evidence_ref_kind":"artifact|diff|hash|test|gate"}],`,
|
|
687
|
+
` "allowed_tools": ["<tool-name>"],`,
|
|
688
|
+
` "forbidden_patterns": ["<pattern>"],`,
|
|
689
|
+
` "required_evidence_refs": ["<evidence ref substring>"]`,
|
|
690
|
+
` }`,
|
|
691
|
+
` ]`,
|
|
692
|
+
`}`,
|
|
693
|
+
].join("\n");
|
|
694
|
+
try {
|
|
695
|
+
const delegated = await runLocalModelTask({
|
|
696
|
+
task: plannerTask,
|
|
697
|
+
role: "planner",
|
|
698
|
+
toolScope: [
|
|
699
|
+
"recall_context",
|
|
700
|
+
"read_workspace_file",
|
|
701
|
+
"build_continuity_packet",
|
|
702
|
+
"get_skill_instructions",
|
|
703
|
+
],
|
|
704
|
+
});
|
|
705
|
+
const rawSummary = delegated.result.summary ?? "";
|
|
706
|
+
const jsonText = extractJsonBlock(rawSummary) ?? rawSummary;
|
|
707
|
+
const parsed = JSON.parse(jsonText);
|
|
708
|
+
return isPlanProposal(parsed) ? parsed : undefined;
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
return undefined;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// ── Plan shape validation ──────────────────────────────────────────────────
|
|
715
|
+
function validatePlanShape(plan_id, proposal) {
|
|
716
|
+
void plan_id;
|
|
717
|
+
const blockingFindings = [];
|
|
718
|
+
const softFindings = [];
|
|
719
|
+
let passingChecks = 0;
|
|
720
|
+
// 1. Coverage: intent_summary non-empty and at least one step exists.
|
|
721
|
+
// We do not compare sentence count to step count because one comprehensive step
|
|
722
|
+
// (e.g. orchestrator) can legitimately cover a multi-clause intent.
|
|
723
|
+
{
|
|
724
|
+
if (!proposal.intent_summary.trim()) {
|
|
725
|
+
blockingFindings.push("coverage: intent_summary is empty");
|
|
726
|
+
}
|
|
727
|
+
else if (proposal.steps.length === 0) {
|
|
728
|
+
blockingFindings.push("coverage: plan has no steps");
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
passingChecks += 1;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
// 2. Verification chain: implementation steps must have a downstream qa/skeptic step.
|
|
735
|
+
// Implementation roles: coders, builder, ops, astgrep (code-mutating roles).
|
|
736
|
+
{
|
|
737
|
+
const IMPL_ROLES = new Set(["coders", "builder", "ops", "astgrep"]);
|
|
738
|
+
const implStepIndices = [];
|
|
739
|
+
for (let i = 0; i < proposal.steps.length; i++) {
|
|
740
|
+
if (IMPL_ROLES.has(proposal.steps[i].role))
|
|
741
|
+
implStepIndices.push(i);
|
|
742
|
+
}
|
|
743
|
+
const unverified = [];
|
|
744
|
+
for (const idx of implStepIndices) {
|
|
745
|
+
const stepRef = `step-${idx + 1}`;
|
|
746
|
+
const hasDownstream = proposal.steps.slice(idx + 1).some((ds) => {
|
|
747
|
+
const isQaOrSkeptic = ds.role === "qa" || ds.role === "skeptic";
|
|
748
|
+
const dependsOnThis = !ds.depends_on || ds.depends_on.length === 0 || ds.depends_on.includes(stepRef);
|
|
749
|
+
return isQaOrSkeptic && dependsOnThis;
|
|
750
|
+
});
|
|
751
|
+
if (!hasDownstream)
|
|
752
|
+
unverified.push(stepRef);
|
|
753
|
+
}
|
|
754
|
+
if (unverified.length === 0) {
|
|
755
|
+
passingChecks += 1;
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
blockingFindings.push(`verification_chain: implementation step(s) ${unverified.join(", ")} lack a downstream qa/skeptic step`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// 3. Tool-scope realism: every step's tool_scope is a subset of the runtime catalog.
|
|
762
|
+
// Fail closed when the catalog is unavailable so a broken registry cannot silently
|
|
763
|
+
// disable the check.
|
|
764
|
+
{
|
|
765
|
+
const available = getAvailableToolNames();
|
|
766
|
+
if (!available) {
|
|
767
|
+
blockingFindings.push("tool_scope_realism: runtime tool catalog unavailable");
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
const unknownTools = [];
|
|
771
|
+
for (const s of proposal.steps) {
|
|
772
|
+
for (const tool of s.tool_scope ?? []) {
|
|
773
|
+
if (!available.has(tool))
|
|
774
|
+
unknownTools.push(tool);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (unknownTools.length === 0) {
|
|
778
|
+
passingChecks += 1;
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
blockingFindings.push(`tool_scope_realism: unknown tool(s) requested: ${[...new Set(unknownTools)].join(", ")}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// 4. Acceptance criteria presence: every step has at least one criterion
|
|
786
|
+
{
|
|
787
|
+
const missingCriteria = proposal.steps
|
|
788
|
+
.map((s, i) => ({ i, s }))
|
|
789
|
+
.filter(({ s }) => !s.acceptance_criteria || s.acceptance_criteria.length === 0)
|
|
790
|
+
.map(({ i }) => `step-${i + 1}`);
|
|
791
|
+
if (missingCriteria.length === 0) {
|
|
792
|
+
passingChecks += 1;
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
softFindings.push(`acceptance_criteria: step(s) ${missingCriteria.join(", ")} have no acceptance criteria`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
// 5. Stop conditions: prefer explicit stop conditions for every step, but do not block
|
|
799
|
+
// legacy plans yet. The deterministic scaffold always emits them.
|
|
800
|
+
{
|
|
801
|
+
const missingStopConditions = proposal.steps
|
|
802
|
+
.map((s, i) => ({ i, s }))
|
|
803
|
+
.filter(({ s }) => !s.stop_condition || s.stop_condition.length === 0)
|
|
804
|
+
.map(({ i }) => `step-${i + 1}`);
|
|
805
|
+
if (missingStopConditions.length > 0) {
|
|
806
|
+
softFindings.push(`stop_conditions: step(s) ${missingStopConditions.join(", ")} have no stop conditions`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
// 6. Contract-class artifact expectations: code and structural edits must say
|
|
810
|
+
// what artifact evidence can prove them before execution.
|
|
811
|
+
{
|
|
812
|
+
const missingArtifacts = proposal.steps
|
|
813
|
+
.map((step, index) => ({
|
|
814
|
+
step: enrichPlanProposalStep(step),
|
|
815
|
+
ref: `step-${index + 1}`,
|
|
816
|
+
}))
|
|
817
|
+
.filter(({ step }) => (step.expected_output_class === "code_artifact" ||
|
|
818
|
+
step.expected_output_class === "structural_edit_plan") &&
|
|
819
|
+
(step.expected_artifacts ?? []).length === 0)
|
|
820
|
+
.map(({ ref }) => ref);
|
|
821
|
+
if (missingArtifacts.length > 0) {
|
|
822
|
+
blockingFindings.push(`contract_artifacts: code/structural step(s) ${missingArtifacts.join(", ")} lack expected_artifacts`);
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
passingChecks += 1;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// 7. Code-mutating steps must route through a structural edit plan or carry
|
|
829
|
+
// an explicit, evidence-backed waiver.
|
|
830
|
+
{
|
|
831
|
+
const missingStructuralPlan = proposal.steps
|
|
832
|
+
.map((step, index) => ({
|
|
833
|
+
step: enrichPlanProposalStep(step),
|
|
834
|
+
ref: `step-${index + 1}`,
|
|
835
|
+
}))
|
|
836
|
+
.filter(({ step }) => isCodeMutatingStep(step) &&
|
|
837
|
+
step.structural_edit_plan_required !== true &&
|
|
838
|
+
!hasStructuralEditWaiver(step))
|
|
839
|
+
.map(({ ref }) => ref);
|
|
840
|
+
if (missingStructuralPlan.length > 0) {
|
|
841
|
+
blockingFindings.push(`structural_edit_plan: code-mutating step(s) ${missingStructuralPlan.join(", ")} require structural_edit_plan_required=true or an evidence-backed waiver`);
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
passingChecks += 1;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const score = Math.round((passingChecks / 6) * 100);
|
|
848
|
+
return {
|
|
849
|
+
ok: blockingFindings.length === 0,
|
|
850
|
+
score,
|
|
851
|
+
blocking_findings: blockingFindings,
|
|
852
|
+
soft_findings: softFindings,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
856
|
+
/**
|
|
857
|
+
* Lightweight synchronous variant used in runtime preflights.
|
|
858
|
+
* Skips the model bridge call entirely and returns the deterministic goal scaffold.
|
|
859
|
+
* This keeps the preflight free of network I/O that could delay session startup.
|
|
860
|
+
*/
|
|
861
|
+
export function proposePlanDeterministic(task) {
|
|
862
|
+
const plan_id = `plan-${randomUUID()}`;
|
|
863
|
+
const scaffold = compileGoalScaffold(task, plan_id);
|
|
864
|
+
const result = {
|
|
865
|
+
...scaffold,
|
|
866
|
+
steps: scaffold.steps.map(enrichPlanProposalStep),
|
|
867
|
+
};
|
|
868
|
+
storeProposal(result);
|
|
869
|
+
return result;
|
|
870
|
+
}
|
|
871
|
+
export async function proposePlan(task, sessionId) {
|
|
872
|
+
void sessionId;
|
|
873
|
+
const plan_id = `plan-${randomUUID()}`;
|
|
874
|
+
const scaffold = compileGoalScaffold(task, plan_id);
|
|
875
|
+
const proposal = await callPlannerModel(task, scaffold);
|
|
876
|
+
if (proposal) {
|
|
877
|
+
const result = {
|
|
878
|
+
plan_id,
|
|
879
|
+
status: "planning",
|
|
880
|
+
intent_summary: proposal.intent_summary,
|
|
881
|
+
success_criteria: proposal.success_criteria,
|
|
882
|
+
steps: proposal.steps.map(enrichPlanProposalStep),
|
|
883
|
+
plan_source: "planner_model",
|
|
884
|
+
};
|
|
885
|
+
const scaffoldStopConditions = scaffold.steps.filter((step) => (step.stop_condition ?? []).length > 0).length;
|
|
886
|
+
const proposalStopConditions = proposal.steps.filter((step) => (step.stop_condition ?? []).length > 0).length;
|
|
887
|
+
if (proposal.steps.length >= scaffold.steps.length &&
|
|
888
|
+
proposal.success_criteria.length >= scaffold.success_criteria.length &&
|
|
889
|
+
proposalStopConditions >= scaffoldStopConditions) {
|
|
890
|
+
storeProposal(result);
|
|
891
|
+
return result;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const enrichedScaffold = {
|
|
895
|
+
...scaffold,
|
|
896
|
+
steps: scaffold.steps.map(enrichPlanProposalStep),
|
|
897
|
+
};
|
|
898
|
+
storeProposal(enrichedScaffold);
|
|
899
|
+
return enrichedScaffold;
|
|
900
|
+
}
|
|
901
|
+
export async function validatePlan(input) {
|
|
902
|
+
let proposal;
|
|
903
|
+
let resolvedPlanId;
|
|
904
|
+
if (input.proposal) {
|
|
905
|
+
proposal = input.proposal;
|
|
906
|
+
resolvedPlanId = input.proposal.plan_id;
|
|
907
|
+
}
|
|
908
|
+
else if (input.plan_id) {
|
|
909
|
+
resolvedPlanId = input.plan_id;
|
|
910
|
+
// Check in-memory cache first, then fall back to the persisted ACCEPTANCE_TRACE_MAP.json
|
|
911
|
+
// so plan_id lookups survive process boundaries.
|
|
912
|
+
proposal = lookupProposal(resolvedPlanId);
|
|
913
|
+
if (!proposal) {
|
|
914
|
+
const contract = loadAcceptanceTraceContract(resolvedPlanId);
|
|
915
|
+
if (contract) {
|
|
916
|
+
proposal = {
|
|
917
|
+
plan_id: resolvedPlanId,
|
|
918
|
+
status: "planning",
|
|
919
|
+
intent_summary: contract.intent_summary ?? contract.task,
|
|
920
|
+
success_criteria: contract.success_criteria ?? [],
|
|
921
|
+
steps: contract.steps.map((s) => ({
|
|
922
|
+
role: s.role,
|
|
923
|
+
task: s.task,
|
|
924
|
+
depends_on: s.depends_on.length > 0 ? s.depends_on : undefined,
|
|
925
|
+
tool_scope: s.tool_scope.length > 0 ? s.tool_scope : undefined,
|
|
926
|
+
acceptance_criteria: s.acceptance_criteria ?? [],
|
|
927
|
+
stop_condition: s.stop_condition && s.stop_condition.length > 0 ? s.stop_condition : undefined,
|
|
928
|
+
expected_output_class: s.expected_output_class,
|
|
929
|
+
expected_artifacts: s.expected_artifacts,
|
|
930
|
+
allowed_tools: s.allowed_tools,
|
|
931
|
+
forbidden_patterns: s.forbidden_patterns,
|
|
932
|
+
required_evidence_refs: s.required_evidence_refs,
|
|
933
|
+
structural_edit_plan_required: s.structural_edit_plan_required,
|
|
934
|
+
structural_edit_waiver: s.structural_edit_waiver,
|
|
935
|
+
})),
|
|
936
|
+
plan_source: contract.plan_source,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
return {
|
|
943
|
+
plan_id: "unknown",
|
|
944
|
+
ok: false,
|
|
945
|
+
score: 0,
|
|
946
|
+
blocking_findings: ["No plan_id or proposal supplied."],
|
|
947
|
+
soft_findings: [],
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
if (!proposal) {
|
|
951
|
+
return {
|
|
952
|
+
plan_id: resolvedPlanId,
|
|
953
|
+
ok: false,
|
|
954
|
+
score: 0,
|
|
955
|
+
blocking_findings: [`Plan ${resolvedPlanId} not found in proposal cache or trace map.`],
|
|
956
|
+
soft_findings: [],
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
const planProposal = {
|
|
960
|
+
intent_summary: proposal.intent_summary,
|
|
961
|
+
success_criteria: proposal.success_criteria,
|
|
962
|
+
steps: proposal.steps,
|
|
963
|
+
};
|
|
964
|
+
const verdict = validatePlanShape(resolvedPlanId, planProposal);
|
|
965
|
+
// Persist transition record
|
|
966
|
+
try {
|
|
967
|
+
await withLocalModelRuntimeRepository(resolveWorkspaceRoot(), async (repo) => {
|
|
968
|
+
await repo.appendTransitionRecord({
|
|
969
|
+
subject_kind: "plan_proposal",
|
|
970
|
+
subject_id: resolvedPlanId,
|
|
971
|
+
from: "proposed",
|
|
972
|
+
to: verdict.ok ? "validated" : "rejected",
|
|
973
|
+
reason: verdict.ok
|
|
974
|
+
? `Plan validated with score ${verdict.score}`
|
|
975
|
+
: `Plan rejected: ${verdict.blocking_findings.join("; ")}`,
|
|
976
|
+
reason_code: verdict.ok ? "plan_validated" : "plan_rejected",
|
|
977
|
+
evidence_refs: [],
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
catch {
|
|
982
|
+
// Non-fatal
|
|
983
|
+
}
|
|
984
|
+
// Emit Vericify process post
|
|
985
|
+
await appendVericifyProcessPostSafe({
|
|
986
|
+
run_id: "workspace:current",
|
|
987
|
+
agent_id: "ace-planner",
|
|
988
|
+
kind: "plan_quality_assessment",
|
|
989
|
+
summary: verdict.ok
|
|
990
|
+
? `Plan ${resolvedPlanId} passed quality gate (score=${verdict.score})`
|
|
991
|
+
: `Plan ${resolvedPlanId} failed quality gate (score=${verdict.score}): ${verdict.blocking_findings.join("; ")}`,
|
|
992
|
+
tool_refs: ["validate_plan"],
|
|
993
|
+
evidence_refs: [resolvedPlanId],
|
|
994
|
+
});
|
|
995
|
+
return { plan_id: resolvedPlanId, ...verdict };
|
|
996
|
+
}
|
|
997
|
+
export function loadAcceptanceTraceContract(plan_id) {
|
|
998
|
+
try {
|
|
999
|
+
const raw = safeRead("agent-state/ACCEPTANCE_TRACE_MAP.json");
|
|
1000
|
+
if (!raw || raw.startsWith("[FILE NOT FOUND]") || raw.startsWith("[ACCESS DENIED]")) {
|
|
1001
|
+
return undefined;
|
|
1002
|
+
}
|
|
1003
|
+
const parsed = JSON.parse(raw);
|
|
1004
|
+
if (!parsed || typeof parsed !== "object")
|
|
1005
|
+
return undefined;
|
|
1006
|
+
const record = parsed;
|
|
1007
|
+
if ("contracts" in record && record.contracts && typeof record.contracts === "object") {
|
|
1008
|
+
const contract = record.contracts[plan_id];
|
|
1009
|
+
return contract && contract.plan_id === plan_id ? contract : undefined;
|
|
1010
|
+
}
|
|
1011
|
+
const contract = record;
|
|
1012
|
+
if (contract.plan_id !== plan_id)
|
|
1013
|
+
return undefined;
|
|
1014
|
+
return contract;
|
|
1015
|
+
}
|
|
1016
|
+
catch {
|
|
1017
|
+
return undefined;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
export async function persistAcceptanceTraceMapWithContract(input) {
|
|
1021
|
+
const current = safeRead("agent-state/ACCEPTANCE_TRACE_MAP.json");
|
|
1022
|
+
const generated_at = new Date().toISOString();
|
|
1023
|
+
let nextMap = {
|
|
1024
|
+
version: 1,
|
|
1025
|
+
generated_at,
|
|
1026
|
+
contracts: {},
|
|
1027
|
+
};
|
|
1028
|
+
if (current && !current.startsWith("[FILE NOT FOUND]") && !current.startsWith("[ACCESS DENIED]")) {
|
|
1029
|
+
try {
|
|
1030
|
+
const parsed = JSON.parse(current);
|
|
1031
|
+
if (parsed && typeof parsed === "object" && "contracts" in parsed) {
|
|
1032
|
+
const existing = parsed;
|
|
1033
|
+
nextMap = {
|
|
1034
|
+
...existing,
|
|
1035
|
+
generated_at,
|
|
1036
|
+
contracts: { ...(existing.contracts ?? {}) },
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
else if (parsed && typeof parsed === "object" && "plan_id" in parsed) {
|
|
1040
|
+
const existing = parsed;
|
|
1041
|
+
nextMap.contracts[existing.plan_id] = existing;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
// keep the new map shape and overwrite corrupted content
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
nextMap.contracts[input.plan_id] = {
|
|
1049
|
+
version: 1,
|
|
1050
|
+
generated_at,
|
|
1051
|
+
plan_id: input.plan_id,
|
|
1052
|
+
task: input.task,
|
|
1053
|
+
plan_source: input.plan_source,
|
|
1054
|
+
intent_summary: input.intent_summary,
|
|
1055
|
+
success_criteria: input.success_criteria,
|
|
1056
|
+
validation_verdict: input.validation_verdict,
|
|
1057
|
+
policies: input.policies,
|
|
1058
|
+
steps: input.steps.map(enrichContractStep),
|
|
1059
|
+
};
|
|
1060
|
+
nextMap = {
|
|
1061
|
+
...nextMap,
|
|
1062
|
+
plan_id: input.plan_id,
|
|
1063
|
+
task: input.task,
|
|
1064
|
+
plan_source: input.plan_source,
|
|
1065
|
+
intent_summary: input.intent_summary,
|
|
1066
|
+
success_criteria: input.success_criteria,
|
|
1067
|
+
validation_verdict: input.validation_verdict,
|
|
1068
|
+
policies: input.policies,
|
|
1069
|
+
steps: input.steps.map(enrichContractStep),
|
|
1070
|
+
};
|
|
1071
|
+
return safeWriteAsync("agent-state/ACCEPTANCE_TRACE_MAP.json", JSON.stringify(nextMap, null, 2));
|
|
1072
|
+
}
|
|
1073
|
+
//# sourceMappingURL=plan-proposal.js.map
|