aiwcli 0.12.1 → 0.12.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 (57) hide show
  1. package/dist/templates/_shared/hooks-ts/session_start.ts +21 -15
  2. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +20 -8
  3. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +151 -29
  4. package/dist/templates/_shared/scripts/resume_handoff.ts +25 -0
  5. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -7
  6. package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-EVOLUTION.md +62 -63
  7. package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-PATTERNS.md +61 -62
  8. package/dist/templates/cc-native/_cc-native/agents/plan-review/ARCH-STRUCTURE.md +62 -63
  9. package/dist/templates/cc-native/_cc-native/agents/plan-review/ASSUMPTION-TRACER.md +56 -57
  10. package/dist/templates/cc-native/_cc-native/agents/plan-review/CLARITY-AUDITOR.md +53 -54
  11. package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-FEASIBILITY.md +66 -67
  12. package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-GAPS.md +70 -71
  13. package/dist/templates/cc-native/_cc-native/agents/plan-review/COMPLETENESS-ORDERING.md +62 -63
  14. package/dist/templates/cc-native/_cc-native/agents/plan-review/CONSTRAINT-VALIDATOR.md +72 -73
  15. package/dist/templates/cc-native/_cc-native/agents/plan-review/DESIGN-ADR-VALIDATOR.md +61 -62
  16. package/dist/templates/cc-native/_cc-native/agents/plan-review/DESIGN-SCALE-MATCHER.md +64 -65
  17. package/dist/templates/cc-native/_cc-native/agents/plan-review/DEVILS-ADVOCATE.md +56 -57
  18. package/dist/templates/cc-native/_cc-native/agents/plan-review/DOCUMENTATION-PHILOSOPHY.md +86 -87
  19. package/dist/templates/cc-native/_cc-native/agents/plan-review/HANDOFF-READINESS.md +59 -60
  20. package/dist/templates/cc-native/_cc-native/agents/plan-review/HIDDEN-COMPLEXITY.md +58 -59
  21. package/dist/templates/cc-native/_cc-native/agents/plan-review/INCREMENTAL-DELIVERY.md +66 -67
  22. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-DEPENDENCY.md +62 -63
  23. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-FMEA.md +66 -67
  24. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-PREMORTEM.md +71 -72
  25. package/dist/templates/cc-native/_cc-native/agents/plan-review/RISK-REVERSIBILITY.md +74 -75
  26. package/dist/templates/cc-native/_cc-native/agents/plan-review/SCOPE-BOUNDARY.md +77 -78
  27. package/dist/templates/cc-native/_cc-native/agents/plan-review/SIMPLICITY-GUARDIAN.md +62 -63
  28. package/dist/templates/cc-native/_cc-native/agents/plan-review/SKEPTIC.md +68 -69
  29. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md +61 -62
  30. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-CHARACTERIZATION.md +71 -72
  31. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-FIRST-VALIDATOR.md +61 -62
  32. package/dist/templates/cc-native/_cc-native/agents/plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md +61 -62
  33. package/dist/templates/cc-native/_cc-native/agents/plan-review/TRADEOFF-COSTS.md +67 -68
  34. package/dist/templates/cc-native/_cc-native/agents/plan-review/TRADEOFF-STAKEHOLDERS.md +65 -66
  35. package/dist/templates/cc-native/_cc-native/agents/plan-review/VERIFY-COVERAGE.md +74 -75
  36. package/dist/templates/cc-native/_cc-native/agents/plan-review/VERIFY-STRENGTH.md +69 -70
  37. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +19 -2
  38. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +28 -1010
  39. package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -0
  40. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +1 -2
  41. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -0
  42. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -0
  43. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -0
  44. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -0
  45. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +19 -821
  46. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +36 -13
  47. package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -0
  48. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +1 -2
  49. package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -0
  50. package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +489 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +1 -1
  53. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +51 -17
  55. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +40 -2
  56. package/oclif.manifest.json +1 -1
  57. package/package.json +1 -1
