aiwcli 0.10.3 → 0.11.0

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 (189) hide show
  1. package/bin/run.js +1 -1
  2. package/dist/commands/clear.js +28 -131
  3. package/dist/commands/init/index.js +3 -3
  4. package/dist/lib/gitignore-manager.d.ts +32 -0
  5. package/dist/lib/gitignore-manager.js +141 -2
  6. package/dist/templates/CLAUDE.md +8 -8
  7. package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
  8. package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
  9. package/dist/templates/_shared/.claude/settings.json +7 -7
  10. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
  11. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
  12. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
  13. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
  14. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
  15. package/dist/templates/_shared/hooks-ts/session_end.ts +104 -0
  16. package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
  17. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
  18. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
  19. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
  20. package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
  21. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
  22. package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
  23. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
  24. package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
  25. package/dist/templates/_shared/lib-ts/base/logger.ts +31 -15
  26. package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
  27. package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
  28. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +139 -0
  29. package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
  30. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
  31. package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
  32. package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
  33. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +61 -37
  34. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
  35. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
  36. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +159 -0
  37. package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
  38. package/dist/templates/_shared/lib-ts/types.ts +68 -55
  39. package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
  40. package/dist/templates/_shared/scripts/resume_handoff.ts +321 -0
  41. package/dist/templates/_shared/scripts/save_handoff.ts +21 -21
  42. package/dist/templates/_shared/scripts/status_line.ts +733 -0
  43. package/dist/templates/cc-native/.claude/settings.json +175 -185
  44. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
  45. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
  46. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
  47. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
  48. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +921 -0
  49. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
  50. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +157 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +709 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
  53. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +124 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
  55. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +119 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +162 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
  60. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +249 -0
  61. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +155 -0
  62. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +106 -0
  64. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +243 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +310 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
  70. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -9
  71. package/oclif.manifest.json +1 -1
  72. package/package.json +1 -1
  73. package/dist/templates/_shared/hooks/__init__.py +0 -16
  74. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  75. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  76. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  77. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  78. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  79. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  80. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  81. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  82. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  83. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  84. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  85. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  86. package/dist/templates/_shared/hooks/archive_plan.py +0 -177
  87. package/dist/templates/_shared/hooks/context_monitor.py +0 -270
  88. package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
  89. package/dist/templates/_shared/hooks/pre_compact.py +0 -104
  90. package/dist/templates/_shared/hooks/session_end.py +0 -173
  91. package/dist/templates/_shared/hooks/session_start.py +0 -206
  92. package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
  93. package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
  94. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
  95. package/dist/templates/_shared/lib/__init__.py +0 -1
  96. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  97. package/dist/templates/_shared/lib/base/__init__.py +0 -65
  98. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  100. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  101. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  102. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  103. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  104. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  107. package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
  108. package/dist/templates/_shared/lib/base/constants.py +0 -358
  109. package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
  110. package/dist/templates/_shared/lib/base/inference.py +0 -307
  111. package/dist/templates/_shared/lib/base/logger.py +0 -305
  112. package/dist/templates/_shared/lib/base/stop_words.py +0 -221
  113. package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
  114. package/dist/templates/_shared/lib/base/utils.py +0 -263
  115. package/dist/templates/_shared/lib/context/__init__.py +0 -102
  116. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  117. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  118. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  119. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  120. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  121. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  122. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  123. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  124. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  125. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  126. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  127. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  128. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  129. package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
  130. package/dist/templates/_shared/lib/context/context_selector.py +0 -508
  131. package/dist/templates/_shared/lib/context/context_store.py +0 -653
  132. package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
  133. package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
  134. package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
  135. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  136. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  137. package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
  138. package/dist/templates/_shared/lib/templates/README.md +0 -206
  139. package/dist/templates/_shared/lib/templates/__init__.py +0 -36
  140. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  141. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  142. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  143. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  144. package/dist/templates/_shared/lib/templates/formatters.py +0 -146
  145. package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
  146. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  147. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  148. package/dist/templates/_shared/scripts/save_handoff.py +0 -357
  149. package/dist/templates/_shared/scripts/status_line.py +0 -716
  150. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
  151. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
  152. package/dist/templates/cc-native/MIGRATION.md +0 -86
  153. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  154. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  155. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  156. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  157. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  158. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  159. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
  160. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
  161. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
  162. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
  163. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
  164. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
  165. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  166. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  167. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  171. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  172. package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
  173. package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
  174. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
  175. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
  176. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  177. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  178. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  179. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  180. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  181. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
  185. package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
  186. package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
  187. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  188. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
  189. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
