aiki-cli 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +16 -8
  3. package/dist/bench/idea-v3-rating.js +1 -1
  4. package/dist/cli/index.js +1 -1
  5. package/dist/cli/run.js +10 -7
  6. package/dist/council/view.js +146 -46
  7. package/dist/orchestration/context.js +3 -3
  8. package/dist/orchestration/decision-dossier.js +467 -80
  9. package/dist/orchestration/decision-graph.js +31 -0
  10. package/dist/orchestration/legacy-idea-adapter.js +3 -0
  11. package/dist/orchestration/modes.js +16 -4
  12. package/dist/orchestration/preflight.js +43 -9
  13. package/dist/orchestration/quick-analysis.js +35 -6
  14. package/dist/orchestration/stages/s10-render.js +179 -79
  15. package/dist/orchestration/stages/s4-analyze.js +3 -1
  16. package/dist/orchestration/stages/s6-positions.js +18 -0
  17. package/dist/orchestration/stages/s7-decision-graph.js +3 -0
  18. package/dist/orchestration/stages/s8-verify.js +26 -9
  19. package/dist/orchestration/stages/s8b-rebuttal.js +11 -4
  20. package/dist/orchestration/stages/s9-judge.js +36 -13
  21. package/dist/orchestration/stages/s9b-plan.js +208 -35
  22. package/dist/orchestration/url-sources.js +200 -0
  23. package/dist/schemas/index.js +134 -6
  24. package/dist/skills/idea-refinement/analyst.md +5 -5
  25. package/dist/skills/idea-refinement/chair.md +18 -0
  26. package/dist/skills/idea-refinement/economics-delivery.md +11 -0
  27. package/dist/skills/idea-refinement/market-adoption.md +12 -0
  28. package/dist/skills/idea-refinement/planner.md +25 -19
  29. package/dist/skills/idea-refinement/rebuttal.md +15 -0
  30. package/dist/skills/idea-refinement/verifier.md +17 -0
  31. package/dist/storage/runs.js +2 -1
  32. package/dist/tui/app.js +19 -4
  33. package/dist/workflows/idea-refinement.js +51 -15
  34. package/package.json +1 -1
@@ -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,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).min(3).max(4),
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 ActionPlan = z
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: ActionPlan,
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 adversarial analyst on a decision panel. Your job is not to like the idea it is to find
4
- where it breaks. Steelman it once, then attack it honestly.
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
- - Attack THAT version, not a weak strawman. If the steelman is thin, that itself is a finding.
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 motivational framing, no pitch language, no summarizing your own output.
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
- # Planner playbook — decisive validation
1
+ # Answer editor playbook — useful first, honest throughout
2
2
 
3
- You write the next tests after the council has already judged the idea. Do not plan the build.
4
- Plan only the cheapest validation work that can change the decision.
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
- ## Order
7
- - Start with upheld HIGH risks. If one fails, the idea should stop or change shape before any other work.
8
- - Then cover blind spots that could hide a blocker.
9
- - Then answer open questions that would flip the verdict.
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
- ## Action quality
12
- - Every action must be concrete enough that a person can run it this week.
13
- - Prefer interviews, landing-page smoke tests, pricing tests, manual concierge trials, prototype probes,
14
- competitor checks, and policy checks over implementation work.
15
- - Tie every action to exactly one anchor in `validates`.
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
- ## Kill signals
18
- - A kill signal is not "learn more". It is the result that stops, pivots, or narrows the idea.
19
- - Make the signal observable: a count, threshold, refusal pattern, legal blocker, cost ceiling, or
20
- behavior that contradicts the assumption.
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 product roadmap, scaffolding plan, architecture plan, or feature list.
24
- - No action for a claim the chair already settled.
25
- - No vague market research. No unanchored action.
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.
@@ -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 },