aiki-cli 0.3.2 → 0.3.5
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/CHANGELOG.md +68 -0
- package/README.md +64 -4
- package/dist/bench/idea-v3-bench.js +104 -5
- package/dist/bench/idea-v3-rating.js +158 -3
- package/dist/cli/bench.js +5 -5
- package/dist/cli/index.js +12 -2
- package/dist/cli/resume.js +20 -0
- package/dist/cli/run.js +75 -4
- package/dist/cli/serve.js +48 -0
- package/dist/config/config.js +7 -2
- package/dist/council/view.js +13 -4
- package/dist/orchestration/auto-profile.js +97 -0
- package/dist/orchestration/claim-groups.js +56 -0
- package/dist/orchestration/context.js +66 -3
- package/dist/orchestration/decision-dossier.js +42 -10
- package/dist/orchestration/decision-graph.js +2 -2
- package/dist/orchestration/engine.js +8 -4
- package/dist/orchestration/evidence-origin.js +17 -0
- package/dist/orchestration/jsonStage.js +47 -5
- package/dist/orchestration/modes.js +4 -2
- package/dist/orchestration/preflight.js +19 -0
- package/dist/orchestration/quick-analysis.js +31 -6
- package/dist/orchestration/sanitize-paths.js +10 -0
- package/dist/orchestration/stages/s10-render.js +31 -7
- package/dist/orchestration/stages/s4-analyze.js +7 -1
- package/dist/orchestration/stages/s4b-challenge.js +97 -0
- package/dist/orchestration/stages/s5-drift.js +13 -3
- package/dist/orchestration/stages/s8-verify.js +44 -7
- package/dist/orchestration/stages/s9-judge.js +20 -5
- package/dist/orchestration/stages/s9b-plan.js +44 -15
- package/dist/orchestration/url-sources.js +21 -0
- package/dist/providers/adapter-core.js +1 -1
- package/dist/providers/claude.js +18 -0
- package/dist/schemas/index.js +46 -0
- package/dist/serve/flight-deck.js +830 -0
- package/dist/serve/followup.js +50 -0
- package/dist/serve/frames.js +168 -0
- package/dist/serve/gates.js +72 -0
- package/dist/serve/projections.js +283 -0
- package/dist/serve/server.js +219 -0
- package/dist/serve/threads.js +145 -0
- package/dist/serve-ui/Five.png +0 -0
- package/dist/serve-ui/app.js +820 -0
- package/dist/serve-ui/index.html +171 -0
- package/dist/serve-ui/workspace.css +662 -0
- package/dist/storage/runs.js +2 -1
- package/dist/workflows/idea-refinement.js +94 -16
- package/package.json +2 -2
|
@@ -39,7 +39,7 @@ export const DEFAULT_DEADLINE_MS = 20 * 60 * 1000; // wall-clock cap
|
|
|
39
39
|
// (2026-07-13) after the spawn timeout became actually enforced and killed a LEGITIMATE deep call: codex's
|
|
40
40
|
// S4 analysis of a hard build case ran ~10 min to a valid output (run 20260713-1341, 13:44→13:54). 900s
|
|
41
41
|
// per attempt covers observed deep work; the wall-clock deadline above remains the outer bound.
|
|
42
|
-
const DEFAULT_CALL_TIMEOUT_MS = 900_000;
|
|
42
|
+
export const DEFAULT_CALL_TIMEOUT_MS = 900_000;
|
|
43
43
|
export class BudgetExceeded extends Error {
|
|
44
44
|
constructor(limit) {
|
|
45
45
|
super(`call budget exhausted (limit ${limit})`);
|
|
@@ -74,6 +74,9 @@ export class RunCtx {
|
|
|
74
74
|
cwd;
|
|
75
75
|
events;
|
|
76
76
|
evidencePack;
|
|
77
|
+
allowBlockedSources;
|
|
78
|
+
urlSources;
|
|
79
|
+
isAuto;
|
|
77
80
|
budget;
|
|
78
81
|
calls = [];
|
|
79
82
|
/** Logical provider-call stages, including resume cache hits. Used by bounded protocol caps. */
|
|
@@ -87,6 +90,8 @@ export class RunCtx {
|
|
|
87
90
|
deadlineAt;
|
|
88
91
|
now;
|
|
89
92
|
replay; // resume replay cache (V6.3)
|
|
93
|
+
autoDecision; // v7 Phase B/D: routing record + output escalation receipt
|
|
94
|
+
fastPathActive;
|
|
90
95
|
seq = 0; // monotonic per-call counter for raw/ filenames (== budget.used on a fresh run)
|
|
91
96
|
constructor(opts) {
|
|
92
97
|
this.runId = opts.runId;
|
|
@@ -97,6 +102,12 @@ export class RunCtx {
|
|
|
97
102
|
this.cwd = opts.cwd;
|
|
98
103
|
this.events = opts.events;
|
|
99
104
|
this.evidencePack = opts.evidencePack;
|
|
105
|
+
this.allowBlockedSources = opts.allowBlockedSources;
|
|
106
|
+
this.urlSources = opts.urlSources;
|
|
107
|
+
this.isAuto = opts.autoDecision !== undefined;
|
|
108
|
+
this.fastPathActive = this.mode === 'quick'
|
|
109
|
+
&& opts.autoDecision?.resolved === 'quick'
|
|
110
|
+
&& opts.autoDecision.fast_path === true;
|
|
100
111
|
this.budget = { limit: opts.budget ?? defaultBudgetFor(opts.workflow, this.mode), used: 0 };
|
|
101
112
|
this.handles = new Map(opts.handles.map((h) => [h.id, h]));
|
|
102
113
|
this.signal = opts.signal;
|
|
@@ -104,6 +115,23 @@ export class RunCtx {
|
|
|
104
115
|
this.now = opts.now ?? Date.now;
|
|
105
116
|
this.deadlineAt = this.now() + this.deadlineMs;
|
|
106
117
|
this.replay = opts.replay;
|
|
118
|
+
this.autoDecision = opts.autoDecision ? { ...opts.autoDecision } : undefined;
|
|
119
|
+
}
|
|
120
|
+
get fastPath() {
|
|
121
|
+
return this.fastPathActive;
|
|
122
|
+
}
|
|
123
|
+
get autoEscalationReasons() {
|
|
124
|
+
return this.autoDecision?.escalation_reasons ?? [];
|
|
125
|
+
}
|
|
126
|
+
/** Phase D: retain initial fast-path eligibility in meta, but stop rendering it as single-pass. */
|
|
127
|
+
markAutoEscalated(reasons) {
|
|
128
|
+
if (!this.autoDecision || reasons.length === 0)
|
|
129
|
+
return;
|
|
130
|
+
this.fastPathActive = false;
|
|
131
|
+
this.autoDecision = {
|
|
132
|
+
...this.autoDecision,
|
|
133
|
+
escalation_reasons: [...new Set([...(this.autoDecision.escalation_reasons ?? []), ...reasons])],
|
|
134
|
+
};
|
|
107
135
|
}
|
|
108
136
|
/** Provider ids available this run (READY at setup). */
|
|
109
137
|
available() {
|
|
@@ -135,6 +163,8 @@ export class RunCtx {
|
|
|
135
163
|
optionalCallsRemaining() {
|
|
136
164
|
if (this.workflow !== 'idea-refinement')
|
|
137
165
|
return 0;
|
|
166
|
+
if (this.isAuto)
|
|
167
|
+
return 0; // Phase D adaptive topology owns its single optional challenge directly.
|
|
138
168
|
const plan = IDEA_MODE_PLANS[this.mode];
|
|
139
169
|
const logicalUsed = this.attemptedStages.filter(isOptionalStage).length;
|
|
140
170
|
const protocolRoom = Math.max(0, plan.optionalCalls - logicalUsed);
|
|
@@ -160,13 +190,16 @@ export class RunCtx {
|
|
|
160
190
|
// Resume (V6.3): if this exact (provider, prompt) already succeeded in the run we're resuming,
|
|
161
191
|
// replay its output — no real call, no budget spend. Only never-completed calls hit the model.
|
|
162
192
|
const cachedOut = this.replay?.get(replayKey(handle.id, req.prompt));
|
|
193
|
+
const category = callCategory(stage);
|
|
194
|
+
const replayed = cachedOut !== undefined;
|
|
195
|
+
if (!replayed && this.budget.used + 1 > this.budget.limit)
|
|
196
|
+
throw new BudgetExceeded(this.budget.limit);
|
|
197
|
+
this.events?.onCallStart?.(handle.id, stage, category, replayed);
|
|
163
198
|
let res;
|
|
164
199
|
if (cachedOut !== undefined) {
|
|
165
200
|
res = { ok: true, text: cachedOut, json: req.expectJson ? extractJson(cachedOut) : undefined, durationMs: 0 };
|
|
166
201
|
}
|
|
167
202
|
else {
|
|
168
|
-
if (this.budget.used + 1 > this.budget.limit)
|
|
169
|
-
throw new BudgetExceeded(this.budget.limit);
|
|
170
203
|
this.budget.used++;
|
|
171
204
|
res = await handle.adapter.run({
|
|
172
205
|
prompt: req.prompt,
|
|
@@ -184,10 +217,12 @@ export class RunCtx {
|
|
|
184
217
|
stage,
|
|
185
218
|
category: callCategory(stage),
|
|
186
219
|
durationMs: res.durationMs,
|
|
220
|
+
usage: (res.ok && res.usage) || estimateUsage(req.prompt, res.ok ? res.text : ''),
|
|
187
221
|
...(res.ok ? {} : { error: res.error }),
|
|
188
222
|
});
|
|
189
223
|
}
|
|
190
224
|
await this.writer.writeRaw(`${stage}-${handle.id}-${seq}.out`, res.ok ? res.text : `[${res.error}]\n${res.stderrTail}`);
|
|
225
|
+
this.events?.onCallEnd?.(handle.id, stage, res.durationMs, res.ok, replayed);
|
|
191
226
|
return res;
|
|
192
227
|
}
|
|
193
228
|
/** Assemble the run's `meta.json` payload (§15) from the handles + call ledger accumulated so
|
|
@@ -211,6 +246,7 @@ export class RunCtx {
|
|
|
211
246
|
this.roles.s4.forEach((id, i) => (roles[`s4_${i + 1}`] = id));
|
|
212
247
|
// Fold stage-raised flags in with any explicitly passed by the caller; dedupe.
|
|
213
248
|
const allFlags = [...new Set([...(flags ?? []), ...this.flags])];
|
|
249
|
+
const usage_totals = this.sumUsage();
|
|
214
250
|
return {
|
|
215
251
|
run_id: this.runId,
|
|
216
252
|
workflow: this.workflow,
|
|
@@ -223,11 +259,38 @@ export class RunCtx {
|
|
|
223
259
|
call_count: this.calls.length,
|
|
224
260
|
budget: { limit: this.budget.limit, used: this.budget.used },
|
|
225
261
|
receipt: this.receipt(),
|
|
262
|
+
...(usage_totals ? { usage_totals } : {}),
|
|
263
|
+
...(this.autoDecision ? { auto_decision: this.autoDecision } : {}),
|
|
226
264
|
exit_status: exitStatus,
|
|
227
265
|
aborted,
|
|
228
266
|
...(allFlags.length ? { flags: allFlags } : {}),
|
|
229
267
|
};
|
|
230
268
|
}
|
|
269
|
+
/** Sum per-call usage into run totals. Undefined when no call carries usage (empty run). */
|
|
270
|
+
sumUsage() {
|
|
271
|
+
let inputTokens = 0, outputTokens = 0, reportedCalls = 0, estimatedCalls = 0, reportedCostUsd = 0, anyCost = false;
|
|
272
|
+
for (const c of this.calls) {
|
|
273
|
+
if (!c.usage)
|
|
274
|
+
continue;
|
|
275
|
+
inputTokens += c.usage.inputTokens ?? 0;
|
|
276
|
+
outputTokens += c.usage.outputTokens ?? 0;
|
|
277
|
+
if (c.usage.estimated)
|
|
278
|
+
estimatedCalls++;
|
|
279
|
+
else
|
|
280
|
+
reportedCalls++;
|
|
281
|
+
if (c.usage.reportedCostUsd !== undefined) {
|
|
282
|
+
reportedCostUsd += c.usage.reportedCostUsd;
|
|
283
|
+
anyCost = true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (reportedCalls === 0 && estimatedCalls === 0)
|
|
287
|
+
return undefined;
|
|
288
|
+
return { inputTokens, outputTokens, reportedCalls, estimatedCalls, ...(anyCost ? { reportedCostUsd } : {}) };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** ponytail: chars/4 heuristic, labeled estimated — good enough until a provider reports. */
|
|
292
|
+
function estimateUsage(prompt, out) {
|
|
293
|
+
return { inputTokens: Math.ceil(prompt.length / 4), outputTokens: Math.ceil(out.length / 4), estimated: true };
|
|
231
294
|
}
|
|
232
295
|
/** Run-fatal errors abort the whole run; everything else (e.g. a single provider failure in a
|
|
233
296
|
* fan-out) is handled locally by the stage (drop provider, check quorum). */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { DISPLAY_NAME } from '../providers/types.js';
|
|
2
|
+
import { evidenceOrigin } from './evidence-origin.js';
|
|
3
|
+
import { sanitizeLocalPaths } from './sanitize-paths.js';
|
|
2
4
|
function cell(value) {
|
|
3
5
|
return value.replaceAll('\n', ' ').replaceAll('|', '\\|');
|
|
4
6
|
}
|
|
@@ -21,6 +23,13 @@ export function claimShortLabel(text, max = 60) {
|
|
|
21
23
|
const boundary = clipped.lastIndexOf(' ');
|
|
22
24
|
return `${clipped.slice(0, boundary > max * 0.6 ? boundary : max).trimEnd()}…`;
|
|
23
25
|
}
|
|
26
|
+
/** Human token summary: "~18.4k in / ~3.2k out (2 calls estimated)". `~` iff any call estimated. */
|
|
27
|
+
export function formatTokenLine(t) {
|
|
28
|
+
const k = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n));
|
|
29
|
+
const tilde = t.estimatedCalls > 0 ? '~' : '';
|
|
30
|
+
const suffix = t.estimatedCalls > 0 ? ` (${t.estimatedCalls} calls estimated)` : '';
|
|
31
|
+
return `${tilde}${k(t.inputTokens)} in / ${tilde}${k(t.outputTokens)} out${suffix}`;
|
|
32
|
+
}
|
|
24
33
|
/** id → claim text lookup for stripReaderClaimIds substitution; shared by readerClaimLabel's fallback and the Markdown/HTML renderers. */
|
|
25
34
|
export function claimLookup(report) {
|
|
26
35
|
return (id) => report.claims.find((claim) => claim.id === id)?.text ?? null;
|
|
@@ -64,6 +73,7 @@ const FLAG_EXPLAIN = {
|
|
|
64
73
|
weak_seat: 'one scout seat contributed far less evidenced material than the other; treat convergence cautiously',
|
|
65
74
|
source_fallback_search: 'a supplied page blocked direct reading; the source-investigation seat searched for accessible sources instead',
|
|
66
75
|
deliverable_gap: 'one or more surviving scouts omitted a requested feature or implementation proposal',
|
|
76
|
+
plan_partial: 'the answer planner omitted part of the requested deliverables; the report shows what was produced',
|
|
67
77
|
};
|
|
68
78
|
function degradation(report, flags) {
|
|
69
79
|
const active = flags.filter((flag) => report.flags.includes(flag));
|
|
@@ -76,6 +86,12 @@ function coverageLabel(value) {
|
|
|
76
86
|
return value >= 0.75 ? 'High' : value >= 0.5 ? 'Medium' : 'Low';
|
|
77
87
|
}
|
|
78
88
|
function councilRead(report) {
|
|
89
|
+
if (report.fastPath)
|
|
90
|
+
return 'Single-pass analysis; council escalation was not required.';
|
|
91
|
+
if (report.adaptiveAuto)
|
|
92
|
+
return report.autoEscalationReasons?.length
|
|
93
|
+
? `The primary analysis tripped structural gates; two task readings checked the interpretation${report.autoChallengeUsed ? ', and one targeted challenger checked eligible claims' : '; no challenger could add information'}. No full council was convened.`
|
|
94
|
+
: 'Two task readings informed one primary decision analyst. No full council was convened.';
|
|
79
95
|
if (report.mode === 'quick')
|
|
80
96
|
return 'One structured analyst produced this result; no council, consensus, or independent-verification claim is being made.';
|
|
81
97
|
const scouts = report.models.filter((model) => model.roles.includes('scout')).length;
|
|
@@ -88,10 +104,13 @@ function councilRead(report) {
|
|
|
88
104
|
export function sanitizeReaderText(text, labelFor) {
|
|
89
105
|
return stripReaderClaimIds(text, labelFor)
|
|
90
106
|
.replace(/\b(?:G|C|D)\d+\b/g, '')
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
.replace(/\
|
|
107
|
+
// v6: UPPERCASE-only — a leaked internal enum is rewritten; ordinary English ("the rules are
|
|
108
|
+
// unverified") is the model's own prose and must survive (f740's "confirmed-unverified" became
|
|
109
|
+
// "confirmed-not yet confirmed" under the old /gi).
|
|
110
|
+
.replace(/\bUNVERIFIABLE\b/g, 'not independently confirmed')
|
|
111
|
+
.replace(/\bUNVERIFIED\b/g, 'not yet confirmed')
|
|
112
|
+
.replace(/\bPARTIAL\b/g, 'partly supported')
|
|
113
|
+
.replace(/\bCONFLICTED\b/g, 'supported by conflicting evidence')
|
|
95
114
|
.replace(/\bstructural score\b/gi, 'confidence estimate')
|
|
96
115
|
.replace(/\bevidence coverage\b/gi, 'source strength')
|
|
97
116
|
.replace(/\bverification coverage\b/gi, 'source strength')
|
|
@@ -108,7 +127,7 @@ export function cleanReaderText(report, text) {
|
|
|
108
127
|
}
|
|
109
128
|
const MATERIAL_WARNING_ORDER = [
|
|
110
129
|
'synthesis_suspect', 'low_diversity', 'weak_seat', 'deliverable_gap', 'plan_skipped',
|
|
111
|
-
'plan_fallback', 'headless_intent', 'verification_skipped', 'research_ungrounded',
|
|
130
|
+
'plan_fallback', 'plan_partial', 'headless_intent', 'verification_skipped', 'research_ungrounded',
|
|
112
131
|
];
|
|
113
132
|
const READER_WARNING = {
|
|
114
133
|
synthesis_suspect: 'The final synthesis needed repair; treat its wording cautiously.',
|
|
@@ -117,6 +136,7 @@ const READER_WARNING = {
|
|
|
117
136
|
deliverable_gap: 'At least one scout omitted a requested feature or implementation proposal.',
|
|
118
137
|
plan_skipped: 'The answer-planning step was skipped because its call budget was unavailable.',
|
|
119
138
|
plan_fallback: 'The answer planner failed validation; this report uses a deterministic fallback.',
|
|
139
|
+
plan_partial: 'The answer planner omitted part of the requested deliverables; the report shows what was produced.',
|
|
120
140
|
headless_intent: 'No person confirmed the interpreted request; documented defaults were used.',
|
|
121
141
|
verification_skipped: 'Independent verification was skipped; important factual claims may remain unchecked.',
|
|
122
142
|
research_ungrounded: 'Source investigation did not produce a cited public source; treat current claims as ungrounded.',
|
|
@@ -232,7 +252,8 @@ function renderReaderBriefMarkdown(report) {
|
|
|
232
252
|
L.push('## Decision numbers', '', '### Decisive numbers', '', '| Metric | Value | What it means |', '|---|---:|---|');
|
|
233
253
|
for (const item of projection.snapshot.decisiveNumbers)
|
|
234
254
|
L.push(`| ${cell(item.label)} | ${cell(item.value)} | ${cell(item.meaning)} |`);
|
|
235
|
-
|
|
255
|
+
// v6: a NOT_COMPUTABLE payback is noise on a non-financial question — omit, don't announce.
|
|
256
|
+
if (projection.snapshot.payback && projection.snapshot.payback.status !== 'NOT_COMPUTABLE') {
|
|
236
257
|
L.push('', `**Payback — ${projection.snapshot.payback.status.replaceAll('_', ' ')}:** ${projection.snapshot.payback.result}`);
|
|
237
258
|
L.push(`Basis: ${projection.snapshot.payback.basis}`, '');
|
|
238
259
|
}
|
|
@@ -289,6 +310,8 @@ function renderReaderBriefMarkdown(report) {
|
|
|
289
310
|
}
|
|
290
311
|
if (projection.sources.length) {
|
|
291
312
|
L.push('## Sources', '');
|
|
313
|
+
const externalCount = report.dossier.evidence.filter((item) => evidenceOrigin(item) === 'EXTERNAL').length;
|
|
314
|
+
L.push(`${externalCount} independent external source${externalCount === 1 ? '' : 's'}; everything else is your own material or model background.`, '');
|
|
292
315
|
for (const source of projection.sources) {
|
|
293
316
|
const citationScope = source.citedFor.length ? ` — Cited for: ${source.citedFor.join('; ')}` : '';
|
|
294
317
|
L.push(`${source.url ? `- [${source.label.replace(/[\[\]]/g, '')}](${source.url})` : `- ${source.label}`}${citationScope}`);
|
|
@@ -325,7 +348,7 @@ function renderLegacyDecisionDossierMarkdown(report) {
|
|
|
325
348
|
for (const item of report.decisionSnapshot.decisiveNumbers) {
|
|
326
349
|
L.push(`| ${cell(item.label)} | ${cell(item.value)} | ${cell(item.meaning)} | ${cell(readerClaimRefs(report, item.claimIds))} |`);
|
|
327
350
|
}
|
|
328
|
-
if (report.decisionSnapshot.payback) {
|
|
351
|
+
if (report.decisionSnapshot.payback && report.decisionSnapshot.payback.status !== 'NOT_COMPUTABLE') {
|
|
329
352
|
const payback = report.decisionSnapshot.payback;
|
|
330
353
|
L.push('', `**Payback — ${payback.status.replaceAll('_', ' ')}:** ${payback.result}`);
|
|
331
354
|
L.push(`Basis: ${payback.basis}`, '');
|
|
@@ -558,9 +581,13 @@ function renderLegacyDecisionDossierMarkdown(report) {
|
|
|
558
581
|
L.push(`- Models and roles: ${report.models.map((model) => `${model.name} (${model.roles.join(', ')})`).join(' · ') || 'none recorded'}`);
|
|
559
582
|
L.push(`- Mode: ${report.mode}`);
|
|
560
583
|
L.push(`- Provider calls: ${report.receipt.calls}/${report.receipt.budget}`);
|
|
584
|
+
if (report.autoEscalationReasons?.length)
|
|
585
|
+
L.push(`- Adaptive escalation: ${report.autoEscalationReasons.join('; ')}`);
|
|
561
586
|
L.push(`- Categories: discovery ${report.receipt.categories.discovery} · verification ${report.receipt.categories.verification} · repair ${report.receipt.categories.repair} · planning ${report.receipt.categories.planning}`);
|
|
562
587
|
L.push(`- By provider: ${Object.entries(report.receipt.byProvider).map(([provider, count]) => `${providerName(provider)} ${count}`).join(', ') || 'none'}`);
|
|
563
588
|
L.push(`- Recorded model time: ${(report.receipt.modelTimeMs / 1000).toFixed(1)}s`);
|
|
589
|
+
if (report.receipt.tokens)
|
|
590
|
+
L.push(`- Tokens: ${formatTokenLine(report.receipt.tokens)}`);
|
|
564
591
|
L.push(`- Degradation flags: ${report.flags.join(', ') || 'none'}`, '');
|
|
565
592
|
// This pass runs over already-built (cell-escaped) table rows, so injected labels must be cell-escaped too.
|
|
566
593
|
for (let index = 0; index < L.length; index++)
|
|
@@ -593,7 +620,11 @@ function renderLegacyDecisionDossierMarkdown(report) {
|
|
|
593
620
|
if (dossier.evidence.length) {
|
|
594
621
|
L.push('| Evidence ID | Source | Date | Freshness | Verification | Linked claims |', '|---|---|---|---|---|---|');
|
|
595
622
|
for (const evidence of dossier.evidence) {
|
|
596
|
-
|
|
623
|
+
// v6: the user's own material can corroborate what they told us — never "verify" it.
|
|
624
|
+
const verification = evidenceOrigin(evidence) === 'USER_MATERIAL'
|
|
625
|
+
? 'consistent with your materials (not independently checked)'
|
|
626
|
+
: evidence.verificationStatus;
|
|
627
|
+
L.push(`| ${evidence.id} | ${cell(evidence.source)} (${evidence.sourceKind}) | ${evidence.date} | ${evidence.freshness} | ${verification} | ${cell(readerClaimRefs(report, evidence.claimIds))} |`);
|
|
597
628
|
}
|
|
598
629
|
}
|
|
599
630
|
else
|
|
@@ -643,7 +674,8 @@ function renderLegacyDecisionDossierMarkdown(report) {
|
|
|
643
674
|
L.push('', '</details>', '');
|
|
644
675
|
return L.join('\n');
|
|
645
676
|
}
|
|
646
|
-
/** Canonical Markdown. HTML and Copy-Markdown consume the same persisted reader brief.
|
|
677
|
+
/** Canonical Markdown. HTML and Copy-Markdown consume the same persisted reader brief.
|
|
678
|
+
* v6: every rendered artifact is path-sanitized; stored run JSON keeps raw locators for audit. */
|
|
647
679
|
export function renderDecisionDossierMarkdown(report) {
|
|
648
|
-
return report.dossier.readerBrief ? renderReaderBriefMarkdown(report) : renderLegacyDecisionDossierMarkdown(report);
|
|
680
|
+
return sanitizeLocalPaths(report.dossier.readerBrief ? renderReaderBriefMarkdown(report) : renderLegacyDecisionDossierMarkdown(report));
|
|
649
681
|
}
|
|
@@ -32,7 +32,7 @@ export function interpretClaimOutcome(graph, claim, adjudication) {
|
|
|
32
32
|
export function positionId(provider, localId, sourceId = provider) {
|
|
33
33
|
return `${sourceId}/${localId}`;
|
|
34
34
|
}
|
|
35
|
-
function
|
|
35
|
+
export function classifyClaimState(positions) {
|
|
36
36
|
const providers = new Set(positions.map((position) => position.provider));
|
|
37
37
|
const stances = new Set(positions.map((position) => position.stance));
|
|
38
38
|
const supporters = positions.filter((position) => position.stance === 'SUPPORT').map((position) => position.provider);
|
|
@@ -139,7 +139,7 @@ export function compileDecisionGraph(submissions, rubric, semanticGroups = []) {
|
|
|
139
139
|
: 'load-bearing claim has no settling evidence';
|
|
140
140
|
evidenceHoles.push({ claim_id: id, reason });
|
|
141
141
|
}
|
|
142
|
-
const baseState =
|
|
142
|
+
const baseState = classifyClaimState(members);
|
|
143
143
|
return {
|
|
144
144
|
id,
|
|
145
145
|
proposition: members[0].proposition,
|
|
@@ -31,7 +31,7 @@ export async function executeRun(ctx, input, fn) {
|
|
|
31
31
|
try {
|
|
32
32
|
await fn(ctx, input);
|
|
33
33
|
await ctx.writer.writeMeta(ctx.buildMeta('ok', false));
|
|
34
|
-
return { ok: true, ...base, callCount: ctx.calls.length };
|
|
34
|
+
return { ok: true, aborted: false, ...base, callCount: ctx.calls.length };
|
|
35
35
|
}
|
|
36
36
|
catch (e) {
|
|
37
37
|
const classified = classifyError(e);
|
|
@@ -40,7 +40,7 @@ export async function executeRun(ctx, input, fn) {
|
|
|
40
40
|
const aborted = ctx.aborted || classified.aborted;
|
|
41
41
|
// Best-effort finalize: never let a meta-write failure mask the original error.
|
|
42
42
|
await ctx.writer.writeMeta(ctx.buildMeta(aborted ? 'aborted' : 'failed', aborted)).catch(() => { });
|
|
43
|
-
return { ok: false, ...base, callCount: ctx.calls.length, error: { code: classified.code, message: e instanceof Error ? e.message : String(e) } };
|
|
43
|
+
return { ok: false, aborted, ...base, callCount: ctx.calls.length, error: { code: classified.code, message: e instanceof Error ? e.message : String(e) } };
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
@@ -54,13 +54,14 @@ export async function run(workflow, input, opts = {}) {
|
|
|
54
54
|
if (handles.length < requiredProviders) {
|
|
55
55
|
return {
|
|
56
56
|
ok: false,
|
|
57
|
+
aborted: false,
|
|
57
58
|
runId: '(none)',
|
|
58
59
|
dir: '',
|
|
59
60
|
callCount: 0,
|
|
60
61
|
error: { code: 'QUORUM', message: `need ≥${requiredProviders} provider${requiredProviders === 1 ? '' : 's'}, found ${handles.length} — run \`aiki doctor\`` },
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
|
-
const runId = makeRunId(workflow);
|
|
64
|
+
const runId = opts.runId ?? makeRunId(workflow);
|
|
64
65
|
const roles = resolveRoles(workflow, handles.map((h) => h.id), opts.roleOverrides);
|
|
65
66
|
const writer = new RunWriter(runId, opts.runsRoot);
|
|
66
67
|
const ctx = new RunCtx({
|
|
@@ -77,6 +78,9 @@ export async function run(workflow, input, opts = {}) {
|
|
|
77
78
|
events: opts.events,
|
|
78
79
|
replay: opts.replay,
|
|
79
80
|
evidencePack: opts.evidencePack,
|
|
81
|
+
allowBlockedSources: opts.allowBlockedSources,
|
|
82
|
+
urlSources: opts.urlSources,
|
|
83
|
+
autoDecision: opts.autoDecision,
|
|
80
84
|
});
|
|
81
85
|
// Register the session (V6.3) so `aiki sessions`/`resume` can find it from anywhere. run() is a
|
|
82
86
|
// real-CLI entry (setupProviders); tests use executeRun directly and never touch the global registry.
|
|
@@ -90,6 +94,6 @@ export async function run(workflow, input, opts = {}) {
|
|
|
90
94
|
...(opts.resumedFrom ? { resumedFrom: opts.resumedFrom } : {}),
|
|
91
95
|
});
|
|
92
96
|
const outcome = await executeRun(ctx, input, WORKFLOWS[workflow]);
|
|
93
|
-
await updateSessionStatus(runId, outcome.ok ? 'ok' : 'failed');
|
|
97
|
+
await updateSessionStatus(runId, outcome.ok ? 'ok' : outcome.aborted ? 'aborted' : 'failed');
|
|
94
98
|
return outcome;
|
|
95
99
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// v6 evidence origin honesty (plan/AIKI-v6-council-integrity-plan.md T7). Run f740's coverage
|
|
2
|
+
// numbers counted the user's own idea-brief as evidence (8 of 12 cards, some marked VERIFIED) —
|
|
3
|
+
// circular. Origin is derived, never trusted from labels alone: EXTERNAL requires BOTH an
|
|
4
|
+
// external source_kind AND a public http(s) locator, so a mislabeled local file stays humble.
|
|
5
|
+
/** Accepts graph-card shape (`source_kind`/`locator`/`url`) and dossier-row shape
|
|
6
|
+
* (`sourceKind`/`source`/`url`). Defaults toward USER_MATERIAL — the humbler label. */
|
|
7
|
+
export function evidenceOrigin(card) {
|
|
8
|
+
const kind = card.source_kind ?? card.sourceKind;
|
|
9
|
+
if (kind === 'MODEL_KNOWLEDGE')
|
|
10
|
+
return 'MODEL_KNOWLEDGE';
|
|
11
|
+
if (kind === 'PRIMARY' || kind === 'SECONDARY') {
|
|
12
|
+
const locator = card.url ?? card.locator ?? card.source ?? '';
|
|
13
|
+
if (/^https?:\/\//i.test(locator.trim()))
|
|
14
|
+
return 'EXTERNAL';
|
|
15
|
+
}
|
|
16
|
+
return 'USER_MATERIAL';
|
|
17
|
+
}
|
|
@@ -10,10 +10,14 @@ import { StageError } from './context.js';
|
|
|
10
10
|
* Two live failures shaped it (run 20260715-1516): the S9 chair's only defect was `dissent` as a
|
|
11
11
|
* string, and the paid repair it triggered flipped the verdict PIVOT→PWC and died on 7 conditions
|
|
12
12
|
* vs max 6. Policy:
|
|
13
|
-
* - LOSSLESS (always): a lone value where an array is expected becomes a one-element array
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* - LOSSLESS (always): a lone value where an array is expected becomes a one-element array, and an
|
|
14
|
+
* EMPTY optional min-1 string is dropped (empty carries no information — absent beats invalid;
|
|
15
|
+
* run f740's codex seat burned a 267s repair on 12 empty rationales). Runs BEFORE the paid
|
|
16
|
+
* repair — such defects cost zero extra calls.
|
|
17
|
+
* - LOSSY (`lossy: true`, last resort once the repair path is spent or disallowed): arrays beyond
|
|
18
|
+
* their schema max are truncated in order; strings beyond their max are clipped at a word
|
|
19
|
+
* boundary with a trailing ellipsis (run f740's planner died on a 173-char headline vs max 160
|
|
20
|
+
* with no repair budget).
|
|
17
21
|
* Never invents a value. The full schema still validates the result; anything else stays invalid. */
|
|
18
22
|
export function coerceToSchema(schema, value, lossy) {
|
|
19
23
|
const def = schema?._def;
|
|
@@ -21,7 +25,12 @@ export function coerceToSchema(schema, value, lossy) {
|
|
|
21
25
|
return value;
|
|
22
26
|
const kind = def.typeName;
|
|
23
27
|
if (kind === 'ZodOptional' || kind === 'ZodNullable' || kind === 'ZodDefault' || kind === 'ZodReadonly' || kind === 'ZodCatch') {
|
|
24
|
-
|
|
28
|
+
if (value === undefined || value === null)
|
|
29
|
+
return value;
|
|
30
|
+
const coerced = coerceToSchema(def.innerType, value, lossy);
|
|
31
|
+
if (kind === 'ZodOptional' && coerced === '' && minStringLength(def.innerType) >= 1)
|
|
32
|
+
return undefined;
|
|
33
|
+
return coerced;
|
|
25
34
|
}
|
|
26
35
|
if (kind === 'ZodEffects')
|
|
27
36
|
return coerceToSchema(def.schema, value, lossy); // refine/preprocess wrappers
|
|
@@ -46,8 +55,36 @@ export function coerceToSchema(schema, value, lossy) {
|
|
|
46
55
|
}
|
|
47
56
|
return out;
|
|
48
57
|
}
|
|
58
|
+
if (kind === 'ZodString') {
|
|
59
|
+
if (!lossy || typeof value !== 'string')
|
|
60
|
+
return value;
|
|
61
|
+
const max = stringCheck(def, 'max');
|
|
62
|
+
if (typeof max !== 'number' || value.length <= max)
|
|
63
|
+
return value;
|
|
64
|
+
const cut = value.slice(0, max - 1);
|
|
65
|
+
const atWord = cut.includes(' ') ? cut.slice(0, cut.lastIndexOf(' ')) : cut;
|
|
66
|
+
return `${atWord}…`;
|
|
67
|
+
}
|
|
49
68
|
return value;
|
|
50
69
|
}
|
|
70
|
+
function stringCheck(def, kind) {
|
|
71
|
+
const checks = def.checks;
|
|
72
|
+
const found = checks?.find((check) => check.kind === kind)?.value;
|
|
73
|
+
return typeof found === 'number' ? found : undefined;
|
|
74
|
+
}
|
|
75
|
+
function minStringLength(schema) {
|
|
76
|
+
const def = schema?._def;
|
|
77
|
+
if (!def)
|
|
78
|
+
return 0;
|
|
79
|
+
const kind = def.typeName;
|
|
80
|
+
if (kind === 'ZodEffects')
|
|
81
|
+
return minStringLength(def.schema);
|
|
82
|
+
if (kind === 'ZodPipeline')
|
|
83
|
+
return minStringLength(def.in);
|
|
84
|
+
if (kind !== 'ZodString')
|
|
85
|
+
return 0;
|
|
86
|
+
return stringCheck(def, 'min') ?? 0;
|
|
87
|
+
}
|
|
51
88
|
export async function jsonCall(ctx, handle, stage, prompt, schema, opts = {}) {
|
|
52
89
|
// Deterministic last resort once the repair path is spent: stage-specific salvage first, then the
|
|
53
90
|
// generic lossy floor (truncate over-cap arrays), then both composed. No extra provider call.
|
|
@@ -79,6 +116,11 @@ export async function jsonCall(ctx, handle, stage, prompt, schema, opts = {}) {
|
|
|
79
116
|
if (wrapped.success)
|
|
80
117
|
return wrapped.data;
|
|
81
118
|
if (opts.repair === false) {
|
|
119
|
+
// Repair is unavailable (budget/policy) — the deterministic floor is all we have. A clipped
|
|
120
|
+
// string beats a discarded artifact (run f740: complete plan lost to a 13-char overflow).
|
|
121
|
+
const saved = trySalvage(first.json);
|
|
122
|
+
if (saved !== undefined)
|
|
123
|
+
return saved;
|
|
82
124
|
throw new StageError(stage, 'BAD_OUTPUT', `output failed validation: ${zodMessage(parsed.error)}`);
|
|
83
125
|
}
|
|
84
126
|
// §14 repair retry — one attempt, same provider.
|
|
@@ -3,7 +3,9 @@ const FULL_COUNCIL_PLAN = {
|
|
|
3
3
|
baseCalls: 6,
|
|
4
4
|
optionalCalls: 4,
|
|
5
5
|
maxCalls: 10,
|
|
6
|
-
|
|
6
|
+
// v6: chair + planner + ONE tail repair. Run f740 proved 2 was not enough — upstream repairs
|
|
7
|
+
// drained the cushion and the planner's complete deliverables died unrepairables at 12/12.
|
|
8
|
+
reservedCalls: 3,
|
|
7
9
|
defaultBudget: 12,
|
|
8
10
|
deadlineMs: 45 * MINUTE,
|
|
9
11
|
};
|
|
@@ -35,7 +37,7 @@ export function callCategory(stage) {
|
|
|
35
37
|
return 'repair';
|
|
36
38
|
if (stage.startsWith('S9b'))
|
|
37
39
|
return 'planning';
|
|
38
|
-
if (stage.startsWith('S8') || stage === 'S9' || stage.startsWith('S9-'))
|
|
40
|
+
if (stage === 'S4b' || stage.startsWith('S8') || stage === 'S9' || stage.startsWith('S9-'))
|
|
39
41
|
return 'verification';
|
|
40
42
|
return 'discovery';
|
|
41
43
|
}
|
|
@@ -37,6 +37,25 @@ USER TEXT:
|
|
|
37
37
|
{{RAW_INPUT}}
|
|
38
38
|
URL SOURCE SNAPSHOTS:
|
|
39
39
|
{{URL_SOURCES_JSON}}`;
|
|
40
|
+
/** Phase C fast path: the CLI already proved this is an unambiguous, decision-only request. */
|
|
41
|
+
export function deterministicContract(rawInput, coreRubric) {
|
|
42
|
+
const successBar = 'a decision-ready recommendation';
|
|
43
|
+
return DecisionContract.parse({
|
|
44
|
+
task: rawInput.trim(),
|
|
45
|
+
task_type: 'idea-refinement',
|
|
46
|
+
constraints: [],
|
|
47
|
+
unknowns: [],
|
|
48
|
+
success_criteria: [successBar],
|
|
49
|
+
alternatives: [],
|
|
50
|
+
success_bar: successBar,
|
|
51
|
+
evidence_supplied: [],
|
|
52
|
+
missing_evidence: [],
|
|
53
|
+
core_rubric: coreRubric,
|
|
54
|
+
user_confirmed: false,
|
|
55
|
+
confirmation: 'headless-defaulted',
|
|
56
|
+
requested_outputs: requestedOutputsFor(rawInput),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
40
59
|
function unique(values, max = Number.POSITIVE_INFINITY) {
|
|
41
60
|
const seen = new Set();
|
|
42
61
|
const result = [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionPlan, JudgeReport, QuickDecisionModel, readerBriefIssues, } from '../schemas/index.js';
|
|
1
|
+
import { ActionPlan, JudgeReport, QuickDecisionModel, readerBriefIssues, salvageIdeaRoleOutputModel, } from '../schemas/index.js';
|
|
2
2
|
import { jsonCall } from './jsonStage.js';
|
|
3
3
|
const QUICK_PROMPT = `ROLE: Single decision analyst. This is explicit QUICK mode, not a multi-model
|
|
4
4
|
council. Give one strong structured analysis and do not claim independent consensus or verification.{{SKILL}}
|
|
@@ -9,8 +9,23 @@ EVIDENCE PACK MANIFEST: {{EVIDENCE_PACK_JSON}}
|
|
|
9
9
|
|
|
10
10
|
Output ONLY JSON:
|
|
11
11
|
{
|
|
12
|
-
"analysis": <the idea analyst object
|
|
13
|
-
|
|
12
|
+
"analysis": <the idea analyst object with EXACTLY these fields:
|
|
13
|
+
task_echo: restate the task in ≤2 sentences.
|
|
14
|
+
strongest_version: the best honest version of this idea in ≤150 words.
|
|
15
|
+
positions: [{local_id, proposition, dimension_id, stance SUPPORT|OPPOSE|MIXED|UNKNOWN,
|
|
16
|
+
basis EVIDENCE|INFERENCE|ASSUMPTION, nature FACTUAL|JUDGMENT, load_bearing,
|
|
17
|
+
if_false STOP|PIVOT|CONDITION|MINOR, reasoning, evidence_ids, depends_on}]
|
|
18
|
+
evidence: [{id, claim_supported, source_kind USER|PRIMARY|SECONDARY|MODEL_KNOWLEDGE (exact token),
|
|
19
|
+
support SUPPORTS|CONTRADICTS|CONTEXT_ONLY, freshness CURRENT|DATED|UNKNOWN (exact token),
|
|
20
|
+
locator/url, accessed_at for current external sources}]. MODEL_KNOWLEDGE freshness is UNKNOWN.
|
|
21
|
+
calculations: [] or per derived numeric claim {id, claim_id, inputs: [{id,name,value,unit,evidence_ids}],
|
|
22
|
+
steps: [{id,operation ADD|SUBTRACT|MULTIPLY|DIVIDE,left,right,result,unit}], result_step}
|
|
23
|
+
coverage: one entry per rubric dimension {dimension_id, status COVERED|NOT_APPLICABLE,
|
|
24
|
+
position_ids ([] when none), rationale (required for NOT_APPLICABLE)} — no other keys.
|
|
25
|
+
decision_questions: [{question, claim_ids}]
|
|
26
|
+
deliverable_proposals: [] unless requested_outputs asks for FEATURE_BACKLOG or IMPLEMENTATION_PLAN,
|
|
27
|
+
then [{output FEATURE_BACKLOG|IMPLEMENTATION_PLAN, title, detail, user_value, why_distinctive, evidence_ids}].
|
|
28
|
+
Use ONLY these field names — no extra or renamed keys anywhere in analysis.>,
|
|
14
29
|
"verdict": "<2-5 sentence decision and core reason>",
|
|
15
30
|
"recommendation": "PROCEED|PROCEED_WITH_CONDITIONS|PIVOT|STOP",
|
|
16
31
|
"conditions": ["<only for PROCEED_WITH_CONDITIONS>"],
|
|
@@ -38,11 +53,21 @@ export function buildQuickPrompt(contract, inputPath, evidencePack, skill) {
|
|
|
38
53
|
.replace('{{INPUT_PATH}}', inputPath)
|
|
39
54
|
.replace('{{EVIDENCE_PACK_JSON}}', JSON.stringify(evidencePack ?? { files: [] }));
|
|
40
55
|
}
|
|
41
|
-
|
|
56
|
+
/** Auto standard path keeps the same typed one-call output without claiming explicit quick mode. */
|
|
57
|
+
export function buildAdaptivePrompt(contract, inputPath, evidencePack, skill) {
|
|
58
|
+
return buildQuickPrompt(contract, inputPath, evidencePack, skill).replace('ROLE: Single decision analyst. This is explicit QUICK mode, not a multi-model\ncouncil. Give one strong structured analysis and do not claim independent consensus or verification.', 'ROLE: Primary decision analyst in an adaptive auto run. Give one strong structured analysis.\nDo not claim council consensus or independent verification. Use provider-native read-only source\ninvestigation when available; otherwise leave current facts visibly unverified.');
|
|
59
|
+
}
|
|
60
|
+
export async function s4QuickAnalyze(ctx, prompt, opts = {}) {
|
|
42
61
|
const provider = ctx.roles.judge;
|
|
43
|
-
const decision = await jsonCall(ctx, ctx.handle(provider), 'Q1', prompt, QuickDecisionModel
|
|
62
|
+
const decision = await jsonCall(ctx, ctx.handle(provider), opts.stage ?? 'Q1', prompt, QuickDecisionModel, {
|
|
63
|
+
// Same deterministic last resort as council S4, applied to the nested analysis object.
|
|
64
|
+
salvage: (json) => (json && typeof json === 'object' && !Array.isArray(json)
|
|
65
|
+
? { ...json, analysis: salvageIdeaRoleOutputModel(json.analysis) }
|
|
66
|
+
: json),
|
|
67
|
+
});
|
|
44
68
|
const output = { workflow: 'idea-refinement', ...decision.analysis };
|
|
45
|
-
|
|
69
|
+
if (opts.persist !== false)
|
|
70
|
+
await ctx.writer.writeRoleOutput(provider, output);
|
|
46
71
|
return { seat: { provider, sample: provider, output }, decision };
|
|
47
72
|
}
|
|
48
73
|
function claimAnchors(graph) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// v6 render-time path sanitization (plan/AIKI-v6-council-integrity-plan.md T8). Run f740's report
|
|
2
|
+
// leaked `/Users/<name>/...` locators into its own evidence table while one of its accepted claims
|
|
3
|
+
// said replays must sanitize local paths — the artifact violated the product's own rule. Every
|
|
4
|
+
// rendered artifact (canonical Markdown, HTML) passes through here; stored run JSON keeps raw
|
|
5
|
+
// locators for audit fidelity, and the terminal keeps the functional local report path.
|
|
6
|
+
/** Replace the user-identifying prefix of absolute home paths with `~`. Idempotent; the
|
|
7
|
+
* lookbehind keeps URL path segments like `example.com/Users/page` untouched. */
|
|
8
|
+
export function sanitizeLocalPaths(text) {
|
|
9
|
+
return text.replace(/(?<![\w.:-])(?:file:\/\/)?\/(?:Users|home)\/[^/\s)\]|"'`]+/g, '~');
|
|
10
|
+
}
|