aiki-cli 0.3.0 → 0.3.1

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.
@@ -0,0 +1,200 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lookup } from 'node:dns/promises';
3
+ import { isIP } from 'node:net';
4
+ import { UrlSourceSet, } from '../schemas/index.js';
5
+ const MAX_SOURCES = 5;
6
+ const MAX_RESPONSE_BYTES = 500_000;
7
+ const MAX_CONTENT_CHARS = 30_000;
8
+ const MAX_REDIRECTS = 3;
9
+ export function extractPublicUrls(input) {
10
+ const matches = input.match(/https?:\/\/[^\s<>"']+/gi) ?? [];
11
+ const seen = new Set();
12
+ const urls = [];
13
+ for (const match of matches) {
14
+ const cleaned = match.replace(/[.,;:!?\]\)}]+$/g, '');
15
+ if (seen.has(cleaned))
16
+ continue;
17
+ seen.add(cleaned);
18
+ urls.push(cleaned);
19
+ if (urls.length === MAX_SOURCES)
20
+ break;
21
+ }
22
+ return urls;
23
+ }
24
+ function isPrivateAddress(address) {
25
+ if (address === '::' || address === '::1')
26
+ return true;
27
+ if (address.toLowerCase().startsWith('fe80:') || address.toLowerCase().startsWith('fc') || address.toLowerCase().startsWith('fd'))
28
+ return true;
29
+ const mapped = address.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i)?.[1];
30
+ const ipv4 = mapped ?? (isIP(address) === 4 ? address : undefined);
31
+ if (!ipv4)
32
+ return false;
33
+ const [a, b] = ipv4.split('.').map(Number);
34
+ return a === 0
35
+ || a === 10
36
+ || a === 127
37
+ || (a === 169 && b === 254)
38
+ || (a === 172 && b >= 16 && b <= 31)
39
+ || (a === 192 && b === 168)
40
+ || (a === 100 && b >= 64 && b <= 127)
41
+ || a >= 224;
42
+ }
43
+ async function assertPublicUrl(url, resolveHostname) {
44
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
45
+ throw new Error('only public http/https URLs are supported');
46
+ const hostname = url.hostname.toLowerCase().replace(/\.$/, '');
47
+ if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname.endsWith('.local') || hostname === '169.254.169.254') {
48
+ throw new Error('private or local URLs are not allowed');
49
+ }
50
+ if (isIP(hostname) && isPrivateAddress(hostname))
51
+ throw new Error('private or local URLs are not allowed');
52
+ if (!resolveHostname || isIP(hostname))
53
+ return;
54
+ const addresses = await lookup(hostname, { all: true });
55
+ if (addresses.length === 0 || addresses.some(({ address }) => isPrivateAddress(address))) {
56
+ throw new Error('private or local URLs are not allowed');
57
+ }
58
+ }
59
+ function npmRegistryUrl(url) {
60
+ if (url.hostname !== 'npmjs.com' && url.hostname !== 'www.npmjs.com')
61
+ return undefined;
62
+ const match = url.pathname.match(/^\/package\/(.+?)\/?$/);
63
+ if (!match)
64
+ return undefined;
65
+ const name = decodeURIComponent(match[1]);
66
+ return `https://registry.npmjs.org/${encodeURIComponent(name)}/latest`;
67
+ }
68
+ function decodeHtml(value) {
69
+ const named = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' };
70
+ return value.replace(/&(#x[\da-f]+|#\d+|\w+);/gi, (entity, code) => {
71
+ if (code[0] === '#') {
72
+ const radix = code[1]?.toLowerCase() === 'x' ? 16 : 10;
73
+ const value = Number.parseInt(code.slice(radix === 16 ? 2 : 1), radix);
74
+ return Number.isFinite(value) ? String.fromCodePoint(value) : entity;
75
+ }
76
+ return named[code.toLowerCase()] ?? entity;
77
+ });
78
+ }
79
+ function readableHtml(html) {
80
+ const titleMatch = html.match(/<title\b[^>]*>([\s\S]*?)<\/title>/i);
81
+ const title = titleMatch ? decodeHtml(titleMatch[1].replace(/<[^>]+>/g, ' ')).replace(/\s+/g, ' ').trim() : undefined;
82
+ const content = decodeHtml(html
83
+ .replace(/<(script|style|noscript|svg)\b[^>]*>[\s\S]*?<\/\1>/gi, ' ')
84
+ .replace(/<[^>]+>/g, ' '))
85
+ .replace(/\s+/g, ' ')
86
+ .trim();
87
+ return { title: title || undefined, content };
88
+ }
89
+ function sha256(content) {
90
+ return createHash('sha256').update(content).digest('hex');
91
+ }
92
+ async function readLimited(response) {
93
+ if (!response.body)
94
+ return '';
95
+ const reader = response.body.getReader();
96
+ const decoder = new TextDecoder();
97
+ let bytes = 0;
98
+ let body = '';
99
+ while (true) {
100
+ const { done, value } = await reader.read();
101
+ if (done)
102
+ return body + decoder.decode();
103
+ bytes += value.byteLength;
104
+ if (bytes > MAX_RESPONSE_BYTES) {
105
+ await reader.cancel();
106
+ throw new Error(`response exceeds ${MAX_RESPONSE_BYTES} bytes`);
107
+ }
108
+ body += decoder.decode(value, { stream: true });
109
+ }
110
+ }
111
+ function failure(id, url, accessedAt, status, error, finalUrl) {
112
+ return { id, url, final_url: finalUrl, status, accessed_at: accessedAt, error };
113
+ }
114
+ async function fetchWithRedirects(startUrl, fetchImpl, resolveHostname) {
115
+ let current = new URL(startUrl);
116
+ for (let redirect = 0; redirect <= MAX_REDIRECTS; redirect++) {
117
+ await assertPublicUrl(current, resolveHostname);
118
+ const response = await fetchImpl(current.toString(), {
119
+ redirect: 'manual',
120
+ headers: { accept: 'text/html,application/json,text/plain;q=0.9,*/*;q=0.1' },
121
+ });
122
+ if (response.status < 300 || response.status >= 400)
123
+ return { response, finalUrl: current.toString() };
124
+ const location = response.headers.get('location');
125
+ if (!location)
126
+ return { response, finalUrl: current.toString() };
127
+ if (redirect === MAX_REDIRECTS)
128
+ throw new Error(`more than ${MAX_REDIRECTS} redirects`);
129
+ current = new URL(location, current);
130
+ }
131
+ throw new Error('redirect limit exceeded');
132
+ }
133
+ async function snapshotOne(sourceUrl, index, options) {
134
+ const id = `U${index + 1}`;
135
+ const accessedAt = (options.now?.() ?? new Date()).toISOString();
136
+ const fetchImpl = options.fetchImpl ?? fetch;
137
+ try {
138
+ const original = new URL(sourceUrl);
139
+ await assertPublicUrl(original, options.fetchImpl === undefined);
140
+ const adapterUrl = npmRegistryUrl(original);
141
+ const { response, finalUrl } = await fetchWithRedirects(adapterUrl ?? sourceUrl, fetchImpl, options.fetchImpl === undefined);
142
+ const declaredLength = Number(response.headers.get('content-length') ?? 0);
143
+ if (declaredLength > MAX_RESPONSE_BYTES) {
144
+ return failure(id, sourceUrl, accessedAt, 'FAILED', `response exceeds ${MAX_RESPONSE_BYTES} bytes`, finalUrl);
145
+ }
146
+ const body = await readLimited(response);
147
+ const contentType = response.headers.get('content-type')?.split(';')[0]?.trim() || 'text/plain';
148
+ const blocked = response.status === 403
149
+ || response.status === 429
150
+ || response.status === 503
151
+ || /cdn-cgi\/challenge|just a moment|captcha|access denied/i.test(body);
152
+ if (blocked) {
153
+ return failure(id, sourceUrl, accessedAt, 'BLOCKED', `site blocked automated access (HTTP ${response.status})`, finalUrl);
154
+ }
155
+ if (!response.ok)
156
+ return failure(id, sourceUrl, accessedAt, 'FAILED', `HTTP ${response.status}`, finalUrl);
157
+ let title;
158
+ let content;
159
+ if (adapterUrl) {
160
+ const metadata = JSON.parse(body);
161
+ const name = typeof metadata.name === 'string' ? metadata.name : original.pathname.split('/').at(-1) ?? 'npm package';
162
+ const version = typeof metadata.version === 'string' ? metadata.version : 'unknown version';
163
+ title = `${name} ${version}`;
164
+ content = [
165
+ `Package: ${name}`,
166
+ `Version: ${version}`,
167
+ typeof metadata.description === 'string' ? `Description: ${metadata.description}` : '',
168
+ typeof metadata.homepage === 'string' ? `Homepage: ${metadata.homepage}` : '',
169
+ typeof metadata.readme === 'string' ? `README:\n${metadata.readme}` : '',
170
+ ].filter(Boolean).join('\n');
171
+ }
172
+ else if (contentType === 'text/html' || /<html\b|<body\b/i.test(body)) {
173
+ ({ title, content } = readableHtml(body));
174
+ }
175
+ else {
176
+ content = body.replace(/\s+/g, ' ').trim();
177
+ }
178
+ content = content.slice(0, MAX_CONTENT_CHARS).trim();
179
+ if (!content)
180
+ return failure(id, sourceUrl, accessedAt, 'FAILED', 'source contained no readable text', finalUrl);
181
+ return {
182
+ id,
183
+ url: sourceUrl,
184
+ final_url: finalUrl,
185
+ status: 'FETCHED',
186
+ title,
187
+ content_type: contentType,
188
+ accessed_at: accessedAt,
189
+ sha256: sha256(content),
190
+ content,
191
+ };
192
+ }
193
+ catch (error) {
194
+ return failure(id, sourceUrl, accessedAt, 'FAILED', error instanceof Error ? error.message : String(error));
195
+ }
196
+ }
197
+ export async function snapshotUrlSources(input, options = {}) {
198
+ const sources = await Promise.all(extractPublicUrls(input).map((url, index) => snapshotOne(url, index, options)));
199
+ return UrlSourceSet.parse({ sources });
200
+ }
@@ -19,6 +19,34 @@ export const TaskTypeSchema = z.enum(['idea-refinement', 'code-review', 'other']
19
19
  export const WorkflowIdSchema = z.enum(['idea-refinement', 'code-review']);
20
20
  /** Explicit user-selected idea protocol. There is deliberately no learned mode router. */
21
21
  export const IdeaModeSchema = z.enum(['quick', 'council', 'research']);
22
+ export const RequestedOutputSchema = z.enum([
23
+ 'DECISION',
24
+ 'FEATURE_BACKLOG',
25
+ 'IMPLEMENTATION_PLAN',
26
+ ]);
27
+ export const UrlSourceStatusSchema = z.enum(['FETCHED', 'BLOCKED', 'FAILED']);
28
+ export const UrlSourceSnapshot = z.object({
29
+ id: z.string().regex(/^U[1-5]$/),
30
+ url: z.string().url(),
31
+ final_url: z.string().url().optional(),
32
+ status: UrlSourceStatusSchema,
33
+ title: z.string().min(1).optional(),
34
+ content_type: z.string().min(1).optional(),
35
+ accessed_at: z.string().datetime(),
36
+ sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
37
+ content: z.string().min(1).optional(),
38
+ error: z.string().min(1).optional(),
39
+ }).strict().superRefine((source, ctx) => {
40
+ if (source.status === 'FETCHED' && (!source.content || !source.sha256)) {
41
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'FETCHED source requires content and sha256' });
42
+ }
43
+ if (source.status !== 'FETCHED' && !source.error) {
44
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `${source.status} source requires an error` });
45
+ }
46
+ });
47
+ export const UrlSourceSet = z.object({
48
+ sources: z.array(UrlSourceSnapshot).max(5),
49
+ }).strict();
22
50
  export const DomainDimension = z