@@ -1,823 +1,21 @@
1
1
  /**
2
2
  * Review artifact writing and formatting.
3
- * See cc-native-plan-review-spec.md §4.3
4
- */
5
-
6
- import * as fs from "node:fs";
7
- import * as path from "node:path";
8
-
9
- import { ENABLE_ROBUST_PLAN_WRITES } from "./constants.js";
10
- import type {
11
- CombinedReviewResult,
12
- ReviewerResult,
13
- DisplaySettings,
14
- CorroborationResult,
15
- } from "./types.js";
16
- import { DEFAULT_DISPLAY } from "./types.js";
17
- import { atomicWrite } from "../../_shared/lib-ts/base/atomic-write.js";
18
- import { sanitizeFilename } from "../../_shared/lib-ts/base/constants.js";
19
- import { logDebug, logWarn, logError } from "../../_shared/lib-ts/base/logger.js";
20
- import { nowIso } from "../../_shared/lib-ts/base/utils.js";
21
-
22
- // ---------------------------------------------------------------------------
23
- // Markdown Formatting
24
- // ---------------------------------------------------------------------------
25
-
26
- /**
27
- * Format review results as markdown (legacy compat format).
28
- */
29
- export function formatReviewMarkdown(
30
- results: ReviewerResult[],
31
- overall: string,
32
- title = "CC-Native Plan Review",
33
- settings?: Record<string, unknown>,
34
- ): string {
35
- const display = resolveDisplay(settings);
36
-
37
- const lines: string[] = [];
38
- lines.push(`# ${title}\n`);
39
- lines.push(`**Overall verdict:** \`${overall.toUpperCase()}\`\n`);
40
-
41
- for (const r of results) {
42
- const displayName = r.name === r.name.toLowerCase() ? titleCase(r.name) : r.name;
43
- lines.push(`## ${displayName}\n`);
44
- lines.push(`- ok: \`${r.ok}\``);
45
- lines.push(`- verdict: \`${r.verdict}\``);
46
-
47
- if (r.data && Object.keys(r.data).length > 0) {
48
- const summary = String(r.data.summary ?? "").trim();
49
- if (r.data.summary_source === "default") {
50
- lines.push(`- summary: ⚠️ ${summary} *(reviewer did not return summary)*`);
51
- } else {
52
- lines.push(`- summary: ${summary}`);
53
- }
54
- appendReviewDetails(lines, r.data, display);
55
- } else {
56
- lines.push(`- note: ${r.err || "no structured output"}`);
57
- }
58
- lines.push("");
59
- }
60
-
61
- return lines.join("\n").trim() + "\n";
62
- }
63
-
64
- /**
65
- * Format combined review result as a single markdown document.
66
- */
67
- export function formatCombinedMarkdown(
68
- result: CombinedReviewResult,
69
- settings?: Record<string, unknown>,
70
- corroboration?: CorroborationResult,
71
- ): string {
72
- const display = resolveDisplay(settings);
73
-
74
- const lines: string[] = [];
75
- lines.push("# CC-Native Plan Review\n");
76
- lines.push(`**Overall Verdict:** \`${result.overall_verdict.toUpperCase()}\``);
77
- lines.push(`**Plan Hash:** \`${result.plan_hash}\`\n`);
78
-
79
- // Corroboration summary
80
- if (corroboration) {
81
- lines.push("## Corroboration Analysis\n");
82
- if (corroboration.blocking.length > 0) {
83
- lines.push("### Blocking Dimensions\n");
84
- for (const group of corroboration.blocking) {
85
- lines.push(`- **${group.dimension}**: ${group.issues.length} issues from ${group.agentCount} agents (threshold: ≥${group.threshold})`);
86
- }
87
- lines.push("");
88
- }
89
- if (corroboration.solo.length > 0) {
90
- lines.push("### Solo Dimensions (informational)\n");
91
- for (const s of corroboration.solo) {
92
- lines.push(`- **${s.dimension}**: ${s.issues.length} issues from ${s.agentCount} agents (threshold: >${s.threshold}, not exceeded)`);
93
- }
94
- lines.push("");
95
- }
96
- if (corroboration.unclassified.length > 0) {
97
- lines.push(`> ${corroboration.unclassified.length} issue(s) without dimension classification (unclassified, not blocking)\n`);
98
- }
99
- }
100
-
101
- lines.push("---\n");
102
-
103
- // Orchestration section
104
- if (result.orchestration) {
105
- lines.push("---\n");
106
- lines.push("## Orchestration\n");
107
- lines.push(`- **Complexity:** \`${result.orchestration.complexity}\``);
108
- lines.push(`- **Category:** \`${result.orchestration.category}\``);
109
- const agentsStr =
110
- result.orchestration.selected_agents.length > 0
111
- ? result.orchestration.selected_agents.join(", ")
112
- : "None";
113
- lines.push(`- **Agents Selected:** ${agentsStr}`);
114
- lines.push(`- **Reasoning:** ${result.orchestration.reasoning}`);
115
- if (result.orchestration.skip_reason) {
116
- lines.push(`- **Skip Reason:** ${result.orchestration.skip_reason}`);
117
- }
118
- if (result.orchestration.error) {
119
- lines.push(`- **Error:** ${result.orchestration.error}`);
120
- }
121
- lines.push("");
122
- }
123
-
124
- // Agent Reviews section
125
- if (Object.keys(result.agents).length > 0) {
126
- lines.push("---\n");
127
- lines.push("## Agent Reviews\n");
128
- for (const [name, r] of Object.entries(result.agents)) {
129
- lines.push(`### ${name}\n`);
130
- lines.push(`- verdict: \`${r.verdict}\``);
131
- if (r.data && Object.keys(r.data).length > 0) {
132
- appendSummaryLine(lines, r.data);
133
- appendReviewDetails(lines, r.data, display);
134
- } else if (r.err) {
135
- lines.push(`- error: ${r.err}`);
136
- }
137
- lines.push("");
138
- }
139
- }
140
-
141
- return lines.join("\n").trim() + "\n";
142
- }
143
-
144
- // ---------------------------------------------------------------------------
145
- // Inline Summaries
146
- // ---------------------------------------------------------------------------
147
-
148
- /**
149
- * Build compact inline summary of HIGH-severity findings for additionalContext.
150
- * When corroboration data is provided, annotates issues as [CORROBORATED] or [perspective].
151
- */
152
- export function buildInlineReviewSummary(
153
- combined: CombinedReviewResult,
154
- maxIssues = 5,
155
- maxChars = 800,
156
- corroboration?: CorroborationResult,
157
- ): string {
158
- const allReviewers = Object.values(combined.agents);
159
-
160
- // Build set of blocking dimensions for annotation
161
- const blockingDims = new Set(
162
- corroboration?.blocking.map(g => g.dimension) ?? [],
163
- );
164
-
165
- const highIssues: Array<Record<string, unknown>> = [];
166
- for (const r of allReviewers) {
167
- if (!r.data) continue;
168
- const issues = r.data.issues as Array<Record<string, unknown>> | undefined;
169
- if (!issues) continue;
170
- for (const issue of issues) {
171
- if (issue.severity === "high") {
172
- highIssues.push({ ...issue, _reviewer: r.name });
173
- }
174
- }
175
- }
176
-
177
- const parts: string[] = [];
178
-
179
- // Overall verdict line
180
- const issueCount = highIssues.length;
181
- const countSuffix =
182
- issueCount > 0
183
- ? ` (${issueCount} high-severity issue${issueCount !== 1 ? "s" : ""})`
184
- : "";
185
- parts.push(`**Plan Review: ${combined.overall_verdict.toUpperCase()}**${countSuffix}`);
186
-
187
- // Corroboration summary if available
188
- if (corroboration) {
189
- const blockCount = corroboration.blocking.length;
190
- const soloCount = corroboration.solo.length;
191
- if (blockCount > 0) {
192
- parts.push(`**Corroboration:** ${blockCount} dimension${blockCount !== 1 ? "s" : ""} exceeded threshold (blocking), ${soloCount} solo (informational)`);
193
- } else {
194
- parts.push(`**Corroboration:** No dimensions exceeded threshold — all ${soloCount} solo (informational)`);
195
- }
196
- }
197
-
198
- // High-severity issue bullets
199
- for (const issue of highIssues.slice(0, maxIssues)) {
200
- const cat = (issue.category as string) ?? "general";
201
- const text = (issue.issue as string) ?? "";
202
- const fix = (issue.suggested_fix as string) ?? "";
203
- const reviewer = (issue._reviewer as string) ?? "unknown";
204
- const dim = issue.dimension as string | undefined;
205
-
206
- let annotation = "";
207
- if (corroboration && dim) {
208
- const group = corroboration.blocking.find(g => g.dimension === dim);
209
- if (group) {
210
- annotation = ` [CORROBORATED — ${group.issues.length} issues from ${group.agentCount} agents exceeds threshold ${group.threshold}]`;
211
- } else {
212
- annotation = " [perspective]";
213
- }
214
- }
215
-
216
- let line = `- [${cat}] ${text}`;
217
- if (fix) line += ` \u2192 ${fix}`;
218
- line += ` (${reviewer})${annotation}`;
219
- parts.push(line);
220
- }
221
-
222
- const remaining = highIssues.length - maxIssues;
223
- if (remaining > 0) {
224
- parts.push(` ...and ${remaining} more`);
225
- }
226
-
227
- let result = parts.join("\n");
228
- if (result.length > maxChars) {
229
- result = result.slice(0, maxChars - 3) + "...";
230
- }
231
- return result;
232
- }
233
-
234
- /**
235
- * Extract top issues as compact text for permissionDecisionReason.
236
- */
237
- export function extractTopIssuesText(
238
- combined: CombinedReviewResult,
239
- maxCount = 3,
240
- severity = "high",
241
- ): string {
242
- const allReviewers = Object.values(combined.agents);
243
-
244
- const issues: string[] = [];
245
- for (const r of allReviewers) {
246
- if (!r.data) continue;
247
- const issueList = r.data.issues as Array<Record<string, unknown>> | undefined;
248
- if (!issueList) continue;
249
- for (const issue of issueList) {
250
- if (issue.severity === severity) {
251
- const text = String(issue.issue ?? "").trim();
252
- if (text) {
253
- issues.push(`[${r.name}] ${text}`);
254
- break; // first high issue per reviewer only
255
- }
256
- }
257
- }
258
- if (issues.length >= maxCount) break;
259
- }
260
-
261
- if (issues.length === 0) return "Review found critical issues";
262
- return issues.join("; ");
263
- }
264
-
265
- /**
266
- * Build markdown document containing high-severity issues.
267
- * When corroboration data is provided, only includes corroborated (blocking) issues.
268
- */
269
- export function buildHighIssuesDocument(
270
- combined: CombinedReviewResult,
271
- corroboration?: CorroborationResult,
272
- ): string {
273
- // When corroboration is available, only show blocking groups
274
- if (corroboration && corroboration.blocking.length > 0) {
275
- const lines = ["# Corroborated High-Severity Issues\n"];
276
- lines.push("> Only issues from dimensions where the total count exceeded the proportional threshold are shown.\n");
277
-
278
- for (const group of corroboration.blocking) {
279
- lines.push(`## ${group.dimension} (${group.issues.length} issues from ${group.agentCount} agents, threshold: ${group.threshold})\n`);
280
- for (const { agent, issue } of group.issues) {
281
- const cat = issue.category ?? "general";
282
- const text = String(issue.issue ?? "").trim();
283
- const fix = String(issue.suggested_fix ?? "").trim();
284
- lines.push(`- **[${cat}]** ${text} *(${agent})*`);
285
- if (fix) lines.push(` - Fix: ${fix}`);
286
- }
287
- lines.push("");
288
- }
289
-
290
- if (corroboration.solo.length > 0) {
291
- lines.push("---\n");
292
- lines.push(`> ${corroboration.solo.length} dimension${corroboration.solo.length !== 1 ? "s" : ""} had issues below threshold (not blocking): ${corroboration.solo.map(s => `${s.dimension} (${s.issues.length}/${s.threshold})`).join(", ")}\n`);
293
- }
294
-
295
- return lines.join("\n");
296
- }
297
-
298
- // Fallback: no corroboration data — show all high-severity issues
299
- const lines = ["# High-Severity Issues\n"];
300
- const allReviewers = Object.values(combined.agents);
301
-
302
- let foundAny = false;
303
- for (const r of allReviewers) {
304
- if (!r.data) continue;
305
- const issues = r.data.issues as Array<Record<string, unknown>> | undefined;
306
- if (!issues) continue;
307
-
308
- const highIssues = issues.filter((i) => i.severity === "high");
309
- if (highIssues.length === 0) continue;
310
-
311
- foundAny = true;
312
- lines.push(`## ${r.name} (${r.verdict})\n`);
313
- for (const issue of highIssues) {
314
- const cat = (issue.category as string) ?? "general";
315
- const text = String(issue.issue ?? "").trim();
316
- const fix = String(issue.suggested_fix ?? "").trim();
317
- lines.push(`- **[${cat}]** ${text}`);
318
- if (fix) lines.push(` - Fix: ${fix}`);
319
- }
320
- lines.push("");
321
- }
322
-
323
- if (!foundAny) {
324
- lines.push("No high-severity issues found.\n");
325
- }
326
-
327
- return lines.join("\n");
328
- }
329
-
330
- // ---------------------------------------------------------------------------
331
- // Index Generation
332
- // ---------------------------------------------------------------------------
333
-
334
- /**
335
- * Generate index.md for a review folder.
336
- */
337
- export function generateReviewIndex(
338
- result: CombinedReviewResult,
339
- iteration?: number,
340
- _settings?: Record<string, unknown>,
341
- ): string {
342
- const now = new Date();
343
-
344
- const lines = [
345
- "---",
346
- `type: review`,
347
- `plan_hash: ${result.plan_hash}`,
348
- `overall_verdict: ${result.overall_verdict}`,
349
- `created_at: ${result.timestamp}`,
350
- ];
351
- if (iteration) lines.push(`iteration: ${iteration}`);
352
- lines.push(
353
- "---",
354
- "",
355
- `# Plan Review - ${formatDate(now)}`,
356
- "",
357
- `**Overall Verdict:** \`${result.overall_verdict.toUpperCase()}\``,
358
- );
359
-
360
- if (iteration) lines.push(`**Iteration:** ${iteration}`);
361
- lines.push(`**Plan Hash:** \`${result.plan_hash}\``, "");
362
-
363
- // Summary from orchestrator
364
- if (result.orchestration) {
365
- lines.push(
366
- "## Analysis",
367
- `- **Complexity:** \`${result.orchestration.complexity}\``,
368
- `- **Category:** \`${result.orchestration.category}\``,
369
- `- **Reasoning:** ${result.orchestration.reasoning}`,
370
- "",
371
- );
372
- }
373
-
374
- // Navigation table
375
- lines.push(
376
- "## Review Files",
377
- "",
378
- "| File | Description |",
379
- "|------|-------------|",
380
- "| [review.md](./review.md) | Full review details |",
381
- "| [review.json](./review.json) | Structured review data |",
382
- "| [plan.md](./plan.md) | Plan snapshot at review time |",
383
- );
384
-
385
- for (const name of Object.keys(result.agents)) {
386
- const safeName = sanitizeFilename(name);
387
- lines.push(
388
- `| [${safeName}.json](./reviewer-output/${safeName}.json) | ${name} agent output |`,
389
- );
390
- }
391
-
392
- lines.push(
393
- "",
394
- "## Verdicts Summary",
395
- "",
396
- "| Reviewer | Verdict |",
397
- "|----------|---------|",
398
- );
399
-
400
- for (const [name, r] of Object.entries(result.agents)) {
401
- lines.push(`| ${name} | \`${r.verdict}\` |`);
402
- }
403
- lines.push("");
404
-
405
- return lines.join("\n");
406
- }
407
-
408
- // ---------------------------------------------------------------------------
409
- // JSON Output
410
- // ---------------------------------------------------------------------------
411
-
412
- /**
413
- * Build combined JSON output structure.
414
- */
415
- export function buildCombinedJson(
416
- result: CombinedReviewResult,
417
- ): Record<string, unknown> {
418
- const output: Record<string, unknown> = {
419
- metadata: {
420
- timestamp: result.timestamp,
421
- plan_hash: result.plan_hash,
422
- },
423
- overall: {
424
- verdict: result.overall_verdict,
425
- },
426
- };
427
-
428
- // Orchestration
429
- if (result.orchestration) {
430
- output.orchestration = {
431
- complexity: result.orchestration.complexity,
432
- category: result.orchestration.category,
433
- selectedAgents: result.orchestration.selected_agents,
434
- reasoning: result.orchestration.reasoning,
435
- skipReason: result.orchestration.skip_reason ?? null,
436
- error: result.orchestration.error ?? null,
437
- };
438
- }
439
-
440
- // Agents
441
- if (Object.keys(result.agents).length > 0) {
442
- const agents: Record<string, unknown> = {};
443
- output.agents = agents;
444
- for (const [name, r] of Object.entries(result.agents)) {
445
- agents[name] = {
446
- verdict: r.verdict,
447
- summary: r.data?.summary ?? null,
448
- summarySource: r.data?.summary_source ?? null,
449
- issues: r.data
450
- ? ((r.data.issues as Array<Record<string, unknown>>) ?? []).filter(
451
- (i) => i.severity !== "low",
452
- )
453
- : [],
454
- missing_sections: r.data?.missing_sections ?? [],
455
- questions: r.data?.questions ?? [],
456
- ok: r.ok,
457
- error: r.err || null,
458
- };
459
- }
460
- }
461
-
462
- return output;
463
- }
464
-
465
- // ---------------------------------------------------------------------------
466
- // Artifact Writing
467
- // ---------------------------------------------------------------------------
468
-
469
- /**
470
- * Write combined review artifacts to context reviews folder.
471
- * Uses atomic writes for critical files when ENABLE_ROBUST_PLAN_WRITES is true.
472
- */
473
- export function writeCombinedArtifacts(
474
- base: string,
475
- plan: string,
476
- result: CombinedReviewResult,
477
- payload: Record<string, unknown>,
478
- settings?: Record<string, unknown>,
479
- contextReviewsDir?: string,
480
- reviewFolder?: string,
481
- iteration?: number,
482
- corroboration?: CorroborationResult,
483
- ): string {
484
- const outDir = reviewFolder ?? contextReviewsDir;
485
- if (!outDir) {
486
- throw new Error("Either contextReviewsDir or reviewFolder is required");
487
- }
488
-
489
- logDebug("utils", `Using review folder: ${outDir}`);
490
-
491
- // Create directory
492
- try {
493
- fs.mkdirSync(outDir, { recursive: true });
494
- } catch (error: unknown) {
495
- logError("utils", `Cannot create directory ${outDir}: ${error}`);
496
- throw error;
497
- }
498
-
499
- // JSON write
500
- const jsonPath = path.join(outDir, "review.json");
501
- const jsonData = buildCombinedJson(result);
502
- writeFile(jsonPath, JSON.stringify(jsonData, null, 2));
503
-
504
- // Markdown write
505
- const mdPath = path.join(outDir, "review.md");
506
- const mdContent = formatCombinedMarkdown(result, settings, corroboration);
507
- writeFile(mdPath, mdContent);
508
-
509
- // Individual reviewer writes (non-critical) — in reviewer-output/ subfolder
510
- const reviewerOutputDir = path.join(outDir, "reviewer-output");
511
- try {
512
- fs.mkdirSync(reviewerOutputDir, { recursive: true });
513
- } catch {
514
- // Best-effort — non-critical
515
- }
516
- for (const [name, r] of Object.entries(result.agents)) {
517
- if (r.data) {
518
- writeFileNonCritical(
519
- path.join(reviewerOutputDir, `${sanitizeFilename(name)}.json`),
520
- JSON.stringify(r.data, null, 2),
521
- );
522
- }
523
- }
524
-
525
- // Generate index.md for folder-based reviews
526
- if (reviewFolder) {
527
- const indexContent = generateReviewIndex(result, iteration, settings);
528
- writeFileNonCritical(path.join(outDir, "index.md"), indexContent);
529
- return path.join(outDir, "index.md");
530
- }
531
-
532
- return mdPath;
533
- }
534
-
535
- // ---------------------------------------------------------------------------
536
- // Helpers
537
- // ---------------------------------------------------------------------------
538
-
539
- function resolveDisplay(
540
- settings?: Record<string, unknown>,
541
- ): DisplaySettings {
542
- if (!settings) return { ...DEFAULT_DISPLAY };
543
- const display = (settings.display as Partial<DisplaySettings>) ?? {};
544
- return { ...DEFAULT_DISPLAY, ...display };
545
- }
546
-
547
- function appendSummaryLine(lines: string[], data: Record<string, unknown>): void {
548
- const summary = String(data.summary ?? "").trim();
549
- if (data.summary_source === "default") {
550
- lines.push(`- summary: ⚠️ ${summary} *(reviewer did not return summary)*`);
551
- } else {
552
- lines.push(`- summary: ${summary}`);
553
- }
554
- }
555
-
556
- function appendReviewDetails(
557
- lines: string[],
558
- data: Record<string, unknown>,
559
- display: DisplaySettings,
560
- ): void {
561
- const issues = ((data.issues as Array<Record<string, unknown>>) ?? []).filter(
562
- (i) => i.severity !== "low",
563
- );
564
- if (issues.length > 0) {
565
- lines.push("\n**Issues:**");
566
- for (const it of issues.slice(0, display.maxIssues)) {
567
- const sev = (it.severity as string) ?? "medium";
568
- const cat = (it.category as string) ?? "general";
569
- const issue = (it.issue as string) ?? "";
570
- const fix = (it.suggested_fix as string) ?? "";
571
- lines.push(`- **[${sev}] ${cat}**: ${issue}`);
572
- if (fix) lines.push(` - fix: ${fix}`);
573
- }
574
- }
575
-
576
- const missing = (data.missing_sections as string[]) ?? [];
577
- if (missing.length > 0) {
578
- lines.push("\n**Missing Sections:**");
579
- for (const m of missing.slice(0, display.maxMissingSections)) {
580
- lines.push(`- ${m}`);
581
- }
582
- }
583
-
584
- const qs = (data.questions as string[]) ?? [];
585
- if (qs.length > 0) {
586
- lines.push("\n**Questions:**");
587
- for (const q of qs.slice(0, display.maxQuestions)) {
588
- lines.push(`- ${q}`);
589
- }
590
- }
591
- }
592
-
593
- function writeFile(filePath: string, content: string): void {
594
- try {
595
- if (ENABLE_ROBUST_PLAN_WRITES) {
596
- const [success, error] = atomicWrite(filePath, content);
597
- if (!success) throw new Error(`Atomic write failed: ${error}`);
598
- } else {
599
- fs.writeFileSync(filePath, content, "utf-8");
600
- }
601
- } catch (error: unknown) {
602
- logError("utils", `Failed to write ${path.basename(filePath)}: ${error}`);
603
- throw error;
604
- }
605
- }
606
-
607
- function writeFileNonCritical(filePath: string, content: string): void {
608
- try {
609
- if (ENABLE_ROBUST_PLAN_WRITES) {
610
- const [success, error] = atomicWrite(filePath, content);
611
- if (!success) {
612
- logWarn("utils", `Failed to write ${path.basename(filePath)}: ${error}`);
613
- }
614
- } else {
615
- fs.writeFileSync(filePath, content, "utf-8");
616
- }
617
- } catch (error: unknown) {
618
- logWarn("utils", `Failed to write ${path.basename(filePath)}: ${error}`);
619
- }
620
- }
621
-
622
- function titleCase(s: string): string {
623
- return s.charAt(0).toUpperCase() + s.slice(1);
624
- }
625
-
626
- function formatDate(d: Date): string {
627
- const year = d.getFullYear();
628
- const month = String(d.getMonth() + 1).padStart(2, "0");
629
- const day = String(d.getDate()).padStart(2, "0");
630
- const hours = String(d.getHours()).padStart(2, "0");
631
- const minutes = String(d.getMinutes()).padStart(2, "0");
632
- return `${year}-${month}-${day} ${hours}:${minutes}`;
633
- }
634
-
635
- // ---------------------------------------------------------------------------
636
- // Review Tracker
637
- // ---------------------------------------------------------------------------
638
-
639
- export interface ReviewTrackerEntry {
640
- iteration: number;
641
- timestamp: string;
642
- planHash: string;
643
- verdict: string;
644
- decision: string;
645
- score: number;
646
- topIssues: string[];
647
- reviewFolder: string;
648
- }
649
-
650
- /**
651
- * Build or update the review-tracker.md in the cc-native reviews directory.
652
- * This file provides a human-readable summary of all review iterations,
653
- * making it easy to see whether feedback was acted on.
654
- */
655
- export function writeReviewTracker(
656
- ccNativeReviewsDir: string,
657
- entry: ReviewTrackerEntry,
658
- ): void {
659
- const trackerPath = path.join(ccNativeReviewsDir, "review-tracker.md");
660
-
661
- // Read existing tracker entries if present
662
- let existingContent = "";
663
- try {
664
- if (fs.existsSync(trackerPath)) {
665
- existingContent = fs.readFileSync(trackerPath, "utf-8");
666
- }
667
- } catch {
668
- // Fresh start
669
- }
670
-
671
- // Parse existing entries to detect plan changes
672
- const previousHashes = extractPreviousHashes(existingContent);
673
- const hashChanged = previousHashes.length > 0 &&
674
- previousHashes.at(-1) !== entry.planHash;
675
-
676
- // Build the new entry section
677
- const lines: string[] = [];
678
- const verdictEmoji = entry.decision === "allow" ? "\u2705" : "\u274C";
679
- const changeNote = previousHashes.length > 0
680
- ? (hashChanged ? "\u2705 Plan was revised (hash changed)" : "\u26A0\uFE0F Plan unchanged since last review")
681
- : "Initial review";
682
-
683
- lines.push(`## Iteration ${entry.iteration} \u2014 ${entry.timestamp} \u2014 ${verdictEmoji} ${entry.verdict.toUpperCase()}`);
684
- lines.push("");
685
- lines.push(`- **Decision:** ${entry.decision}`);
686
- lines.push(`- **Score:** ${entry.score.toFixed(2)}`);
687
- lines.push(`- **Plan hash:** \`${entry.planHash}\``);
688
- lines.push(`- **Status:** ${changeNote}`);
689
- lines.push(`- **Full review:** [\`${path.basename(entry.reviewFolder)}/\`](${path.basename(entry.reviewFolder)}/index.md)`);
690
-
691
- if (entry.topIssues.length > 0) {
692
- lines.push("");
693
- lines.push("**Top issues:**");
694
- for (const issue of entry.topIssues) {
695
- lines.push(`- ${issue}`);
696
- }
697
- }
698
- lines.push("");
699
-
700
- // Build full file
701
- let output: string;
702
- if (!existingContent || !existingContent.includes("# Plan Review Tracker")) {
703
- // New file
704
- output = [
705
- "# Plan Review Tracker",
706
- "",
707
- "> Auto-generated by plan review hook. Shows review lifecycle across iterations.",
708
- "> Check `plan.md` in each iteration folder to diff plan changes.",
709
- "",
710
- ...lines,
711
- ].join("\n");
712
- } else {
713
- // Append to existing
714
- output = existingContent.trimEnd() + "\n\n" + lines.join("\n");
715
- }
716
-
717
- try {
718
- fs.writeFileSync(trackerPath, output, "utf-8");
719
- } catch (error) {
720
- logWarn("artifacts", `Failed to write review tracker: ${error}`);
721
- }
722
- }
723
-
724
- function extractPreviousHashes(content: string): string[] {
725
- const hashes: string[] = [];
726
- const regex = /\*\*Plan hash:\*\* `([a-f0-9]+)`/g;
727
- let match: RegExpExecArray | null;
728
- while ((match = regex.exec(content)) !== null) {
729
- hashes.push(match[1]!);
730
- }
731
- return hashes;
732
- }
733
-
734
- // ---------------------------------------------------------------------------
735
- // Corroboration Report
736
- // ---------------------------------------------------------------------------
737
-
738
- /**
739
- * Build a detailed markdown report of the corroboration analysis.
740
- * Shows blocking vs solo findings with threshold comparison.
741
- */
742
- export function buildCorroborationReport(
743
- corroborationResult: CorroborationResult,
744
- ): string {
745
- const lines: string[] = [
746
- "# Corroboration Analysis",
747
- "",
748
- "## Verdict: " + corroborationResult.verdict.toUpperCase(),
749
- "",
750
- ];
751
-
752
- // Blocking groups table
753
- if (corroborationResult.blocking.length > 0) {
754
- lines.push("## Blocking Issues (Corroborated)");
755
- lines.push("");
756
- lines.push("| Dimension | Issues | Agents | Threshold | Status |");
757
- lines.push("|-----------|--------|--------|-----------|--------|");
758
-
759
- for (const group of corroborationResult.blocking) {
760
- lines.push(
761
- `| ${group.dimension} | ${group.issues.length} | ${group.agentCount} | ${group.threshold} | ⛔ EXCEEDED |`
762
- );
763
- }
764
- lines.push("");
765
-
766
- // Issue details
767
- for (const group of corroborationResult.blocking) {
768
- lines.push(`### ${group.dimension} (${group.issues.length} issues)`);
769
- lines.push("");
770
- for (const {agent, issue} of group.issues) {
771
- lines.push(`- **[${agent}]** ${issue.description || issue.issue || "No description"}`);
772
- }
773
- lines.push("");
774
- }
775
- }
776
-
777
- // Solo findings table
778
- if (corroborationResult.solo.length > 0) {
779
- lines.push("## Solo Findings (Below Threshold)");
780
- lines.push("");
781
- lines.push("| Dimension | Issues | Agents | Threshold | Status |");
782
- lines.push("|-----------|--------|--------|-----------|--------|");
783
-
784
- for (const group of corroborationResult.solo) {
785
- lines.push(
786
- `| ${group.dimension} | ${group.issues.length} | ${group.agentCount} | ${group.threshold} | ℹ️ SOLO |`
787
- );
788
- }
789
- lines.push("");
790
-
791
- // Issue details
792
- for (const group of corroborationResult.solo) {
793
- lines.push(`### ${group.dimension} (${group.issues.length} issues)`);
794
- lines.push("");
795
- for (const {agent, issue} of group.issues) {
796
- lines.push(`- **[${agent}]** ${issue.description || issue.issue || "No description"}`);
797
- }
798
- lines.push("");
799
- }
800
- }
801
-
802
- // Unclassified issues
803
- if (corroborationResult.unclassified.length > 0) {
804
- lines.push("## Unclassified Issues (No Dimension)");
805
- lines.push("");
806
- for (const {agent, issue} of corroborationResult.unclassified) {
807
- lines.push(`- **[${agent}]** ${issue.description || issue.issue || "No description"}`);
808
- }
809
- lines.push("");
810
- }
811
-
812
- // Summary
813
- lines.push("## Summary");
814
- lines.push("");
815
- lines.push(`- **Blocking groups**: ${corroborationResult.blocking.length}`);
816
- lines.push(`- **Solo findings**: ${corroborationResult.solo.length}`);
817
- lines.push(`- **Unclassified**: ${corroborationResult.unclassified.length}`);
818
- lines.push(`- **Final verdict**: ${corroborationResult.verdict}`);
819
- lines.push("");
820
- lines.push("**Threshold rule**: Issues in a dimension block when count ≥ 2× distinct agents in that dimension.");
821
-
822
- return lines.join("\n");
823
- }
3
+ * Re-exports from artifacts/ subdirectory for backward compatibility.
4
+ */
5
+
6
+ export {
7
+ formatReviewMarkdown,
8
+ formatCombinedMarkdown,
9
+ buildInlineReviewSummary,
10
+ extractTopIssuesText,
11
+ buildHighIssuesDocument,
12
+ buildCorroborationReport,
13
+ generateReviewIndex,
14
+ buildCombinedJson,
15
+ writeCombinedArtifacts,
16
+ writeFile,
17
+ writeFileNonCritical,
18
+ writeReviewTracker,
19
+ extractPreviousHashes,
20
+ } from "./artifacts/index.js";
21
+ export type { ReviewTrackerEntry } from "./artifacts/index.js";