@@ -0,0 +1,709 @@
1
+ /**
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
+ DisplaySettings,
13
+ ReviewerResult,
14
+ } from "./types.js";
15
+ import { DEFAULT_DISPLAY } from "./types.js";
16
+ import { atomicWrite } from "../../_shared/lib-ts/base/atomic-write.js";
17
+ import { sanitizeFilename } from "../../_shared/lib-ts/base/constants.js";
18
+ import { logDebug, logError, logWarn } from "../../_shared/lib-ts/base/logger.js";
19
+ import { nowIso as _nowIso } from "../../_shared/lib-ts/base/utils.js";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Markdown Formatting
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /**
26
+ * Format review results as markdown (legacy compat format).
27
+ */
28
+ export function formatReviewMarkdown(
29
+ results: ReviewerResult[],
30
+ overall: string,
31
+ title = "CC-Native Plan Review",
32
+ settings?: Record<string, unknown>,
33
+ ): string {
34
+ const display = resolveDisplay(settings);
35
+
36
+ const lines: string[] = [];
37
+ lines.push(`# ${title}\n`);
38
+ lines.push(`**Overall verdict:** \`${overall.toUpperCase()}\`\n`);
39
+
40
+ for (const r of results) {
41
+ const displayName = r.name === r.name.toLowerCase() ? titleCase(r.name) : r.name;
42
+ lines.push(`## ${displayName}\n`, `- ok: \`${r.ok}\``, `- verdict: \`${r.verdict}\``);
43
+
44
+ if (r.data && Object.keys(r.data).length > 0) {
45
+ const summary = String(r.data.summary ?? "").trim();
46
+ if (r.data.summary_source === "default") {
47
+ lines.push(`- summary: ⚠️ ${summary} *(reviewer did not return summary)*`);
48
+ } else {
49
+ lines.push(`- summary: ${summary}`);
50
+ }
51
+
52
+ appendReviewDetails(lines, r.data, display);
53
+ } else {
54
+ lines.push(`- note: ${r.err || "no structured output"}`);
55
+ }
56
+
57
+ lines.push("");
58
+ }
59
+
60
+ return lines.join("\n").trim() + "\n";
61
+ }
62
+
63
+ /**
64
+ * Format combined review result as a single markdown document.
65
+ */
66
+ export function formatCombinedMarkdown(
67
+ result: CombinedReviewResult,
68
+ settings?: Record<string, unknown>,
69
+ ): string {
70
+ const display = resolveDisplay(settings);
71
+
72
+ const lines: string[] = [];
73
+ lines.push("# CC-Native Plan Review\n");
74
+ lines.push(`**Overall Verdict:** \`${result.overall_verdict.toUpperCase()}\``, `**Plan Hash:** \`${result.plan_hash}\`\n`, "---\n");
75
+
76
+ // CLI Reviewers section
77
+ if (Object.keys(result.cli_reviewers).length > 0) {
78
+ lines.push("## CLI Reviewers\n");
79
+ for (const [name, r] of Object.entries(result.cli_reviewers)) {
80
+ lines.push(`### ${titleCase(name)}\n`, `- verdict: \`${r.verdict}\``);
81
+ if (r.data && Object.keys(r.data).length > 0) {
82
+ appendSummaryLine(lines, r.data);
83
+ appendReviewDetails(lines, r.data, display);
84
+ } else if (r.err) {
85
+ lines.push(`- error: ${r.err}`);
86
+ }
87
+
88
+ lines.push("");
89
+ }
90
+ }
91
+
92
+ // Orchestration section
93
+ if (result.orchestration) {
94
+ lines.push("---\n", "## Orchestration\n", `- **Complexity:** \`${result.orchestration.complexity}\``, `- **Category:** \`${result.orchestration.category}\``);
95
+ const agentsStr =
96
+ result.orchestration.selected_agents.length > 0
97
+ ? result.orchestration.selected_agents.join(", ")
98
+ : "None";
99
+ lines.push(`- **Agents Selected:** ${agentsStr}`, `- **Reasoning:** ${result.orchestration.reasoning}`);
100
+ if (result.orchestration.skip_reason) {
101
+ lines.push(`- **Skip Reason:** ${result.orchestration.skip_reason}`);
102
+ }
103
+
104
+ if (result.orchestration.error) {
105
+ lines.push(`- **Error:** ${result.orchestration.error}`);
106
+ }
107
+
108
+ lines.push("");
109
+ }
110
+
111
+ // Agent Reviews section
112
+ if (Object.keys(result.agents).length > 0) {
113
+ lines.push("---\n", "## Agent Reviews\n");
114
+ for (const [name, r] of Object.entries(result.agents)) {
115
+ lines.push(`### ${name}\n`, `- verdict: \`${r.verdict}\``);
116
+ if (r.data && Object.keys(r.data).length > 0) {
117
+ appendSummaryLine(lines, r.data);
118
+ appendReviewDetails(lines, r.data, display);
119
+ } else if (r.err) {
120
+ lines.push(`- error: ${r.err}`);
121
+ }
122
+
123
+ lines.push("");
124
+ }
125
+ }
126
+
127
+ return lines.join("\n").trim() + "\n";
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Inline Summaries
132
+ // ---------------------------------------------------------------------------
133
+
134
+ /**
135
+ * Build compact inline summary of HIGH-severity findings for additionalContext.
136
+ */
137
+ export function buildInlineReviewSummary(
138
+ combined: CombinedReviewResult,
139
+ maxIssues = 5,
140
+ maxChars = 800,
141
+ ): string {
142
+ const allReviewers = [
143
+ ...Object.values(combined.cli_reviewers),
144
+ ...Object.values(combined.agents),
145
+ ];
146
+
147
+ const highIssues: Array<Record<string, unknown>> = [];
148
+ for (const r of allReviewers) {
149
+ if (!r.data) continue;
150
+ const issues = r.data.issues as Array<Record<string, unknown>> | undefined;
151
+ if (!issues) continue;
152
+ for (const issue of issues) {
153
+ if (issue.severity === "high") {
154
+ highIssues.push({ ...issue, _reviewer: r.name });
155
+ }
156
+ }
157
+ }
158
+
159
+ const parts: string[] = [];
160
+
161
+ // Overall verdict line
162
+ const issueCount = highIssues.length;
163
+ const countSuffix =
164
+ issueCount > 0
165
+ ? ` (${issueCount} high-severity issue${issueCount === 1 ? "" : "s"})`
166
+ : "";
167
+ parts.push(`**Plan Review: ${combined.overall_verdict.toUpperCase()}**${countSuffix}`);
168
+
169
+ // High-severity issue bullets
170
+ for (const issue of highIssues.slice(0, maxIssues)) {
171
+ const cat = (issue.category as string) ?? "general";
172
+ const text = (issue.issue as string) ?? "";
173
+ const fix = (issue.suggested_fix as string) ?? "";
174
+ const reviewer = (issue._reviewer as string) ?? "unknown";
175
+ let line = `- [${cat}] ${text}`;
176
+ if (fix) line += ` \u2192 ${fix}`;
177
+ line += ` (${reviewer})`;
178
+ parts.push(line);
179
+ }
180
+
181
+ const remaining = highIssues.length - maxIssues;
182
+ if (remaining > 0) {
183
+ parts.push(` ...and ${remaining} more`);
184
+ }
185
+
186
+ let result = parts.join("\n");
187
+ if (result.length > maxChars) {
188
+ result = result.slice(0, maxChars - 3) + "...";
189
+ }
190
+
191
+ return result;
192
+ }
193
+
194
+ /**
195
+ * Extract top issues as compact text for permissionDecisionReason.
196
+ */
197
+ export function extractTopIssuesText(
198
+ combined: CombinedReviewResult,
199
+ maxCount = 3,
200
+ severity = "high",
201
+ ): string {
202
+ const allReviewers = [
203
+ ...Object.values(combined.cli_reviewers),
204
+ ...Object.values(combined.agents),
205
+ ];
206
+
207
+ const issues: string[] = [];
208
+ for (const r of allReviewers) {
209
+ if (!r.data) continue;
210
+ const issueList = r.data.issues as Array<Record<string, unknown>> | undefined;
211
+ if (!issueList) continue;
212
+ for (const issue of issueList) {
213
+ if (issue.severity === severity) {
214
+ const text = String(issue.issue ?? "").trim();
215
+ if (text) {
216
+ issues.push(`[${r.name}] ${text}`);
217
+ break; // first high issue per reviewer only
218
+ }
219
+ }
220
+ }
221
+
222
+ if (issues.length >= maxCount) break;
223
+ }
224
+
225
+ if (issues.length === 0) return "Review found critical issues";
226
+ return issues.join("; ");
227
+ }
228
+
229
+ /**
230
+ * Build markdown document containing ONLY high-severity issues.
231
+ */
232
+ export function buildHighIssuesDocument(
233
+ combined: CombinedReviewResult,
234
+ ): string {
235
+ const lines = ["# High-Severity Issues\n"];
236
+ const allReviewers = [
237
+ ...Object.values(combined.cli_reviewers),
238
+ ...Object.values(combined.agents),
239
+ ];
240
+
241
+ let foundAny = false;
242
+ for (const r of allReviewers) {
243
+ if (!r.data) continue;
244
+ const issues = r.data.issues as Array<Record<string, unknown>> | undefined;
245
+ if (!issues) continue;
246
+
247
+ const highIssues = issues.filter((i) => i.severity === "high");
248
+ if (highIssues.length === 0) continue;
249
+
250
+ foundAny = true;
251
+ lines.push(`## ${r.name} (${r.verdict})\n`);
252
+ for (const issue of highIssues) {
253
+ const cat = (issue.category as string) ?? "general";
254
+ const text = String(issue.issue ?? "").trim();
255
+ const fix = String(issue.suggested_fix ?? "").trim();
256
+ lines.push(`- **[${cat}]** ${text}`);
257
+ if (fix) lines.push(` - Fix: ${fix}`);
258
+ }
259
+
260
+ lines.push(""); // blank line between agents
261
+ }
262
+
263
+ if (!foundAny) {
264
+ lines.push("No high-severity issues found.\n");
265
+ }
266
+
267
+ return lines.join("\n");
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Index Generation
272
+ // ---------------------------------------------------------------------------
273
+
274
+ /**
275
+ * Generate index.md for a review folder.
276
+ */
277
+ export function generateReviewIndex(
278
+ result: CombinedReviewResult,
279
+ iteration?: number,
280
+ _settings?: Record<string, unknown>,
281
+ ): string {
282
+ const now = new Date();
283
+
284
+ const lines = [
285
+ "---",
286
+ `type: review`,
287
+ `plan_hash: ${result.plan_hash}`,
288
+ `overall_verdict: ${result.overall_verdict}`,
289
+ `created_at: ${result.timestamp}`,
290
+ ];
291
+ if (iteration) lines.push(`iteration: ${iteration}`);
292
+ lines.push(
293
+ "---",
294
+ "",
295
+ `# Plan Review - ${formatDate(now)}`,
296
+ "",
297
+ `**Overall Verdict:** \`${result.overall_verdict.toUpperCase()}\``,
298
+ );
299
+
300
+ if (iteration) lines.push(`**Iteration:** ${iteration}`);
301
+ lines.push(`**Plan Hash:** \`${result.plan_hash}\``, "");
302
+
303
+ // Summary from orchestrator
304
+ if (result.orchestration) {
305
+ lines.push(
306
+ "## Analysis",
307
+ `- **Complexity:** \`${result.orchestration.complexity}\``,
308
+ `- **Category:** \`${result.orchestration.category}\``,
309
+ `- **Reasoning:** ${result.orchestration.reasoning}`,
310
+ "",
311
+ );
312
+ }
313
+
314
+ // Navigation table
315
+ lines.push(
316
+ "## Review Files",
317
+ "",
318
+ "| File | Description |",
319
+ "|------|-------------|",
320
+ "| [review.md](./review.md) | Full review details |",
321
+ "| [review.json](./review.json) | Structured review data |",
322
+ "| [plan.md](./plan.md) | Plan snapshot at review time |",
323
+ );
324
+
325
+ for (const name of Object.keys(result.cli_reviewers)) {
326
+ lines.push(
327
+ `| [${name}.json](./reviewer-output/${name}.json) | ${titleCase(name)} reviewer output |`,
328
+ );
329
+ }
330
+
331
+ for (const name of Object.keys(result.agents)) {
332
+ const safeName = sanitizeFilename(name);
333
+ lines.push(
334
+ `| [${safeName}.json](./reviewer-output/${safeName}.json) | ${name} agent output |`,
335
+ );
336
+ }
337
+
338
+ lines.push(
339
+ "",
340
+ "## Verdicts Summary",
341
+ "",
342
+ "| Reviewer | Verdict |",
343
+ "|----------|---------|",
344
+ );
345
+
346
+ for (const [name, r] of Object.entries(result.cli_reviewers)) {
347
+ lines.push(`| ${titleCase(name)} | \`${r.verdict}\` |`);
348
+ }
349
+
350
+ for (const [name, r] of Object.entries(result.agents)) {
351
+ lines.push(`| ${name} | \`${r.verdict}\` |`);
352
+ }
353
+
354
+ lines.push("");
355
+
356
+ return lines.join("\n");
357
+ }
358
+
359
+ // ---------------------------------------------------------------------------
360
+ // JSON Output
361
+ // ---------------------------------------------------------------------------
362
+
363
+ /**
364
+ * Build combined JSON output structure.
365
+ */
366
+ export function buildCombinedJson(
367
+ result: CombinedReviewResult,
368
+ ): Record<string, unknown> {
369
+ const output: Record<string, unknown> = {
370
+ metadata: {
371
+ timestamp: result.timestamp,
372
+ plan_hash: result.plan_hash,
373
+ },
374
+ overall: {
375
+ verdict: result.overall_verdict,
376
+ },
377
+ };
378
+
379
+ // CLI reviewers
380
+ if (Object.keys(result.cli_reviewers).length > 0) {
381
+ const cliReviewers: Record<string, unknown> = {};
382
+ output.cliReviewers = cliReviewers;
383
+ for (const [name, r] of Object.entries(result.cli_reviewers)) {
384
+ cliReviewers[name] = {
385
+ verdict: r.verdict,
386
+ summary: r.data?.summary ?? null,
387
+ summarySource: r.data?.summary_source ?? null,
388
+ issues: r.data
389
+ ? ((r.data.issues as Array<Record<string, unknown>>) ?? []).filter(
390
+ (i) => i.severity !== "low",
391
+ )
392
+ : [],
393
+ ok: r.ok,
394
+ error: r.err || null,
395
+ };
396
+ }
397
+ }
398
+
399
+ // Orchestration
400
+ if (result.orchestration) {
401
+ output.orchestration = {
402
+ complexity: result.orchestration.complexity,
403
+ category: result.orchestration.category,
404
+ selectedAgents: result.orchestration.selected_agents,
405
+ reasoning: result.orchestration.reasoning,
406
+ skipReason: result.orchestration.skip_reason ?? null,
407
+ error: result.orchestration.error ?? null,
408
+ };
409
+ }
410
+
411
+ // Agents
412
+ if (Object.keys(result.agents).length > 0) {
413
+ const agents: Record<string, unknown> = {};
414
+ output.agents = agents;
415
+ for (const [name, r] of Object.entries(result.agents)) {
416
+ agents[name] = {
417
+ verdict: r.verdict,
418
+ summary: r.data?.summary ?? null,
419
+ summarySource: r.data?.summary_source ?? null,
420
+ issues: r.data
421
+ ? ((r.data.issues as Array<Record<string, unknown>>) ?? []).filter(
422
+ (i) => i.severity !== "low",
423
+ )
424
+ : [],
425
+ missing_sections: r.data?.missing_sections ?? [],
426
+ questions: r.data?.questions ?? [],
427
+ ok: r.ok,
428
+ error: r.err || null,
429
+ };
430
+ }
431
+ }
432
+
433
+ return output;
434
+ }
435
+
436
+ // ---------------------------------------------------------------------------
437
+ // Artifact Writing
438
+ // ---------------------------------------------------------------------------
439
+
440
+ /**
441
+ * Write combined review artifacts to context reviews folder.
442
+ * Uses atomic writes for critical files when ENABLE_ROBUST_PLAN_WRITES is true.
443
+ */
444
+ export function writeCombinedArtifacts(
445
+ base: string,
446
+ plan: string,
447
+ result: CombinedReviewResult,
448
+ payload: Record<string, unknown>,
449
+ settings?: Record<string, unknown>,
450
+ contextReviewsDir?: string,
451
+ reviewFolder?: string,
452
+ iteration?: number,
453
+ ): string {
454
+ const outDir = reviewFolder ?? contextReviewsDir;
455
+ if (!outDir) {
456
+ throw new Error("Either contextReviewsDir or reviewFolder is required");
457
+ }
458
+
459
+ logDebug("utils", `Using review folder: ${outDir}`);
460
+
461
+ // Create directory
462
+ try {
463
+ fs.mkdirSync(outDir, { recursive: true });
464
+ } catch (error: unknown) {
465
+ logError("utils", `Cannot create directory ${outDir}: ${error}`);
466
+ throw error;
467
+ }
468
+
469
+ // JSON write
470
+ const jsonPath = path.join(outDir, "review.json");
471
+ const jsonData = buildCombinedJson(result);
472
+ writeFile(jsonPath, JSON.stringify(jsonData, null, 2));
473
+
474
+ // Markdown write
475
+ const mdPath = path.join(outDir, "review.md");
476
+ const mdContent = formatCombinedMarkdown(result, settings);
477
+ writeFile(mdPath, mdContent);
478
+
479
+ // Individual reviewer writes (non-critical) — in reviewer-output/ subfolder
480
+ const reviewerOutputDir = path.join(outDir, "reviewer-output");
481
+ try {
482
+ fs.mkdirSync(reviewerOutputDir, { recursive: true });
483
+ } catch {
484
+ // Best-effort — non-critical
485
+ }
486
+
487
+ for (const [name, r] of Object.entries(result.cli_reviewers)) {
488
+ if (r.data) {
489
+ writeFileNonCritical(
490
+ path.join(reviewerOutputDir, `${name}.json`),
491
+ JSON.stringify(r.data, null, 2),
492
+ );
493
+ }
494
+ }
495
+
496
+ for (const [name, r] of Object.entries(result.agents)) {
497
+ if (r.data) {
498
+ writeFileNonCritical(
499
+ path.join(reviewerOutputDir, `${sanitizeFilename(name)}.json`),
500
+ JSON.stringify(r.data, null, 2),
501
+ );
502
+ }
503
+ }
504
+
505
+ // Generate index.md for folder-based reviews
506
+ if (reviewFolder) {
507
+ const indexContent = generateReviewIndex(result, iteration, settings);
508
+ writeFileNonCritical(path.join(outDir, "index.md"), indexContent);
509
+ return path.join(outDir, "index.md");
510
+ }
511
+
512
+ return mdPath;
513
+ }
514
+
515
+ // ---------------------------------------------------------------------------
516
+ // Helpers
517
+ // ---------------------------------------------------------------------------
518
+
519
+ function resolveDisplay(
520
+ settings?: Record<string, unknown>,
521
+ ): DisplaySettings {
522
+ if (!settings) return { ...DEFAULT_DISPLAY };
523
+ const display = (settings.display as Partial<DisplaySettings>) ?? {};
524
+ return { ...DEFAULT_DISPLAY, ...display };
525
+ }
526
+
527
+ function appendSummaryLine(lines: string[], data: Record<string, unknown>): void {
528
+ const summary = String(data.summary ?? "").trim();
529
+ if (data.summary_source === "default") {
530
+ lines.push(`- summary: ⚠️ ${summary} *(reviewer did not return summary)*`);
531
+ } else {
532
+ lines.push(`- summary: ${summary}`);
533
+ }
534
+ }
535
+
536
+ function appendReviewDetails(
537
+ lines: string[],
538
+ data: Record<string, unknown>,
539
+ display: DisplaySettings,
540
+ ): void {
541
+ const issues = ((data.issues as Array<Record<string, unknown>>) ?? []).filter(
542
+ (i) => i.severity !== "low",
543
+ );
544
+ if (issues.length > 0) {
545
+ lines.push("\n**Issues:**");
546
+ for (const it of issues.slice(0, display.maxIssues)) {
547
+ const sev = (it.severity as string) ?? "medium";
548
+ const cat = (it.category as string) ?? "general";
549
+ const issue = (it.issue as string) ?? "";
550
+ const fix = (it.suggested_fix as string) ?? "";
551
+ lines.push(`- **[${sev}] ${cat}**: ${issue}`);
552
+ if (fix) lines.push(` - fix: ${fix}`);
553
+ }
554
+ }
555
+
556
+ const missing = (data.missing_sections as string[]) ?? [];
557
+ if (missing.length > 0) {
558
+ lines.push("\n**Missing Sections:**");
559
+ for (const m of missing.slice(0, display.maxMissingSections)) {
560
+ lines.push(`- ${m}`);
561
+ }
562
+ }
563
+
564
+ const qs = (data.questions as string[]) ?? [];
565
+ if (qs.length > 0) {
566
+ lines.push("\n**Questions:**");
567
+ for (const q of qs.slice(0, display.maxQuestions)) {
568
+ lines.push(`- ${q}`);
569
+ }
570
+ }
571
+ }
572
+
573
+ function writeFile(filePath: string, content: string): void {
574
+ try {
575
+ if (ENABLE_ROBUST_PLAN_WRITES) {
576
+ const [success, error] = atomicWrite(filePath, content);
577
+ if (!success) throw new Error(`Atomic write failed: ${error}`);
578
+ } else {
579
+ fs.writeFileSync(filePath, content, "utf-8");
580
+ }
581
+ } catch (error: unknown) {
582
+ logError("utils", `Failed to write ${path.basename(filePath)}: ${error}`);
583
+ throw error;
584
+ }
585
+ }
586
+
587
+ function writeFileNonCritical(filePath: string, content: string): void {
588
+ try {
589
+ if (ENABLE_ROBUST_PLAN_WRITES) {
590
+ const [success, error] = atomicWrite(filePath, content);
591
+ if (!success) {
592
+ logWarn("utils", `Failed to write ${path.basename(filePath)}: ${error}`);
593
+ }
594
+ } else {
595
+ fs.writeFileSync(filePath, content, "utf-8");
596
+ }
597
+ } catch (error: unknown) {
598
+ logWarn("utils", `Failed to write ${path.basename(filePath)}: ${error}`);
599
+ }
600
+ }
601
+
602
+ function titleCase(s: string): string {
603
+ return s.charAt(0).toUpperCase() + s.slice(1);
604
+ }
605
+
606
+ function formatDate(d: Date): string {
607
+ const year = d.getFullYear();
608
+ const month = String(d.getMonth() + 1).padStart(2, "0");
609
+ const day = String(d.getDate()).padStart(2, "0");
610
+ const hours = String(d.getHours()).padStart(2, "0");
611
+ const minutes = String(d.getMinutes()).padStart(2, "0");
612
+ return `${year}-${month}-${day} ${hours}:${minutes}`;
613
+ }
614
+
615
+ // ---------------------------------------------------------------------------
616
+ // Review Tracker
617
+ // ---------------------------------------------------------------------------
618
+
619
+ export interface ReviewTrackerEntry {
620
+ decision: string;
621
+ iteration: number;
622
+ planHash: string;
623
+ reviewFolder: string;
624
+ score: number;
625
+ timestamp: string;
626
+ topIssues: string[];
627
+ verdict: string;
628
+ }
629
+
630
+ /**
631
+ * Build or update the review-tracker.md in the cc-native reviews directory.
632
+ * This file provides a human-readable summary of all review iterations,
633
+ * making it easy to see whether feedback was acted on.
634
+ */
635
+ export function writeReviewTracker(
636
+ ccNativeReviewsDir: string,
637
+ entry: ReviewTrackerEntry,
638
+ ): void {
639
+ const trackerPath = path.join(ccNativeReviewsDir, "review-tracker.md");
640
+
641
+ // Read existing tracker entries if present
642
+ let existingContent = "";
643
+ try {
644
+ if (fs.existsSync(trackerPath)) {
645
+ existingContent = fs.readFileSync(trackerPath, "utf8");
646
+ }
647
+ } catch {
648
+ // Fresh start
649
+ }
650
+
651
+ // Parse existing entries to detect plan changes
652
+ const previousHashes = extractPreviousHashes(existingContent);
653
+ const hashChanged = previousHashes.length > 0 &&
654
+ previousHashes.at(-1) !== entry.planHash;
655
+
656
+ // Build the new entry section
657
+ const lines: string[] = [];
658
+ const verdictEmoji = entry.decision === "allow" ? "\u2705" : "\u274C";
659
+ const changeNote = previousHashes.length > 0
660
+ ? (hashChanged ? "\u2705 Plan was revised (hash changed)" : "\u26A0\uFE0F Plan unchanged since last review")
661
+ : "Initial review";
662
+
663
+ lines.push(`## Iteration ${entry.iteration} \u2014 ${entry.timestamp} \u2014 ${verdictEmoji} ${entry.verdict.toUpperCase()}`, "", `- **Decision:** ${entry.decision}`);
664
+ lines.push(`- **Score:** ${entry.score.toFixed(2)}`, `- **Plan hash:** \`${entry.planHash}\``, `- **Status:** ${changeNote}`);
665
+ lines.push(`- **Full review:** [\`${path.basename(entry.reviewFolder)}/\`](${path.basename(entry.reviewFolder)}/index.md)`);
666
+
667
+ if (entry.topIssues.length > 0) {
668
+ lines.push("", "**Top issues:**");
669
+ for (const issue of entry.topIssues) {
670
+ lines.push(`- ${issue}`);
671
+ }
672
+ }
673
+
674
+ lines.push("");
675
+
676
+ // Build full file
677
+ let output: string;
678
+ if (!existingContent || !existingContent.includes("# Plan Review Tracker")) {
679
+ // New file
680
+ output = [
681
+ "# Plan Review Tracker",
682
+ "",
683
+ "> Auto-generated by plan review hook. Shows review lifecycle across iterations.",
684
+ "> Check `plan.md` in each iteration folder to diff plan changes.",
685
+ "",
686
+ ...lines,
687
+ ].join("\n");
688
+ } else {
689
+ // Append to existing
690
+ output = existingContent.trimEnd() + "\n\n" + lines.join("\n");
691
+ }
692
+
693
+ try {
694
+ fs.writeFileSync(trackerPath, output, "utf-8");
695
+ } catch (error) {
696
+ logWarn("artifacts", `Failed to write review tracker: ${error}`);
697
+ }
698
+ }
699
+
700
+ function extractPreviousHashes(content: string): string[] {
701
+ const hashes: string[] = [];
702
+ const regex = /\*\*Plan hash:\*\* `([a-f0-9]+)`/g;
703
+ let match: null | RegExpExecArray;
704
+ while ((match = regex.exec(content)) !== null) {
705
+ hashes.push(match[1]!);
706
+ }
707
+
708
+ return hashes;
709
+ }