23
51
  .object({
24
52
  id: z.string().regex(/^D[1-5]$/),
@@ -46,6 +74,7 @@ export const DecisionContract = IntentContract.extend({
46
74
  core_rubric: z.array(z.string().min(1)).min(1),
47
75
  user_confirmed: z.boolean(),
48
76
  confirmation: z.enum(['user-confirmed', 'headless-defaulted']),
77
+ requested_outputs: z.array(RequestedOutputSchema).min(1).max(4).optional(),
49
78
  }).strict();
50
79
  /** Code review and old idea runs keep the smaller v1 contract. */
51
80
  export const DecisionContractArtifact = z.union([DecisionContract, IntentContract]);
@@ -88,7 +117,7 @@ const RunBriefDraftBase = z
88
117
  evidence_supplied: z.array(z.string().min(1)).max(8),
89
118
  missing_axes: z.array(z.string().min(1)).max(8),
90
119
  domain_dimensions: z.array(DomainDimension).min(3).max(5),
91
- questions: z.array(RunBriefQuestion).min(3).max(4),
120
+ questions: z.array(RunBriefQuestion).max(4),
92
121
  })
93
122
  .strict();
94
123
  function checkQuestionIds(questions, ctx) {
@@ -121,7 +150,7 @@ export const GrillAnswer = z
121
150
  })
