@alisio/alisio-code 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/main.js +34 -1
- package/dist/proof.d.ts +52 -0
- package/dist/proof.js +121 -0
- package/dist/tui/app.js +24 -2
- package/dist/tui/goal.d.ts +5 -0
- package/dist/tui/goal.js +2 -0
- package/dist/tui/plan-review.d.ts +7 -1
- package/dist/tui/plan-review.js +24 -0
- package/dist/tui/proof.d.ts +51 -0
- package/dist/tui/proof.js +126 -0
- package/dist/tui/state.js +6 -0
- package/package.json +8 -8
package/dist/main.js
CHANGED
|
@@ -21,6 +21,8 @@ async function cliDefaults() {
|
|
|
21
21
|
reservedPromptNames: reservedCommandNames(),
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
/** Commander collector of a repeatable option. */
|
|
25
|
+
const collect = (value, previous) => [...previous, value];
|
|
24
26
|
const program = new Command();
|
|
25
27
|
program
|
|
26
28
|
.name("alisio")
|
|
@@ -118,6 +120,11 @@ async function run(cmd, prompt, sessionId) {
|
|
|
118
120
|
const trusted = await withProjectTrust(opts);
|
|
119
121
|
return runTui({ ...trusted, ...(sessionId ? { session: sessionId } : {}) });
|
|
120
122
|
}
|
|
123
|
+
const proofFlags = opts;
|
|
124
|
+
// Invalid criteria fail before a session or a provider exists.
|
|
125
|
+
const proofCriteria = proofFlags.criterion?.length || proofFlags.verify?.length || proofFlags.proofFile
|
|
126
|
+
? await (await import("./proof.js")).collectProofCriteria(proofFlags)
|
|
127
|
+
: [];
|
|
121
128
|
const { createApplication, formatBytes, KIND_LABELS } = await import("@alisio/core");
|
|
122
129
|
const app = await createApplication({
|
|
123
130
|
...(await cliDefaults()),
|
|
@@ -201,12 +208,34 @@ async function run(cmd, prompt, sessionId) {
|
|
|
201
208
|
const match = /^\/skill:([a-z0-9-]+)\s*([\s\S]*)$/.exec(prompt);
|
|
202
209
|
if (match?.[1])
|
|
203
210
|
prompt = `${await app.skills.load(match[1])}\n\nUser request: ${match[2] ?? ""}`;
|
|
204
|
-
|
|
211
|
+
const proof = await import("./proof.js");
|
|
212
|
+
const judged = proof.proofRequested(proofFlags, app.config.proof);
|
|
213
|
+
for (const criterion of proofCriteria)
|
|
214
|
+
app.proof.addCriterion({ sessionId: session, ...criterion });
|
|
215
|
+
if (judged &&
|
|
216
|
+
opts.readOnly &&
|
|
217
|
+
proofCriteria.some((c) => c.verifier?.kind === "command" ||
|
|
218
|
+
(c.verifier?.kind === "coverage" && app.config.proof.coverage.enabled)))
|
|
219
|
+
process.stderr.write("proof: --read-only denies processes, so command and coverage criteria stay not_checked\n");
|
|
220
|
+
const coverageEnabled = app.config.proof.coverage.enabled;
|
|
221
|
+
if (proofCriteria.some((c) => c.verifier?.kind === "coverage") && !coverageEnabled)
|
|
222
|
+
process.stderr.write("proof: coverage verification is off (proof.coverage.enabled is false), so coverage criteria stay not_checked and Proof never runs their command\n");
|
|
223
|
+
await app.runner.run(session, proof.withProofBrief(template?.text ?? prompt, proofCriteria, { coverageEnabled }), controller.signal, {
|
|
205
224
|
...activeRunOptions,
|
|
206
225
|
...(template ? { display: template.display } : {}),
|
|
207
226
|
});
|
|
208
227
|
if (!opts.json)
|
|
209
228
|
process.stdout.write(`\nSession: ${session}\n`);
|
|
229
|
+
if (judged) {
|
|
230
|
+
const code = await proof.finishProof({
|
|
231
|
+
proof: app.proof,
|
|
232
|
+
write: (text) => process.stdout.write(text),
|
|
233
|
+
writeErr: (text) => process.stderr.write(text),
|
|
234
|
+
publisher: proof.sessionPublisher(app, session),
|
|
235
|
+
}, session, { json: !!opts.json, codes: app.config.proof.exitCodes });
|
|
236
|
+
if (code)
|
|
237
|
+
process.exitCode = code;
|
|
238
|
+
}
|
|
210
239
|
return;
|
|
211
240
|
}
|
|
212
241
|
if (opts.json || !process.stdin.isTTY)
|
|
@@ -323,6 +352,10 @@ program
|
|
|
323
352
|
.command("run")
|
|
324
353
|
.description('Run one prompt headless; "/name args" runs a prompt template (e.g. "/init") or a plugin command ("/plugin.id:name")')
|
|
325
354
|
.argument("<prompt>")
|
|
355
|
+
.option("--proof", "Judge the run against its acceptance criteria: print the Proof ledger (the final JSONL event with --json) and exit 0, 10 (a required criterion failed) or 11 (a required one is not verified); the codes are proof.exitCodes")
|
|
356
|
+
.option("--criterion <text>", "An acceptance criterion (repeatable); pair it with --verify <command>", collect, [])
|
|
357
|
+
.option("--verify <command>", "The command that verifies the n-th --criterion; Alisio records the evidence when the agent runs exactly it", collect, [])
|
|
358
|
+
.option("--proof-file <path>", 'JSON file with {"criteria":[{"text","verify"|"verifier","required","scope"}]} (or a plain array)')
|
|
326
359
|
.action((prompt, _options, cmd) => run(cmd, prompt));
|
|
327
360
|
program
|
|
328
361
|
.command("resume")
|
package/dist/proof.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ArtifactStore, type NewCriterion, type ProofExitCodes, type ProofStore } from "@alisio/core";
|
|
2
|
+
import type { ArtifactPublisher } from "@alisio/sdk";
|
|
3
|
+
export interface ProofFlags {
|
|
4
|
+
proof?: boolean;
|
|
5
|
+
/** Repeatable `--criterion "text"`. */
|
|
6
|
+
criterion?: string[];
|
|
7
|
+
/** Repeatable `--verify "cmd"`: the n-th pairs with the n-th `--criterion`. */
|
|
8
|
+
verify?: string[];
|
|
9
|
+
proofFile?: string;
|
|
10
|
+
json?: boolean;
|
|
11
|
+
/** `proof.exitCodes` of the configuration. */
|
|
12
|
+
codes?: ProofExitCodes;
|
|
13
|
+
}
|
|
14
|
+
export type CliCriterion = Omit<NewCriterion, "sessionId">;
|
|
15
|
+
/** Whether the run produces a ledger report: a flag, a criteria source, or `proof.ci`. */
|
|
16
|
+
export declare function proofRequested(flags: ProofFlags, config: {
|
|
17
|
+
ci: boolean;
|
|
18
|
+
}): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The criteria of the run (O4): the proof file first, then the flags. Every one is `origin: "cli"`
|
|
21
|
+
* and required unless the file says otherwise. Throws a readable error on anything invalid.
|
|
22
|
+
*/
|
|
23
|
+
export declare function collectProofCriteria(flags: Pick<ProofFlags, "criterion" | "verify" | "proofFile">, env?: {
|
|
24
|
+
readFile?: (path: string) => Promise<string>;
|
|
25
|
+
}): Promise<CliCriterion[]>;
|
|
26
|
+
export interface ProofFinishEnv {
|
|
27
|
+
proof: Pick<ProofStore, "view">;
|
|
28
|
+
write: (text: string) => void;
|
|
29
|
+
writeErr: (text: string) => void;
|
|
30
|
+
/** Publishes `proof.md` and `proof.json` into the session; absent when it cannot. */
|
|
31
|
+
publisher?: Pick<ArtifactPublisher, "publishText">;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reports the ledger of the session after the run and returns the exit code: 0, or the code of
|
|
35
|
+
* `proof.exitCodes` (10/11 by default). With `--json` the report is ONE final JSONL line
|
|
36
|
+
* (`type: "proof_ledger"`); otherwise a plain table on stdout. A failure to publish the
|
|
37
|
+
* artifacts never changes the exit code.
|
|
38
|
+
*/
|
|
39
|
+
export declare function finishProof(env: ProofFinishEnv, sessionId: string, flags: Pick<ProofFlags, "json" | "codes">): Promise<number>;
|
|
40
|
+
/**
|
|
41
|
+
* The prompt of a run with criteria: the original text, then the criteria and the exact commands
|
|
42
|
+
* whose execution (or file read, or dataset query) Alisio records as evidence (D3: the agent cannot declare a criterion met).
|
|
43
|
+
* Without criteria the prompt is returned untouched (D7).
|
|
44
|
+
*/
|
|
45
|
+
export declare function withProofBrief(prompt: string, criteria: readonly CliCriterion[], options?: {
|
|
46
|
+
coverageEnabled?: boolean;
|
|
47
|
+
}): string;
|
|
48
|
+
/** The publisher of a session's artifacts (`proof.md`, `proof.json`) for a headless run. */
|
|
49
|
+
export declare function sessionPublisher(app: {
|
|
50
|
+
artifacts: ArtifactStore;
|
|
51
|
+
workspace: string;
|
|
52
|
+
}, sessionId: string): Pick<ArtifactPublisher, "publishText">;
|
package/dist/proof.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `alisio run --proof` (specs/011 3.7, O4, O5). The ledger is judged after the run with the same
|
|
3
|
+
* pure functions every surface uses (`@alisio/core` `proof/export`); this file only wires flags,
|
|
4
|
+
* files and output. Functions take an injected environment so they run without a subprocess.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile as readFileFs } from "node:fs/promises";
|
|
7
|
+
import { createArtifactPublisher, DEFAULT_PROOF_EXIT_CODES, PROOF_EXPORT_SCHEMA, parseCriteriaInput, proofExitCode, proofExportV1, publishProofExport, renderProofText, verifierCall, } from "@alisio/core";
|
|
8
|
+
/** Whether the run produces a ledger report: a flag, a criteria source, or `proof.ci`. */
|
|
9
|
+
export function proofRequested(flags, config) {
|
|
10
|
+
return !!(flags.proof ||
|
|
11
|
+
config.ci ||
|
|
12
|
+
flags.criterion?.length ||
|
|
13
|
+
flags.verify?.length ||
|
|
14
|
+
flags.proofFile);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The criteria of the run (O4): the proof file first, then the flags. Every one is `origin: "cli"`
|
|
18
|
+
* and required unless the file says otherwise. Throws a readable error on anything invalid.
|
|
19
|
+
*/
|
|
20
|
+
export async function collectProofCriteria(flags, env = {}) {
|
|
21
|
+
const read = env.readFile ?? ((path) => readFileFs(path, "utf8"));
|
|
22
|
+
const out = [];
|
|
23
|
+
if (flags.proofFile) {
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(await read(flags.proofFile));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
throw new Error(`--proof-file ${flags.proofFile}: ${error instanceof Error ? error.message : "unreadable"}`);
|
|
30
|
+
}
|
|
31
|
+
const list = Array.isArray(parsed)
|
|
32
|
+
? parsed
|
|
33
|
+
: parsed?.criteria;
|
|
34
|
+
const checked = parseCriteriaInput(list, { origin: "cli" });
|
|
35
|
+
const first = checked.errors[0];
|
|
36
|
+
if (first)
|
|
37
|
+
throw new Error(`--proof-file: criterion ${first.index + 1}: ${first.reason}`);
|
|
38
|
+
out.push(...checked.criteria);
|
|
39
|
+
}
|
|
40
|
+
const texts = flags.criterion ?? [];
|
|
41
|
+
const commands = flags.verify ?? [];
|
|
42
|
+
if (commands.length > texts.length)
|
|
43
|
+
throw new Error("--verify needs a --criterion before it (they pair by position)");
|
|
44
|
+
const entries = texts.map((text, i) => ({
|
|
45
|
+
text,
|
|
46
|
+
...(commands[i] ? { verify: commands[i] } : {}),
|
|
47
|
+
}));
|
|
48
|
+
const checked = parseCriteriaInput(entries, { origin: "cli" });
|
|
49
|
+
const first = checked.errors[0];
|
|
50
|
+
if (first)
|
|
51
|
+
throw new Error(`--criterion ${first.index + 1}: ${first.reason}`);
|
|
52
|
+
out.push(...checked.criteria);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Reports the ledger of the session after the run and returns the exit code: 0, or the code of
|
|
57
|
+
* `proof.exitCodes` (10/11 by default). With `--json` the report is ONE final JSONL line
|
|
58
|
+
* (`type: "proof_ledger"`); otherwise a plain table on stdout. A failure to publish the
|
|
59
|
+
* artifacts never changes the exit code.
|
|
60
|
+
*/
|
|
61
|
+
export async function finishProof(env, sessionId, flags) {
|
|
62
|
+
const ledger = env.proof.view(sessionId);
|
|
63
|
+
const code = proofExitCode(ledger, flags.codes ?? DEFAULT_PROOF_EXIT_CODES);
|
|
64
|
+
if (env.publisher) {
|
|
65
|
+
try {
|
|
66
|
+
await publishProofExport(env.publisher, ledger);
|
|
67
|
+
env.writeErr("proof: proof.md and proof.json were published as session artifacts\n");
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
env.writeErr(`proof: the export was not published (${error instanceof Error ? error.message : "error"})\n`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (flags.json)
|
|
74
|
+
env.write(`${JSON.stringify({
|
|
75
|
+
type: "proof_ledger",
|
|
76
|
+
schema: PROOF_EXPORT_SCHEMA,
|
|
77
|
+
sessionId,
|
|
78
|
+
exitCode: code,
|
|
79
|
+
ledger: proofExportV1(ledger),
|
|
80
|
+
})}\n`);
|
|
81
|
+
else
|
|
82
|
+
env.write(`\n${renderProofText(ledger)}\n`);
|
|
83
|
+
return code;
|
|
84
|
+
}
|
|
85
|
+
/** The one call that counts as evidence for a criterion, said to the agent (empty when none). */
|
|
86
|
+
function hint(verifier, coverageEnabled) {
|
|
87
|
+
if (verifier?.kind === "command")
|
|
88
|
+
return ` Run \`${verifier.command}\`.`;
|
|
89
|
+
if (verifier?.kind === "coverage")
|
|
90
|
+
return coverageEnabled
|
|
91
|
+
? ` Run \`${verifier.command}\` (at least ${verifier.minPercent}% coverage).`
|
|
92
|
+
: " Coverage verification is disabled (proof.coverage.enabled is false): this criterion stays not_checked.";
|
|
93
|
+
const call = verifier ? verifierCall(verifier) : undefined;
|
|
94
|
+
if (!call)
|
|
95
|
+
return "";
|
|
96
|
+
if (call.name === "read_file")
|
|
97
|
+
return ` Read \`${verifier?.kind === "file" ? verifier.path : ""}\` with read_file.`;
|
|
98
|
+
return ` Run data_query on dataset ${String(call.input.datasetId)} with exactly \`${String(call.input.sql)}\`.`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The prompt of a run with criteria: the original text, then the criteria and the exact commands
|
|
102
|
+
* whose execution (or file read, or dataset query) Alisio records as evidence (D3: the agent cannot declare a criterion met).
|
|
103
|
+
* Without criteria the prompt is returned untouched (D7).
|
|
104
|
+
*/
|
|
105
|
+
export function withProofBrief(prompt, criteria, options = {}) {
|
|
106
|
+
if (!criteria.length)
|
|
107
|
+
return prompt;
|
|
108
|
+
const lines = criteria.map((c) => {
|
|
109
|
+
return `- ${c.text}${c.required ? " (required)" : ""}.${hint(c.verifier, options.coverageEnabled === true)}`;
|
|
110
|
+
});
|
|
111
|
+
return [
|
|
112
|
+
prompt,
|
|
113
|
+
"",
|
|
114
|
+
"Acceptance criteria. Alisio checks them from the commands you actually run, not from what you say; a criterion with a command is verified only when you run exactly that command and it succeeds:",
|
|
115
|
+
...lines,
|
|
116
|
+
].join("\n");
|
|
117
|
+
}
|
|
118
|
+
/** The publisher of a session's artifacts (`proof.md`, `proof.json`) for a headless run. */
|
|
119
|
+
export function sessionPublisher(app, sessionId) {
|
|
120
|
+
return createArtifactPublisher(app.artifacts, { sessionId, rootSessionId: sessionId, workspace: app.workspace }, () => undefined);
|
|
121
|
+
}
|
package/dist/tui/app.js
CHANGED
|
@@ -19,7 +19,7 @@ import { formatGoalStatus, GoalBar, goalPickerItems, goalPickerPrefill, goalStop
|
|
|
19
19
|
import { agentCycleHint, isPermissionMode, modeDisplay, permissionMenuRows, permissionTransition, reloadGuard, shiftTabDecision, } from "./modes.js";
|
|
20
20
|
import { cannotOpenMessage, openPath, revealPath } from "./open-path.js";
|
|
21
21
|
import { initialPanelState, reducePanel, visibleRows } from "./panel.js";
|
|
22
|
-
import { planDiagramsMarkdown, planTranscriptMarkdown } from "./plan-review.js";
|
|
22
|
+
import { planCriteriaMarkdown, planDiagramsMarkdown, planTranscriptMarkdown, } from "./plan-review.js";
|
|
23
23
|
import { summarizeAnswers } from "./questions.js";
|
|
24
24
|
import { InteractiveQueue } from "./queue.js";
|
|
25
25
|
import { settingsMenuRows } from "./settings.js";
|
|
@@ -730,6 +730,13 @@ export async function runTui(options) {
|
|
|
730
730
|
reasoning: "",
|
|
731
731
|
done: true,
|
|
732
732
|
});
|
|
733
|
+
if (plan.acceptanceCriteria?.length)
|
|
734
|
+
push({
|
|
735
|
+
kind: "assistant",
|
|
736
|
+
text: planCriteriaMarkdown(plan.acceptanceCriteria),
|
|
737
|
+
reasoning: "",
|
|
738
|
+
done: true,
|
|
739
|
+
});
|
|
733
740
|
if (plan.diagrams?.length) {
|
|
734
741
|
// The folder is resolved from the store, never from the model.
|
|
735
742
|
const record = plan.artifact ? app.artifacts.get(plan.artifact.id) : undefined;
|
|
@@ -996,6 +1003,10 @@ export async function runTui(options) {
|
|
|
996
1003
|
applied(outcome, done);
|
|
997
1004
|
return plan.action === "resume" ? kickGoal() : undefined;
|
|
998
1005
|
}
|
|
1006
|
+
case "override": {
|
|
1007
|
+
applied(app.goals.overrideComplete(root, { reason: plan.reason }), "Goal completed by your override; the reason is kept with the goal.");
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
999
1010
|
case "edit": {
|
|
1000
1011
|
applied(app.goals.edit(root, plan.objective), "Goal objective updated.");
|
|
1001
1012
|
return;
|
|
@@ -2644,7 +2655,7 @@ export async function runTui(options) {
|
|
|
2644
2655
|
"- `/init` (above) writes AGENTS.md; the shell command `alisio setup` only scaffolds `.alisio/config.json`",
|
|
2645
2656
|
"- `/plugin.id:name args` or `/command plugin.id:name args` — run a plugin command",
|
|
2646
2657
|
"",
|
|
2647
|
-
"**Keys**: Enter send · Shift+Enter / Alt+Enter / Ctrl+J newline · Tab complete · ↑↓ history · Esc interrupt · Ctrl+C clear input (twice to exit) · Ctrl+D exit on empty input · c / y copy last response (empty input) · x expand/collapse latest thought/tool batch/output (empty input) · click a collapsible header row to toggle it · PgUp/PgDn or mouse wheel scroll · Ctrl+X agent panel · Ctrl+B background running agents · Shift+Tab cycle the main agents (build, plan, your own)",
|
|
2658
|
+
"**Keys**: Enter send · Shift+Enter / Alt+Enter / Ctrl+J newline · Tab complete · ↑↓ history · Esc interrupt · Ctrl+C clear input (twice to exit) · Ctrl+D exit on empty input · c / y copy last response (empty input) · x expand/collapse latest thought/tool batch/output (empty input) · click a collapsible header row to toggle it · PgUp/PgDn or mouse wheel scroll (Ctrl+Home / Ctrl+End jump to the top / bottom) · Ctrl+X agent panel · Ctrl+B background running agents · Shift+Tab cycle the main agents (build, plan, your own)",
|
|
2648
2659
|
`**Paste**: multi-line text pastes as one block automatically · Ctrl+V attach a clipboard image (PNG/JPEG/GIF/WebP, up to ${(MAX_IMAGE_BYTES / (1024 * 1024)).toFixed(0)} MB, up to ${MAX_ATTACHMENTS_PER_MESSAGE} per message) · Ctrl+R remove the last attached image`,
|
|
2649
2660
|
].join("\n");
|
|
2650
2661
|
// `/btw` side questions: answered in the picker slot, never appended to the transcript. One
|
|
@@ -2807,6 +2818,17 @@ export async function runTui(options) {
|
|
|
2807
2818
|
return showTasks();
|
|
2808
2819
|
case "goal":
|
|
2809
2820
|
return goalCommand(parsed.args);
|
|
2821
|
+
case "proof": {
|
|
2822
|
+
const { runProofCommand } = await import("./proof.js");
|
|
2823
|
+
const { sessionPublisher } = await import("../proof.js");
|
|
2824
|
+
const proofSession = goalRoot();
|
|
2825
|
+
const result = await runProofCommand(parsed.args, {
|
|
2826
|
+
proof: app.proof,
|
|
2827
|
+
sessionId: proofSession,
|
|
2828
|
+
publisher: sessionPublisher(app, proofSession),
|
|
2829
|
+
});
|
|
2830
|
+
return result.tone === "notice" ? notice(result.text) : info(result.text);
|
|
2831
|
+
}
|
|
2810
2832
|
case "permission": {
|
|
2811
2833
|
const action = parsePermissionCommand(parsed.args);
|
|
2812
2834
|
if (action.type === "menu")
|
package/dist/tui/goal.d.ts
CHANGED
|
@@ -29,6 +29,11 @@ export type GoalPlan = {
|
|
|
29
29
|
} | {
|
|
30
30
|
kind: "act";
|
|
31
31
|
action: "pause" | "resume" | "clear";
|
|
32
|
+
}
|
|
33
|
+
/** The user completes the goal over unmet required Proof criteria. */
|
|
34
|
+
| {
|
|
35
|
+
kind: "override";
|
|
36
|
+
reason: string;
|
|
32
37
|
} | {
|
|
33
38
|
kind: "edit";
|
|
34
39
|
objective: string;
|
package/dist/tui/goal.js
CHANGED
|
@@ -43,6 +43,8 @@ export function planGoalCommand(command, context) {
|
|
|
43
43
|
case "resume":
|
|
44
44
|
case "clear":
|
|
45
45
|
return goal ? { kind: "act", action: command.type } : say(NO_GOAL_TEXT);
|
|
46
|
+
case "override":
|
|
47
|
+
return goal ? { kind: "override", reason: command.reason } : say(NO_GOAL_TEXT);
|
|
46
48
|
case "edit":
|
|
47
49
|
if (!goal)
|
|
48
50
|
return say(NO_GOAL_TEXT);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* {state, effect}, fully testable without a terminal. Esc means "Skip for now" while choosing and
|
|
5
5
|
* "back to the choices" while typing; Enter submits; an empty text never submits.
|
|
6
6
|
*/
|
|
7
|
-
import type { PlanDiagramInfo } from "@alisio/sdk";
|
|
7
|
+
import type { PlanCriterionDraft, PlanDiagramInfo } from "@alisio/sdk";
|
|
8
8
|
import { type ConnectInputState } from "./connect-input.ts";
|
|
9
9
|
export interface PlanReviewOption {
|
|
10
10
|
value: string;
|
|
@@ -61,3 +61,9 @@ export declare function planDiagramsMarkdown(plan: {
|
|
|
61
61
|
/** Display path of the plan folder on disk (absent when the plan could not be saved). */
|
|
62
62
|
folder?: string;
|
|
63
63
|
}): string;
|
|
64
|
+
/**
|
|
65
|
+
* The acceptance criteria the plan proposes (Proof), as Markdown for the transcript. The terminal
|
|
66
|
+
* keeps the review all-or-nothing: approving activates every one of them (`/proof` edits or marks
|
|
67
|
+
* them afterwards); the web review lets the user keep, edit or reject each one first.
|
|
68
|
+
*/
|
|
69
|
+
export declare function planCriteriaMarkdown(drafts: readonly PlanCriterionDraft[] | undefined): string;
|
package/dist/tui/plan-review.js
CHANGED
|
@@ -108,3 +108,27 @@ export function planDiagramsMarkdown(plan) {
|
|
|
108
108
|
: "The plan folder could not be saved, so these diagrams are only shown here.",
|
|
109
109
|
].join("\n\n");
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* The acceptance criteria the plan proposes (Proof), as Markdown for the transcript. The terminal
|
|
113
|
+
* keeps the review all-or-nothing: approving activates every one of them (`/proof` edits or marks
|
|
114
|
+
* them afterwards); the web review lets the user keep, edit or reject each one first.
|
|
115
|
+
*/
|
|
116
|
+
export function planCriteriaMarkdown(drafts) {
|
|
117
|
+
if (!drafts?.length)
|
|
118
|
+
return "";
|
|
119
|
+
const lines = drafts.map((draft, index) => {
|
|
120
|
+
const how = draft.verifier?.kind === "command"
|
|
121
|
+
? ` · verified by \`${draft.verifier.command}\``
|
|
122
|
+
: draft.verifier?.kind === "coverage"
|
|
123
|
+
? ` · coverage ≥ ${draft.verifier.minPercent}% by \`${draft.verifier.command}\``
|
|
124
|
+
: draft.verifier?.kind === "manual"
|
|
125
|
+
? " · manual review"
|
|
126
|
+
: "";
|
|
127
|
+
return `${index + 1}. ${draft.text}${draft.required ? " (required)" : ""}${how}`;
|
|
128
|
+
});
|
|
129
|
+
return [
|
|
130
|
+
`**Acceptance criteria (${drafts.length})** · proposed by the plan`,
|
|
131
|
+
...lines,
|
|
132
|
+
"Approving the plan activates all of them as acceptance criteria (use `/proof` to edit or mark them later, or review them one by one in the web UI).",
|
|
133
|
+
].join("\n\n");
|
|
134
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/proof` in the terminal (specs/011 phase 5). It reads the same ledger as the web card and the
|
|
3
|
+
* `--json` event (`renderProofText` is the one formatter) and carries the user's own actions: add,
|
|
4
|
+
* edit, mark required, mark not applicable (with a reason), export. Criteria are addressed by
|
|
5
|
+
* their position in the list shown. Everything here is the USER acting; a tool result or the model
|
|
6
|
+
* never reaches this module.
|
|
7
|
+
*/
|
|
8
|
+
import { type ProofStore } from "@alisio/core";
|
|
9
|
+
import type { ArtifactPublisher } from "@alisio/sdk";
|
|
10
|
+
export declare const PROOF_USAGE = "Usage: /proof | add [required] <text> [-- <command>] | edit <n> <text> | required <n> | optional <n> | na <n> <reason> | export | help";
|
|
11
|
+
export declare const PROOF_HELP: string;
|
|
12
|
+
export type ProofCommand = {
|
|
13
|
+
type: "show";
|
|
14
|
+
} | {
|
|
15
|
+
type: "help";
|
|
16
|
+
} | {
|
|
17
|
+
type: "add";
|
|
18
|
+
text: string;
|
|
19
|
+
command?: string;
|
|
20
|
+
required: boolean;
|
|
21
|
+
} | {
|
|
22
|
+
type: "edit";
|
|
23
|
+
index: number;
|
|
24
|
+
text: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: "required";
|
|
27
|
+
index: number;
|
|
28
|
+
required: boolean;
|
|
29
|
+
} | {
|
|
30
|
+
type: "na";
|
|
31
|
+
index: number;
|
|
32
|
+
reason: string;
|
|
33
|
+
} | {
|
|
34
|
+
type: "export";
|
|
35
|
+
} | {
|
|
36
|
+
type: "error";
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
39
|
+
export declare function parseProofCommand(args: string): ProofCommand;
|
|
40
|
+
export interface ProofCommandEnv {
|
|
41
|
+
proof: ProofStore;
|
|
42
|
+
sessionId: string;
|
|
43
|
+
/** Publishes the export; absent when the session cannot publish artifacts. */
|
|
44
|
+
publisher?: Pick<ArtifactPublisher, "publishText">;
|
|
45
|
+
}
|
|
46
|
+
export interface ProofCommandResult {
|
|
47
|
+
text: string;
|
|
48
|
+
tone: "info" | "notice";
|
|
49
|
+
}
|
|
50
|
+
/** Runs a typed `/proof …`; every failure is a notice, never an exception. */
|
|
51
|
+
export declare function runProofCommand(args: string, env: ProofCommandEnv): Promise<ProofCommandResult>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/proof` in the terminal (specs/011 phase 5). It reads the same ledger as the web card and the
|
|
3
|
+
* `--json` event (`renderProofText` is the one formatter) and carries the user's own actions: add,
|
|
4
|
+
* edit, mark required, mark not applicable (with a reason), export. Criteria are addressed by
|
|
5
|
+
* their position in the list shown. Everything here is the USER acting; a tool result or the model
|
|
6
|
+
* never reaches this module.
|
|
7
|
+
*/
|
|
8
|
+
import { parseCriteriaInput, publishProofExport, renderProofText, } from "@alisio/core";
|
|
9
|
+
export const PROOF_USAGE = "Usage: /proof | add [required] <text> [-- <command>] | edit <n> <text> | required <n> | optional <n> | na <n> <reason> | export | help";
|
|
10
|
+
export const PROOF_HELP = [
|
|
11
|
+
"**/proof** shows the acceptance criteria of this session and the evidence behind each one.",
|
|
12
|
+
"",
|
|
13
|
+
"- `/proof` lists them: `verified` needs evidence of a command that really ran; `not_checked` has none yet.",
|
|
14
|
+
"- `/proof add [required] <text> [-- <command>]` adds a criterion; with a command it is verified when the agent runs exactly that command and it succeeds.",
|
|
15
|
+
"- `/proof edit <n> <text>`, `/proof required <n>`, `/proof optional <n>` change the criterion at that position.",
|
|
16
|
+
"- `/proof na <n> <reason>` marks it not applicable (only you can; the reason is kept).",
|
|
17
|
+
"- `/proof export` publishes `proof.md` and `proof.json` (`proof/v1`) as session artifacts.",
|
|
18
|
+
"",
|
|
19
|
+
"Proof records operational evidence; it does not prove that the code is correct.",
|
|
20
|
+
].join("\n");
|
|
21
|
+
const bad = (message) => ({ type: "error", message });
|
|
22
|
+
const position = (word) => word && /^[1-9]\d*$/.test(word) ? Number(word) : undefined;
|
|
23
|
+
export function parseProofCommand(args) {
|
|
24
|
+
const text = args.trim();
|
|
25
|
+
if (!text)
|
|
26
|
+
return { type: "show" };
|
|
27
|
+
const [verb = "", ...rest] = text.split(/\s+/);
|
|
28
|
+
const tail = (skip) => text.split(/\s+/).slice(skip).join(" ").trim();
|
|
29
|
+
switch (verb.toLowerCase()) {
|
|
30
|
+
case "help":
|
|
31
|
+
return { type: "help" };
|
|
32
|
+
case "export":
|
|
33
|
+
return { type: "export" };
|
|
34
|
+
case "add": {
|
|
35
|
+
let body = tail(1);
|
|
36
|
+
let required = false;
|
|
37
|
+
if (/^required(\s|$)/i.test(body)) {
|
|
38
|
+
required = true;
|
|
39
|
+
body = body.replace(/^required\s*/i, "");
|
|
40
|
+
}
|
|
41
|
+
const split = body.split(/\s+--\s+/);
|
|
42
|
+
const statement = (split[0] ?? "").trim();
|
|
43
|
+
const command = split.slice(1).join(" -- ").trim();
|
|
44
|
+
if (!statement)
|
|
45
|
+
return bad("/proof add needs the criterion text.");
|
|
46
|
+
return { type: "add", text: statement, ...(command ? { command } : {}), required };
|
|
47
|
+
}
|
|
48
|
+
case "edit": {
|
|
49
|
+
const index = position(rest[0]);
|
|
50
|
+
const wording = tail(2);
|
|
51
|
+
if (index === undefined || !wording)
|
|
52
|
+
return bad("/proof edit needs a position and the text.");
|
|
53
|
+
return { type: "edit", index, text: wording };
|
|
54
|
+
}
|
|
55
|
+
case "required":
|
|
56
|
+
case "optional": {
|
|
57
|
+
const index = position(rest[0]);
|
|
58
|
+
if (index === undefined)
|
|
59
|
+
return bad(`/proof ${verb} needs a position.`);
|
|
60
|
+
return { type: "required", index, required: verb.toLowerCase() === "required" };
|
|
61
|
+
}
|
|
62
|
+
case "na": {
|
|
63
|
+
const index = position(rest[0]);
|
|
64
|
+
const reason = tail(2);
|
|
65
|
+
if (index === undefined || !reason)
|
|
66
|
+
return bad("/proof na needs a position and a reason.");
|
|
67
|
+
return { type: "na", index, reason };
|
|
68
|
+
}
|
|
69
|
+
default:
|
|
70
|
+
return bad(`Unknown /proof verb "${verb}".`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Runs a typed `/proof …`; every failure is a notice, never an exception. */
|
|
74
|
+
export async function runProofCommand(args, env) {
|
|
75
|
+
const command = parseProofCommand(args);
|
|
76
|
+
const notice = (text) => ({ text, tone: "notice" });
|
|
77
|
+
const show = () => ({
|
|
78
|
+
text: renderProofText(env.proof.view(env.sessionId)),
|
|
79
|
+
tone: "info",
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
switch (command.type) {
|
|
83
|
+
case "error":
|
|
84
|
+
return notice(`${command.message}\n${PROOF_USAGE}`);
|
|
85
|
+
case "help":
|
|
86
|
+
return { text: PROOF_HELP, tone: "info" };
|
|
87
|
+
case "show":
|
|
88
|
+
return show();
|
|
89
|
+
case "export": {
|
|
90
|
+
if (!env.publisher)
|
|
91
|
+
return notice("This session cannot publish artifacts.");
|
|
92
|
+
await publishProofExport(env.publisher, env.proof.view(env.sessionId));
|
|
93
|
+
return notice("Published proof.md and proof.json (proof/v1) as session artifacts.");
|
|
94
|
+
}
|
|
95
|
+
case "add": {
|
|
96
|
+
const parsed = parseCriteriaInput([
|
|
97
|
+
{
|
|
98
|
+
text: command.text,
|
|
99
|
+
required: command.required,
|
|
100
|
+
...(command.command ? { verify: command.command } : {}),
|
|
101
|
+
},
|
|
102
|
+
], { origin: "user", defaultRequired: false });
|
|
103
|
+
const criterion = parsed.criteria[0];
|
|
104
|
+
if (!criterion)
|
|
105
|
+
return notice(parsed.errors[0]?.reason ?? "Invalid criterion.");
|
|
106
|
+
env.proof.addCriterion({ sessionId: env.sessionId, ...criterion });
|
|
107
|
+
return show();
|
|
108
|
+
}
|
|
109
|
+
default: {
|
|
110
|
+
const target = env.proof.criteria(env.sessionId)[command.index - 1];
|
|
111
|
+
if (!target)
|
|
112
|
+
return notice(`There is no criterion ${command.index}.`);
|
|
113
|
+
if (command.type === "na")
|
|
114
|
+
env.proof.markNotApplicable(target.criterionId, command.reason);
|
|
115
|
+
else
|
|
116
|
+
env.proof.updateCriterion(target.criterionId, {
|
|
117
|
+
...(command.type === "edit" ? { text: command.text } : { required: command.required }),
|
|
118
|
+
});
|
|
119
|
+
return show();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return notice(error instanceof Error ? error.message : String(error));
|
|
125
|
+
}
|
|
126
|
+
}
|
package/dist/tui/state.js
CHANGED
|
@@ -857,6 +857,12 @@ function reduceEventItems(state, event) {
|
|
|
857
857
|
}
|
|
858
858
|
return ended;
|
|
859
859
|
}
|
|
860
|
+
case "proof_divergence":
|
|
861
|
+
// D8: the claim is only shown; no status changes and nothing is corrected in silence.
|
|
862
|
+
return addItem(state, {
|
|
863
|
+
kind: "notice",
|
|
864
|
+
text: `Proof: "${oneLine(String(d.claim ?? ""))}" has no backing evidence (${String(d.actual ?? "not_checked")}). /proof shows the ledger.`,
|
|
865
|
+
});
|
|
860
866
|
case "truncation_recovery":
|
|
861
867
|
return { ...state, recovery: { attempt: Number(d.attempt) || 1, of: Number(d.of) || 1 } };
|
|
862
868
|
case "request_retry":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/alisio-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Alisio: an extensible, provider-agnostic coding-agent harness for your terminal. TUI, OpenAI-compatible providers, permissioned local tools, context compaction, persistent memory and a typed plugin SDK.",
|
|
5
5
|
"author": "Gustavo Gutiérrez",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
"alisio": "./dist/main.js"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@alisio/core": "0.
|
|
49
|
-
"@alisio/plugin-memory": "0.
|
|
50
|
-
"@alisio/plugin-openai-compatible": "0.
|
|
51
|
-
"@alisio/plugin-subagents": "0.
|
|
52
|
-
"@alisio/sdk": "0.
|
|
53
|
-
"@alisio/server": "0.
|
|
54
|
-
"@earendil-works/pi-tui": "
|
|
48
|
+
"@alisio/core": "0.8.0",
|
|
49
|
+
"@alisio/plugin-memory": "0.8.0",
|
|
50
|
+
"@alisio/plugin-openai-compatible": "0.8.0",
|
|
51
|
+
"@alisio/plugin-subagents": "0.8.0",
|
|
52
|
+
"@alisio/sdk": "0.8.0",
|
|
53
|
+
"@alisio/server": "0.8.0",
|
|
54
|
+
"@earendil-works/pi-tui": "1.2.0",
|
|
55
55
|
"commander": "15.0.0"
|
|
56
56
|
},
|
|
57
57
|
"publishConfig": {
|