@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.
Files changed (64) hide show
  1. package/dist/application.d.ts +15 -0
  2. package/dist/application.js +38 -3
  3. package/dist/changelog/data.js +23 -0
  4. package/dist/commands/builtins.js +7 -0
  5. package/dist/config.d.ts +30 -0
  6. package/dist/config.js +32 -0
  7. package/dist/core/runner.d.ts +4 -0
  8. package/dist/core/runner.js +57 -1
  9. package/dist/eval/engine.d.ts +1 -0
  10. package/dist/eval/engine.js +3 -0
  11. package/dist/eval/secrets.d.ts +2 -0
  12. package/dist/eval/secrets.js +10 -0
  13. package/dist/goal/command.d.ts +7 -2
  14. package/dist/goal/command.js +10 -2
  15. package/dist/goal/format.js +2 -0
  16. package/dist/goal/machine.d.ts +50 -3
  17. package/dist/goal/machine.js +80 -2
  18. package/dist/goal/service.d.ts +22 -2
  19. package/dist/goal/service.js +94 -7
  20. package/dist/goal/store.js +22 -3
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.js +2 -1
  23. package/dist/plan/criteria.d.ts +30 -0
  24. package/dist/plan/criteria.js +256 -0
  25. package/dist/plan/exit-plan.d.ts +8 -0
  26. package/dist/plan/exit-plan.js +89 -8
  27. package/dist/plan/manifest.d.ts +4 -2
  28. package/dist/plan/manifest.js +6 -1
  29. package/dist/plan/state.d.ts +13 -2
  30. package/dist/plan/state.js +7 -1
  31. package/dist/plugins/host.d.ts +7 -0
  32. package/dist/plugins/host.js +15 -0
  33. package/dist/proof/command.d.ts +24 -0
  34. package/dist/proof/command.js +109 -0
  35. package/dist/proof/coverage.d.ts +40 -0
  36. package/dist/proof/coverage.js +102 -0
  37. package/dist/proof/divergence.d.ts +24 -0
  38. package/dist/proof/divergence.js +120 -0
  39. package/dist/proof/export.d.ts +65 -0
  40. package/dist/proof/export.js +144 -0
  41. package/dist/proof/index.d.ts +12 -0
  42. package/dist/proof/index.js +12 -0
  43. package/dist/proof/ledger.d.ts +38 -0
  44. package/dist/proof/ledger.js +57 -0
  45. package/dist/proof/plugin-verifiers.d.ts +23 -0
  46. package/dist/proof/plugin-verifiers.js +39 -0
  47. package/dist/proof/recorder.d.ts +69 -0
  48. package/dist/proof/recorder.js +347 -0
  49. package/dist/proof/scope.d.ts +12 -0
  50. package/dist/proof/scope.js +121 -0
  51. package/dist/proof/store.d.ts +52 -0
  52. package/dist/proof/store.js +192 -0
  53. package/dist/proof/tree.d.ts +6 -0
  54. package/dist/proof/tree.js +78 -0
  55. package/dist/proof/types.d.ts +21 -0
  56. package/dist/proof/types.js +1 -0
  57. package/dist/proof/verifiers.d.ts +46 -0
  58. package/dist/proof/verifiers.js +176 -0
  59. package/dist/proof/verify.d.ts +36 -0
  60. package/dist/proof/verify.js +171 -0
  61. package/dist/runtime/store.js +60 -0
  62. package/dist/tools/goal.d.ts +5 -1
  63. package/dist/tools/goal.js +35 -4
  64. package/package.json +2 -2
@@ -44,11 +44,46 @@ function moveTo(goal, now, status, reason, extra = {}) {
44
44
  ...extra,
45
45
  }, true);
46
46
  }
