aiki-cli 0.3.0 → 0.3.2

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 (34) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +16 -8
  3. package/dist/bench/idea-v3-rating.js +1 -1
  4. package/dist/cli/index.js +1 -1
  5. package/dist/cli/run.js +10 -7
  6. package/dist/council/view.js +146 -46
  7. package/dist/orchestration/context.js +3 -3
  8. package/dist/orchestration/decision-dossier.js +467 -80
  9. package/dist/orchestration/decision-graph.js +31 -0
  10. package/dist/orchestration/legacy-idea-adapter.js +3 -0
  11. package/dist/orchestration/modes.js +16 -4
  12. package/dist/orchestration/preflight.js +43 -9
  13. package/dist/orchestration/quick-analysis.js +35 -6
  14. package/dist/orchestration/stages/s10-render.js +179 -79
  15. package/dist/orchestration/stages/s4-analyze.js +3 -1
  16. package/dist/orchestration/stages/s6-positions.js +18 -0
  17. package/dist/orchestration/stages/s7-decision-graph.js +3 -0
  18. package/dist/orchestration/stages/s8-verify.js +26 -9
  19. package/dist/orchestration/stages/s8b-rebuttal.js +11 -4
  20. package/dist/orchestration/stages/s9-judge.js +36 -13
  21. package/dist/orchestration/stages/s9b-plan.js +208 -35
  22. package/dist/orchestration/url-sources.js +200 -0
  23. package/dist/schemas/index.js +134 -6
  24. package/dist/skills/idea-refinement/analyst.md +5 -5
  25. package/dist/skills/idea-refinement/chair.md +18 -0
  26. package/dist/skills/idea-refinement/economics-delivery.md +11 -0
  27. package/dist/skills/idea-refinement/market-adoption.md +12 -0
  28. package/dist/skills/idea-refinement/planner.md +25 -19
  29. package/dist/skills/idea-refinement/rebuttal.md +15 -0
  30. package/dist/skills/idea-refinement/verifier.md +17 -0
  31. package/dist/storage/runs.js +2 -1
  32. package/dist/tui/app.js +19 -4
  33. package/dist/workflows/idea-refinement.js +51 -15
  34. package/package.json +1 -1
package/dist/tui/app.js CHANGED
@@ -23,7 +23,7 @@ import { computeDiff, computeWorkingTreeDiff, detectRepoStatus } from '../orches
23
23
  import { loadCouncilView } from '../council/view.js';
24
24
  import { openCouncilHtml } from '../council/open.js';
25
25
  import { readJsonArtifact } from '../storage/runs-read.js';
26
- import { defaultBudgetFor } from '../orchestration/modes.js';
26
+ import { defaultBudgetFor, defaultDeadlineFor, inferIdeaMode } from '../orchestration/modes.js';
27
27
  import { GLYPH, displayNames, elapsedLabel, initTimeline, markEnd, markStart, progressBar, runningPhrase, totalElapsed } from './timeline.js';
28
28
  import { formatCompletion, formatError } from './format.js';
29
29
  import { COMMANDS, PRODUCT_LINE, filterCommands, parseCommand, routeInput, scopeRedirect, suggestCommand } from './smart-entry.js';
@@ -260,7 +260,20 @@ export function App(props) {
260
260
  setPhase('clarify');
261
261
  }),
262
262
  };
263
- const ctx = new RunCtx({ runId, workflow: wf, mode, handles, roles: rs, writer, cwd: cwd ?? writer.dir, budget: budgetOverride, signal: controller.signal, events, replay });
263
+ const ctx = new RunCtx({
264
+ runId,
265
+ workflow: wf,
266
+ mode,
267
+ handles,
268
+ roles: rs,
269
+ writer,
270
+ cwd: cwd ?? writer.dir,
271
+ budget: budgetOverride,
272
+ deadlineMs: defaultDeadlineFor(wf, mode),
273
+ signal: controller.signal,
274
+ events,
275
+ replay,
276
+ });
264
277
  ctxRef.current = ctx;
265
278
  setWorkflow(wf);
266
279
  setRows(initTimeline(stages, rs, available));
@@ -286,7 +299,9 @@ export function App(props) {
286
299
  setPhase('finished');
287
300
  });
288
301
  };
