@alisio/core 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application.d.ts +15 -0
- package/dist/application.js +38 -3
- package/dist/changelog/data.js +39 -0
- package/dist/commands/builtins.js +7 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +32 -0
- package/dist/core/runner.d.ts +4 -0
- package/dist/core/runner.js +57 -1
- package/dist/eval/canonical.d.ts +14 -0
- package/dist/eval/canonical.js +40 -0
- package/dist/eval/cassette.d.ts +180 -0
- package/dist/eval/cassette.js +424 -0
- package/dist/eval/compare.d.ts +147 -0
- package/dist/eval/compare.js +436 -0
- package/dist/eval/engine.d.ts +145 -0
- package/dist/eval/engine.js +731 -0
- package/dist/eval/export.d.ts +40 -0
- package/dist/eval/export.js +90 -0
- package/dist/eval/fingerprint.d.ts +88 -0
- package/dist/eval/fingerprint.js +186 -0
- package/dist/eval/normalize.d.ts +68 -0
- package/dist/eval/normalize.js +138 -0
- package/dist/eval/paths.d.ts +11 -0
- package/dist/eval/paths.js +50 -0
- package/dist/eval/plan.d.ts +26 -0
- package/dist/eval/plan.js +44 -0
- package/dist/eval/report.d.ts +53 -0
- package/dist/eval/report.js +258 -0
- package/dist/eval/run-suite.d.ts +68 -0
- package/dist/eval/run-suite.js +94 -0
- package/dist/eval/secrets.d.ts +14 -0
- package/dist/eval/secrets.js +55 -0
- package/dist/eval/stats.d.ts +17 -0
- package/dist/eval/stats.js +50 -0
- package/dist/eval/suite.d.ts +20 -0
- package/dist/eval/suite.js +165 -0
- package/dist/eval/variants.d.ts +35 -0
- package/dist/eval/variants.js +137 -0
- package/dist/eval/verifiers.d.ts +49 -0
- package/dist/eval/verifiers.js +264 -0
- package/dist/forge/javascript/hosts/worker-host.d.ts +9 -0
- package/dist/forge/javascript/hosts/worker-host.js +16 -1
- package/dist/forge/kernel.js +3 -2
- package/dist/goal/command.d.ts +7 -2
- package/dist/goal/command.js +10 -2
- package/dist/goal/format.js +2 -0
- package/dist/goal/machine.d.ts +50 -3
- package/dist/goal/machine.js +80 -2
- package/dist/goal/service.d.ts +22 -2
- package/dist/goal/service.js +94 -7
- package/dist/goal/store.js +22 -3
- package/dist/index.d.ts +18 -2
- package/dist/index.js +18 -2
- package/dist/plan/criteria.d.ts +30 -0
- package/dist/plan/criteria.js +256 -0
- package/dist/plan/exit-plan.d.ts +8 -0
- package/dist/plan/exit-plan.js +91 -9
- package/dist/plan/manifest.d.ts +4 -2
- package/dist/plan/manifest.js +6 -1
- package/dist/plan/state.d.ts +13 -2
- package/dist/plan/state.js +7 -1
- package/dist/plugins/host.d.ts +7 -0
- package/dist/plugins/host.js +15 -0
- package/dist/proof/command.d.ts +24 -0
- package/dist/proof/command.js +109 -0
- package/dist/proof/coverage.d.ts +40 -0
- package/dist/proof/coverage.js +102 -0
- package/dist/proof/divergence.d.ts +24 -0
- package/dist/proof/divergence.js +120 -0
- package/dist/proof/export.d.ts +65 -0
- package/dist/proof/export.js +144 -0
- package/dist/proof/index.d.ts +12 -0
- package/dist/proof/index.js +12 -0
- package/dist/proof/ledger.d.ts +38 -0
- package/dist/proof/ledger.js +57 -0
- package/dist/proof/plugin-verifiers.d.ts +23 -0
- package/dist/proof/plugin-verifiers.js +39 -0
- package/dist/proof/recorder.d.ts +69 -0
- package/dist/proof/recorder.js +347 -0
- package/dist/proof/scope.d.ts +12 -0
- package/dist/proof/scope.js +121 -0
- package/dist/proof/store.d.ts +52 -0
- package/dist/proof/store.js +192 -0
- package/dist/proof/tree.d.ts +6 -0
- package/dist/proof/tree.js +78 -0
- package/dist/proof/types.d.ts +21 -0
- package/dist/proof/types.js +1 -0
- package/dist/proof/verifiers.d.ts +46 -0
- package/dist/proof/verifiers.js +176 -0
- package/dist/proof/verify.d.ts +36 -0
- package/dist/proof/verify.js +171 -0
- package/dist/runtime/ids.d.ts +21 -0
- package/dist/runtime/ids.js +41 -1
- package/dist/runtime/store.js +63 -2
- package/dist/tools/goal.d.ts +5 -1
- package/dist/tools/goal.js +35 -4
- package/package.json +2 -2
package/dist/plan/exit-plan.js
CHANGED
|
@@ -15,10 +15,30 @@ import { tmpdir } from "node:os";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { textResult } from "@alisio/sdk";
|
|
17
17
|
import { EXIT_PLAN_TOOL } from "../core/opt-in.js";
|
|
18
|
+
import { newUuid } from "../runtime/ids.js";
|
|
18
19
|
import { objectSchema } from "../tools/standard.js";
|
|
20
|
+
import { applyCriteriaSelection, validateCriteriaDrafts, } from "./criteria.js";
|
|
19
21
|
import { DIAGRAM_MAX_BYTES, DIAGRAM_MAX_NODES, validateDiagrams, } from "./diagrams.js";
|
|
20
22
|
import { buildPlanManifest } from "./manifest.js";
|
|
21
|
-
import { currentPlan, decidePlan, nextPlanRevision, PLAN_CONTEXT_MAX_CHARS, PLAN_MAX_CHARS, PLAN_OPTIONS, PLAN_QUESTION_ID, PLAN_REVIEW_TITLE, PLAN_TEXT_KEY, planHash, planTitle, proposePlan, withdrawPendingPlan, } from "./state.js";
|
|
23
|
+
import { currentPlan, decidePlan, nextPlanRevision, PLAN_CONTEXT_MAX_CHARS, PLAN_CRITERIA_KEY, PLAN_MAX_CHARS, PLAN_OPTIONS, PLAN_QUESTION_ID, PLAN_REVIEW_TITLE, PLAN_TEXT_KEY, planHash, planTitle, proposePlan, withdrawPendingPlan, } from "./state.js";
|
|
24
|
+
const CRITERIA_SCHEMA = {
|
|
25
|
+
type: "array",
|
|
26
|
+
description: "Optional acceptance criteria you PROPOSE for this plan. They are drafts: they only become " +
|
|
27
|
+
"active if the user approves the plan, and a criterion with no verifier stays unchecked.",
|
|
28
|
+
items: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
text: { type: "string", description: "A verifiable statement, e.g. 'Unit tests pass'" },
|
|
32
|
+
required: { type: "boolean", description: "Must hold before the work counts as done" },
|
|
33
|
+
verifier: {
|
|
34
|
+
type: "object",
|
|
35
|
+
description: "How to check it: {kind:'command', command:'pnpm test'}, {kind:'file', path} or {kind:'manual'}",
|
|
36
|
+
},
|
|
37
|
+
scope: { type: "array", items: { type: "string" }, description: "Globs it covers" },
|
|
38
|
+
},
|
|
39
|
+
required: ["text"],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
22
42
|
const DIAGRAM_SCHEMA = {
|
|
23
43
|
type: "array",
|
|
24
44
|
description: `Optional Mermaid diagrams; see the plan agent's instructions`,
|
|
@@ -96,7 +116,13 @@ export function registerExitPlan(registry, deps) {
|
|
|
96
116
|
description: "The whole plan in Markdown",
|
|
97
117
|
},
|
|
98
118
|
}, ["plan"]);
|
|
99
|
-
const
|
|
119
|
+
const withCriteria = deps.proof ? { acceptanceCriteria: CRITERIA_SCHEMA } : {};
|
|
120
|
+
const plainCriteria = objectSchema({ ...plain.properties, ...withCriteria }, ["plan"]);
|
|
121
|
+
const withDiagrams = objectSchema({
|
|
122
|
+
...plain.properties,
|
|
123
|
+
...withCriteria,
|
|
124
|
+
diagrams: DIAGRAM_SCHEMA,
|
|
125
|
+
}, ["plan"]);
|
|
100
126
|
// The input is always VALIDATED with `diagrams` allowed (a call made while they were switched
|
|
101
127
|
// off must not fail), but the model is only OFFERED the field while diagrams are on: the
|
|
102
128
|
// registry compiles the schema once, while the provider reads it on every request.
|
|
@@ -105,12 +131,15 @@ export function registerExitPlan(registry, deps) {
|
|
|
105
131
|
name: EXIT_PLAN_TOOL,
|
|
106
132
|
effect: "read",
|
|
107
133
|
get description() {
|
|
108
|
-
|
|
109
|
-
? `${BASE_DESCRIPTION} Optional \`
|
|
134
|
+
const base = deps.proof
|
|
135
|
+
? `${BASE_DESCRIPTION} Optional \`acceptanceCriteria\`: drafts the user approves together with the plan.`
|
|
110
136
|
: BASE_DESCRIPTION;
|
|
137
|
+
return offered()
|
|
138
|
+
? `${base} Optional \`diagrams\`: up to ${settings().max} small Mermaid diagrams (see your instructions).`
|
|
139
|
+
: base;
|
|
111
140
|
},
|
|
112
141
|
get inputSchema() {
|
|
113
|
-
return registering || offered() ? withDiagrams :
|
|
142
|
+
return registering || offered() ? withDiagrams : plainCriteria;
|
|
114
143
|
},
|
|
115
144
|
async execute(input, context) {
|
|
116
145
|
const plan = String(input.plan ?? "").trim();
|
|
@@ -118,7 +147,7 @@ export function registerExitPlan(registry, deps) {
|
|
|
118
147
|
throw new Error("exit_plan needs the whole plan in the `plan` field");
|
|
119
148
|
const sessionId = context.session;
|
|
120
149
|
const title = planTitle(plan, input.title);
|
|
121
|
-
const planId =
|
|
150
|
+
const planId = newUuid();
|
|
122
151
|
const reviewable = !!sessionId && deps.ui.interactive() && !deps.readOnly;
|
|
123
152
|
const revision = sessionId ? nextPlanRevision(deps.store, sessionId) : 1;
|
|
124
153
|
const previous = sessionId
|
|
@@ -144,6 +173,15 @@ export function registerExitPlan(registry, deps) {
|
|
|
144
173
|
outcome.dropped = validated.dropped;
|
|
145
174
|
}
|
|
146
175
|
}
|
|
176
|
+
// Acceptance criteria are DRAFTS here (O3): validated, shown in the review, stored in the plan
|
|
177
|
+
// state, and turned into real criteria only by the user's approval below. Nothing in this
|
|
178
|
+
// block writes the ledger.
|
|
179
|
+
const rawCriteria = deps.proof ? input.acceptanceCriteria : undefined;
|
|
180
|
+
const proposed = rawCriteria === undefined || (Array.isArray(rawCriteria) && !rawCriteria.length)
|
|
181
|
+
? undefined
|
|
182
|
+
: validateCriteriaDrafts(rawCriteria);
|
|
183
|
+
const drafts = proposed?.accepted ?? [];
|
|
184
|
+
const droppedCriteria = proposed?.dropped ?? [];
|
|
147
185
|
const preManifest = buildPlanManifest({
|
|
148
186
|
planId,
|
|
149
187
|
revision,
|
|
@@ -152,11 +190,12 @@ export function registerExitPlan(registry, deps) {
|
|
|
152
190
|
markdown: plan,
|
|
153
191
|
diagrams: outcome.accepted,
|
|
154
192
|
...(previous ? { previous } : {}),
|
|
193
|
+
...(drafts.length ? { acceptanceCriteria: drafts } : {}),
|
|
155
194
|
});
|
|
156
195
|
outcome.removed = preManifest.removed;
|
|
157
196
|
if (outcome.removed.length)
|
|
158
197
|
outcome.involved = true;
|
|
159
|
-
const hash = planHash(plan, preManifest.diagrams.map((d) => ({ id: d.id, hash: d.hash })));
|
|
198
|
+
const hash = planHash(plan, preManifest.diagrams.map((d) => ({ id: d.id, hash: d.hash })), drafts);
|
|
160
199
|
const manifest = { ...preManifest, hash };
|
|
161
200
|
const diagrams = manifest.diagrams.map((entry, index) => ({
|
|
162
201
|
id: entry.id,
|
|
@@ -175,7 +214,7 @@ export function registerExitPlan(registry, deps) {
|
|
|
175
214
|
const publisher = context.artifacts;
|
|
176
215
|
if (publisher) {
|
|
177
216
|
// A folder (plan.md + plan.json + diagrams) when diagrams are involved, else plan.md alone.
|
|
178
|
-
if (outcome.accepted.length || outcome.removed.length)
|
|
217
|
+
if (outcome.accepted.length || outcome.removed.length || drafts.length)
|
|
179
218
|
try {
|
|
180
219
|
artifact = await publishPlanFolder(publisher, [
|
|
181
220
|
{ path: "plan.md", text: `${plan}\n` },
|
|
@@ -208,12 +247,23 @@ export function registerExitPlan(registry, deps) {
|
|
|
208
247
|
title,
|
|
209
248
|
...(context.callId ? { callId: context.callId } : {}),
|
|
210
249
|
...(artifact ? { artifactId: artifact.id } : {}),
|
|
250
|
+
...(drafts.length ? { criteria: drafts } : {}),
|
|
211
251
|
...(manifest.diagrams.length
|
|
212
252
|
? {
|
|
213
253
|
diagrams: manifest.diagrams.map((d) => ({ id: d.id, title: d.title, hash: d.hash })),
|
|
214
254
|
}
|
|
215
255
|
: {}),
|
|
216
256
|
};
|
|
257
|
+
const criteriaReport = (activated) => drafts.length || droppedCriteria.length
|
|
258
|
+
? {
|
|
259
|
+
criteria: {
|
|
260
|
+
proposed: drafts.length,
|
|
261
|
+
...(droppedCriteria.length ? { dropped: droppedCriteria } : {}),
|
|
262
|
+
...(activated === undefined ? {} : { activated }),
|
|
263
|
+
...(rejectedDrafts ? { rejected: rejectedDrafts } : {}),
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
: {};
|
|
217
267
|
const report = diagramReport(outcome);
|
|
218
268
|
if (!reviewable) {
|
|
219
269
|
// Nobody to ask: keep the revision count honest and tell the model what to do instead.
|
|
@@ -238,6 +288,7 @@ export function registerExitPlan(registry, deps) {
|
|
|
238
288
|
markdown: plan,
|
|
239
289
|
...(artifact ? { artifact } : {}),
|
|
240
290
|
...(diagrams.length ? { diagrams } : {}),
|
|
291
|
+
...(drafts.length ? { acceptanceCriteria: drafts } : {}),
|
|
241
292
|
};
|
|
242
293
|
const result = await deps.ui.askQuestions({
|
|
243
294
|
questions: [
|
|
@@ -270,16 +321,47 @@ export function registerExitPlan(registry, deps) {
|
|
|
270
321
|
hash,
|
|
271
322
|
decision,
|
|
272
323
|
});
|
|
324
|
+
// The only place a draft becomes a criterion: the user's own approval, applied once by the
|
|
325
|
+
// compare-and-set above, of exactly the drafts stored with that proposal.
|
|
326
|
+
let activated;
|
|
327
|
+
let rejectedDrafts = 0;
|
|
328
|
+
if (decision === "approve" && deps.proof && decided.criteria?.length) {
|
|
329
|
+
// The user may approve, edit or reject each draft (`plan:criteria`); a selection that is
|
|
330
|
+
// not clean activates none of them.
|
|
331
|
+
const picked = applyCriteriaSelection(decided.criteria, result[PLAN_CRITERIA_KEY]);
|
|
332
|
+
rejectedDrafts = picked ? picked.rejected : decided.criteria.length;
|
|
333
|
+
try {
|
|
334
|
+
for (const draft of picked?.kept ?? [])
|
|
335
|
+
deps.proof.addCriterion({
|
|
336
|
+
sessionId,
|
|
337
|
+
text: draft.text,
|
|
338
|
+
origin: draft.origin,
|
|
339
|
+
required: draft.required,
|
|
340
|
+
...(draft.verifier ? { verifier: draft.verifier } : {}),
|
|
341
|
+
...(draft.scope ? { scope: draft.scope } : {}),
|
|
342
|
+
});
|
|
343
|
+
activated = picked?.kept.length ?? 0;
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
activated = "failed";
|
|
347
|
+
}
|
|
348
|
+
}
|
|
273
349
|
const base = {
|
|
274
350
|
planId,
|
|
275
351
|
revision,
|
|
276
352
|
...(artifact ? { artifactId: artifact.id } : {}),
|
|
277
353
|
...report,
|
|
354
|
+
...criteriaReport(activated),
|
|
278
355
|
};
|
|
279
356
|
const body = decision === "approve"
|
|
280
357
|
? answer("approved", "The user approved the plan. The build agent will implement exactly this plan in the " +
|
|
281
358
|
"next turn. Do not implement anything yourself: reply with one short sentence and " +
|
|
282
|
-
"call no more tools."
|
|
359
|
+
"call no more tools." +
|
|
360
|
+
(typeof activated === "number"
|
|
361
|
+
? ` ${activated} acceptance criteria are now active.`
|
|
362
|
+
: activated === "failed"
|
|
363
|
+
? " The acceptance criteria could not be saved; they are not active."
|
|
364
|
+
: ""), base)
|
|
283
365
|
: decision === "context"
|
|
284
366
|
? answer("feedback", "The user added context instead of deciding. Revise the plan to account for it and " +
|
|
285
367
|
"call exit_plan again with the complete updated plan" +
|
package/dist/plan/manifest.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PlanDiagramStatus, PlanManifest } from "@alisio/sdk";
|
|
1
|
+
import type { PlanCriterionDraft, PlanDiagramStatus, PlanManifest } from "@alisio/sdk";
|
|
2
2
|
import { type PlanDiagram } from "./diagrams.ts";
|
|
3
3
|
export interface PlanHeading {
|
|
4
4
|
id: string;
|
|
@@ -31,6 +31,8 @@ export interface ManifestInput {
|
|
|
31
31
|
diagrams: PlanDiagram[];
|
|
32
32
|
/** Diagrams of the previous revision (from the plan state), when there was one. */
|
|
33
33
|
previous?: DiagramRecord[];
|
|
34
|
+
/** Acceptance-criteria drafts of this revision (specs/011); absent or empty = none. */
|
|
35
|
+
acceptanceCriteria?: PlanCriterionDraft[];
|
|
34
36
|
}
|
|
35
37
|
/** The diff of this revision's diagrams against the previous one. */
|
|
36
38
|
export declare function diffDiagrams(diagrams: Array<{
|
|
@@ -50,4 +52,4 @@ export declare function buildPlanManifest(input: ManifestInput): PlanManifest;
|
|
|
50
52
|
export declare function planContentHash(markdown: string, diagrams: Array<{
|
|
51
53
|
id: string;
|
|
52
54
|
hash: string;
|
|
53
|
-
}
|
|
55
|
+
}>, criteria?: PlanCriterionDraft[]): string;
|
package/dist/plan/manifest.js
CHANGED
|
@@ -308,12 +308,17 @@ export function buildPlanManifest(input) {
|
|
|
308
308
|
};
|
|
309
309
|
}),
|
|
310
310
|
removed,
|
|
311
|
+
...(input.acceptanceCriteria?.length ? { acceptanceCriteria: input.acceptanceCriteria } : {}),
|
|
311
312
|
};
|
|
312
313
|
}
|
|
313
314
|
/** The hash of a plan revision: the Markdown alone, or the Markdown plus its diagrams. */
|
|
314
|
-
export function planContentHash(markdown, diagrams) {
|
|
315
|
+
export function planContentHash(markdown, diagrams, criteria = []) {
|
|
315
316
|
const hash = createHash("sha256").update(markdown, "utf8");
|
|
316
317
|
if (diagrams.length)
|
|
317
318
|
hash.update(`\n--diagrams--\n${diagrams.map((d) => `${d.id}:${d.hash}`).join("\n")}`, "utf8");
|
|
319
|
+
// The criteria are part of what the user approves, so they are part of the hash (only when
|
|
320
|
+
// present: a plan without criteria keeps the hash it always had).
|
|
321
|
+
if (criteria.length)
|
|
322
|
+
hash.update(`\n--criteria--\n${JSON.stringify(criteria.map((c) => [c.text, c.required, c.verifier ?? null, c.scope ?? null]))}`, "utf8");
|
|
318
323
|
return hash.digest("hex");
|
|
319
324
|
}
|
package/dist/plan/state.d.ts
CHANGED
|
@@ -8,12 +8,18 @@
|
|
|
8
8
|
* ├────skip──────▶ skipped └─discard─▶ cancelled (the run was cancelled)
|
|
9
9
|
* └────context───▶ feedback (a later `exit_plan` call starts a new revision)
|
|
10
10
|
*/
|
|
11
|
-
import type { QuestionOption } from "@alisio/sdk";
|
|
11
|
+
import type { PlanCriterionDraft, QuestionOption } from "@alisio/sdk";
|
|
12
12
|
import { type DiagramRecord } from "./manifest.ts";
|
|
13
13
|
/** The decision screen title (web: localized; the TUI prints it as is). */
|
|
14
14
|
export declare const PLAN_REVIEW_TITLE = "Plan complete. What would you like to do?";
|
|
15
15
|
export declare const PLAN_QUESTION_ID = "plan";
|
|
16
16
|
export declare const PLAN_TEXT_KEY = "plan:text";
|
|
17
|
+
/**
|
|
18
|
+
* Per-draft decisions on the acceptance criteria (specs/011 phase 5): a JSON object
|
|
19
|
+
* `{reject?: number[], edit?: {"<index>": "text"}, required?: {"<index>": boolean}}` over the
|
|
20
|
+
* drafts as the review lists them. Only read together with `approve`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const PLAN_CRITERIA_KEY = "plan:criteria";
|
|
17
23
|
/** The agent an approved plan is handed to. */
|
|
18
24
|
export declare const IMPLEMENTATION_AGENT_ID = "build";
|
|
19
25
|
export declare const PLAN_MAX_CHARS = 60000;
|
|
@@ -33,6 +39,11 @@ export interface PlanState {
|
|
|
33
39
|
artifactId?: string;
|
|
34
40
|
/** The accepted diagrams of this revision (absent without any): what the next one is diffed against. */
|
|
35
41
|
diagrams?: DiagramRecord[];
|
|
42
|
+
/**
|
|
43
|
+
* Acceptance-criteria DRAFTS of this revision (specs/011, O3). They are not criteria: they only
|
|
44
|
+
* become active when the user approves this plan (`exit-plan.ts`), and a later revision replaces them.
|
|
45
|
+
*/
|
|
46
|
+
criteria?: PlanCriterionDraft[];
|
|
36
47
|
/** The approved snapshot; kept only while `approved` (afterwards it lives in the user message). */
|
|
37
48
|
plan?: string;
|
|
38
49
|
updatedAt: number;
|
|
@@ -54,7 +65,7 @@ export interface PlanStore {
|
|
|
54
65
|
export declare const planHash: (plan: string, diagrams?: Array<{
|
|
55
66
|
id: string;
|
|
56
67
|
hash: string;
|
|
57
|
-
}
|
|
68
|
+
}>, criteria?: PlanCriterionDraft[]) => string;
|
|
58
69
|
/** The plan state stored in `options`, or undefined when absent or malformed. */
|
|
59
70
|
export declare function readPlanState(options: Record<string, unknown> | undefined): PlanState | undefined;
|
|
60
71
|
export declare const currentPlan: (store: PlanStore, sessionId: string) => PlanState | undefined;
|
package/dist/plan/state.js
CHANGED
|
@@ -3,6 +3,12 @@ import { planContentHash } from "./manifest.js";
|
|
|
3
3
|
export const PLAN_REVIEW_TITLE = "Plan complete. What would you like to do?";
|
|
4
4
|
export const PLAN_QUESTION_ID = "plan";
|
|
5
5
|
export const PLAN_TEXT_KEY = `${PLAN_QUESTION_ID}:text`;
|
|
6
|
+
/**
|
|
7
|
+
* Per-draft decisions on the acceptance criteria (specs/011 phase 5): a JSON object
|
|
8
|
+
* `{reject?: number[], edit?: {"<index>": "text"}, required?: {"<index>": boolean}}` over the
|
|
9
|
+
* drafts as the review lists them. Only read together with `approve`.
|
|
10
|
+
*/
|
|
11
|
+
export const PLAN_CRITERIA_KEY = `${PLAN_QUESTION_ID}:criteria`;
|
|
6
12
|
/** The agent an approved plan is handed to. */
|
|
7
13
|
export const IMPLEMENTATION_AGENT_ID = "build";
|
|
8
14
|
export const PLAN_MAX_CHARS = 60_000;
|
|
@@ -22,7 +28,7 @@ export const PLAN_OPTIONS = [
|
|
|
22
28
|
* The hash of a plan revision. Without diagrams it is the SHA-256 of the Markdown (as before);
|
|
23
29
|
* with diagrams it also covers them, so a change in a diagram alone is a different plan.
|
|
24
30
|
*/
|
|
25
|
-
export const planHash = (plan, diagrams = []) => planContentHash(plan, diagrams);
|
|
31
|
+
export const planHash = (plan, diagrams = [], criteria = []) => planContentHash(plan, diagrams, criteria);
|
|
26
32
|
const STATUSES = new Set([
|
|
27
33
|
"pending",
|
|
28
34
|
"approved",
|
package/dist/plugins/host.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { ToolRegistry } from "../core/registry.ts";
|
|
|
5
5
|
import type { DecisionRegistry } from "../decisions/registry.ts";
|
|
6
6
|
import type { DecisionService, DecisionsConfig } from "../decisions/service.ts";
|
|
7
7
|
import { ExtensionRegistry } from "../extensions/registry.ts";
|
|
8
|
+
import type { ProofVerifierRegistry } from "../proof/plugin-verifiers.ts";
|
|
8
9
|
import type { ProviderRegistry } from "../providers/registry.ts";
|
|
9
10
|
export declare const pluginPrefix: (id: string) => string;
|
|
10
11
|
export interface PluginHostOptions {
|
|
@@ -165,6 +166,12 @@ export declare class PluginHost implements RunnerExtensions {
|
|
|
165
166
|
* Binds the directories and options plugins receive (`api.paths`, `api.options`). Plugins
|
|
166
167
|
* activated before this call, or on a host without it, see neither member.
|
|
167
168
|
*/
|
|
169
|
+
/**
|
|
170
|
+
* Binds the Proof verifier registry. Plugins activated afterwards see `api.proof`; without it
|
|
171
|
+
* the member is absent, exactly like on a core that predates Proof.
|
|
172
|
+
*/
|
|
173
|
+
setProofVerifiers(registry: ProofVerifierRegistry): void;
|
|
174
|
+
private proofVerifiers?;
|
|
168
175
|
setPluginEnvironment(environment: PluginEnvironment): void;
|
|
169
176
|
private environment?;
|
|
170
177
|
/** Binds credential-free model discovery/resolution after provider startup is complete. */
|
package/dist/plugins/host.js
CHANGED
|
@@ -178,6 +178,14 @@ export class PluginHost {
|
|
|
178
178
|
* Binds the directories and options plugins receive (`api.paths`, `api.options`). Plugins
|
|
179
179
|
* activated before this call, or on a host without it, see neither member.
|
|
180
180
|
*/
|
|
181
|
+
/**
|
|
182
|
+
* Binds the Proof verifier registry. Plugins activated afterwards see `api.proof`; without it
|
|
183
|
+
* the member is absent, exactly like on a core that predates Proof.
|
|
184
|
+
*/
|
|
185
|
+
setProofVerifiers(registry) {
|
|
186
|
+
this.proofVerifiers = registry;
|
|
187
|
+
}
|
|
188
|
+
proofVerifiers;
|
|
181
189
|
setPluginEnvironment(environment) {
|
|
182
190
|
this.environment = environment;
|
|
183
191
|
}
|
|
@@ -428,6 +436,13 @@ export class PluginHost {
|
|
|
428
436
|
};
|
|
429
437
|
})(this.environment)
|
|
430
438
|
: {}),
|
|
439
|
+
...(this.proofVerifiers
|
|
440
|
+
? {
|
|
441
|
+
proof: ((registry) => ({
|
|
442
|
+
registerVerifier: (verifier) => track(registry.register(plugin.id, verifier)),
|
|
443
|
+
}))(this.proofVerifiers),
|
|
444
|
+
}
|
|
445
|
+
: {}),
|
|
431
446
|
...(this.decisionsImpl
|
|
432
447
|
? {
|
|
433
448
|
decisions: (({ registry, service, activation }) => ({
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type CommandTool = "run_process" | "shell";
|
|
2
|
+
/** The canonical workspace-relative directory: forward slashes, no `./` prefix, `.` when empty. */
|
|
3
|
+
export declare function normalizeCwd(cwd: string): string;
|
|
4
|
+
/** Directory of a call relative to the workspace (`.` when it is the workspace itself). */
|
|
5
|
+
export declare function relativeCwd(workspace: string, cwd: string | undefined): string;
|
|
6
|
+
export declare function commandFingerprint(argv: readonly string[], cwd: string): string;
|
|
7
|
+
/** Trims and collapses whitespace runs OUTSIDE single/double quotes; quoted text stays verbatim. */
|
|
8
|
+
export declare function normalizeShell(command: string): string;
|
|
9
|
+
/** Fingerprint of a call as the tool received it. */
|
|
10
|
+
export declare function callFingerprint(tool: CommandTool, input: {
|
|
11
|
+
command: string;
|
|
12
|
+
args?: readonly string[];
|
|
13
|
+
}, cwd: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Splits a plain command line into an argv (single and double quotes group words). Returns
|
|
16
|
+
* undefined when the line needs a shell (operators, expansions, unbalanced quotes): only the
|
|
17
|
+
* shell form can then match it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function splitCommand(command: string): string[] | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Every fingerprint a call may have to satisfy the criterion command `command` run in `cwd`: the
|
|
22
|
+
* `shell` form always and the `run_process` form when the line is a plain argv.
|
|
23
|
+
*/
|
|
24
|
+
export declare function criterionFingerprints(command: string, cwd: string): string[];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command fingerprint v1 (specs/011, Fase 0 decision): `sha256(JSON.stringify({v:1, argv, cwd}))`.
|
|
3
|
+
* `run_process` uses `[command, ...args]`; `shell` and `bg_run` use `["sh","-c", normalized]`; the
|
|
4
|
+
* working directory is relative to the workspace with `/` and `.` by default. No environment.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
8
|
+
/** The canonical workspace-relative directory: forward slashes, no `./` prefix, `.` when empty. */
|
|
9
|
+
export function normalizeCwd(cwd) {
|
|
10
|
+
return (cwd
|
|
11
|
+
.split("\\")
|
|
12
|
+
.join("/")
|
|
13
|
+
.replace(/^(\.\/)+/, "")
|
|
14
|
+
.replace(/\/+$/, "") || ".");
|
|
15
|
+
}
|
|
16
|
+
/** Directory of a call relative to the workspace (`.` when it is the workspace itself). */
|
|
17
|
+
export function relativeCwd(workspace, cwd) {
|
|
18
|
+
if (!cwd)
|
|
19
|
+
return ".";
|
|
20
|
+
const abs = isAbsolute(cwd) ? cwd : resolve(workspace, cwd);
|
|
21
|
+
return normalizeCwd(relative(workspace, abs));
|
|
22
|
+
}
|
|
23
|
+
export function commandFingerprint(argv, cwd) {
|
|
24
|
+
return createHash("sha256")
|
|
25
|
+
.update(JSON.stringify({ v: 1, argv, cwd: normalizeCwd(cwd) }))
|
|
26
|
+
.digest("hex");
|
|
27
|
+
}
|
|
28
|
+
/** Trims and collapses whitespace runs OUTSIDE single/double quotes; quoted text stays verbatim. */
|
|
29
|
+
export function normalizeShell(command) {
|
|
30
|
+
let out = "";
|
|
31
|
+
let quote;
|
|
32
|
+
let pendingSpace = false;
|
|
33
|
+
for (const ch of command.trim()) {
|
|
34
|
+
if (quote) {
|
|
35
|
+
out += ch;
|
|
36
|
+
if (ch === quote)
|
|
37
|
+
quote = undefined;
|
|
38
|
+
}
|
|
39
|
+
else if (/\s/.test(ch)) {
|
|
40
|
+
pendingSpace = true;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (pendingSpace)
|
|
44
|
+
out += " ";
|
|
45
|
+
pendingSpace = false;
|
|
46
|
+
out += ch;
|
|
47
|
+
if (ch === '"' || ch === "'")
|
|
48
|
+
quote = ch;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** Fingerprint of a call as the tool received it. */
|
|
54
|
+
export function callFingerprint(tool, input, cwd) {
|
|
55
|
+
const argv = tool === "run_process"
|
|
56
|
+
? [input.command, ...(input.args ?? [])]
|
|
57
|
+
: ["sh", "-c", normalizeShell(input.command)];
|
|
58
|
+
return commandFingerprint(argv, cwd);
|
|
59
|
+
}
|
|
60
|
+
const SHELL_OPERATORS = /[|&;<>()$`\\*?~#{}!\n]/;
|
|
61
|
+
/**
|
|
62
|
+
* Splits a plain command line into an argv (single and double quotes group words). Returns
|
|
63
|
+
* undefined when the line needs a shell (operators, expansions, unbalanced quotes): only the
|
|
64
|
+
* shell form can then match it.
|
|
65
|
+
*/
|
|
66
|
+
export function splitCommand(command) {
|
|
67
|
+
const argv = [];
|
|
68
|
+
let word;
|
|
69
|
+
let quote;
|
|
70
|
+
for (const ch of command.trim()) {
|
|
71
|
+
if (quote) {
|
|
72
|
+
if (ch === quote)
|
|
73
|
+
quote = undefined;
|
|
74
|
+
else
|
|
75
|
+
word += ch;
|
|
76
|
+
}
|
|
77
|
+
else if (ch === '"' || ch === "'") {
|
|
78
|
+
quote = ch;
|
|
79
|
+
word ??= "";
|
|
80
|
+
}
|
|
81
|
+
else if (/\s/.test(ch)) {
|
|
82
|
+
if (word !== undefined)
|
|
83
|
+
argv.push(word);
|
|
84
|
+
word = undefined;
|
|
85
|
+
}
|
|
86
|
+
else if (SHELL_OPERATORS.test(ch)) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
word = (word ?? "") + ch;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (quote)
|
|
94
|
+
return undefined;
|
|
95
|
+
if (word !== undefined)
|
|
96
|
+
argv.push(word);
|
|
97
|
+
return argv.length ? argv : undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Every fingerprint a call may have to satisfy the criterion command `command` run in `cwd`: the
|
|
101
|
+
* `shell` form always and the `run_process` form when the line is a plain argv.
|
|
102
|
+
*/
|
|
103
|
+
export function criterionFingerprints(command, cwd) {
|
|
104
|
+
const out = [callFingerprint("shell", { command }, cwd)];
|
|
105
|
+
const argv = splitCommand(command);
|
|
106
|
+
if (argv)
|
|
107
|
+
out.unshift(commandFingerprint(argv, cwd));
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parser and judge of the `coverage` verifier (specs/011 3.1, Fase 7). They read the text a
|
|
3
|
+
* coverage command printed and say whether the overall percentage reaches `minPercent`; they never
|
|
4
|
+
* run anything and never generate tests. The evidence itself is captured by the recorder from the
|
|
5
|
+
* RAW result of a call the runner executed under its gate (D3, D6).
|
|
6
|
+
*
|
|
7
|
+
* Formats, tried from the most explicit overall figure to the least:
|
|
8
|
+
* 1. `coverage.py` / pytest-cov: the `TOTAL` row (last percentage) or `Total coverage: N%`.
|
|
9
|
+
* 2. A text table (vitest, c8, istanbul): the `All files` row, `% Lines` column.
|
|
10
|
+
* 3. An istanbul `text-summary`: the `Lines : N%` row.
|
|
11
|
+
* 4. `lcov --summary`: the `lines......: N%` row.
|
|
12
|
+
* 5. Raw `lcov` tracefile: sum of `LH` over sum of `LF`.
|
|
13
|
+
* Per-file rows never stand in for the total: a missing overall figure is "not found", so an
|
|
14
|
+
* unparseable output can never verify a criterion. Several matches of one format: the last wins.
|
|
15
|
+
*/
|
|
16
|
+
import type { ProofVerifierSpec } from "@alisio/sdk";
|
|
17
|
+
export type CoverageSpec = Extract<ProofVerifierSpec, {
|
|
18
|
+
kind: "coverage";
|
|
19
|
+
}>;
|
|
20
|
+
export type CoverageFormat = "coverage.py" | "table" | "summary" | "lcov-summary" | "lcov";
|
|
21
|
+
export interface ParsedCoverage {
|
|
22
|
+
percent: number;
|
|
23
|
+
format: CoverageFormat;
|
|
24
|
+
}
|
|
25
|
+
/** The overall coverage percentage a command printed, or undefined when there is none to trust. */
|
|
26
|
+
export declare function parseCoverage(output: string): ParsedCoverage | undefined;
|
|
27
|
+
/** What a coverage judgement concluded: the verdict and a short, secret-free sentence about it. */
|
|
28
|
+
export interface CoverageJudgement {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
summary: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Judges what the coverage command did. It verifies only when the command exited 0 AND an overall
|
|
34
|
+
* percentage was found AND it reaches `minPercent` (inclusive); every other case is a failure with
|
|
35
|
+
* a reason, so an unparseable output or a crashed run is never silently verified.
|
|
36
|
+
*/
|
|
37
|
+
export declare function judgeCoverage(spec: CoverageSpec, run: {
|
|
38
|
+
exitCode: number;
|
|
39
|
+
output: string;
|
|
40
|
+
}): CoverageJudgement;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const ANSI = /\u001b\[[0-9;]*[A-Za-z]/g;
|
|
2
|
+
const NUMBER = String.raw `(\d{1,3}(?:\.\d+)?)`;
|
|
3
|
+
const valid = (value) => Number.isFinite(value) && value >= 0 && value <= 100;
|
|
4
|
+
const lastMatch = (text, pattern) => {
|
|
5
|
+
let found;
|
|
6
|
+
for (const match of text.matchAll(pattern))
|
|
7
|
+
found = match;
|
|
8
|
+
return found;
|
|
9
|
+
};
|
|
10
|
+
function pythonTotal(text) {
|
|
11
|
+
// pytest-cov prints the precise figure on its own line; prefer it over the rounded TOTAL row.
|
|
12
|
+
const line = lastMatch(text, new RegExp(String.raw `Total coverage:\s*${NUMBER}%`, "gi"));
|
|
13
|
+
if (line?.[1])
|
|
14
|
+
return Number(line[1]);
|
|
15
|
+
const row = lastMatch(text, new RegExp(String.raw `^\s*TOTAL\b[^\n]*?(?<![\d.])${NUMBER}%[ \t]*$`, "gim"));
|
|
16
|
+
return row?.[1] ? Number(row[1]) : undefined;
|
|
17
|
+
}
|
|
18
|
+
function tableTotal(text) {
|
|
19
|
+
const lines = text.split("\n");
|
|
20
|
+
let column = -1;
|
|
21
|
+
let result;
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
if (!line.includes("|"))
|
|
24
|
+
continue;
|
|
25
|
+
const cells = line.split("|").map((cell) => cell.trim());
|
|
26
|
+
const header = cells.findIndex((cell) => /^%\s*lines?$/i.test(cell));
|
|
27
|
+
if (header >= 0) {
|
|
28
|
+
column = header;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (column < 0 || !/^all files$/i.test(cells[0] ?? ""))
|
|
32
|
+
continue;
|
|
33
|
+
const raw = (cells[column] ?? "").replace(/%$/, "");
|
|
34
|
+
result = /^\d{1,3}(?:\.\d+)?$/.test(raw) ? Number(raw) : undefined;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
function summaryLines(text) {
|
|
39
|
+
const row = lastMatch(text, new RegExp(String.raw `^\s*Lines\s*:\s*${NUMBER}%`, "gim"));
|
|
40
|
+
return row?.[1] ? Number(row[1]) : undefined;
|
|
41
|
+
}
|
|
42
|
+
function lcovSummary(text) {
|
|
43
|
+
const row = lastMatch(text, new RegExp(String.raw `^\s*lines\.+:\s*${NUMBER}%`, "gim"));
|
|
44
|
+
return row?.[1] ? Number(row[1]) : undefined;
|
|
45
|
+
}
|
|
46
|
+
function lcovTracefile(text) {
|
|
47
|
+
let found = 0;
|
|
48
|
+
let hit = 0;
|
|
49
|
+
for (const match of text.matchAll(/^(LF|LH):(\d+)\s*$/gm)) {
|
|
50
|
+
if (match[1] === "LF")
|
|
51
|
+
found += Number(match[2]);
|
|
52
|
+
else
|
|
53
|
+
hit += Number(match[2]);
|
|
54
|
+
}
|
|
55
|
+
return found > 0 ? (hit / found) * 100 : undefined;
|
|
56
|
+
}
|
|
57
|
+
const PARSERS = [
|
|
58
|
+
["coverage.py", pythonTotal],
|
|
59
|
+
["table", tableTotal],
|
|
60
|
+
["summary", summaryLines],
|
|
61
|
+
["lcov-summary", lcovSummary],
|
|
62
|
+
["lcov", lcovTracefile],
|
|
63
|
+
];
|
|
64
|
+
/** The overall coverage percentage a command printed, or undefined when there is none to trust. */
|
|
65
|
+
export function parseCoverage(output) {
|
|
66
|
+
const text = output.replace(ANSI, "");
|
|
67
|
+
for (const [format, parse] of PARSERS) {
|
|
68
|
+
const percent = parse(text);
|
|
69
|
+
if (percent !== undefined && valid(percent))
|
|
70
|
+
return { percent, format };
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const shown = (value) => String(Math.round(value * 100) / 100);
|
|
75
|
+
/**
|
|
76
|
+
* Judges what the coverage command did. It verifies only when the command exited 0 AND an overall
|
|
77
|
+
* percentage was found AND it reaches `minPercent` (inclusive); every other case is a failure with
|
|
78
|
+
* a reason, so an unparseable output or a crashed run is never silently verified.
|
|
79
|
+
*/
|
|
80
|
+
export function judgeCoverage(spec, run) {
|
|
81
|
+
const parsed = parseCoverage(run.output);
|
|
82
|
+
if (!parsed)
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
summary: `no coverage percentage found in the output (command exited ${run.exitCode}); expected lcov, a vitest/istanbul table or coverage.py report`,
|
|
86
|
+
};
|
|
87
|
+
const percent = `${shown(parsed.percent)}%`;
|
|
88
|
+
if (run.exitCode !== 0)
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
summary: `the coverage command exited ${run.exitCode} (reported ${percent}, ${parsed.format})`,
|
|
92
|
+
};
|
|
93
|
+
if (parsed.percent < spec.minPercent)
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
summary: `coverage ${percent} is below the minimum ${shown(spec.minPercent)}% (${parsed.format})`,
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
summary: `coverage ${percent} reaches the minimum ${shown(spec.minPercent)}% (${parsed.format})`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim/evidence divergence (specs/011 3.5, D7, D8). `detectDivergence` is pure: it reads closed
|
|
3
|
+
* EN/ES claim patterns in a final answer ("tests pass", "typecheck ok", "lint clean", "los tests
|
|
4
|
+
* pasan"...), cross-references them with the criteria of an equivalent kind and reports the ones
|
|
5
|
+
* the ledger does not back. It never edits text or statuses; it is a heuristic signal, not a
|
|
6
|
+
* guarantee, and claims inside quotes and code are ignored to limit false positives.
|
|
7
|
+
*/
|
|
8
|
+
import type { ProofLedger, ProofStatus } from "@alisio/sdk";
|
|
9
|
+
export type ClaimKind = "test" | "typecheck" | "lint";
|
|
10
|
+
export interface Divergence {
|
|
11
|
+
/** The matched wording, whitespace-collapsed and capped. */
|
|
12
|
+
claim: string;
|
|
13
|
+
kind: ClaimKind;
|
|
14
|
+
/** What would have backed the claim. */
|
|
15
|
+
expected: "verified";
|
|
16
|
+
/** The status of the equivalent criterion (`not_checked` when there is none). */
|
|
17
|
+
actualState: ProofStatus;
|
|
18
|
+
criterionId?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The claims of `finalText` that `ledger` does not back, in order of appearance. An empty ledger
|
|
22
|
+
* yields nothing (D7: without criteria Proof stays out of the way).
|
|
23
|
+
*/
|
|
24
|
+
export declare function detectDivergence(finalText: string, ledger: ProofLedger): Divergence[];
|