47
- const refuse = (code, message) => ({
47
+ const refuse = (code, message, unmet) => ({
48
48
  ok: false,
49
49
  code,
50
50
  message,
51
+ ...(unmet ? { unmet } : {}),
51
52
  });
53
+ /** The criteria that apply to a goal: its own and the session-wide ones (no `goalId`). */
54
+ export const criteriaForGoal = (ledger, goalId) => ledger.criteria.filter((c) => c.goalId === undefined || c.goalId === goalId);
55
+ /**
56
+ * Required criteria that are `failed` or `not_checked`. `manual_review` and `not_applicable` do
57
+ * not block (O6 names only the first two; a manual review is the user's own call).
58
+ */
59
+ export function unmetRequired(criteria) {
60
+ const unmet = [];
61
+ for (const c of criteria)
62
+ if (c.required && (c.status === "failed" || c.status === "not_checked"))
63
+ unmet.push({ criterionId: c.criterionId, text: c.text, status: c.status });
64
+ return unmet;
65
+ }
66
+ const proofInfo = (proof, unmet, override) => ({
67
+ criteria: proof.criteria.map((c) => ({
68
+ criterionId: c.criterionId,
69
+ required: c.required,
70
+ status: c.status,
71
+ })),
72
+ unmet: unmet.map((u) => u.criterionId),
73
+ linkedEvidence: [...new Set(proof.linkedEvidence)],
74
+ ...(override ? { override } : {}),
75
+ });
76
+ const UNMET_LISTED = 5;
77
+ const unmetMessage = (unmet) => {
78
+ const listed = unmet
79
+ .slice(0, UNMET_LISTED)
80
+ .map((u) => `"${clip(u.text, 80)}" (${u.status})`)
81
+ .join(", ");
82
+ const more = unmet.length > UNMET_LISTED ? ` and ${unmet.length - UNMET_LISTED} more` : "";
83
+ return (`Completion refused: required acceptance criteria are not verified: ${listed}${more}. ` +
84
+ "Run their verifiers and report again, or report the goal blocked. Only the user can " +
85
+ "override this.");
86
+ };
52
87
  /** A new goal (the user's `/goal <objective>`). The caller supplies the id and the limits. */
53
88
  export function newGoal(input) {
54
89
  return {
@@ -146,17 +181,59 @@ export function userBudget(goal, now, tokens, ownerPid) {
146
181
  * The model reports the goal complete. Evidence is mandatory, and only an `active` goal can be
147
182
  * completed (a goal the user paused stays paused).
148
183
  */
149
- export function modelComplete(goal, now, report) {
184
+ export function modelComplete(goal, now, report, proof) {
150
185
  if (goal.status !== "active")
151
186
  return refuse("not_active", `The goal is ${goal.status}; only an active goal can be completed.`);
152
187
  if (!report.evidence.length)
153
188
  return refuse("not_allowed", "Completion needs evidence (files, tests, logs or commands).");
189
+ // O6: with criteria the ledger decides, not the model's declaration (free text backs nothing).
190
+ const unmet = proof ? unmetRequired(proof.criteria) : [];
191
+ if (unmet.length)
192
+ return refuse("not_allowed", unmetMessage(unmet), unmet);
154
193
  return {
155
194
  ok: true,
156
195
  goal: moveTo(goal, now, "complete", "model_complete", {
157
196
  summary: report.summary,
158
197
  evidence: report.evidence,
159
198
  inflight: goal.inflight,
199
+ ...(proof?.criteria.length ? { proof: proofInfo(proof, unmet) } : {}),
200
+ }),
201
+ };
202
+ }
203
+ /** Different runs that may be refused a completion over the ledger before the goal pauses. */
204
+ export const COMPLETION_REFUSAL_LIMIT = 3;
205
+ /**
206
+ * A model that keeps reporting `complete` over unmet required criteria, run after run, is not
207
+ * making progress: the goal pauses as `no_progress` (the user resumes or overrides it). The caller
208
+ * counts the refusals; the in-flight claim stays so the running turn still settles.
209
+ */
210
+ export function pauseForRefusedCompletions(goal, now) {
211
+ return moveTo(goal, now, "paused", "no_progress", {
212
+ detail: "completion_refused",
213
+ inflight: goal.inflight,
214
+ });
215
+ }
216
+ /** The longest override reason kept. */
217
+ export const GOAL_OVERRIDE_REASON_MAX = 500;
218
+ /**
219
+ * The USER completes a goal over unmet required criteria (O6). It exists only to bypass that
220
+ * gate: with nothing unmet there is nothing to override. Only a goal that can still be worked
221
+ * on (`active`, `paused`, `blocked`) can be overridden; `budget_limited` and `complete` are final
222
+ * (raise the budget first). The in-flight claim stays, as for a model completion: the running
223
+ * turn still settles. The caller (the service) has already checked that a person asked for it.
224
+ */
225
+ export function userOverrideComplete(goal, now, input) {
226
+ if (goal.status !== "active" && goal.status !== "paused" && goal.status !== "blocked")
227
+ return refuse("not_allowed", `The goal is ${goal.status}; only an active, paused or blocked goal can be overridden.`);
228
+ const unmet = unmetRequired(input.proof.criteria);
229
+ if (!unmet.length)
230
+ return refuse("not_allowed", "Nothing to override: no required criterion is failed or not checked.");
231
+ const reason = clip(input.reason.trim(), GOAL_OVERRIDE_REASON_MAX);
232
+ return {
233
+ ok: true,
234
+ goal: moveTo(goal, now, "complete", "user_override", {
235
+ proof: proofInfo(input.proof, unmet, { reason, at: now }),
236
+ inflight: goal.inflight,
160
237
  }),
161
238
  };
162
239
  }
@@ -336,6 +413,7 @@ export function toGoalInfo(goal) {
336
413
  maxWallMs: goal.maxWallMs,
337
414
  ...(goal.summary ? { summary: goal.summary } : {}),
338
415
  ...(goal.evidence?.length ? { evidence: goal.evidence } : {}),
416
+ ...(goal.proof ? { proof: goal.proof } : {}),
339
417
  actions: userActions(goal.status),
340
418
  createdAt: goal.createdAt,
341
419
  updatedAt: goal.updatedAt,
@@ -1,6 +1,6 @@
1
- import type { GoalEvidence, GoalInfo, GoalWaiting } from "@alisio/sdk";
1
+ import type { GoalEvidence, GoalInfo, GoalWaiting, ProofLedger } from "@alisio/sdk";
2
2
  import { type GoalBlocker, type GoalHostState } from "./blockers.ts";
3
- import { type GoalLimits, type GoalLog, type GoalRecord, type GoalRunOutcome } from "./machine.ts";
3
+ import { type GoalLimits, type GoalLog, type GoalRecord, type GoalRunOutcome, type GoalUnmet } from "./machine.ts";
4
4
  import { type GoalPrompt } from "./prompts.ts";
5
5
  import { type GoalExpectation, type GoalStore } from "./store.ts";
6
6
  /** `config.goal`, as the service reads it (live: a settings change applies to the next call). */
@@ -58,6 +58,13 @@ export interface GoalServiceOptions {
58
58
  onChange?: (sessionId: string, goal: GoalRecord | undefined) => void;
59
59
  /** Breaker observations (counts only; never the objective or the reply). */
60
60
  onLog?: (sessionId: string, log: GoalLog) => void;
61
+ /**
62
+ * Where the Proof ledger comes from (`ProofStore` fits). Without it criteria do not exist for
63
+ * the goal: completion behaves as in 0.7.x and a declared `evidenceId` is dropped.
64
+ */
65
+ proof?: {
66
+ ledger(sessionId: string): ProofLedger;
67
+ };
61
68
  }
62
69
  export declare class GoalService {
63
70
  private readonly options;
@@ -93,6 +100,10 @@ export declare class GoalService {
93
100
  next(sessionId: string, view: GoalHostView): GoalDecision;
94
101
  /** The claimed continuation could not start: give the claim back (the next call may retry). */
95
102
  abandon(sessionId: string, requestId: string, kind: GoalPrompt["kind"]): void;
103
+ /** Runs whose `complete` the ledger refused, per goal (memory only: a restart pauses goals). */
104
+ private readonly refusals;
105
+ /** Records a refused completion of `runId`; true once enough different runs were refused. */
106
+ private refusedRuns;
96
107
  /** `update_goal` (the model): the only transitions the model can cause. */
97
108
  report(sessionId: string, runId: string | undefined, report: {
98
109
  status: "complete" | "blocked";
@@ -107,7 +118,16 @@ export declare class GoalService {
107
118
  ok: false;
108
119
  code: GoalErrorCode;
109
120
  message: string;
121
+ unmet?: GoalUnmet[];
110
122
  };
123
+ /**
124
+ * The USER completes the goal over unmet required criteria (O6). A separate method on purpose:
125
+ * `report` is the model's only door and has no way to carry an override, so the model cannot
126
+ * reach this path whatever it sends. Hosts call it from a user action only (Phase 5 surfaces).
127
+ */
128
+ overrideComplete(sessionId: string, input: {
129
+ reason: string;
130
+ }, expect?: GoalExpectation): GoalOutcome;
111
131
  /** Startup: goals whose driver process is gone pause with reason `restart`. */
112
132
  recover(): GoalRecord[];
113
133
  }
@@ -18,7 +18,7 @@
18
18
  import { randomUUID } from "node:crypto";
19
19
  import { goalBlocker, waitingOf } from "./blockers.js";
20
20
  import { objectiveError } from "./command.js";
21
- import { checkCaps, DEFAULT_GOAL_LIMITS, modelBlocked, modelComplete, newGoal, settleRun, toGoalInfo, userBudget, userEdit, userPause, userResume, } from "./machine.js";
21
+ import { COMPLETION_REFUSAL_LIMIT, checkCaps, criteriaForGoal, DEFAULT_GOAL_LIMITS, modelBlocked, modelComplete, newGoal, pauseForRefusedCompletions, settleRun, toGoalInfo, userBudget, userEdit, userOverrideComplete, userPause, userResume, } from "./machine.js";
22
22
  import { buildGoalPrompt } from "./prompts.js";
23
23
  export const settingsToLimits = (settings) => ({
24
24
  maxTurns: settings.maxTurns,
@@ -27,6 +27,24 @@ export const settingsToLimits = (settings) => ({
27
27
  noToolTurnsLimit: settings.noToolTurnsLimit,
28
28
  blockedRepeats: settings.blockedRepeats,
29
29
  });
30
+ /**
31
+ * The declared evidence with every `evidenceId` that does not resolve to captured evidence of the
32
+ * session removed (such a declaration is free text and backs nothing), and the ids that resolved.
33
+ */
34
+ function linkEvidence(evidence, ledger) {
35
+ const known = new Set(ledger?.evidence.map((e) => e.evidenceId));
36
+ const linked = [];
37
+ const out = evidence.map((item) => {
38
+ const { evidenceId, ...rest } = item;
39
+ if (evidenceId === undefined)
40
+ return item;
41
+ if (!known.has(evidenceId))
42
+ return rest;
43
+ linked.push(evidenceId);
44
+ return item;
45
+ });
46
+ return { evidence: out, linked };
47
+ }
30
48
  export class GoalService {
31
49
  options;
32
50
  store;
@@ -109,6 +127,9 @@ export class GoalService {
109
127
  if (!this.enabled)
110
128
  return { ok: false, code: "disabled", message: "Goals are turned off (goal.enabled)." };
111
129
  const limits = this.limits();
130
+ const current = this.store.get(sessionId);
131
+ if (current)
132
+ this.refusals.delete(current.goalId);
112
133
  return this.act(sessionId, expect, (goal) => userResume(goal, this.clock(), limits, this.pid));
113
134
  }
114
135
  edit(sessionId, objective, expect) {
@@ -232,17 +253,49 @@ export class GoalService {
232
253
  if (result.ok)
233
254
  this.emit(sessionId, result.goal);
234
255
  }
256
+ /** Runs whose `complete` the ledger refused, per goal (memory only: a restart pauses goals). */
257
+ refusals = new Map();
258
+ /** Records a refused completion of `runId`; true once enough different runs were refused. */
259
+ refusedRuns(goalId, runId) {
260
+ const runs = this.refusals.get(goalId) ?? new Set();
261
+ runs.add(runId);
262
+ this.refusals.set(goalId, runs);
263
+ if (runs.size < COMPLETION_REFUSAL_LIMIT)
264
+ return false;
265
+ this.refusals.delete(goalId);
266
+ return true;
267
+ }
235
268
  /** `update_goal` (the model): the only transitions the model can cause. */
236
269
  report(sessionId, runId, report) {
237
270
  const limits = this.limits();
238
271
  const outcome = this.store.update(sessionId, (goal) => {
272
+ // The ledger is read inside this transaction, so the verdict and the write see one state.
273
+ // Only a completion, or a declaration that links evidence, needs it (D7: no extra reads).
274
+ const needsLedger = report.status === "complete" || report.evidence.some((e) => e.evidenceId !== undefined);
275
+ const ledger = needsLedger ? this.options.proof?.ledger(sessionId) : undefined;
276
+ const linked = linkEvidence(report.evidence, ledger);
239
277
  if (report.status === "complete") {
240
- const result = modelComplete(goal, this.clock(), report);
241
- return result.ok
242
- ? { goal: result.goal, value: { ok: true, stopped: true, needed: undefined } }
243
- : { value: { ok: false, message: result.message } };
278
+ const proof = ledger
279
+ ? { criteria: criteriaForGoal(ledger, goal.goalId), linkedEvidence: linked.linked }
280
+ : undefined;
281
+ const result = modelComplete(goal, this.clock(), { summary: report.summary, evidence: linked.evidence }, proof);
282
+ if (result.ok)
283
+ return { goal: result.goal, value: { ok: true, stopped: true, needed: undefined } };
284
+ // A refusal over the ledger counts per run; the third run that tries pauses the goal.
285
+ if (result.unmet?.length && runId && this.refusedRuns(goal.goalId, runId)) {
286
+ return {
287
+ goal: pauseForRefusedCompletions(goal, this.clock()),
288
+ value: {
289
+ ok: false,
290
+ message: `${result.message} The goal is paused: completion was refused in ${COMPLETION_REFUSAL_LIMIT} different runs. The user decides: resume, or complete it with an override.`,
291
+ unmet: result.unmet,
292
+ paused: true,
293
+ },
294
+ };
295
+ }
296
+ return { value: { ok: false, message: result.message, unmet: result.unmet } };
244
297
  }
245
- const result = modelBlocked(goal, this.clock(), { summary: report.summary, evidence: report.evidence, runId }, limits);
298
+ const result = modelBlocked(goal, this.clock(), { summary: report.summary, evidence: linked.evidence, runId }, limits);
246
299
  return result.ok
247
300
  ? {
248
301
  goal: result.goal.goal,
@@ -253,8 +306,15 @@ export class GoalService {
253
306
  if (!outcome.ok)
254
307
  return { ok: false, code: outcome.code, message: outcome.message };
255
308
  const value = outcome.value;
309
+ if (!value.ok && value.paused)
310
+ this.emit(sessionId, outcome.goal);
256
311
  if (!value.ok)
257
- return { ok: false, code: "not_allowed", message: value.message };
312
+ return {
313
+ ok: false,
314
+ code: "not_allowed",
315
+ message: value.message,
316
+ ...(value.unmet ? { unmet: value.unmet } : {}),
317
+ };
258
318
  this.emit(sessionId, outcome.goal);
259
319
  return {
260
320
  ok: true,
@@ -263,6 +323,33 @@ export class GoalService {
263
323
  ...(value.needed ? { needed: value.needed } : {}),
264
324
  };
265
325
  }
326
+ /**
327
+ * The USER completes the goal over unmet required criteria (O6). A separate method on purpose:
328
+ * `report` is the model's only door and has no way to carry an override, so the model cannot
329
+ * reach this path whatever it sends. Hosts call it from a user action only (Phase 5 surfaces).
330
+ */
331
+ overrideComplete(sessionId, input, expect) {
332
+ const reason = input.reason.trim();
333
+ if (!reason)
334
+ return { ok: false, code: "invalid", message: "An override needs a reason." };
335
+ const outcome = this.store.update(sessionId, (goal) => {
336
+ const ledger = this.options.proof?.ledger(sessionId);
337
+ const result = userOverrideComplete(goal, this.clock(), {
338
+ reason,
339
+ proof: {
340
+ criteria: ledger ? criteriaForGoal(ledger, goal.goalId) : [],
341
+ linkedEvidence: goal.proof?.linkedEvidence ?? [],
342
+ },
343
+ });
344
+ return result.ok ? { goal: result.goal, value: result } : { value: result };
345
+ }, expect);
346
+ if (!outcome.ok)
347
+ return { ok: false, code: outcome.code, message: outcome.message };
348
+ const result = outcome.value;
349
+ if (!result.ok)
350
+ return { ok: false, code: "not_allowed", message: result.message };
351
+ return this.done(sessionId, { ok: true, goal: result.goal });
352
+ }
266
353
  /** Startup: goals whose driver process is gone pause with reason `restart`. */
267
354
  recover() {
268
355
  const paused = this.store.pauseOrphans(this.clock());
@@ -13,6 +13,24 @@ function parseEvidence(raw) {
13
13
  return undefined;
14
14
  }
15
15
  }
16
+ function parseProof(raw) {
17
+ if (typeof raw !== "string" || !raw)
18
+ return undefined;
19
+ try {
20
+ const value = JSON.parse(raw);
21
+ return value && Array.isArray(value.criteria) && Array.isArray(value.unmet)
22
+ ? {
23
+ criteria: value.criteria,
24
+ unmet: value.unmet,
25
+ linkedEvidence: Array.isArray(value.linkedEvidence) ? value.linkedEvidence : [],
26
+ ...(value.override ? { override: value.override } : {}),
27
+ }
28
+ : undefined;
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
16
34
  export function goalFromRow(row) {
17
35
  const status = String(row.status);
18
36
  return {
@@ -26,6 +44,7 @@ export function goalFromRow(row) {
26
44
  ...(parseEvidence(row.evidence)
27
45
  ? { evidence: parseEvidence(row.evidence) }
28
46
  : {}),
47
+ ...(parseProof(row.proof_json) ? { proof: parseProof(row.proof_json) } : {}),
29
48
  epoch: Number(row.epoch ?? 1),
30
49
  ...(num(row.token_budget) !== undefined
31
50
  ? { tokenBudget: num(row.token_budget) }
@@ -55,7 +74,7 @@ export function goalFromRow(row) {
55
74
  const COLUMNS = `session, goal_id, objective, status, reason, detail, summary, evidence, epoch,
56
75
  token_budget, tokens_used, turns_used, max_turns, max_wall_ms, active_ms, no_tool_streak,
57
76
  repeat_streak, last_reply_hash, blocked_streak, blocked_run, continuations, inflight,
58
- last_run_id, kickoff_sent, owner_pid, created_at, updated_at, completed_at`;
77
+ last_run_id, kickoff_sent, owner_pid, created_at, updated_at, completed_at, proof_json`;
59
78
  export class GoalStore {
60
79
  db;
61
80
  constructor(db) {
@@ -89,8 +108,8 @@ export class GoalStore {
89
108
  continuations=excluded.continuations, inflight=excluded.inflight,
90
109
  last_run_id=excluded.last_run_id, kickoff_sent=excluded.kickoff_sent,
91
110
  owner_pid=excluded.owner_pid, updated_at=excluded.updated_at,
92
- completed_at=excluded.completed_at`)
93
- .run(goal.sessionId, goal.goalId, goal.objective, goal.status, goal.reason ?? null, goal.detail ?? null, goal.summary ?? null, goal.evidence?.length ? JSON.stringify(goal.evidence) : null, goal.epoch, goal.tokenBudget ?? null, goal.tokensUsed, goal.turnsUsed, goal.maxTurns, goal.maxWallMs, goal.activeMs, goal.noToolStreak, goal.repeatStreak, goal.lastReplyHash ?? null, goal.blockedStreak, goal.blockedRun ?? null, goal.continuations, goal.inflight ?? null, goal.lastRunId ?? null, goal.kickoffSent ? 1 : 0, goal.ownerPid ?? null, goal.createdAt, goal.updatedAt, goal.completedAt ?? null);
111
+ completed_at=excluded.completed_at, proof_json=excluded.proof_json`)
112
+ .run(goal.sessionId, goal.goalId, goal.objective, goal.status, goal.reason ?? null, goal.detail ?? null, goal.summary ?? null, goal.evidence?.length ? JSON.stringify(goal.evidence) : null, goal.epoch, goal.tokenBudget ?? null, goal.tokensUsed, goal.turnsUsed, goal.maxTurns, goal.maxWallMs, goal.activeMs, goal.noToolStreak, goal.repeatStreak, goal.lastReplyHash ?? null, goal.blockedStreak, goal.blockedRun ?? null, goal.continuations, goal.inflight ?? null, goal.lastRunId ?? null, goal.kickoffSent ? 1 : 0, goal.ownerPid ?? null, goal.createdAt, goal.updatedAt, goal.completedAt ?? null, goal.proof ? JSON.stringify(goal.proof) : null);
94
113
  }
95
114
  /**
96
115
  * Replaces (or creates) the goal of a session. `build` gets the previous goal, if any, so the
package/dist/index.d.ts CHANGED
@@ -70,9 +70,10 @@ export { McpConnector, type McpServerInfo, type McpStatus, type McpToolInfo, } f
70
70
  export { describePermissionStatus, type EffectState, effectState, FULL_ACCESS_WARNING, type ModeFlags, type ModePolicy, modeFromFlags, modeToPreset, PERMISSION_MODE_TABLE, PERMISSION_MODES, PERMISSION_MODES_LOCKED, PERMISSION_USAGE, type PermissionCommand, type PermissionModeLabel, type PermissionModeSpec, parsePermissionCommand, parsePermissionMode, presetToMode, READ_ONLY_POLICY, } from "./permissions/modes.ts";
71
71
  export { DIAGRAM_MAX_BYTES, DIAGRAM_MAX_NODES, DIAGRAM_SYNTAXES, DIAGRAMS_MAX_ALLOWED, mermaidProblem, type PlanDiagram, validateDiagrams, } from "./plan/diagrams.ts";
72
72
  export { buildPlanManifest, parsePlanHeadings, slugifyHeading, } from "./plan/manifest.ts";
73
- export { claimApprovedPlan, currentPlan, discardApprovedPlan, IMPLEMENTATION_AGENT_ID, implementationPrompt, PLAN_CONTEXT_MAX_CHARS, PLAN_OPTIONS, PLAN_QUESTION_ID, PLAN_REVIEW_TITLE, PLAN_TEXT_KEY, type PlanFollowUp, type PlanState, type PlanStatus, planHash, readPlanState, settlePlanRun, withdrawPendingPlan, } from "./plan/state.ts";
73
+ export { claimApprovedPlan, currentPlan, discardApprovedPlan, IMPLEMENTATION_AGENT_ID, implementationPrompt, PLAN_CONTEXT_MAX_CHARS, PLAN_CRITERIA_KEY, PLAN_OPTIONS, PLAN_QUESTION_ID, PLAN_REVIEW_TITLE, PLAN_TEXT_KEY, type PlanFollowUp, type PlanState, type PlanStatus, planHash, readPlanState, settlePlanRun, withdrawPendingPlan, } from "./plan/state.ts";
74
74
  export { discoverPlugins, type PluginEnvironment, PluginHost, type PluginHostOptions, pluginPrefix, type ViewInfo, ViewRunError, } from "./plugins/host.ts";
75
75
  export { type CliInstallOptions, cliInstall, cliUpdateAll, INSTALL_TIMEOUT_MS, type InstallPluginInput, type InstallPluginResult, type InstallRunner, installedGlobalPluginPackages, installedNpmPlugins, installPlugin, isPeerConflict, npmInstallTarget, type PluginSpec, parsePluginSpec, pluginTrustRemark, printInstallResult, registerPluginInstallTool, sanitizeNpmError, updateAllPlugins, } from "./plugins/install.ts";
76
+ export * from "./proof/index.ts";
76
77
  export { ActiveProvider, ProviderRegistry, UnconfiguredProvider } from "./providers/registry.ts";
77
78
  export { availableProviderModels, type ProviderModelCatalog, resolveProviderModel, } from "./providers/routing.ts";
78
79
  export { maskSecret, type ProviderProfile, type ProviderSettings, ProviderSettingsStore, } from "./providers/settings.ts";
package/dist/index.js CHANGED
@@ -69,9 +69,10 @@ export { McpConnector, } from "./mcp/connector.js";
69
69
  export { describePermissionStatus, effectState, FULL_ACCESS_WARNING, modeFromFlags, modeToPreset, PERMISSION_MODE_TABLE, PERMISSION_MODES, PERMISSION_MODES_LOCKED, PERMISSION_USAGE, parsePermissionCommand, parsePermissionMode, presetToMode, READ_ONLY_POLICY, } from "./permissions/modes.js";
70
70
  export { DIAGRAM_MAX_BYTES, DIAGRAM_MAX_NODES, DIAGRAM_SYNTAXES, DIAGRAMS_MAX_ALLOWED, mermaidProblem, validateDiagrams, } from "./plan/diagrams.js";
71
71
  export { buildPlanManifest, parsePlanHeadings, slugifyHeading, } from "./plan/manifest.js";
72
- export { claimApprovedPlan, currentPlan, discardApprovedPlan, IMPLEMENTATION_AGENT_ID, implementationPrompt, PLAN_CONTEXT_MAX_CHARS, PLAN_OPTIONS, PLAN_QUESTION_ID, PLAN_REVIEW_TITLE, PLAN_TEXT_KEY, planHash, readPlanState, settlePlanRun, withdrawPendingPlan, } from "./plan/state.js";
72
+ export { claimApprovedPlan, currentPlan, discardApprovedPlan, IMPLEMENTATION_AGENT_ID, implementationPrompt, PLAN_CONTEXT_MAX_CHARS, PLAN_CRITERIA_KEY, PLAN_OPTIONS, PLAN_QUESTION_ID, PLAN_REVIEW_TITLE, PLAN_TEXT_KEY, planHash, readPlanState, settlePlanRun, withdrawPendingPlan, } from "./plan/state.js";
73
73
  export { discoverPlugins, PluginHost, pluginPrefix, ViewRunError, } from "./plugins/host.js";
74
74
  export { cliInstall, cliUpdateAll, INSTALL_TIMEOUT_MS, installedGlobalPluginPackages, installedNpmPlugins, installPlugin, isPeerConflict, npmInstallTarget, parsePluginSpec, pluginTrustRemark, printInstallResult, registerPluginInstallTool, sanitizeNpmError, updateAllPlugins, } from "./plugins/install.js";
75
+ export * from "./proof/index.js";
75
76
  export { ActiveProvider, ProviderRegistry, UnconfiguredProvider } from "./providers/registry.js";
76
77
  export { availableProviderModels, resolveProviderModel, } from "./providers/routing.js";
77
78
  export { maskSecret, ProviderSettingsStore, } from "./providers/settings.js";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Validation of the acceptance criteria a model proposes in `exit_plan` (specs/011 3.6, O3).
3
+ * Pure: no I/O. The result is a DRAFT: nothing here creates a criterion; the plan decision of
4
+ * the user does (`exit-plan.ts`). The model may only propose verifier kinds Alisio can run today
5
+ * (`command`, `file`, `data-check`, `coverage`, `plugin`, `manual`) and plain workspace-relative scopes.
6
+ */
7
+ import type { PlanCriterionDraft } from "@alisio/sdk";
8
+ export declare const CRITERIA_MAX = 20;
9
+ export declare const CRITERION_TEXT_MAX = 500;
10
+ export interface DroppedCriterion {
11
+ /** Zero-based position in the proposed list. */
12
+ index: number;
13
+ reason: string;
14
+ }
15
+ export interface ValidatedCriteria {
16
+ accepted: PlanCriterionDraft[];
17
+ dropped: DroppedCriterion[];
18
+ }
19
+ /** Turns the model's `acceptanceCriteria` input into drafts, reporting what it could not keep. */
20
+ export declare function validateCriteriaDrafts(raw: unknown): ValidatedCriteria;
21
+ /**
22
+ * Applies the user's per-draft decisions (`plan:criteria`) to the drafts of the plan they approved.
23
+ * `undefined` selection keeps every draft (the plain "approve"). Anything malformed, any index
24
+ * outside the list or an empty edit returns `undefined` for "nothing is activated": a selection
25
+ * the user did not make cleanly never activates a criterion (fail closed).
26
+ */
27
+ export declare function applyCriteriaSelection(drafts: readonly PlanCriterionDraft[], raw: unknown): {
28
+ kept: PlanCriterionDraft[];
29
+ rejected: number;
30
+ } | undefined;