289
- const startIdea = (text) => startRun('idea-refinement', text, null, runIdeaRefinement, IDEA_STAGES);
302
+ const startIdea = (text) => {
303
+ startRun('idea-refinement', text, null, runIdeaRefinement, IDEA_STAGES, undefined, inferIdeaMode(text));
304
+ };
290
305
  const startCodeReview = async (action) => {
291
306
  if (!repo || !repo.defaultBranch) {
292
307
  setRouterMessage('code review needs a git repo with a detectable default branch');
@@ -447,7 +462,7 @@ export function App(props) {
447
462
  : _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), ' ', line ? line.label : `${DISPLAY_NAME[id]} — checking…`, line?.fix ? _jsxs(Text, { dimColor: true, children: [" \u2192 ", line.fix] }) : null] }, id));
448
463
  }), preflightFail && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: "red", bold: true, children: ["preflight failed \u2014 ", preflightFail.ready, "/3 providers ready (aiki needs 2)"] }), _jsx(Text, { dimColor: true, children: "apply the fix shown next to each \u2716, then run `aiki` again \u00B7 `aiki doctor --fresh` re-checks live \u00B7 q quits" })] }))] })), (phase === 'input' || phase === 'running' || phase === 'grill' || phase === 'clarify' || phase === 'finished') && handles.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [repo && (_jsxs(Text, { children: ["repo: ", repo.name, " \u2014 ", repo.changedFiles, " changed files vs ", repo.defaultBranch ?? 'unknown default branch'] })), _jsxs(Text, { children: [handles.map((h, i) => (_jsxs(Text, { children: [i > 0 ? _jsx(Text, { dimColor: true, children: " \u00B7 " }) : null, _jsx(Text, { color: "green", children: "\u2714" }), " ", DISPLAY_NAME[h.id], h.version ? _jsxs(Text, { dimColor: true, children: [" ", h.version] }) : null] }, h.id))), _jsx(Text, { dimColor: true, children: " \u2014 council ready" })] }), degraded.length > 0 && (_jsxs(Text, { color: "yellow", children: ["\u26A0 ", degraded.map((line) => `${line.label}${line.fix ? ` (${line.fix})` : ''}`).join(' · '), " \u2014 continuing with ", Math.max(handles.length - degraded.length, 0), "/3 providers"] }))] })), phase === 'input' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "/idea " }), _jsx(Text, { dimColor: true, children: '<text>' }), ' stress-test an idea'] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "/review " }), _jsx(Text, { dimColor: true, children: "[--branch]" }), ' review your changes', repo ? '' : _jsx(Text, { dimColor: true, children: " (needs a git repo)" })] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "/resume " }), _jsx(Text, { dimColor: true, children: '<id>' }), ' continue a stopped run · ', _jsx(Text, { color: "cyan", children: "/sessions" }), ' ', _jsx(Text, { color: "cyan", children: "/models" }), ' ', _jsx(Text, { color: "cyan", children: "/config" }), ' ', _jsx(Text, { color: "cyan", children: "/help" })] })] }), pendingIdea !== null ? (
449
464
  /* V10 confirm gate — nothing spends model calls until Enter. */
450
- _jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsx(Text, { children: "Run the idea council on:" }), _jsxs(Text, { color: "cyan", children: [" \u201C", pendingIdea.length > 100 ? `${pendingIdea.slice(0, 97)}…` : pendingIdea, "\u201D"] }), _jsxs(Text, { dimColor: true, children: [" 10-stage pipeline \u00B7 up to ", budgetOverride ?? defaultBudgetFor('idea-refinement', 'council'), " model calls \u00B7 Ctrl+C aborts mid-run"] }), _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "enter" }), " run \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " cancel"] })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Type a command, or just describe your idea and press Enter:" }), _jsxs(Box, { borderStyle: "round", paddingX: 1, children: [_jsx(Text, { children: "\u25B8 " }), _jsx(TextInput, { value: idea, onChange: (v) => setIdea(v.replace(/\s*[\r\n]+\s*/g, ' ').replace(/\t/g, '')), onSubmit: submitInput }, inputEpoch)] })] })), paletteMatches.length > 0 && (
465
+ _jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsx(Text, { children: "Run the idea council on:" }), _jsxs(Text, { color: "cyan", children: [" \u201C", pendingIdea.length > 100 ? `${pendingIdea.slice(0, 97)}…` : pendingIdea, "\u201D"] }), _jsxs(Text, { dimColor: true, children: [" 10-stage pipeline \u00B7 ", inferIdeaMode(pendingIdea), " mode \u00B7 up to ", budgetOverride ?? defaultBudgetFor('idea-refinement', inferIdeaMode(pendingIdea)), " model calls \u00B7 Ctrl+C aborts mid-run"] }), _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "enter" }), " run \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " cancel"] })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Type a command, or just describe your idea and press Enter:" }), _jsxs(Box, { borderStyle: "round", paddingX: 1, children: [_jsx(Text, { children: "\u25B8 " }), _jsx(TextInput, { value: idea, onChange: (v) => setIdea(v.replace(/\s*[\r\n]+\s*/g, ' ').replace(/\t/g, '')), onSubmit: submitInput }, inputEpoch)] })] })), paletteMatches.length > 0 && (
451
466
  /* V10 live command palette — filtered as you type; ↑↓ move, Tab completes, Enter runs. */
452
467
  _jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [paletteMatches.map((m, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: i === selIdx ? 'cyan' : undefined, bold: i === selIdx, children: [i === selIdx ? '▸ ' : ' ', "/", m.name] }), _jsx(Text, { dimColor: true, children: ` ${m.usage.replace(`/${m.name}`, '').trim().padEnd(10)} ${m.help}` })] }, m.name))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u00B7 tab complete \u00B7 enter run" })] })), routerMessage ? _jsx(Text, { color: "yellow", children: routerMessage }) : null, panel ? (_jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", paddingX: 1, children: panel.split('\n').map((l, i) => (_jsx(Text, { children: l }, i))) })) : (_jsx(Text, { dimColor: true, children: "new here? /help explains how aiki works \u00B7 long idea? `aiki run idea-refinement ./idea.md`" }))] })), (phase === 'running' || phase === 'grill' || phase === 'clarify' || phase === 'finished') && rows.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [rows.map((r) => {
453
468
  const color = r.status === 'done' ? 'green' : r.status === 'failed' ? 'red' : r.status === 'running' ? 'yellow' : 'gray';
@@ -15,6 +15,7 @@ import { loadSkill } from '../orchestration/skills.js';
15
15
  import { buildLanePrompts } from '../orchestration/idea-lanes.js';
16
16
  import { preflight, renderDecisionInput } from '../orchestration/preflight.js';
17
17
  import { buildQuickPrompt, quickActionPlan, quickJudgeReport, s4QuickAnalyze } from '../orchestration/quick-analysis.js';
18
+ import { snapshotUrlSources } from '../orchestration/url-sources.js';
18
19
  /** Idea-vetting core rubric: 13 mandatory coverage items. S0 adds 3-5 domain dimensions per run.
19
20
  * Inlined here (like the S4 template) while the skill/`rubric.json` loader (§11)
20
21
  * is deferred; it moves to skills/idea-refinement/rubric.json when that loader lands. */
@@ -41,7 +42,7 @@ export function buildIdeaRubric(domainDimensions = []) {
41
42
  }
42
43
  /** Idea S4 analyst template. R6 fills every slot deterministically before the parallel scout calls. */
43
44
  export const IDEA_S4_ANALYST_TEMPLATE = `ROLE: Independent analyst on a decision panel. You work ALONE; you will not see
44
- other analysts' output. Be adversarial toward the idea, not polite.
45
+ other analysts' output. Improve the strongest version and pressure-test it honestly within your assigned lane.
45
46
 
46
47
  TASK CONTRACT: {{INTENT_CONTRACT_JSON}}
47
48
  INPUT DOCUMENT: read the file at {{INPUT_PATH}}{{SKILL}}
@@ -53,7 +54,10 @@ Produce ONLY JSON matching {{S4_SCHEMA_REF}} with:
53
54
  - task_echo: restate the task in ≤2 sentences (drift check).
54
55
  - strongest_version: the best honest version of this idea in ≤150 words.
55
56
  - positions: explicit claim positions with local_id, proposition, rubric dimension_id, stance
56
- SUPPORT|OPPOSE|MIXED|UNKNOWN, basis EVIDENCE|INFERENCE|ASSUMPTION, load_bearing, if_false
57
+ SUPPORT|OPPOSE|MIXED|UNKNOWN, basis EVIDENCE|INFERENCE|ASSUMPTION, nature FACTUAL when the position
58
+ asserts something independently checkable about the world (rules, deadlines, prices, observed CLI
59
+ behavior, published numbers) or JUDGMENT for a design choice, recommendation, prioritization, or
60
+ prediction (when unsure use JUDGMENT), load_bearing, if_false
57
61
  STOP|PIVOT|CONDITION|MINOR, concise reasoning, evidence_ids, and depends_on position ids.
58
62
  - evidence: evidence cards {id, claim_supported, source_kind USER|PRIMARY|SECONDARY|MODEL_KNOWLEDGE,
59
63
  support SUPPORTS|CONTRADICTS|CONTEXT_ONLY (exact token, no extra words), freshness CURRENT|DATED|UNKNOWN,
@@ -65,8 +69,11 @@ Produce ONLY JSON matching {{S4_SCHEMA_REF}} with:
65
69
  - coverage: one entry per rubric dimension {dimension_id, status COVERED|NOT_APPLICABLE,
66
70
  position_ids ([] when none), rationale (required for NOT_APPLICABLE)}.
67
71
  - decision_questions: questions {question, claim_ids} whose answers could change the verdict.
68
- Caps: at most 12 positions, 20 evidence cards, 8 calculations, 8 decision_questions.
69
- Rules: no motivation, no summaries of your own output, no markdown, JSON only.`;
72
+ - deliverable_proposals: when requested_outputs includes FEATURE_BACKLOG or IMPLEMENTATION_PLAN, propose
73
+ concrete user-facing work as {output,title,detail,user_value,why_distinctive,evidence_ids}; otherwise [].
74
+ Proposals are recommendations, not factual claims. Use evidence_ids only when evidence directly supports them.
75
+ Caps: at most 12 positions, 20 evidence cards, 8 calculations, 8 decision_questions, 8 deliverable_proposals.
76
+ Rules: no unsupported hype, no summaries of your own output, no markdown, JSON only.`;
70
77
  /**
71
78
  * Resolve the {{SKILL}} slot before deterministic structural fill. An empty skill collapses the slot
72
79
  * to the pre-skill baseline.
@@ -76,11 +83,11 @@ export function buildAnalystTemplate(skill) {
76
83
  }
77
84
  /** R6 deterministic S4 prompt fill. No model-authored prompt generation remains. */
78
85
  export function buildAnalystPrompt(contract, inputPath, evidencePack, mode, skill) {
79
- const modeRules = mode === 'research'
80
- ? `\n\nMODE: research. Use provider-native read-only source investigation when available. Every current
86
+ const modeRules = mode !== 'quick'
87
+ ? `\n\nMODE: full council. Use provider-native read-only source investigation when available. Every current
81
88
  fact must have an independently checkable locator and access date. If investigation is unavailable,
82
89
  leave the claim unverified; never invent a source.`
83
- : `\n\nMODE: council. Analyze independently; do not assume another seat will cover your lane.`;
90
+ : `\n\nMODE: quick. Analyze independently and keep unsupported claims visibly unverified.`;
84
91
  const prompt = buildAnalystTemplate(skill)
85
92
  .replace('{{INTENT_CONTRACT_JSON}}', JSON.stringify(contract))
86
93
  .replace('{{INPUT_PATH}}', inputPath)
@@ -106,14 +113,45 @@ export const IDEA_STAGES = [
106
113
  { id: 'S9b', label: 'Validation plan', role: 'judge' },
107
114
  { id: 'S10', label: 'Report', role: null },
108
115
  ];
116
+ /** Record guarantees observed at the surviving typed S4 boundary; never infer them from prompts. */
117
+ export function recordIdeaOutcomeFlags(ctx, contract, seats) {
118
+ if (ctx.mode === 'quick')
119
+ return;
120
+ const requested = (contract.requested_outputs ?? [])
121
+ .filter((output) => output === 'FEATURE_BACKLOG' || output === 'IMPLEMENTATION_PLAN');
122
+ if (requested.some((output) => seats.some((seat) => !seat.output.deliverable_proposals.some((proposal) => proposal.output === output)))) {
123
+ ctx.addFlag('deliverable_gap');
124
+ }
125
+ const grounded = seats.some((seat) => {
126
+ const cited = new Set(seat.output.positions.flatMap((position) => position.evidence_ids));
127
+ return seat.output.evidence.some((card) => cited.has(card.id)
128
+ && (card.source_kind === 'PRIMARY' || card.source_kind === 'SECONDARY')
129
+ && card.freshness === 'CURRENT'
130
+ && Boolean(card.accessed_at && (card.url || card.locator))
131
+ && card.support !== 'CONTEXT_ONLY');
132
+ });
133
+ if (!grounded)
134
+ ctx.addFlag('research_ungrounded');
135
+ }
109
136
  /** Runs the full idea-refinement pipeline S0–S10. Throws on any fatal condition; the engine's
110
137
  * `executeRun` wrapper turns that into a graceful failure + meta. Each stage is wrapped in
111
138
  * `runStage` so the TUI timeline (T8) gets start/end events; headless, that's a no-op. */
112
139
  export async function runIdeaRefinement(ctx, input) {
113
140
  if (ctx.evidencePack)
114
141
  await ctx.writer.writeInput('evidence-pack.json', JSON.stringify(ctx.evidencePack, null, 2));
115
- const { contract, brief } = await runStage(ctx, 'S0', () => preflight(ctx, input, IDEA_RUBRIC.map((item) => item.label)));
116
- const grilledInput = renderDecisionInput(input, brief);
142
+ const urlSources = await snapshotUrlSources(input);
143
+ await ctx.writer.writeJson('url-sources', urlSources);
144
+ const unreadableSources = urlSources.sources.filter((source) => source.status !== 'FETCHED');
145
+ const needsSourceFallback = ctx.mode !== 'quick' && unreadableSources.length > 0;
146
+ const hasReadableResearch = Boolean(ctx.evidencePack?.files.length)
147
+ || urlSources.sources.some((source) => source.status === 'FETCHED');
148
+ const canSearch = ctx.roles.s4.includes('codex');
149
+ if (needsSourceFallback && !canSearch && !hasReadableResearch) {
150
+ const details = unreadableSources.map((source) => `${source.url} (${source.status}: ${source.error})`).join('; ');
151
+ throw new StageError('S0', 'SOURCE_UNREADABLE', `source investigation stopped before model calls because no source was readable and Codex search is unavailable${details ? `: ${details}` : ''}. Paste the relevant text, provide a public export, or enable Codex, then rerun.`);
152
+ }
153
+ const { contract, brief } = await runStage(ctx, 'S0', () => preflight(ctx, input, IDEA_RUBRIC.map((item) => item.label), urlSources));
154
+ const grilledInput = renderDecisionInput(input, brief, urlSources);
117
155
  // Persist the input as a file so S4's "read the file at {{INPUT_PATH}}" resolves (not a stage).
118
156
  await ctx.writer.writeInput('idea.md', input);
119
157
  await ctx.writer.writeInput('idea-brief.md', grilledInput);
@@ -144,26 +182,24 @@ export async function runIdeaRefinement(ctx, input) {
144
182
  return report;
145
183
  });
146
184
  const actionPlan = await runStage(ctx, 'S9b', async () => {
147
- const plan = quickActionPlan(ctx, quick.seat.provider, quick.decision, graph);
185
+ const plan = quickActionPlan(ctx, quick.seat.provider, quick.decision, graph, contract);
148
186
  await ctx.writer.writeJson('action-plan', plan);
149
187
  return plan;
150
188
  });
151
189
  await runStage(ctx, 'S10', () => s10Render(ctx, { contract, seats: kept, graph, verifications, rebuttals, judgeReport, actionPlan, rubric, original: input }));
152
190
  return;
153
191
  }
154
- if (ctx.mode === 'research' && !ctx.evidencePack && !ctx.available().includes('codex')) {
155
- ctx.addFlag('research_ungrounded');
156
- }
157
192
  const analystPrompt = buildAnalystPrompt(contract, inputPath, ctx.evidencePack, ctx.mode, analystSkill);
158
193
  await ctx.writer.writePrompt('analyst.md', analystPrompt);
159
194
  const lanePrompts = buildLanePrompts(analystPrompt, rubric);
160
- const seats = await runStage(ctx, 'S4', () => s4Analyze(ctx, lanePrompts));
195
+ const seats = await runStage(ctx, 'S4', () => s4Analyze(ctx, lanePrompts, needsSourceFallback));
161
196
  const { kept } = await runStage(ctx, 'S5', () => s5Drift(ctx, contract, seats));
197
+ recordIdeaOutcomeFlags(ctx, contract, kept);
162
198
  const positions = await runStage(ctx, 'S6', () => s6Positions(ctx, kept));
163
199
  const graph = await runStage(ctx, 'S7', () => s7DecisionGraph(ctx, positions, rubric, contract.task));
164
200
  const verifications = await runStage(ctx, 'S8', () => s8Verify(ctx, graph));
165
201
  const rebuttals = await runStage(ctx, 'S8b', () => s8bRebuttal(ctx, graph, verifications, ctx.mode));
166
202
  const judgeReport = await runStage(ctx, 'S9', () => s9Judge(ctx, contract, graph, verifications, rubric, rebuttals));
167
- const actionPlan = await runStage(ctx, 'S9b', () => s9bPlan(ctx, contract, kept, graph, judgeReport));
203
+ const actionPlan = await runStage(ctx, 'S9b', () => s9bPlan(ctx, contract, kept, graph, judgeReport, input));
168
204
  await runStage(ctx, 'S10', () => s10Render(ctx, { contract, seats: kept, graph, verifications, rebuttals, judgeReport, actionPlan, rubric, original: input }));
169
205
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiki-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Local-first AI model council for code review and idea stress-testing using Claude Code, Codex, and Gemini/Antigravity CLIs. No API keys.",
5
5
  "type": "module",
6
6
  "license": "MIT",