122
151
  .strict();
123
152
  export const RunBrief = RunBriefDraftBase.extend({
124
- answers: z.array(GrillAnswer).min(3).max(4),
153
+ answers: z.array(GrillAnswer).max(4),
125
154
  }).superRefine((brief, ctx) => {
126
155
  checkQuestionIds(brief.questions, ctx);
127
156
  checkDomainDimensionIds(brief.domain_dimensions, ctx);
@@ -156,7 +185,7 @@ export const PreflightReading = z.object({
156
185
  evidence_supplied: z.array(z.string().min(1)).max(8),
157
186
  missing_evidence: z.array(z.string().min(1)).max(8),
158
187
  domain_dimensions: z.array(DomainDimension).min(3).max(5),
159
- questions: z.array(RunBriefQuestion).min(3).max(4),
188
+ questions: z.array(RunBriefQuestion).max(4),
160
189
  }).strict().superRefine((reading, ctx) => {
161
190
  checkQuestionIds(reading.questions, ctx);
162
191
  checkDomainDimensionIds(reading.domain_dimensions, ctx);
@@ -837,6 +866,39 @@ export const IdeaChairReportModel = JudgeReportModel.omit({ adjudications: true
837
866
  adjudications: z.array(IdeaChairRuling),
838
867
  });
839
868
  // ── S9b: ActionPlan (idea-refinement report v3) ─────────────────────────────
869
+ export const FeatureBacklog = z.object({
870
+ must: z.array(z.object({
871
+ feature: z.string().min(1),
872
+ user_value: z.string().min(1),
873
+ rationale: z.string().min(1),
874
+ effort: z.enum(['S', 'M', 'L']),
875
+ }).strict()).min(1).max(6),
876
+ should: z.array(z.object({
877
+ feature: z.string().min(1),
878
+ user_value: z.string().min(1),
879
+ rationale: z.string().min(1),
880
+ effort: z.enum(['S', 'M', 'L']),
881
+ }).strict()).max(6),
882
+ later: z.array(z.object({
883
+ feature: z.string().min(1),
884
+ user_value: z.string().min(1),
885
+ rationale: z.string().min(1),
886
+ effort: z.enum(['S', 'M', 'L']),
887
+ }).strict()).max(6),
888
+ wont: z.array(z.object({
889
+ feature: z.string().min(1),
890
+ reason: z.string().min(1),
891
+ }).strict()).max(6),
892
+ }).strict();
893
+ export const ImplementationPlan = z.object({
894
+ milestones: z.array(z.object({
895
+ order: z.number().int().min(1),
896
+ timebox: z.string().min(1),
897
+ outcome: z.string().min(1),
898
+ tasks: z.array(z.string().min(1)).min(1).max(6),
899
+ acceptance_test: z.string().min(1),
900
+ }).strict()).min(1).max(7),
901
+ }).strict();
840
902
  export const ActionPlan = z
841
903
  .object({
842
904
  actions: z
@@ -853,6 +915,8 @@ export const ActionPlan = z
853
915
  .min(1)
854
916
  .max(7),
855
917
  sequencing_note: z.string().min(1),
918
+ feature_backlog: FeatureBacklog.optional(),
919
+ implementation_plan: ImplementationPlan.optional(),
856
920
  })
857
921
  .strict();
858
922
  export const PlannerUnavailable = z.object({
@@ -12,7 +12,7 @@
12
12
  // an invalid payload throws and writes nothing.
13
13
  import { mkdir, rename, writeFile } from 'node:fs/promises';
14
14
  import { dirname, join } from 'node:path';
15
- import { ActionPlanArtifact, DecisionContractArtifact, DecisionGraph, DisagreementMap, IdeaRoleOutput, JudgeReport, PreflightArtifact, RebuttalEventSet, ReviewMap, RoleOutput, RunBrief, RunMeta, VerificationArtifact } from '../schemas/index.js';
15
+ import { ActionPlanArtifact, DecisionContractArtifact, DecisionGraph, DisagreementMap, IdeaRoleOutput, JudgeReport, PreflightArtifact, RebuttalEventSet, ReviewMap, RoleOutput, RunBrief, RunMeta, UrlSourceSet, VerificationArtifact } from '../schemas/index.js';
16
16
  export class OutOfOrderWriteError extends Error {
17
17
  constructor(slot, ord, maxOrd) {
18
18
  super(`out-of-order write: "${slot}" (stage ${ord}) after stage ${maxOrd} already written`);
@@ -28,6 +28,7 @@ export class DuplicateWriteError extends Error {
28
28
  /** JSON stage slots. Composites without a T4 core schema (misunderstanding-guard, drift, claims)
29
29
  * are written as-is; their schemas land with S2/S5/S6 (T5–T6). */
30
30
  const JSON_SLOTS = {
31
+ 'url-sources': { ord: 0.25, path: '00a-url-sources.json', schema: UrlSourceSet },
31
32
  'run-brief': { ord: 0.5, path: '00b-run-brief.json', schema: RunBrief },
32
33
  'intent-contract': { ord: 1, path: '01-intent-contract.json', schema: DecisionContractArtifact },
33
34
  'preflight-readings': { ord: 2, path: '02-preflight-readings.json', schema: PreflightArtifact },
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. */
@@ -112,8 +113,15 @@ export const IDEA_STAGES = [
112
113
  export async function runIdeaRefinement(ctx, input) {
113
114
  if (ctx.evidencePack)
114
115
  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);
116
+ const urlSources = await snapshotUrlSources(input);
117
+ await ctx.writer.writeJson('url-sources', urlSources);
118
+ const unreadableSources = urlSources.sources.filter((source) => source.status !== 'FETCHED');
119
+ if (ctx.mode === 'research' && unreadableSources.length > 0) {
120
+ const details = unreadableSources.map((source) => `${source.url} (${source.status}: ${source.error})`).join('; ');
121
+ throw new StageError('S0', 'SOURCE_UNREADABLE', `research stopped before model calls because a supplied source could not be read: ${details}. Paste the relevant text or provide a public export, then rerun.`);
122
+ }
123
+ const { contract, brief } = await runStage(ctx, 'S0', () => preflight(ctx, input, IDEA_RUBRIC.map((item) => item.label), urlSources));
124
+ const grilledInput = renderDecisionInput(input, brief, urlSources);
117
125
  // Persist the input as a file so S4's "read the file at {{INPUT_PATH}}" resolves (not a stage).
118
126
  await ctx.writer.writeInput('idea.md', input);
119
127
  await ctx.writer.writeInput('idea-brief.md', grilledInput);
@@ -144,7 +152,7 @@ export async function runIdeaRefinement(ctx, input) {
144
152
  return report;
145
153
  });
146
154
  const actionPlan = await runStage(ctx, 'S9b', async () => {
147
- const plan = quickActionPlan(ctx, quick.seat.provider, quick.decision, graph);
155
+ const plan = quickActionPlan(ctx, quick.seat.provider, quick.decision, graph, contract);
148
156
  await ctx.writer.writeJson('action-plan', plan);
149
157
  return plan;
150
158
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiki-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",