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.
- package/CHANGELOG.md +23 -0
- package/README.md +16 -8
- package/dist/bench/idea-v3-rating.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/run.js +10 -7
- package/dist/council/view.js +146 -46
- package/dist/orchestration/context.js +3 -3
- package/dist/orchestration/decision-dossier.js +467 -80
- package/dist/orchestration/decision-graph.js +31 -0
- package/dist/orchestration/legacy-idea-adapter.js +3 -0
- package/dist/orchestration/modes.js +16 -4
- package/dist/orchestration/preflight.js +43 -9
- package/dist/orchestration/quick-analysis.js +35 -6
- package/dist/orchestration/stages/s10-render.js +179 -79
- package/dist/orchestration/stages/s4-analyze.js +3 -1
- package/dist/orchestration/stages/s6-positions.js +18 -0
- package/dist/orchestration/stages/s7-decision-graph.js +3 -0
- package/dist/orchestration/stages/s8-verify.js +26 -9
- package/dist/orchestration/stages/s8b-rebuttal.js +11 -4
- package/dist/orchestration/stages/s9-judge.js +36 -13
- package/dist/orchestration/stages/s9b-plan.js +208 -35
- package/dist/orchestration/url-sources.js +200 -0
- package/dist/schemas/index.js +134 -6
- package/dist/skills/idea-refinement/analyst.md +5 -5
- package/dist/skills/idea-refinement/chair.md +18 -0
- package/dist/skills/idea-refinement/economics-delivery.md +11 -0
- package/dist/skills/idea-refinement/market-adoption.md +12 -0
- package/dist/skills/idea-refinement/planner.md +25 -19
- package/dist/skills/idea-refinement/rebuttal.md +15 -0
- package/dist/skills/idea-refinement/verifier.md +17 -0
- package/dist/storage/runs.js +2 -1
- package/dist/tui/app.js +19 -4
- package/dist/workflows/idea-refinement.js +51 -15
- 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,8 @@ 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),
|
|
189
|
+
requested_outputs: z.array(RequestedOutputSchema).max(4).default([]),
|
|
160
190
|
}).strict().superRefine((reading, ctx) => {
|
|
161
191
|
checkQuestionIds(reading.questions, ctx);
|
|
162
192
|
checkDomainDimensionIds(reading.domain_dimensions, ctx);
|
|
@@ -207,6 +237,7 @@ export const ClaimPosition = z
|
|
|
207
237
|
dimension_id: z.string().min(1),
|
|
208
238
|
stance: z.enum(['SUPPORT', 'OPPOSE', 'MIXED', 'UNKNOWN']),
|
|
209
239
|
basis: z.enum(['EVIDENCE', 'INFERENCE', 'ASSUMPTION']),
|
|
240
|
+
nature: z.enum(['FACTUAL', 'JUDGMENT']).default('JUDGMENT'),
|
|
210
241
|
load_bearing: z.boolean(),
|
|
211
242
|
if_false: z.enum(['STOP', 'PIVOT', 'CONDITION', 'MINOR']),
|
|
212
243
|
reasoning: z.string().min(1),
|
|
@@ -287,6 +318,14 @@ export const DecisionQuestion = z
|
|
|
287
318
|
claim_ids: z.array(z.string().min(1)),
|
|
288
319
|
})
|
|
289
320
|
.strict();
|
|
321
|
+
export const DeliverableProposal = z.object({
|
|
322
|
+
output: z.enum(['FEATURE_BACKLOG', 'IMPLEMENTATION_PLAN']),
|
|
323
|
+
title: z.string().min(1),
|
|
324
|
+
detail: z.string().min(1),
|
|
325
|
+
user_value: z.string().min(1),
|
|
326
|
+
why_distinctive: z.string().min(1),
|
|
327
|
+
evidence_ids: z.array(z.string().min(1)).max(6),
|
|
328
|
+
}).strict();
|
|
290
329
|
const IdeaRoleOutputBase = z
|
|
291
330
|
.object({
|
|
292
331
|
workflow: z.literal('idea-refinement'),
|
|
@@ -297,6 +336,7 @@ const IdeaRoleOutputBase = z
|
|
|
297
336
|
calculations: z.array(CalculationLedger).max(8).default([]),
|
|
298
337
|
coverage: z.array(CoverageEntry).max(18), // 13 core + up to 5 preflight domain dimensions
|
|
299
338
|
decision_questions: z.array(DecisionQuestion).max(8),
|
|
339
|
+
deliverable_proposals: z.array(DeliverableProposal).max(8).default([]),
|
|
300
340
|
})
|
|
301
341
|
.strict();
|
|
302
342
|
function checkSubmissionRefs(submission, ctx) {
|
|
@@ -366,6 +406,12 @@ function checkSubmissionRefs(submission, ctx) {
|
|
|
366
406
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['decision_questions', index, 'claim_ids'], message: `unknown position id: ${id}` });
|
|
367
407
|
}
|
|
368
408
|
}
|
|
409
|
+
for (const [index, proposal] of submission.deliverable_proposals.entries()) {
|
|
410
|
+
for (const id of proposal.evidence_ids) {
|
|
411
|
+
if (!evidenceIds.has(id))
|
|
412
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['deliverable_proposals', index, 'evidence_ids'], message: `unknown evidence id: ${id}` });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
369
415
|
}
|
|
370
416
|
export const IdeaRoleOutput = IdeaRoleOutputBase.superRefine(checkSubmissionRefs);
|
|
371
417
|
/** Defect categories a finding (and a seeded bug, T11) can carry. BENCHMARK.md's "defect class" match
|
|
@@ -498,6 +544,17 @@ export function salvageIdeaRoleOutputModel(input) {
|
|
|
498
544
|
&& calc.inputs.every((input) => input.evidence_ids.every((id) => keptEvidence.has(id))))
|
|
499
545
|
.slice(0, 8)
|
|
500
546
|
: output.calculations;
|
|
547
|
+
const deliverableProposals = parseOrDrop(output.deliverable_proposals, (item) => {
|
|
548
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
549
|
+
return DeliverableProposal.safeParse(item);
|
|
550
|
+
const proposal = item;
|
|
551
|
+
return DeliverableProposal.strip().safeParse({
|
|
552
|
+
...proposal,
|
|
553
|
+
evidence_ids: Array.isArray(proposal.evidence_ids)
|
|
554
|
+
? proposal.evidence_ids.filter((id) => keptEvidence.has(id))
|
|
555
|
+
: proposal.evidence_ids,
|
|
556
|
+
});
|
|
557
|
+
});
|
|
501
558
|
return {
|
|
502
559
|
...output,
|
|
503
560
|
evidence,
|
|
@@ -505,6 +562,7 @@ export function salvageIdeaRoleOutputModel(input) {
|
|
|
505
562
|
calculations,
|
|
506
563
|
coverage: parseOrDrop(scrubIds(output.coverage, 'position_ids'), (item) => CoverageEntryBase.strip().superRefine(checkCoverageEntry).safeParse(item)),
|
|
507
564
|
decision_questions: parseOrDrop(scrubIds(output.decision_questions, 'claim_ids'), (item) => DecisionQuestion.strip().safeParse(item)),
|
|
565
|
+
deliverable_proposals: deliverableProposals,
|
|
508
566
|
};
|
|
509
567
|
}
|
|
510
568
|
/** The model-facing S4 shape. Exact known enum aliases are canonicalized, then the strict schema validates
|
|
@@ -559,6 +617,7 @@ export const DecisionClaim = z.object({
|
|
|
559
617
|
position_ids: z.array(z.string().min(1)).min(1),
|
|
560
618
|
state: z.enum(['CONSENSUS', 'SHARED_CONCERN', 'DISAGREEMENT', 'UNIQUE', 'UNCERTAIN']),
|
|
561
619
|
evidence_state: z.enum(['SUPPORTED', 'CONFLICTED', 'UNVERIFIED']),
|
|
620
|
+
nature: z.enum(['FACTUAL', 'JUDGMENT']).default('JUDGMENT'),
|
|
562
621
|
load_bearing: z.boolean(),
|
|
563
622
|
if_false: z.enum(['STOP', 'PIVOT', 'CONDITION', 'MINOR']),
|
|
564
623
|
sensitivity: z.enum(['DECISIVE', 'MATERIAL', 'LOW']),
|
|
@@ -837,7 +896,65 @@ export const IdeaChairReportModel = JudgeReportModel.omit({ adjudications: true
|
|
|
837
896
|
adjudications: z.array(IdeaChairRuling),
|
|
838
897
|
});
|
|
839
898
|
// ── S9b: ActionPlan (idea-refinement report v3) ─────────────────────────────
|
|
840
|
-
export const
|
|
899
|
+
export const FeatureBacklog = z.object({
|
|
900
|
+
must: z.array(z.object({
|
|
901
|
+
feature: z.string().min(1),
|
|
902
|
+
user_value: z.string().min(1),
|
|
903
|
+
rationale: z.string().min(1),
|
|
904
|
+
effort: z.enum(['S', 'M', 'L']),
|
|
905
|
+
}).strict()).min(1).max(6),
|
|
906
|
+
should: z.array(z.object({
|
|
907
|
+
feature: z.string().min(1),
|
|
908
|
+
user_value: z.string().min(1),
|
|
909
|
+
rationale: z.string().min(1),
|
|
910
|
+
effort: z.enum(['S', 'M', 'L']),
|
|
911
|
+
}).strict()).max(6),
|
|
912
|
+
later: z.array(z.object({
|
|
913
|
+
feature: z.string().min(1),
|
|
914
|
+
user_value: z.string().min(1),
|
|
915
|
+
rationale: z.string().min(1),
|
|
916
|
+
effort: z.enum(['S', 'M', 'L']),
|
|
917
|
+
}).strict()).max(6),
|
|
918
|
+
wont: z.array(z.object({
|
|
919
|
+
feature: z.string().min(1),
|
|
920
|
+
reason: z.string().min(1),
|
|
921
|
+
}).strict()).max(6),
|
|
922
|
+
}).strict();
|
|
923
|
+
export const ImplementationPlan = z.object({
|
|
924
|
+
milestones: z.array(z.object({
|
|
925
|
+
order: z.number().int().min(1),
|
|
926
|
+
timebox: z.string().min(1),
|
|
927
|
+
outcome: z.string().min(1),
|
|
928
|
+
tasks: z.array(z.string().min(1)).min(1).max(6),
|
|
929
|
+
acceptance_test: z.string().min(1),
|
|
930
|
+
}).strict()).min(1).max(7),
|
|
931
|
+
}).strict();
|
|
932
|
+
const ReaderBriefBase = z.object({
|
|
933
|
+
headline: z.string().min(1).max(160),
|
|
934
|
+
bottom_line: z.string().min(1).max(1200),
|
|
935
|
+
sections: z.array(z.object({
|
|
936
|
+
heading: z.string().min(1).max(120),
|
|
937
|
+
summary: z.string().min(1).max(1000),
|
|
938
|
+
bullets: z.array(z.string().min(1).max(500)).max(6),
|
|
939
|
+
}).strict()).min(2).max(6),
|
|
940
|
+
next_step: z.string().min(1).max(600),
|
|
941
|
+
caveats: z.array(z.string().min(1).max(500)).max(3),
|
|
942
|
+
source_ids: z.array(z.string().min(1)).max(8),
|
|
943
|
+
}).strict();
|
|
944
|
+
const READER_AUDIT_ENUM = /\b(?:UNVERIFIED|UNVERIFIABLE|PARTIAL|CONFLICTED)\b/;
|
|
945
|
+
const READER_AUDIT_LANGUAGE = /\b(?:structural score|evidence coverage|verification coverage|claim ids?|graph ids?|provider calls?|model calls?|answer editor|reader_brief)\b/i;
|
|
946
|
+
export const ReaderBrief = ReaderBriefBase.superRefine((brief, ctx) => {
|
|
947
|
+
const text = JSON.stringify(brief);
|
|
948
|
+
if (READER_AUDIT_ENUM.test(text) || READER_AUDIT_LANGUAGE.test(text)) {
|
|
949
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'reader_brief contains internal audit language' });
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
/** Contextual reader-language check: only reject IDs that actually exist in this run. */
|
|
953
|
+
export function readerBriefIssues(brief, knownIds) {
|
|
954
|
+
const text = JSON.stringify(brief);
|
|
955
|
+
return [...knownIds].filter((id) => new RegExp(`\\b${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(text));
|
|
956
|
+
}
|
|
957
|
+
const ActionPlanBase = z
|
|
841
958
|
.object({
|
|
842
959
|
actions: z
|
|
843
960
|
.array(z
|
|
@@ -850,11 +967,19 @@ export const ActionPlan = z
|
|
|
850
967
|
kill_signal: z.string().min(1),
|
|
851
968
|
})
|
|
852
969
|
.strict())
|
|
853
|
-
.min(1)
|
|
854
970
|
.max(7),
|
|
855
971
|
sequencing_note: z.string().min(1),
|
|
972
|
+
feature_backlog: FeatureBacklog.optional(),
|
|
973
|
+
implementation_plan: ImplementationPlan.optional(),
|
|
974
|
+
// Optional only so pre-v5 run artifacts remain replayable. New decision contracts require it in S9b.
|
|
975
|
+
reader_brief: ReaderBrief.optional(),
|
|
856
976
|
})
|
|
857
977
|
.strict();
|
|
978
|
+
export const ActionPlan = ActionPlanBase.superRefine((plan, ctx) => {
|
|
979
|
+
if (plan.actions.length === 0 && !plan.reader_brief) {
|
|
980
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['actions'], message: 'an empty action plan requires reader_brief' });
|
|
981
|
+
}
|
|
982
|
+
});
|
|
858
983
|
export const PlannerUnavailable = z.object({
|
|
859
984
|
kind: z.literal('PlannerUnavailable'),
|
|
860
985
|
reason: z.enum(['budget_exhausted', 'planner_failed']),
|
|
@@ -870,7 +995,7 @@ export const QuickDecisionModel = z.object({
|
|
|
870
995
|
key_points: z.array(z.string().min(1)).min(2).max(8),
|
|
871
996
|
dissent: z.array(z.string().min(1)).min(1).max(4),
|
|
872
997
|
confidence_notes: z.string().min(1),
|
|
873
|
-
action_plan:
|
|
998
|
+
action_plan: ActionPlanBase.extend({ reader_brief: ReaderBrief }),
|
|
874
999
|
}).strict().superRefine((decision, ctx) => {
|
|
875
1000
|
const hasConditions = decision.conditions.length > 0;
|
|
876
1001
|
if (decision.recommendation === 'PROCEED_WITH_CONDITIONS' && !hasConditions) {
|
|
@@ -923,11 +1048,14 @@ export const RunMeta = z.object({
|
|
|
923
1048
|
flags: z.array(z.enum([
|
|
924
1049
|
'synthesis_suspect',
|
|
925
1050
|
'low_diversity',
|
|
1051
|
+
'weak_seat',
|
|
926
1052
|
'plan_skipped',
|
|
927
1053
|
'plan_fallback',
|
|
928
1054
|
'headless_intent',
|
|
929
1055
|
'verification_skipped',
|
|
930
1056
|
'research_ungrounded',
|
|
1057
|
+
'source_fallback_search',
|
|
1058
|
+
'deliverable_gap',
|
|
931
1059
|
'single_model',
|
|
932
1060
|
])).optional(),
|
|
933
1061
|
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Analyst playbook — pressure-test the idea
|
|
1
|
+
# Analyst playbook — strengthen and pressure-test the idea
|
|
2
2
|
|
|
3
|
-
You are an
|
|
4
|
-
where it breaks.
|
|
3
|
+
You are an independent analyst on a decision panel. Build the strongest useful version for your lane,
|
|
4
|
+
then expose where it breaks. Creation and criticism are both required.
|
|
5
5
|
|
|
6
6
|
## Strongest version first
|
|
7
7
|
- Before attacking, state the best honest version of the idea: who it is for and why it would win.
|
|
8
|
-
-
|
|
8
|
+
- Test THAT version, not a weak strawman. If the steelman is thin, that itself is a finding.
|
|
9
9
|
|
|
10
10
|
## Positions — make the stance explicit
|
|
11
11
|
- State each decision-critical proposition and take an explicit SUPPORT, OPPOSE, MIXED, or UNKNOWN
|
|
@@ -45,5 +45,5 @@ ownership; emit one explicit coverage entry for every owned dimension.
|
|
|
45
45
|
- Phrase each so that a yes/no or a single number would actually flip your assessment.
|
|
46
46
|
|
|
47
47
|
## Do not
|
|
48
|
-
- No
|
|
48
|
+
- No unsupported hype or generic feature wishlists. Explain the mechanism and user value.
|
|
49
49
|
- No unanchored position or evidence. No proposition you cannot tie to the decision.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Chair playbook — rule on the real disagreement
|
|
2
|
+
|
|
3
|
+
Add to your base instructions; the schema and caps are unchanged and always win.
|
|
4
|
+
|
|
5
|
+
## Synthesis discipline
|
|
6
|
+
- Count convergence only when seats reached the same conclusion independently with different evidence.
|
|
7
|
+
Two seats repeating the same hedge is not agreement.
|
|
8
|
+
- Never smooth over a clash. State both readings, compare their evidence quality, and rule.
|
|
9
|
+
- A strong dissent beats a weak majority. Side with the better-evidenced minority when warranted and
|
|
10
|
+
explain why in `key_points`.
|
|
11
|
+
- Never answer "it depends". Choose the required verdict enum; put contingencies into concrete,
|
|
12
|
+
checkable conditions that name their claim.
|
|
13
|
+
- Lead `key_points` with the evidence or result that would change the verdict.
|
|
14
|
+
- Report a blind spot only when verification or rebuttal actually exposed it.
|
|
15
|
+
|
|
16
|
+
## First action
|
|
17
|
+
- Anchor the first action to the single cheapest check that could kill or confirm the decision.
|
|
18
|
+
- Pick one thing first, not a list disguised as one action.
|
|
@@ -3,5 +3,16 @@
|
|
|
3
3
|
Own unit economics and capital needs, technical and operational feasibility, supply constraints,
|
|
4
4
|
legal and compliance exposure, scalability, failure modes, and the cheapest decisive kill criteria.
|
|
5
5
|
|
|
6
|
+
Act as the skeptic/executor for requested deliverables. When FEATURE_BACKLOG is requested, produce 2–4
|
|
7
|
+
`deliverable_proposals` that preserve the standout value while remaining credible under the stated deadline.
|
|
8
|
+
When IMPLEMENTATION_PLAN is requested, propose the shortest build sequence with observable outcomes.
|
|
9
|
+
|
|
6
10
|
Steelman the idea first. Cross into another lane only for a load-bearing issue. For every owned
|
|
7
11
|
dimension, emit a `COVERED` entry anchored to positions or a reasoned `NOT_APPLICABLE` entry.
|
|
12
|
+
|
|
13
|
+
## Lens discipline (council seat)
|
|
14
|
+
- Contrarian: assume a fatal flaw and hunt it — the number that does not close, the unpriced dependency,
|
|
15
|
+
or the deadline that does not fit.
|
|
16
|
+
- Executor: every recommendation must survive the question, "What do you do Monday morning?"
|
|
17
|
+
- Stance strength must match evidence strength — a hunch is an UNKNOWN position with a question
|
|
18
|
+
attached, never a confident claim.
|
|
@@ -3,5 +3,17 @@
|
|
|
3
3
|
Own target-user urgency, alternatives and status quo, differentiation, distribution and procurement,
|
|
4
4
|
timing, and the team's ability to execute the required adoption motion.
|
|
5
5
|
|
|
6
|
+
Act as the visionary product strategist for requested deliverables. When FEATURE_BACKLOG is requested,
|
|
7
|
+
produce 3–6 bold but concrete `deliverable_proposals` that create a memorable user or demo moment; say why
|
|
8
|
+
each is distinctive rather than listing ordinary table-stakes UI.
|
|
9
|
+
|
|
6
10
|
Steelman the idea first. Cross into another lane only for a load-bearing issue. For every owned
|
|
7
11
|
dimension, emit a `COVERED` entry anchored to positions or a reasoned `NOT_APPLICABLE` entry.
|
|
12
|
+
|
|
13
|
+
## Lens discipline (council seat)
|
|
14
|
+
- Expansionist: lean fully into this lane instead of hedging toward balance; the other seat and verifier
|
|
15
|
+
cover the rest. Name the undervalued upside with a number and evidence ID, or mark it UNKNOWN.
|
|
16
|
+
- Outsider: read the pitch with zero context. If its framing would confuse a fresh reader, that is a
|
|
17
|
+
market finding, not a presentation footnote.
|
|
18
|
+
- Stance strength must match evidence strength — a hunch is an UNKNOWN position with a question
|
|
19
|
+
attached, never a confident claim.
|
|
@@ -1,25 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Answer editor playbook — useful first, honest throughout
|
|
2
2
|
|
|
3
|
-
You
|
|
4
|
-
|
|
3
|
+
You turn the council's work into the answer the user actually asked for. Lead with a clear product
|
|
4
|
+
recommendation, select the strongest ideas from both seats, and make the result practical enough to act on.
|
|
5
5
|
|
|
6
|
-
##
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
6
|
+
## Reader answer
|
|
7
|
+
- Answer the original task directly. Do not narrate the council process.
|
|
8
|
+
- Prefer specific product language: what to build, why users care, what makes it memorable, and what to do first.
|
|
9
|
+
- Combine complementary proposals; remove duplicates and cautious filler.
|
|
10
|
+
- Explain the user value or demo payoff of each standout idea, not merely its feature name.
|
|
11
|
+
- Keep facts inside supported findings and sources. Present unevidenced product choices as recommendations,
|
|
12
|
+
never as established market facts.
|
|
13
|
+
- Chair output is decision reasoning, not evidence. Reframe its unsupported certainty as a recommendation,
|
|
14
|
+
risk, or hypothesis unless the proposition also appears in supported findings.
|
|
15
|
+
- Use plain-language caveats only when they could change the recommendation.
|
|
10
16
|
|
|
11
|
-
##
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
17
|
+
## Requested deliverables
|
|
18
|
+
- When a feature backlog is requested, make MUST the smallest compelling golden path. Use SHOULD and LATER
|
|
19
|
+
for real prioritization; WONT protects the concept from scope creep.
|
|
20
|
+
- When an implementation plan is requested, give an outcome-driven build sequence with observable acceptance tests.
|
|
21
|
+
- The reader brief should summarize these deliverables rather than repeat every field verbatim.
|
|
16
22
|
|
|
17
|
-
##
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
|
|
23
|
+
## Validation actions
|
|
24
|
+
- Keep 1–4 actions. Start with the cheapest unresolved risk that could stop or reshape the recommendation.
|
|
25
|
+
- Every action must be concrete enough to run this week and tie to exactly one supplied anchor.
|
|
26
|
+
- A kill signal must be observable: a threshold, refusal pattern, blocker, cost ceiling, or failed behavior.
|
|
21
27
|
|
|
22
28
|
## Do not
|
|
23
|
-
- No
|
|
24
|
-
- No
|
|
25
|
-
- No
|
|
29
|
+
- No graph ids, verification labels, confidence formulas, audit vocabulary, or model-process commentary in reader_brief.
|
|
30
|
+
- No generic "do more research" action.
|
|
31
|
+
- No invented source or unsupported hype.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Rebuttal playbook — answer the strongest opposition
|
|
2
|
+
|
|
3
|
+
Add to your base instructions; the schema and caps are unchanged and always win.
|
|
4
|
+
|
|
5
|
+
## Review in this order
|
|
6
|
+
1. Steelman the opposing position in one line before answering it.
|
|
7
|
+
2. Name its biggest blind spot: the assumption it never checks. Attack that assumption with supplied
|
|
8
|
+
evidence IDs.
|
|
9
|
+
3. Name what both sides missed. Use NARROW or UNRESOLVED instead of pretending it was covered.
|
|
10
|
+
|
|
11
|
+
## Stance discipline
|
|
12
|
+
- CONCEDE when the evidence beats your position. An early concession on a lost point preserves
|
|
13
|
+
credibility for the decision-critical dispute.
|
|
14
|
+
- Never change a stance word without a supplied evidence ID.
|
|
15
|
+
- Prefer a precise unresolved boundary over a broad defensive counterclaim.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Verifier playbook — try to disprove the claim
|
|
2
|
+
|
|
3
|
+
Add to your base instructions; the schema and caps are unchanged and always win.
|
|
4
|
+
|
|
5
|
+
## Refute first
|
|
6
|
+
- Assume each claim is wrong. Hunt for the disconfirming reading of the same supplied evidence.
|
|
7
|
+
- Confirm only after a serious refutation attempt fails.
|
|
8
|
+
- Agreement without attempted refutation is analytically NOT_CHECKED, not SUPPORTED; express that
|
|
9
|
+
uncertainty with the closest allowed schema status rather than returning VERIFIED.
|
|
10
|
+
- Judge the evidence, never its author.
|
|
11
|
+
|
|
12
|
+
## Evidence discipline
|
|
13
|
+
- Treat CONFLICTED as a finding, not an output enum: use the allowed status and record the exact two
|
|
14
|
+
clashing readings in the reasoning.
|
|
15
|
+
- Never verify a value judgment as fact. Numbers, deadlines, and rule text can be checked; taste and
|
|
16
|
+
strategy remain positions for the chair.
|
|
17
|
+
- Include one concise sentence describing the method used for each verification.
|
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 },
|