codehere 0.7.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +22 -13
  2. package/dist/application/services/verdict-service.js +154 -0
  3. package/dist/application/use-cases/assess-cross-vendor.js +126 -0
  4. package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
  5. package/dist/application/use-cases/export-aat.js +107 -0
  6. package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
  7. package/dist/application/use-cases/generate-final-report.js +140 -15
  8. package/dist/application/use-cases/generate-narration.js +5 -3
  9. package/dist/application/use-cases/generate-thread-title.js +5 -2
  10. package/dist/application/use-cases/narrator-prompt-template.js +1 -1
  11. package/dist/application/use-cases/plan-auto-review.js +57 -0
  12. package/dist/application/use-cases/reconcile-audit.js +100 -0
  13. package/dist/application/use-cases/rerun-tests.js +226 -0
  14. package/dist/application/use-cases/search-sessions.js +70 -0
  15. package/dist/application/use-cases/should-auto-review.js +71 -0
  16. package/dist/application/use-cases/thread-export.js +90 -0
  17. package/dist/application/use-cases/verify-claim.js +187 -5
  18. package/dist/domain/audit.js +11 -0
  19. package/dist/domain/services/codebase-detector.js +0 -6
  20. package/dist/domain/task.js +91 -0
  21. package/dist/error-handler.js +3 -115
  22. package/dist/index.js +279 -31
  23. package/dist/infrastructure/completion/bash-completion.js +7 -3
  24. package/dist/infrastructure/completion/fish-completion.js +7 -12
  25. package/dist/infrastructure/completion/zsh-completion.js +7 -12
  26. package/dist/infrastructure/config/config-manager.js +4 -3
  27. package/dist/infrastructure/config/product-context.js +1 -19
  28. package/dist/infrastructure/context/adapters/figma-source.js +37 -2
  29. package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
  30. package/dist/infrastructure/context/github-api.js +110 -0
  31. package/dist/infrastructure/context/github-device-flow.js +65 -0
  32. package/dist/infrastructure/context/oauth-store.js +5 -2
  33. package/dist/infrastructure/context/registry-bootstrap.js +0 -4
  34. package/dist/infrastructure/cost/budget-config.js +4 -3
  35. package/dist/infrastructure/cost/cost-router.js +16 -23
  36. package/dist/infrastructure/cost/spend-aggregator.js +91 -16
  37. package/dist/infrastructure/cost/team-ledger.js +355 -0
  38. package/dist/infrastructure/discovery/discovery-log.js +85 -0
  39. package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
  40. package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
  41. package/dist/infrastructure/hooks/hook-mode.js +20 -4
  42. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
  43. package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
  44. package/dist/infrastructure/observability/execution-tracer.js +1 -1
  45. package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
  46. package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
  47. package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
  48. package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
  49. package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
  50. package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
  51. package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
  52. package/dist/infrastructure/orchestration/context-handoff.js +32 -14
  53. package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
  54. package/dist/infrastructure/orchestration/failover-config.js +1 -11
  55. package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
  56. package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
  57. package/dist/infrastructure/orchestration/graphify-context.js +14 -59
  58. package/dist/infrastructure/orchestration/memory-injector.js +4 -1
  59. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
  60. package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
  61. package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
  62. package/dist/infrastructure/orchestration/session-manager.js +593 -94
  63. package/dist/infrastructure/orchestration/session-persistence.js +177 -41
  64. package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
  65. package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
  66. package/dist/infrastructure/protocols/cloud-sync.js +2 -2
  67. package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
  68. package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
  69. package/dist/infrastructure/protocols/mcp-tools.js +401 -0
  70. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
  71. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
  72. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
  73. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
  74. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
  75. package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
  76. package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
  77. package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
  78. package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
  79. package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
  80. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
  81. package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
  82. package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
  83. package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
  84. package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
  85. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
  86. package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
  87. package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
  88. package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
  89. package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
  90. package/dist/infrastructure/protocols/ui/styles.js +2147 -0
  91. package/dist/infrastructure/protocols/ui/tokens.js +96 -0
  92. package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
  93. package/dist/infrastructure/protocols/verify-page.js +884 -148
  94. package/dist/infrastructure/security/fix-code.js +144 -0
  95. package/dist/infrastructure/security/fix-secret.js +89 -0
  96. package/dist/infrastructure/security/permissions-config.js +3 -2
  97. package/dist/infrastructure/security/scan-types.js +10 -0
  98. package/dist/infrastructure/security/ship-scan.js +195 -0
  99. package/dist/infrastructure/security/trust-scorer.js +70 -3
  100. package/dist/infrastructure/security/vibe-checks.js +99 -0
  101. package/dist/infrastructure/storage/atomic-write.js +86 -0
  102. package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
  103. package/dist/infrastructure/storage/project-thread-reader.js +262 -25
  104. package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
  105. package/dist/monitoring.js +2 -2
  106. package/dist/presentation/cli/commands/audit-command.js +79 -4
  107. package/dist/presentation/cli/commands/budget-command.js +2 -2
  108. package/dist/presentation/cli/commands/check-command.js +96 -0
  109. package/dist/presentation/cli/commands/context-source-command.js +20 -1
  110. package/dist/presentation/cli/commands/help-command.js +5 -5
  111. package/dist/presentation/cli/commands/hook-command.js +5 -5
  112. package/dist/presentation/cli/commands/init-command.js +89 -66
  113. package/dist/presentation/cli/commands/memory-command.js +28 -28
  114. package/dist/presentation/cli/commands/permission-command.js +1 -12
  115. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  116. package/dist/presentation/cli/commands/protocols-command.js +4 -4
  117. package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
  118. package/dist/presentation/cli/commands/report-command.js +48 -32
  119. package/dist/presentation/cli/commands/scan-command.js +52 -0
  120. package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
  121. package/dist/presentation/cli/commands/search-command.js +51 -0
  122. package/dist/presentation/cli/commands/setup-command.js +31 -8
  123. package/dist/presentation/cli/commands/status-command.js +39 -86
  124. package/dist/presentation/cli/commands/team-command.js +256 -0
  125. package/dist/presentation/cli/commands/thread-command.js +87 -5
  126. package/dist/presentation/cli/commands/trust-command.js +53 -31
  127. package/dist/presentation/cli/commands/verify-command.js +74 -9
  128. package/dist/presentation/cli/error-display.js +0 -8
  129. package/dist/presentation/cli/repl.js +15 -7
  130. package/dist/presentation/cli/task-deliverable.js +36 -10
  131. package/dist/testing/replay-adapter.js +0 -19
  132. package/dist/ui.js +51 -239
  133. package/dist/utils/version.js +0 -6
  134. package/package.json +30 -10
  135. package/dist/domain/entities/task.js +0 -61
  136. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  137. package/dist/infrastructure/security/input-validator.js +0 -152
  138. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  139. package/dist/infrastructure/storage/task-repository.js +0 -206
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Acceptance-criteria agent — REQUEST_METHOD.md stage 2 (the Contract), the
3
+ * keystone of the request method.
4
+ *
5
+ * Before an agent runs, derive 2-5 testable acceptance criteria from the
6
+ * request: the up-front, checkable definition of "done" that verify (stage 5)
7
+ * later grades AGAINST, instead of judging the result post-hoc. This is the one
8
+ * change that turns "route a prompt and audit it after" into "make the agent
9
+ * work to a contract and prove it met it."
10
+ *
11
+ * Mirrors generate-thread-title.ts: single-shot, no streaming, `claude --bare`,
12
+ * ~1 cheap call, degrades SILENTLY to [] when no provider responds (a request
13
+ * with no contract still runs — the method is additive, never a hard gate here).
14
+ */
15
+ import { spawn } from 'child_process';
16
+ import { isMockMode } from '../../infrastructure/orchestration/mocks/resolve-agent-spawn.js';
17
+ const CRITERIA_TIMEOUT_MS = 15_000;
18
+ const MAX_CRITERIA = 5;
19
+ const PROMPT_TEMPLATE = (firstPrompt, cwd) => `Generate 2 to 5 acceptance criteria for this coding request. Each criterion is one short, testable statement of what "done" means — something a reviewer could check pass/fail.
20
+
21
+ Working directory: ${cwd}
22
+ Request: ${firstPrompt}
23
+
24
+ Rules:
25
+ - 2 to 5 criteria, ONE per line
26
+ - Each is a checkable OUTCOME (e.g. "greet() throws a TypeError when name is not a string"), not a step and not a vibe
27
+ - Ground each in the request only; do not invent scope
28
+ - No numbering, no bullets, no preamble, no trailing period
29
+ - Output ONLY the criteria, one per line
30
+
31
+ Criteria:`;
32
+ /**
33
+ * Parse model output into criteria. Rejects refusal/error-shaped lines (the
34
+ * `claude --bare` call can return rate-limit / auth text on stdout with exit 0,
35
+ * same failure mode the title agent guards against).
36
+ */
37
+ export function sanitizeCriteria(raw) {
38
+ const bad = /\b(i cannot|i can'?t|i apologi[sz]e|i'?m sorry|as an ai|rate.?limit|unauthorized|request id|api key)\b/i;
39
+ return String(raw || '')
40
+ .split('\n')
41
+ .map((l) => l.replace(/^[\s\-*•–>#\d.)\]]+/, '').trim())
42
+ .filter((l) => l.length >= 3 && l.length <= 140 && !bad.test(l))
43
+ .slice(0, MAX_CRITERIA)
44
+ .map((text) => ({ text, met: null }));
45
+ }
46
+ /**
47
+ * The scaling principle (REQUEST_METHOD §"no ceremony tax"): a trivial ask gets
48
+ * no contract. Trivial = very short, or a short read-only lookup. Everything
49
+ * else earns a contract. Keeps "just type and go" free for simple requests and
50
+ * skips the per-request LLM call where it would only be noise.
51
+ */
52
+ export function isTrivialRequest(prompt) {
53
+ const p = String(prompt || '')
54
+ .trim()
55
+ .toLowerCase();
56
+ if (!p)
57
+ return true;
58
+ const words = p.split(/\s+/).filter(Boolean).length;
59
+ if (words <= 3)
60
+ return true;
61
+ if (words <= 6 && /^(list|ls|show|cat|print|echo|pwd|help|read|open|what is|what'?s)\b/.test(p))
62
+ return true;
63
+ return false;
64
+ }
65
+ /** Deterministic mock: grounded-enough criteria for demos + tests, no LLM. */
66
+ export function mockCriteria(firstPrompt) {
67
+ const p = String(firstPrompt || '').toLowerCase();
68
+ const out = [];
69
+ if (/rate.?limit|throttle/.test(p))
70
+ out.push('The rate limit is enforced under load');
71
+ if (/throw|error|validation|validate|guard|invalid/.test(p))
72
+ out.push('Invalid input is rejected with a clear error');
73
+ if (/test/.test(p))
74
+ out.push('Tests are added and pass');
75
+ if (!out.length)
76
+ out.push('The change does what the request asked', 'No existing behaviour regresses');
77
+ return out.slice(0, MAX_CRITERIA).map((text) => ({ text, met: null }));
78
+ }
79
+ function selectProviderBinary() {
80
+ const override = process.env.CODEHERE_CRITERIA_BINARY;
81
+ if (override)
82
+ return [override];
83
+ return ['claude'];
84
+ }
85
+ const GRADE_PROMPT = (criteria, ctx) => `You are grading whether a coding task met its acceptance criteria. For EACH criterion, answer strictly MET or UNMET based only on the evidence. If the evidence does not clearly show a criterion was met, answer UNMET (never guess "met").
86
+
87
+ Request: ${ctx.prompt}
88
+ Result summary: ${ctx.reportSummary || '(none)'}
89
+ Files changed: ${(ctx.files || []).join(', ') || '(none)'}
90
+
91
+ Criteria (one per line):
92
+ ${criteria.map((c, i) => `${i + 1}. ${c.text}`).join('\n')}
93
+
94
+ Output exactly one line per criterion, in order, each "MET" or "UNMET" — nothing else.`;
95
+ /** Parse "MET/UNMET" lines back onto the criteria, in order. */
96
+ export function applyGrades(criteria, raw) {
97
+ const verdicts = String(raw || '')
98
+ .split('\n')
99
+ .map((l) => l.trim().toUpperCase())
100
+ .filter((l) => l === 'MET' || l === 'UNMET');
101
+ return criteria.map((c, i) => {
102
+ const v = verdicts[i];
103
+ return { text: c.text, met: v === 'MET' ? true : v === 'UNMET' ? false : (c.met ?? null) };
104
+ });
105
+ }
106
+ /**
107
+ * Deterministic mock grade — honest by the same rule as the real one: a
108
+ * criterion is met ONLY if the run was verified AND the criterion's salient
109
+ * terms show up in the result; otherwise unmet. No fake green.
110
+ */
111
+ export function mockGrade(criteria, ctx) {
112
+ const hay = ((ctx.reportSummary || '') + ' ' + (ctx.files || []).join(' ')).toLowerCase();
113
+ return criteria.map((c) => {
114
+ if (!ctx.verdictGreen)
115
+ return { text: c.text, met: false };
116
+ const terms = c.text
117
+ .toLowerCase()
118
+ .split(/\s+/)
119
+ .filter((w) => w.length > 4);
120
+ const hit = terms.filter((t) => hay.includes(t)).length;
121
+ return { text: c.text, met: hit >= Math.max(1, Math.ceil(terms.length * 0.3)) };
122
+ });
123
+ }
124
+ /**
125
+ * Grade the contract against the result (REQUEST_METHOD stage 5). Runs at
126
+ * completion. Real path is a small single-shot assessment; folding this into
127
+ * the existing final-report call (truly no new call) is a noted follow-up.
128
+ * Returns the criteria unchanged (all pending) if grading can't run — never
129
+ * marks met without evidence.
130
+ */
131
+ export async function gradeAcceptanceCriteria(criteria, ctx) {
132
+ if (!Array.isArray(criteria) || !criteria.length)
133
+ return criteria || [];
134
+ if (isMockMode())
135
+ return mockGrade(criteria, ctx);
136
+ const prompt = GRADE_PROMPT(criteria, ctx);
137
+ for (const binary of selectProviderBinary()) {
138
+ const raw = await new Promise((resolve) => {
139
+ let out = '';
140
+ let settled = false;
141
+ const done = (r) => {
142
+ if (!settled) {
143
+ settled = true;
144
+ resolve(r);
145
+ }
146
+ };
147
+ const child = spawn(binary, ['--bare', '-p', prompt], { stdio: ['ignore', 'pipe', 'pipe'] });
148
+ const to = setTimeout(() => {
149
+ child.kill('SIGTERM');
150
+ done(null);
151
+ }, CRITERIA_TIMEOUT_MS);
152
+ child.on('error', () => {
153
+ clearTimeout(to);
154
+ done(null);
155
+ });
156
+ child.stdout?.on('data', (c) => {
157
+ out += c.toString('utf-8');
158
+ });
159
+ child.on('exit', (code) => {
160
+ clearTimeout(to);
161
+ done(code === 0 ? out : null);
162
+ });
163
+ });
164
+ if (raw)
165
+ return applyGrades(criteria, raw);
166
+ }
167
+ return criteria; // no grade available → stay pending, never fake a green
168
+ }
169
+ function tryProvider(binary, prompt) {
170
+ return new Promise((resolve) => {
171
+ let stdout = '';
172
+ let settled = false;
173
+ const finish = (result) => {
174
+ if (settled)
175
+ return;
176
+ settled = true;
177
+ resolve(result);
178
+ };
179
+ const child = spawn(binary, ['--bare', '-p', prompt], {
180
+ stdio: ['ignore', 'pipe', 'pipe'],
181
+ });
182
+ const timeout = setTimeout(() => {
183
+ child.kill('SIGTERM');
184
+ if (process.env.CODEHERE_DEBUG === '1') {
185
+ console.error(`[criteria-agent] ${binary} timeout after ${CRITERIA_TIMEOUT_MS}ms`);
186
+ }
187
+ finish(null);
188
+ }, CRITERIA_TIMEOUT_MS);
189
+ child.on('error', () => {
190
+ clearTimeout(timeout);
191
+ finish(null);
192
+ });
193
+ child.stdout?.on('data', (chunk) => {
194
+ stdout += chunk.toString('utf-8');
195
+ });
196
+ child.on('exit', (code) => {
197
+ clearTimeout(timeout);
198
+ if (code !== 0) {
199
+ finish(null);
200
+ return;
201
+ }
202
+ const criteria = sanitizeCriteria(stdout);
203
+ finish(criteria.length ? criteria : null);
204
+ });
205
+ });
206
+ }
207
+ /**
208
+ * The contract. Returns [] (not null) when no provider responds so callers can
209
+ * store the field unconditionally; an empty contract simply means the request
210
+ * ran without one (never blocks the run).
211
+ */
212
+ export async function generateAcceptanceCriteria(firstPrompt, cwd) {
213
+ if (isMockMode())
214
+ return mockCriteria(firstPrompt);
215
+ const prompt = PROMPT_TEMPLATE(firstPrompt, cwd);
216
+ for (const binary of selectProviderBinary()) {
217
+ const result = await tryProvider(binary, prompt);
218
+ if (result)
219
+ return result;
220
+ }
221
+ return [];
222
+ }
223
+ //# sourceMappingURL=generate-acceptance-criteria.js.map
@@ -21,7 +21,10 @@
21
21
  * was available, and NEVER implies success — so a completed task always shows a
22
22
  * deliverable, and it's honest about which kind it is (see `source`).
23
23
  */
24
- const MAX_SUMMARY_CHARS = 1600;
24
+ // Generous cap — the old 1600 clipped reports mid-code-block (a code fence +
25
+ // prose easily exceeds it). High enough to never trim a real report's code,
26
+ // still bounded against a runaway agent dump.
27
+ const MAX_SUMMARY_CHARS = 20000;
25
28
  /**
26
29
  * UAT-003 — patterns indicating the agent's own text is actually a network /
27
30
  * auth / quota failure, not a real summary. Such "summaries" must NOT be shown
@@ -31,6 +34,33 @@ const NETWORK_FAILURE_PATTERNS = /(?:all connection attempts failed|ECONNREFUSED
31
34
  function isLikelyNetworkFailureSummary(text) {
32
35
  return NETWORK_FAILURE_PATTERNS.test(text);
33
36
  }
37
+ /**
38
+ * UAT (real-mode dogfood 2026-06-13) — some CLIs (aider) have no narration
39
+ * channel; their captured "say" text is setup/housekeeping chatter, not a
40
+ * summary of the work ("You can skip this check with --no-gitignore", the
41
+ * `.gitignore` housekeeping, the version banner). Shown as the report it reads
42
+ * as broken — and it buries the honest deterministic facts (files changed +
43
+ * trust score). Strip the housekeeping; if nothing real is left, fall through.
44
+ */
45
+ const TOOL_NOISE_PATTERNS = [
46
+ /you can skip this check with --no-gitignore/gi,
47
+ /added\s+\.?aider\*?\s+to\s+\.?gitignore/gi,
48
+ /\baider v[\d.]+(?:\s*\([^)]*\))?/gi,
49
+ /\binitialized empty git repository[^\n]*/gi,
50
+ // aider session-banner lines (loop-1 sweep 2026-07-06: real runs whose whole
51
+ // "summary" was this chatter passed the length bar and rendered as the report)
52
+ /\bnote: in-chat filenames are always relative to[^\n.]*\.?/gi,
53
+ /\bcur working dir:[^\n]*/gi,
54
+ /\bgit working dir:[^\n]*/gi,
55
+ /\brepo-map can't include[^\n]*/gi,
56
+ /\bhas it been deleted from the file system but not from git\?/gi,
57
+ ];
58
+ function stripToolNoise(text) {
59
+ let t = text;
60
+ for (const re of TOOL_NOISE_PATTERNS)
61
+ t = t.replace(re, ' ');
62
+ return t.replace(/\s+/g, ' ').trim();
63
+ }
34
64
  /**
35
65
  * ENGINE: the agent's own narration. kind:'say' steps are the words the agent
36
66
  * already emitted (reasoning + wrap-up). Joined newest-context-last, they ARE
@@ -42,7 +72,19 @@ function extractAgentSummary(steps) {
42
72
  .map((s) => s.label.trim());
43
73
  if (!says.length)
44
74
  return null;
45
- const text = says.join('\n\n').slice(0, MAX_SUMMARY_CHARS);
75
+ // Inverted pyramid (loop-1 lesson 2026-07-06): agents narrate chronologically,
76
+ // so joined-in-order reports LEAD with opening narration ("Let me check…")
77
+ // while the finding sits at the end — and every summary-of-the-summary
78
+ // downstream (glance, cards, search snippets) inherits the wrong lead. The
79
+ // agent's LAST say is its wrap-up: put it first, keep the earlier narration
80
+ // below under an honest label. Content intact, order presented, not invented.
81
+ const ordered = says.length > 1 ? [says[says.length - 1], 'Earlier notes:', ...says.slice(0, -1)] : says;
82
+ const text = ordered.join('\n\n').slice(0, MAX_SUMMARY_CHARS);
83
+ // A "summary" that is only tool housekeeping (no real prose left after
84
+ // stripping it) is NOT a summary → let the deterministic fallback give the
85
+ // honest facts (files changed + trust score) instead of chatter.
86
+ if (stripToolNoise(text).length < 12)
87
+ return null;
46
88
  // Too short to be a real summary (e.g. a one-word ack) → let the fallback give facts.
47
89
  return text.length >= 12 ? text : null;
48
90
  }
@@ -56,10 +98,13 @@ function buildDeterministicReport(input) {
56
98
  const noGit = input.artifacts?.noGitTracking;
57
99
  const agentName = input.agent === 'conductor' ? 'the agent codehere chose' : input.agent;
58
100
  const parts = [
59
- `No plain-English summary was available for this run — ${agentName} didn't leave a written summary, and codehere doesn't fabricate one. Here are the recorded facts.`,
101
+ `No written summary for this run. ${agentName} didn't leave one, and codehere doesn't fabricate one. Here are the recorded facts.`,
60
102
  `The session ran ${input.steps.length} step${input.steps.length === 1 ? '' : 's'}` +
61
103
  (toolSteps.length
62
- ? ` (${toolSteps.slice(0, 5).map((s) => s.label).join(', ')}${toolSteps.length > 5 ? ', …' : ''})`
104
+ ? ` (${toolSteps
105
+ .slice(0, 5)
106
+ .map((s) => s.label)
107
+ .join(', ')}${toolSteps.length > 5 ? ', …' : ''})`
63
108
  : '') +
64
109
  `.`,
65
110
  ];
@@ -78,19 +123,99 @@ function buildDeterministicReport(input) {
78
123
  }
79
124
  return { summary: parts.join('\n\n'), generatedAt: Date.now(), durationMs: 0, source: 'deterministic' };
80
125
  }
126
+ /** Per-vendor recovery hints — the SPECIFIC auth/quota fix for EACH agent, so a
127
+ * failure report names the right login command + provider, not a claude-centric
128
+ * or generic guess. Commands/URLs mirror the readiness authHints (Phase 1):
129
+ * codex→`codex login`/platform.openai.com, gemini→AI-Studio free tier, aider→its
130
+ * LiteLLM model provider (free Gemini path), opencode→`opencode auth`. */
131
+ const VENDOR_FIX = {
132
+ 'claude-code': {
133
+ label: 'Claude',
134
+ auth: 'codehere runs Claude through your *subscription* (your `claude` login) — it ignores `ANTHROPIC_API_KEY` on purpose. Open a terminal, run `claude`, complete `/login`, then rerun.',
135
+ quota: "If you authenticate with an API key, top up at console.anthropic.com; if you're on a Claude subscription, check your plan's usage limits — then rerun.",
136
+ },
137
+ codex: {
138
+ label: 'Codex',
139
+ auth: 'Run `codex login` with a ChatGPT Plus/Pro account (flat, no per-token charge), or set `OPENAI_API_KEY` — then rerun.',
140
+ quota: 'OpenAI quota exhausted — top up at platform.openai.com, or switch to a free agent (gemini / opencode).',
141
+ },
142
+ gemini: {
143
+ label: 'Gemini',
144
+ auth: 'Set `GEMINI_API_KEY` from the free tier (https://aistudio.google.com/apikey), then rerun.',
145
+ quota: "Gemini's free-tier limit was hit — wait for it to reset, raise your AI Studio limits, or switch agents.",
146
+ },
147
+ aider: {
148
+ label: 'Aider',
149
+ // Provider-neutral on purpose: codehere can't know WHICH provider aider was
150
+ // pointed at, so name the credential for each path rather than leading with one.
151
+ auth: 'Aider needs a model + a matching provider key. Set the key for whatever provider you point it at — a billed `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` (with credit), or a free `GEMINI_API_KEY` run as `--model gemini/gemini-2.5-flash` — then rerun.',
152
+ quota: 'The provider key aider is using has no credit — top up that provider, or switch aider to a free one (e.g. a Gemini key with `--model gemini/…`, or a local Ollama model).',
153
+ },
154
+ opencode: {
155
+ label: 'OpenCode',
156
+ auth: 'Run `opencode auth` once to add a provider (a local Ollama model or a free endpoint = $0), then rerun.',
157
+ quota: 'The provider OpenCode is using hit its limit — run `opencode auth` to switch providers, or point at a local Ollama model.',
158
+ },
159
+ };
160
+ const GENERIC_FIX = {
161
+ label: 'The agent',
162
+ auth: "Sign in to the agent's provider — run `codehere health` to see which agent and how — then rerun.",
163
+ quota: "Top up or switch the agent's provider; `codehere health` shows the per-agent status. codehere is vendor-neutral.",
164
+ };
165
+ /** Read the captured error/output text and name the SPECIFIC failure + the fix
166
+ * FOR THIS AGENT, instead of a generic "network hiccup" or a claude-only guess.
167
+ * Uses input.errorText (codehere:error events) + steps' errorText/output. The
168
+ * detection patterns are vendor-neutral (each adapter forwards the provider's
169
+ * native auth/quota/rate words); the RECOVERY text is selected per input.agent.
170
+ * Returns null when the text doesn't clearly indicate a known failure — honest:
171
+ * we don't fabricate a specific cause we can't see. */
172
+ function diagnoseFailure(input) {
173
+ const blob = (input.errorText ? [input.errorText] : [])
174
+ .concat((input.steps || []).map((s) => `${s.errorText || ''} ${s.output || ''} ${s.kind === 'say' ? s.label : ''}`))
175
+ .join(' ')
176
+ .toLowerCase();
177
+ if (!blob.trim())
178
+ return null;
179
+ // Prefer the direct agent's fix; for a conductor task, fall back to the
180
+ // specialist it delegated to (so a conductor→codex failure names codex's fix,
181
+ // not the generic one); only then the vendor-neutral generic.
182
+ const fix = VENDOR_FIX[input.agent] ?? (input.delegatedTo ? VENDOR_FIX[input.delegatedTo] : undefined) ?? GENERIC_FIX;
183
+ if (/not logged in|please run.{0,6}\/login|\/login|no valid session|unauthorized|invalid api key|api key not valid|401\b/.test(blob)) {
184
+ return `**${fix.label} isn't logged in.** ${fix.auth}`;
185
+ }
186
+ if (/credit balance is too low|insufficient.{0,12}credit|out of credit|quota|billing/.test(blob)) {
187
+ return `**Out of credits / quota.** ${fix.quota}`;
188
+ }
189
+ if (/rate.?limit|\b429\b|too many requests/.test(blob)) {
190
+ return `**Rate limited by the provider.** Wait a minute and rerun, or switch agents — codehere is vendor-neutral.`;
191
+ }
192
+ return null;
193
+ }
81
194
  function buildErrorRecoveryReport(input, durationMs) {
82
195
  const agentName = input.agent === 'conductor' ? 'the agent codehere chose' : input.agent;
83
- return {
84
- summary: `The ${agentName} session ended without completing the task. ` +
85
- `This usually means a network hiccup, an expired login, or a rate limit on the provider.\n\n` +
86
- `What to try: check your internet, run \`codehere health\` to confirm reachability + auth, ` +
87
- `or wait a minute and rerun the same prompt. If it keeps happening, try a different agent ` +
88
- `from the dropdown — codehere is vendor-neutral on purpose.\n\n` +
89
- `codehere couldn't confirm any file changes from this attempt.`,
90
- generatedAt: Date.now(),
91
- durationMs,
92
- source: 'error-recovery',
93
- };
196
+ const specific = diagnoseFailure(input);
197
+ // When we can't classify the failure but the agent TOLD us why, show its real
198
+ // words — not a generic three-way guess. A product whose pitch is "show the
199
+ // truth" should surface "You've hit your usage limit — try again Jul 14", not
200
+ // "this usually means a network hiccup, an expired login, or a rate limit".
201
+ const realReason = (input.errorText || '').trim();
202
+ // NEVER claim "no files changed" without checking. A failed run that still
203
+ // edited files must say so — claiming otherwise is a lie the dogfood caught
204
+ // (opencode edited the file, codehere reported "no files were changed").
205
+ const files = (input.artifacts?.files || []).filter(Boolean);
206
+ const filesTail = files.length
207
+ ? `Note: ${files.length} file${files.length === 1 ? '' : 's'} ${files.length === 1 ? 'was' : 'were'} already changed before it stopped (${files.slice(0, 5).join(', ')}${files.length > 5 ? ', …' : ''}) — review before retrying.`
208
+ : `codehere couldn't confirm any file changes from this attempt.`;
209
+ const summary = specific
210
+ ? `The ${agentName} session couldn't complete.\n\n${specific}\n\n${filesTail}`
211
+ : realReason
212
+ ? `The ${agentName} session couldn't complete. It reported:\n\n${realReason}\n\n${filesTail}`
213
+ : `The ${agentName} session ended without completing the task. ` +
214
+ `This usually means a network hiccup, an expired login, or a rate limit on the provider.\n\n` +
215
+ `What to try: check your internet, run \`codehere health\` to confirm reachability + auth, ` +
216
+ `or wait a minute and rerun the same prompt. If it keeps happening, try a different agent ` +
217
+ `from the dropdown — codehere is vendor-neutral on purpose.\n\n${filesTail}`;
218
+ return { summary, generatedAt: Date.now(), durationMs, source: 'error-recovery' };
94
219
  }
95
220
  /**
96
221
  * Generate a Final Report. Reuses the agent's own summary; falls back to a
@@ -16,7 +16,7 @@
16
16
  * anti-hallucination rule as generate-final-report (ground every claim
17
17
  * in the structured data).
18
18
  *
19
- * Provider strategy: same as generate-final-report (try claude-free
19
+ * Provider strategy: env override, else claude (the dead claude-free
20
20
  * first, fall back to claude). Returns null if neither is installed.
21
21
  */
22
22
  import { spawn } from 'child_process';
@@ -32,11 +32,13 @@ function buildPrompt(input) {
32
32
  return renderTemplate(template, input);
33
33
  }
34
34
  function selectProviderBinary() {
35
- // Same precedence as generate-final-report: env override → claude-free → claude.
35
+ // MED #27 (atomic audit) · the old chain led with 'claude-free' (the dead
36
+ // FCC/NIM proxy final-report.ts abandoned) — every narration burned its
37
+ // timeout against a down proxy first. Env override → claude, that's it.
36
38
  const override = process.env.CODEHERE_NARRATOR_BINARY?.trim();
37
39
  if (override)
38
40
  return [override];
39
- return ['claude-free', 'claude'];
41
+ return ['claude'];
40
42
  }
41
43
  export async function generateNarration(input) {
42
44
  if (isMockMode()) {
@@ -7,7 +7,7 @@
7
7
  * tab + future thread switcher UI.
8
8
  *
9
9
  * Companion to generate-final-report.ts:
10
- * - Same provider chain pattern (claude-free → claude → graceful skip)
10
+ * - Provider: env override → claude → graceful skip (dead claude-free hop removed)
11
11
  * - Same single-shot, no streaming, returns string
12
12
  * - Different prompt (terse title generation, NOT 3-paragraph narrative)
13
13
  * - Cheaper budget (~50 tokens per title vs 200 per Final Report paragraph)
@@ -43,10 +43,13 @@ Rules:
43
43
 
44
44
  Title:`;
45
45
  function selectProviderBinary() {
46
+ // MED #28 (atomic audit) · same dead 'claude-free' first hop as narration —
47
+ // removed so a title either comes from claude or degrades fast, not after
48
+ // a proxy timeout.
46
49
  const override = process.env.CODEHERE_TITLE_BINARY;
47
50
  if (override)
48
51
  return [override];
49
- return ['claude-free', 'claude'];
52
+ return ['claude'];
50
53
  }
51
54
  function tryProvider(binary, prompt) {
52
55
  return new Promise((resolve) => {
@@ -79,7 +79,7 @@ Write the paragraph now. Do not preface with "Update:" or anything similar.
79
79
  Plain prose. No "I'm currently…" — narrate the AGENT, not yourself.
80
80
  `;
81
81
  function templateFile() {
82
- return join(homedir(), '.codehere', 'narrator-prompt.md');
82
+ return join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'narrator-prompt.md');
83
83
  }
84
84
  /** Read the user-customized template if present and valid. Falls back to
85
85
  * baked-in default on missing file, parse failure, or missing required
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Tier 2.1 · slice 2b — turn the eligibility decision into a concrete,
3
+ * loop-safe plan for the auto cross-vendor review spawn. Still pure: it decides
4
+ * WHAT to spawn (vendor, prompt, cwd, tag), not how — the live spawn + the
5
+ * real-spend verification are the next slice. Kept pure so the loop guard and
6
+ * the refute framing are unit-testable without a daemon or spend.
7
+ *
8
+ * THE LOOP GUARD is the load-bearing safety here: an auto-review is itself a
9
+ * task that completes and flows through the same completion handler. Without a
10
+ * guard it would trigger its own auto-review, recursing + burning spend forever.
11
+ * A reviewer session is tagged; planAutoReview returns null for tagged sessions.
12
+ *
13
+ * The reviewer runs in REFUTE mode and READ-ONLY: codehere proves the PROCESS
14
+ * (a different vendor independently reviewed + recorded findings), it never
15
+ * adjudicates code correctness (assess-cross-vendor honesty note). The prompt
16
+ * asks the reviewer to try to disprove "done", not to fix anything.
17
+ */
18
+ import { shouldAutoReview } from './should-auto-review.js';
19
+ /** Tag stamped on an auto-review reviewer session so it never triggers its own
20
+ * auto-review (the loop guard) and so surfaces can label it. */
21
+ export const AUTO_REVIEW_TAG = 'auto-cross-vendor-review';
22
+ /** The refute-mode brief: read-only, adversarial, names the writer's files. */
23
+ function buildRefutePrompt(writerAgent, files) {
24
+ const fileList = files.length <= 10 ? files.join(', ') : `${files.slice(0, 10).join(', ')} (+${files.length - 10} more)`;
25
+ return (`A different agent (${writerAgent}) just changed these files in this repo: ${fileList}. ` +
26
+ `Independently review that work READ-ONLY — do not edit anything. ` +
27
+ `Try to disprove that the task is correctly done: look for bugs, missed edge cases, ` +
28
+ `or claims the diff doesn't actually support. Report what you find (or that it holds up). ` +
29
+ `You are the second vendor in a cross-vendor check; your job is to refute, not to fix.`);
30
+ }
31
+ /**
32
+ * Build the auto-review spawn plan, or null if it shouldn't / can't run.
33
+ * Returns null (with no side effects) for: the loop guard, or any failed
34
+ * eligibility gate. Pure.
35
+ */
36
+ export function planAutoReview(facts, thresholds) {
37
+ // Loop guard FIRST — a reviewer's own completion must never spawn a review.
38
+ if (facts.isAutoReviewSession)
39
+ return null;
40
+ const inputs = {
41
+ writerAgent: facts.writerAgent,
42
+ installedAgents: facts.installedAgents,
43
+ filesChanged: facts.writerFiles.length,
44
+ budgetRemainingUsd: facts.budgetRemainingUsd,
45
+ };
46
+ const decision = shouldAutoReview(inputs, thresholds);
47
+ if (!decision.eligible || !decision.reviewerVendor)
48
+ return null;
49
+ return {
50
+ reviewerVendor: decision.reviewerVendor,
51
+ prompt: buildRefutePrompt(facts.writerAgent, facts.writerFiles),
52
+ cwd: facts.cwd,
53
+ tag: AUTO_REVIEW_TAG,
54
+ reason: decision.reason,
55
+ };
56
+ }
57
+ //# sourceMappingURL=plan-auto-review.js.map
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Tier 1.2 (verdict-trust punch list, 2026-06-09) · reconcile the AUDIT against
3
+ * the real on-disk effect.
4
+ *
5
+ * The gap this closes: an audit entry records the agent's DECLARED tool call
6
+ * (PreToolUse intent for claude, a post-hoc mirror for the others) — not an
7
+ * independent observation that the write actually landed. An agent (or adapter)
8
+ * that emits a file-write tool_use which never really changed anything lands that
9
+ * fiction in the integrity-checked chain as a first-class entry. The chain then
10
+ * faithfully shows the CLAIM was never casually edited — it never checks the claim
11
+ * against reality.
12
+ *
13
+ * This reconciles the audit's file-write tool calls against the SAME git-diff
14
+ * ground truth verify-claim already uses for file claims. It does NOT mutate the
15
+ * append-only, hash-chained audit (that would break tamper-evidence) — it's a
16
+ * read-only layer that produces an honest verdict the surface can show:
17
+ * - consistent · writes recorded AND files changed on disk
18
+ * - unconfirmed · writes recorded but codehere couldn't tie them to a tracked
19
+ * git change. Honest, NOT an accusation: the writes may have
20
+ * targeted gitignored / scratch / .codehere paths (which the
21
+ * git-diff ground truth excludes), been reverted within the
22
+ * run, or — the case worth a look — never really happened.
23
+ * "couldn't confirm", never "it's fake" (refuter 2026-06-09:
24
+ * a false fabrication accusation on honest work is the worst
25
+ * error class on a proof surface).
26
+ * - no-writes · nothing claimed, nothing to reconcile
27
+ * - unobservable · not a git repo, so codehere can't confirm
28
+ *
29
+ * Count-based by design: toolInput is truncated/heterogeneous across adapters, so
30
+ * per-file path matching is unreliable; the file-write COUNT vs the git-change
31
+ * count is robust. Bash/shell tools can also write files but codehere can't tell,
32
+ * so they're deliberately NOT counted as declared writes (conservative — never
33
+ * fabricates a "declared-not-observed" the user can't act on).
34
+ */
35
+ /** Tool names that explicitly mutate a file on disk, across every adapter.
36
+ * Derived from the real audit's toolName distribution (Write/Edit/write_file/
37
+ * edit_file observed) plus the standard variants. Shell tools are excluded — a
38
+ * bash `echo > f` can't be distinguished from a read, so counting it would
39
+ * manufacture false "declared-not-observed" verdicts. */
40
+ // Matched case-INSENSITIVELY (adapters emit varied casings: claude 'Edit',
41
+ // codex 'Delete', gemini raw tool_name like 'write_file'/'replace'). A DELETE is
42
+ // a real file mutation that shows in git — omitting it made a codex delete-only
43
+ // run falsely read "no writes recorded" (self-audit 2026-06-09). Keep this broad;
44
+ // over-counting a non-write is far less harmful than a false "nothing happened".
45
+ const WRITE_TOOLS = new Set(
46
+ // SPECIFIC file-mutation tool names only. Bare generic verbs ('replace',
47
+ // 'patch') are deliberately EXCLUDED — a non-file tool named 'replace' (an
48
+ // in-memory string replace) would false-count and produce a false 'consistent'
49
+ // (worse than under-counting, which is an honest 'unconfirmed'). A real write
50
+ // the set misses → 'unconfirmed' (honest), never a fabricated-write laundered
51
+ // into green (self-audit round 2, 2026-06-09).
52
+ [
53
+ 'Write',
54
+ 'Edit',
55
+ 'MultiEdit',
56
+ 'NotebookEdit',
57
+ 'Delete',
58
+ 'write_file',
59
+ 'edit_file',
60
+ 'create_file',
61
+ 'delete_file',
62
+ 'str_replace',
63
+ 'str_replace_editor',
64
+ 'apply_patch',
65
+ ].map((t) => t.toLowerCase()));
66
+ export function reconcileAudit(entries, artifacts) {
67
+ const writeToolCalls = entries.filter((e) => WRITE_TOOLS.has((e.toolName || '').toLowerCase())).length;
68
+ const filesObserved = artifacts?.files?.length ?? 0;
69
+ if (artifacts?.noGitTracking) {
70
+ return {
71
+ writeToolCalls,
72
+ filesObserved: 0,
73
+ verdict: 'unobservable',
74
+ note: `${writeToolCalls} file-write tool call${writeToolCalls === 1 ? '' : 's'} recorded, but this folder isn't a git repo — codehere can't confirm them against disk.`,
75
+ };
76
+ }
77
+ if (writeToolCalls === 0) {
78
+ return {
79
+ writeToolCalls,
80
+ filesObserved,
81
+ verdict: 'no-writes',
82
+ note: 'No file-write tool calls recorded — nothing to reconcile against disk.',
83
+ };
84
+ }
85
+ if (filesObserved === 0) {
86
+ return {
87
+ writeToolCalls,
88
+ filesObserved,
89
+ verdict: 'unconfirmed',
90
+ note: `${writeToolCalls} file-write tool call${writeToolCalls === 1 ? '' : 's'} recorded, but codehere couldn't confirm them against tracked git changes — they may have targeted gitignored/scratch paths or been reverted. Worth a look, not proof of a problem.`,
91
+ };
92
+ }
93
+ return {
94
+ writeToolCalls,
95
+ filesObserved,
96
+ verdict: 'consistent',
97
+ note: `${writeToolCalls} file-write tool call${writeToolCalls === 1 ? '' : 's'} recorded · ${filesObserved} file${filesObserved === 1 ? '' : 's'} confirmed changed on disk.`,
98
+ };
99
+ }
100
+ //# sourceMappingURL=reconcile-audit.js.map