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.
- package/CHANGELOG.md +16 -0
- package/README.md +15 -5
- package/dist/cli/index.js +1 -1
- package/dist/cli/run.js +9 -6
- package/dist/council/view.js +99 -39
- package/dist/orchestration/decision-dossier.js +79 -21
- package/dist/orchestration/modes.js +6 -0
- package/dist/orchestration/preflight.js +28 -8
- package/dist/orchestration/quick-analysis.js +15 -3
- package/dist/orchestration/stages/s10-render.js +12 -0
- package/dist/orchestration/stages/s9b-plan.js +33 -10
- package/dist/orchestration/url-sources.js +200 -0
- package/dist/schemas/index.js +67 -3
- package/dist/storage/runs.js +2 -1
- package/dist/tui/app.js +19 -4
- package/dist/workflows/idea-refinement.js +11 -3
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/schemas/index.js
CHANGED
|
@@ -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).
|
|
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).
|
|
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).
|
|
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({
|
package/dist/storage/runs.js
CHANGED
|
@@ -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({
|
|
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) =>
|
|
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',
|
|
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
|
|
116
|
-
|
|
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