@alisio/alisio-code 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/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
- await app.runner.run(session, template?.text ?? prompt, controller.signal, {
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")
@@ -487,6 +520,68 @@ forge
487
520
  const { printForgeStatus } = await import("./forge.js");
488
521
  await printForgeStatus(options(cmd), !!flags.probe);
489
522
  });
523
+ const evalCommand = program
524
+ .command("eval")
525
+ .description("Reliability lab: run a task suite k times, report pass rates with Wilson intervals, compare reports or config variants, record cassettes and replay them without a model");
526
+ /** `alisio eval` entry points: lazy, and exit codes are returned, not thrown (0 ok, 1 error, 2 replay diverged, 3 incompatible). */
527
+ async function runEval(cmd, action) {
528
+ const api = await import("./eval.js");
529
+ const env = await api.realEvalEnv(async () => options(cmd), async () => cliDefaults());
530
+ process.exitCode = await action(api, env, cmd.optsWithGlobals());
531
+ }
532
+ const evalRunOptions = (command) => command
533
+ .option("--tasks <ids>", "Comma-separated task ids (default: every task of the suite)")
534
+ .option("--k <n>", "Repetitions per task (default 5; below 5 the report warns of low power)")
535
+ .option("--seed <n>", "Seed of the run order; each run gets seed + repetition")
536
+ .option("--mode <mode>", "live (default) or replay (serve the recorded cassettes: no provider, no network; measures the harness, not the model)")
537
+ .option("--cassette-dir <dir>", "Cassette directory (default: <suite>/cassettes; used by --mode replay and by record)")
538
+ .option("--keep", "Keep each run's temporary directory (it prints the paths)")
539
+ .option("--out <dir>", "Write the report files here (default: <state>/eval/<runId>/)")
540
+ .option("--suite-dir <dir>", "A suite, or a directory of suites; third-party code: needs --yes")
541
+ .option("--allow-suite-full", "Let a suite run tasks that ask for the full permission mode")
542
+ .option("--yes", "Confirm running a --suite-dir suite without asking (required if not interactive)")
543
+ .option("--input-rate <usd>", "USD per million input tokens, to estimate cost (needs --output-rate)")
544
+ .option("--output-rate <usd>", "USD per million output tokens (needs --input-rate)");
545
+ evalCommand
546
+ .command("list")
547
+ .description("List the available suites (bundled ones plus --suite-dir)")
548
+ .option("--suite-dir <dir>", "A suite, or a directory of suites")
549
+ .action((_flags, cmd) => runEval(cmd, (api, env, flags) => api.evalList(env, flags)));
550
+ evalRunOptions(evalCommand
551
+ .command("run")
552
+ .description("Run a suite k times (live, or --mode replay from cassettes) and write report.json, report.md and runs.jsonl; --json prints the run records as JSONL and the report last. Exit 0 when it completed (whatever passed), 1 on a usage or infrastructure error, 2 when a replay diverged from its cassette")
553
+ .argument("<suite>")).action((suite, _flags, cmd) => runEval(cmd, (api, env, flags) => api.evalRun(env, flags, suite)));
554
+ evalCommand
555
+ .command("record")
556
+ .description("Run each task of a suite ONCE live and write <suite>/cassettes/<taskId>.cassette.jsonl for `eval run --mode replay`. Refuses (exit 1) a cassette that holds a secret or is over 256 KB (2 MB per directory)")
557
+ .argument("<suite>")
558
+ .option("--tasks <ids>", "Comma-separated task ids (default: every task of the suite)")
559
+ .option("--seed <n>", "Seed of the run order")
560
+ .option("--keep", "Keep each run's temporary directory (it prints the paths)")
561
+ .option("--cassette-dir <dir>", "Write the cassettes here (default: <suite>/cassettes)")
562
+ .option("--suite-dir <dir>", "A suite, or a directory of suites; third-party code: needs --yes")
563
+ .option("--allow-suite-full", "Let a suite run tasks that ask for the full permission mode")
564
+ .option("--yes", "Confirm running a --suite-dir suite without asking (required if not interactive)")
565
+ .action((suite, _flags, cmd) => runEval(cmd, (api, env, flags) => api.evalRecord(env, flags, suite)));
566
+ evalCommand
567
+ .command("compare")
568
+ .description("Compare two reports of the same suite and version; exit 3 when the suites are incompatible")
569
+ .argument("<reportA>", "Baseline report.json (or the directory that holds it)")
570
+ .argument("<reportB>", "Candidate report.json (or the directory that holds it)")
571
+ .option("--md", "Print Markdown (the default; --json prints JSON instead)")
572
+ .action((a, b, _flags, cmd) => runEval(cmd, (api, env, flags) => api.evalCompare(env, flags, a, b)));
573
+ evalRunOptions(evalCommand
574
+ .command("variants")
575
+ .description('Run two or more config variants over the same tasks and seeds and compare them with the first. compare.json: {"suite":"id","variants":[{"id":"baseline","overrides":{}},{"id":"early","overrides":{"compaction.threshold":0.6}}]}')
576
+ .argument("<suite>")
577
+ .requiredOption("--variants <file>", "The compare.json file")).action((suite, _flags, cmd) => runEval(cmd, (api, env, flags) => api.evalVariants(env, flags, suite)));
578
+ evalCommand
579
+ .command("export")
580
+ .description("Write ONE shareable JSON bundle ({schemaVersion, report, runs, markdown}) of a stored run (<state>/eval/<runId>/) with absolute paths replaced by placeholders and endpoint hosts removed from provider ids. Refuses (exit 1) when a secret is found, naming the field; refuses to overwrite FILE without --force. Local only: nothing is uploaded. Scrubbing is pattern-based, not a guarantee")
581
+ .argument("<runId>", "Directory name of the run under <state>/eval/")
582
+ .requiredOption("--out <file>", "The bundle to write")
583
+ .option("--force", "Overwrite FILE if it exists")
584
+ .action((runId, _flags, cmd) => runEval(cmd, (api, env, flags) => api.evalExport(env, flags, runId)));
490
585
  const trust = program.command("trust").description("Inspect or revoke per-directory project trust");
491
586
  trust.command("list").action(async () => {
492
587
  const { listTrust } = await import("@alisio/core");
@@ -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")
@@ -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;
@@ -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":