@alisio/core 0.7.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 +23 -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/engine.d.ts +1 -0
- package/dist/eval/engine.js +3 -0
- package/dist/eval/secrets.d.ts +2 -0
- package/dist/eval/secrets.js +10 -0
- 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 +2 -1
- package/dist/index.js +2 -1
- 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 +89 -8
- 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/store.js +60 -0
- package/dist/tools/goal.d.ts +5 -1
- package/dist/tools/goal.js +35 -4
- package/package.json +2 -2
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
export const CRITERIA_MAX = 20;
|
|
2
|
+
export const CRITERION_TEXT_MAX = 500;
|
|
3
|
+
const COMMAND_MAX = 1000;
|
|
4
|
+
const PATH_MAX = 500;
|
|
5
|
+
const PATTERN_MAX = 200;
|
|
6
|
+
const SCOPE_ENTRIES_MAX = 20;
|
|
7
|
+
const SCOPE_ENTRY_MAX = 256;
|
|
8
|
+
const EXIT_CODES_MAX = 10;
|
|
9
|
+
const DATASET_ID_MAX = 64;
|
|
10
|
+
const IDENTIFIER_MAX = 200;
|
|
11
|
+
const EXPECTED_MAX = 500;
|
|
12
|
+
const PLUGIN_INPUT_MAX = 4096;
|
|
13
|
+
const PLUGIN_VERIFIER_ID = /^[a-z0-9][a-z0-9._-]{0,63}:[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
14
|
+
const DATA_CHECKS = new Set(["row_count", "sum", "equals"]);
|
|
15
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
const text = (value, max) => typeof value === "string" && value.trim() && value.length <= max ? value : undefined;
|
|
17
|
+
function verifierOf(raw) {
|
|
18
|
+
if (!isRecord(raw))
|
|
19
|
+
return "verifier must be an object";
|
|
20
|
+
if (raw.kind === "manual")
|
|
21
|
+
return { kind: "manual" };
|
|
22
|
+
if (raw.kind === "command") {
|
|
23
|
+
const command = text(raw.command, COMMAND_MAX)?.trim();
|
|
24
|
+
if (!command)
|
|
25
|
+
return "a command verifier needs a command";
|
|
26
|
+
const cwd = raw.cwd === undefined ? undefined : text(raw.cwd, PATH_MAX);
|
|
27
|
+
if (raw.cwd !== undefined && !cwd)
|
|
28
|
+
return "cwd must be a short string";
|
|
29
|
+
if (cwd && (isAbsoluteLike(cwd) || hasParentSegment(cwd)))
|
|
30
|
+
return "cwd must stay in the workspace";
|
|
31
|
+
const codes = raw.successExitCodes;
|
|
32
|
+
if (codes !== undefined &&
|
|
33
|
+
!(Array.isArray(codes) &&
|
|
34
|
+
codes.length > 0 &&
|
|
35
|
+
codes.length <= EXIT_CODES_MAX &&
|
|
36
|
+
codes.every((c) => Number.isInteger(c) && c >= 0 && c <= 255)))
|
|
37
|
+
return "successExitCodes must be a short list of exit codes";
|
|
38
|
+
const pattern = raw.outputPattern === undefined ? undefined : text(raw.outputPattern, PATTERN_MAX);
|
|
39
|
+
if (raw.outputPattern !== undefined && !pattern)
|
|
40
|
+
return "outputPattern must be a short string";
|
|
41
|
+
return {
|
|
42
|
+
kind: "command",
|
|
43
|
+
command,
|
|
44
|
+
...(cwd ? { cwd } : {}),
|
|
45
|
+
...(codes ? { successExitCodes: codes } : {}),
|
|
46
|
+
...(pattern ? { outputPattern: pattern } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (raw.kind === "file") {
|
|
50
|
+
const path = text(raw.path, PATH_MAX)?.trim();
|
|
51
|
+
if (!path || isAbsoluteLike(path) || hasParentSegment(path))
|
|
52
|
+
return "a file verifier needs a workspace-relative path";
|
|
53
|
+
const sha = typeof raw.sha256 === "string" && /^[0-9a-f]{64}$/.test(raw.sha256);
|
|
54
|
+
const pattern = raw.pattern === undefined ? undefined : text(raw.pattern, PATTERN_MAX);
|
|
55
|
+
if (raw.pattern !== undefined && !pattern)
|
|
56
|
+
return "pattern must be a short string";
|
|
57
|
+
if (raw.sha256 !== undefined && !sha)
|
|
58
|
+
return "sha256 must be 64 hex characters";
|
|
59
|
+
return {
|
|
60
|
+
kind: "file",
|
|
61
|
+
path,
|
|
62
|
+
...(sha ? { sha256: raw.sha256 } : {}),
|
|
63
|
+
...(pattern ? { pattern } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (raw.kind === "data-check")
|
|
67
|
+
return dataCheckOf(raw);
|
|
68
|
+
if (raw.kind === "coverage") {
|
|
69
|
+
const command = text(raw.command, COMMAND_MAX)?.trim();
|
|
70
|
+
if (!command)
|
|
71
|
+
return "a coverage verifier needs a command";
|
|
72
|
+
const min = raw.minPercent;
|
|
73
|
+
if (typeof min !== "number" || !Number.isFinite(min) || min < 0 || min > 100)
|
|
74
|
+
return "a coverage verifier needs minPercent between 0 and 100";
|
|
75
|
+
return { kind: "coverage", command, minPercent: min };
|
|
76
|
+
}
|
|
77
|
+
if (raw.kind === "plugin") {
|
|
78
|
+
const verifierId = typeof raw.verifierId === "string" ? raw.verifierId : "";
|
|
79
|
+
if (!PLUGIN_VERIFIER_ID.test(verifierId))
|
|
80
|
+
return "a plugin verifier needs a verifierId like pluginId:verifier";
|
|
81
|
+
if (raw.input === undefined)
|
|
82
|
+
return { kind: "plugin", verifierId };
|
|
83
|
+
let size = Number.POSITIVE_INFINITY;
|
|
84
|
+
try {
|
|
85
|
+
size = JSON.stringify(raw.input)?.length ?? Number.POSITIVE_INFINITY;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
/* Not serializable: refused below. */
|
|
89
|
+
}
|
|
90
|
+
if (size > PLUGIN_INPUT_MAX)
|
|
91
|
+
return "a plugin verifier input must be short JSON";
|
|
92
|
+
return { kind: "plugin", verifierId, input: raw.input };
|
|
93
|
+
}
|
|
94
|
+
return "that verifier kind cannot be proposed (use command, file, data-check, coverage, plugin or manual)";
|
|
95
|
+
}
|
|
96
|
+
function dataCheckOf(raw) {
|
|
97
|
+
const datasetId = text(raw.datasetId, DATASET_ID_MAX)?.trim();
|
|
98
|
+
if (!datasetId)
|
|
99
|
+
return "a data-check verifier needs a datasetId";
|
|
100
|
+
const check = typeof raw.check === "string" ? raw.check : "";
|
|
101
|
+
if (!DATA_CHECKS.has(check))
|
|
102
|
+
return "check must be row_count, sum or equals";
|
|
103
|
+
const expected = raw.expected;
|
|
104
|
+
const expectedOk = (typeof expected === "number" && Number.isFinite(expected)) ||
|
|
105
|
+
(typeof expected === "string" && expected.length > 0 && expected.length <= EXPECTED_MAX);
|
|
106
|
+
if (!expectedOk)
|
|
107
|
+
return "a data-check verifier needs an expected number or short string";
|
|
108
|
+
const column = raw.column === undefined ? undefined : text(raw.column, IDENTIFIER_MAX);
|
|
109
|
+
if (raw.column !== undefined && !column)
|
|
110
|
+
return "column must be a short string";
|
|
111
|
+
if (check !== "row_count" && !column)
|
|
112
|
+
return `a ${check} check needs a column`;
|
|
113
|
+
const table = raw.table === undefined ? undefined : text(raw.table, IDENTIFIER_MAX);
|
|
114
|
+
if (raw.table !== undefined && !table)
|
|
115
|
+
return "table must be a short string";
|
|
116
|
+
return {
|
|
117
|
+
kind: "data-check",
|
|
118
|
+
datasetId,
|
|
119
|
+
check: check,
|
|
120
|
+
...(column ? { column } : {}),
|
|
121
|
+
expected: expected,
|
|
122
|
+
...(table ? { table } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const isAbsoluteLike = (path) => path.startsWith("/") || path.startsWith("\\") || /^[A-Za-z]:/.test(path);
|
|
126
|
+
const hasParentSegment = (path) => path.split(/[\\/]/).includes("..");
|
|
127
|
+
function scopeOf(raw) {
|
|
128
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.length > SCOPE_ENTRIES_MAX)
|
|
129
|
+
return "scope must be a short list of globs";
|
|
130
|
+
const out = [];
|
|
131
|
+
for (const entry of raw) {
|
|
132
|
+
const glob = text(entry, SCOPE_ENTRY_MAX)?.trim();
|
|
133
|
+
if (!glob)
|
|
134
|
+
return "every scope entry must be a short string";
|
|
135
|
+
if (isAbsoluteLike(glob) || hasParentSegment(glob))
|
|
136
|
+
return "scope entries must stay inside the workspace";
|
|
137
|
+
out.push(glob);
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/** Turns the model's `acceptanceCriteria` input into drafts, reporting what it could not keep. */
|
|
142
|
+
export function validateCriteriaDrafts(raw) {
|
|
143
|
+
const accepted = [];
|
|
144
|
+
const dropped = [];
|
|
145
|
+
if (!Array.isArray(raw)) {
|
|
146
|
+
return { accepted, dropped: [{ index: 0, reason: "acceptanceCriteria must be an array" }] };
|
|
147
|
+
}
|
|
148
|
+
raw.forEach((entry, index) => {
|
|
149
|
+
if (accepted.length >= CRITERIA_MAX) {
|
|
150
|
+
dropped.push({ index, reason: `more than ${CRITERIA_MAX} criteria were proposed` });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!isRecord(entry)) {
|
|
154
|
+
dropped.push({ index, reason: "a criterion must be an object" });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const statement = typeof entry.text === "string" ? entry.text.replace(/\s+/g, " ").trim() : "";
|
|
158
|
+
if (!statement) {
|
|
159
|
+
dropped.push({ index, reason: "a criterion needs text" });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
let verifier;
|
|
163
|
+
if (entry.verifier !== undefined) {
|
|
164
|
+
const checked = verifierOf(entry.verifier);
|
|
165
|
+
if (typeof checked === "string") {
|
|
166
|
+
dropped.push({ index, reason: checked });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
verifier = checked;
|
|
170
|
+
}
|
|
171
|
+
let scope;
|
|
172
|
+
if (entry.scope !== undefined) {
|
|
173
|
+
const checked = scopeOf(entry.scope);
|
|
174
|
+
if (typeof checked === "string") {
|
|
175
|
+
dropped.push({ index, reason: checked });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
scope = checked;
|
|
179
|
+
}
|
|
180
|
+
accepted.push({
|
|
181
|
+
text: statement.length > CRITERION_TEXT_MAX ? statement.slice(0, CRITERION_TEXT_MAX) : statement,
|
|
182
|
+
required: entry.required === true,
|
|
183
|
+
...(verifier ? { verifier } : {}),
|
|
184
|
+
...(scope ? { scope } : {}),
|
|
185
|
+
origin: "inferred",
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
return { accepted, dropped };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Applies the user's per-draft decisions (`plan:criteria`) to the drafts of the plan they approved.
|
|
192
|
+
* `undefined` selection keeps every draft (the plain "approve"). Anything malformed, any index
|
|
193
|
+
* outside the list or an empty edit returns `undefined` for "nothing is activated": a selection
|
|
194
|
+
* the user did not make cleanly never activates a criterion (fail closed).
|
|
195
|
+
*/
|
|
196
|
+
export function applyCriteriaSelection(drafts, raw) {
|
|
197
|
+
if (raw === undefined || raw === "")
|
|
198
|
+
return { kept: [...drafts], rejected: 0 };
|
|
199
|
+
let selection;
|
|
200
|
+
try {
|
|
201
|
+
selection = typeof raw === "string" ? JSON.parse(raw) : undefined;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
if (!isRecord(selection))
|
|
207
|
+
return undefined;
|
|
208
|
+
const index = (key) => {
|
|
209
|
+
const n = typeof key === "string" ? Number(key) : key;
|
|
210
|
+
return Number.isInteger(n) && n >= 0 && n < drafts.length ? n : undefined;
|
|
211
|
+
};
|
|
212
|
+
const rejected = new Set();
|
|
213
|
+
if (selection.reject !== undefined) {
|
|
214
|
+
if (!Array.isArray(selection.reject))
|
|
215
|
+
return undefined;
|
|
216
|
+
for (const entry of selection.reject) {
|
|
217
|
+
const i = index(entry);
|
|
218
|
+
if (i === undefined)
|
|
219
|
+
return undefined;
|
|
220
|
+
rejected.add(i);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const edits = new Map();
|
|
224
|
+
if (selection.edit !== undefined) {
|
|
225
|
+
if (!isRecord(selection.edit))
|
|
226
|
+
return undefined;
|
|
227
|
+
for (const [key, value] of Object.entries(selection.edit)) {
|
|
228
|
+
const i = index(key);
|
|
229
|
+
const statement = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
|
230
|
+
if (i === undefined || !statement)
|
|
231
|
+
return undefined;
|
|
232
|
+
edits.set(i, statement.slice(0, CRITERION_TEXT_MAX));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const required = new Map();
|
|
236
|
+
if (selection.required !== undefined) {
|
|
237
|
+
if (!isRecord(selection.required))
|
|
238
|
+
return undefined;
|
|
239
|
+
for (const [key, value] of Object.entries(selection.required)) {
|
|
240
|
+
const i = index(key);
|
|
241
|
+
if (i === undefined || typeof value !== "boolean")
|
|
242
|
+
return undefined;
|
|
243
|
+
required.set(i, value);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const kept = drafts.flatMap((draft, i) => rejected.has(i)
|
|
247
|
+
? []
|
|
248
|
+
: [
|
|
249
|
+
{
|
|
250
|
+
...draft,
|
|
251
|
+
text: edits.get(i) ?? draft.text,
|
|
252
|
+
required: required.get(i) ?? draft.required,
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
return { kept, rejected: rejected.size };
|
|
256
|
+
}
|
package/dist/plan/exit-plan.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AskQuestionsRequest, AskQuestionsResult } from "@alisio/sdk";
|
|
2
2
|
import type { ToolRegistry } from "../core/registry.ts";
|
|
3
|
+
import type { NewCriterion } from "../proof/types.ts";
|
|
3
4
|
import { type PlanStore } from "./state.ts";
|
|
4
5
|
export interface ExitPlanDeps {
|
|
5
6
|
store: PlanStore;
|
|
@@ -14,5 +15,12 @@ export interface ExitPlanDeps {
|
|
|
14
15
|
enabled: boolean;
|
|
15
16
|
max: number;
|
|
16
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Where approved acceptance criteria go (`ProofStore` fits). Without it the `acceptanceCriteria`
|
|
20
|
+
* field is not offered and is ignored: a core without Proof plans exactly as before.
|
|
21
|
+
*/
|
|
22
|
+
proof?: {
|
|
23
|
+
addCriterion(input: NewCriterion): unknown;
|
|
24
|
+
};
|
|
17
25
|
}
|
|
18
26
|
export declare function registerExitPlan(registry: ToolRegistry, deps: ExitPlanDeps): void;
|
package/dist/plan/exit-plan.js
CHANGED
|
@@ -17,9 +17,28 @@ import { textResult } from "@alisio/sdk";
|
|
|
17
17
|
import { EXIT_PLAN_TOOL } from "../core/opt-in.js";
|
|
18
18
|
import { newUuid } from "../runtime/ids.js";
|
|
19
19
|
import { objectSchema } from "../tools/standard.js";
|
|
20
|
+
import { applyCriteriaSelection, validateCriteriaDrafts, } from "./criteria.js";
|
|
20
21
|
import { DIAGRAM_MAX_BYTES, DIAGRAM_MAX_NODES, validateDiagrams, } from "./diagrams.js";
|
|
21
22
|
import { buildPlanManifest } from "./manifest.js";
|
|
22
|
-
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
|
+
};
|
|
23
42
|
const DIAGRAM_SCHEMA = {
|
|
24
43
|
type: "array",
|
|
25
44
|
description: `Optional Mermaid diagrams; see the plan agent's instructions`,
|
|
@@ -97,7 +116,13 @@ export function registerExitPlan(registry, deps) {
|
|
|
97
116
|
description: "The whole plan in Markdown",
|
|
98
117
|
},
|
|
99
118
|
}, ["plan"]);
|
|
100
|
-
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"]);
|
|
101
126
|
// The input is always VALIDATED with `diagrams` allowed (a call made while they were switched
|
|
102
127
|
// off must not fail), but the model is only OFFERED the field while diagrams are on: the
|
|
103
128
|
// registry compiles the schema once, while the provider reads it on every request.
|
|
@@ -106,12 +131,15 @@ export function registerExitPlan(registry, deps) {
|
|
|
106
131
|
name: EXIT_PLAN_TOOL,
|
|
107
132
|
effect: "read",
|
|
108
133
|
get description() {
|
|
109
|
-
|
|
110
|
-
? `${BASE_DESCRIPTION} Optional \`
|
|
134
|
+
const base = deps.proof
|
|
135
|
+
? `${BASE_DESCRIPTION} Optional \`acceptanceCriteria\`: drafts the user approves together with the plan.`
|
|
111
136
|
: BASE_DESCRIPTION;
|
|
137
|
+
return offered()
|
|
138
|
+
? `${base} Optional \`diagrams\`: up to ${settings().max} small Mermaid diagrams (see your instructions).`
|
|
139
|
+
: base;
|
|
112
140
|
},
|
|
113
141
|
get inputSchema() {
|
|
114
|
-
return registering || offered() ? withDiagrams :
|
|
142
|
+
return registering || offered() ? withDiagrams : plainCriteria;
|
|
115
143
|
},
|
|
116
144
|
async execute(input, context) {
|
|
117
145
|
const plan = String(input.plan ?? "").trim();
|
|
@@ -145,6 +173,15 @@ export function registerExitPlan(registry, deps) {
|
|
|
145
173
|
outcome.dropped = validated.dropped;
|
|
146
174
|
}
|
|
147
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 ?? [];
|
|
148
185
|
const preManifest = buildPlanManifest({
|
|
149
186
|
planId,
|
|
150
187
|
revision,
|
|
@@ -153,11 +190,12 @@ export function registerExitPlan(registry, deps) {
|
|
|
153
190
|
markdown: plan,
|
|
154
191
|
diagrams: outcome.accepted,
|
|
155
192
|
...(previous ? { previous } : {}),
|
|
193
|
+
...(drafts.length ? { acceptanceCriteria: drafts } : {}),
|
|
156
194
|
});
|
|
157
195
|
outcome.removed = preManifest.removed;
|
|
158
196
|
if (outcome.removed.length)
|
|
159
197
|
outcome.involved = true;
|
|
160
|
-
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);
|
|
161
199
|
const manifest = { ...preManifest, hash };
|
|
162
200
|
const diagrams = manifest.diagrams.map((entry, index) => ({
|
|
163
201
|
id: entry.id,
|
|
@@ -176,7 +214,7 @@ export function registerExitPlan(registry, deps) {
|
|
|
176
214
|
const publisher = context.artifacts;
|
|
177
215
|
if (publisher) {
|
|
178
216
|
// A folder (plan.md + plan.json + diagrams) when diagrams are involved, else plan.md alone.
|
|
179
|
-
if (outcome.accepted.length || outcome.removed.length)
|
|
217
|
+
if (outcome.accepted.length || outcome.removed.length || drafts.length)
|
|
180
218
|
try {
|
|
181
219
|
artifact = await publishPlanFolder(publisher, [
|
|
182
220
|
{ path: "plan.md", text: `${plan}\n` },
|
|
@@ -209,12 +247,23 @@ export function registerExitPlan(registry, deps) {
|
|
|
209
247
|
title,
|
|
210
248
|
...(context.callId ? { callId: context.callId } : {}),
|
|
211
249
|
...(artifact ? { artifactId: artifact.id } : {}),
|
|
250
|
+
...(drafts.length ? { criteria: drafts } : {}),
|
|
212
251
|
...(manifest.diagrams.length
|
|
213
252
|
? {
|
|
214
253
|
diagrams: manifest.diagrams.map((d) => ({ id: d.id, title: d.title, hash: d.hash })),
|
|
215
254
|
}
|
|
216
255
|
: {}),
|
|
217
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
|
+
: {};
|
|
218
267
|
const report = diagramReport(outcome);
|
|
219
268
|
if (!reviewable) {
|
|
220
269
|
// Nobody to ask: keep the revision count honest and tell the model what to do instead.
|
|
@@ -239,6 +288,7 @@ export function registerExitPlan(registry, deps) {
|
|
|
239
288
|
markdown: plan,
|
|
240
289
|
...(artifact ? { artifact } : {}),
|
|
241
290
|
...(diagrams.length ? { diagrams } : {}),
|
|
291
|
+
...(drafts.length ? { acceptanceCriteria: drafts } : {}),
|
|
242
292
|
};
|
|
243
293
|
const result = await deps.ui.askQuestions({
|
|
244
294
|
questions: [
|
|
@@ -271,16 +321,47 @@ export function registerExitPlan(registry, deps) {
|
|
|
271
321
|
hash,
|
|
272
322
|
decision,
|
|
273
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
|
+
}
|
|
274
349
|
const base = {
|
|
275
350
|
planId,
|
|
276
351
|
revision,
|
|
277
352
|
...(artifact ? { artifactId: artifact.id } : {}),
|
|
278
353
|
...report,
|
|
354
|
+
...criteriaReport(activated),
|
|
279
355
|
};
|
|
280
356
|
const body = decision === "approve"
|
|
281
357
|
? answer("approved", "The user approved the plan. The build agent will implement exactly this plan in the " +
|
|
282
358
|
"next turn. Do not implement anything yourself: reply with one short sentence and " +
|
|
283
|
-
"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)
|
|
284
365
|
: decision === "context"
|
|
285
366
|
? answer("feedback", "The user added context instead of deciding. Revise the plan to account for it and " +
|
|
286
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[];
|