codehere 0.8.0 → 1.0.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 (119) hide show
  1. package/README.md +37 -184
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +329 -0
  5. package/dist/index.js +74 -1004
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
  7. package/dist/presentation/cli/commands/check-command.js +251 -0
  8. package/dist/presentation/cli/commands/help-command.js +22 -286
  9. package/dist/ui.js +19 -438
  10. package/dist/utils/version.js +0 -12
  11. package/package.json +18 -27
  12. package/dist/application/use-cases/domain-trust.js +0 -110
  13. package/dist/application/use-cases/generate-final-report.js +0 -134
  14. package/dist/application/use-cases/generate-narration.js +0 -128
  15. package/dist/application/use-cases/generate-thread-title.js +0 -178
  16. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  17. package/dist/application/use-cases/thread-export.js +0 -155
  18. package/dist/application/use-cases/verify-claim.js +0 -274
  19. package/dist/application/use-cases/write-pass.js +0 -277
  20. package/dist/domain/entities/task.js +0 -61
  21. package/dist/domain/services/codebase-detector.js +0 -91
  22. package/dist/error-handler.js +0 -251
  23. package/dist/infrastructure/completion/bash-completion.js +0 -25
  24. package/dist/infrastructure/completion/fish-completion.js +0 -22
  25. package/dist/infrastructure/completion/zsh-completion.js +0 -30
  26. package/dist/infrastructure/config/config-manager.js +0 -173
  27. package/dist/infrastructure/config/product-context.js +0 -67
  28. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  29. package/dist/infrastructure/context/adapters/figma-source.js +0 -163
  30. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
  31. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  32. package/dist/infrastructure/context/context-resolver.js +0 -144
  33. package/dist/infrastructure/context/context-source.js +0 -114
  34. package/dist/infrastructure/context/oauth-flow.js +0 -207
  35. package/dist/infrastructure/context/oauth-store.js +0 -119
  36. package/dist/infrastructure/context/registry-bootstrap.js +0 -99
  37. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  38. package/dist/infrastructure/cost/budget-config.js +0 -113
  39. package/dist/infrastructure/cost/cost-router.js +0 -104
  40. package/dist/infrastructure/cost/spend-aggregator.js +0 -147
  41. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
  42. package/dist/infrastructure/hooks/hook-mode.js +0 -71
  43. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
  44. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  45. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  46. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  47. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  48. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  49. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
  50. package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
  51. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  52. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
  53. package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
  54. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
  55. package/dist/infrastructure/orchestration/context-handoff.js +0 -144
  56. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  57. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  58. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  59. package/dist/infrastructure/orchestration/failover-config.js +0 -77
  60. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  61. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
  62. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
  63. package/dist/infrastructure/orchestration/graphify-context.js +0 -254
  64. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  65. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
  66. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  67. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  68. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  69. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
  70. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
  71. package/dist/infrastructure/orchestration/readiness.js +0 -103
  72. package/dist/infrastructure/orchestration/session-manager.js +0 -1091
  73. package/dist/infrastructure/orchestration/session-persistence.js +0 -258
  74. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  75. package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
  76. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  77. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  78. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
  79. package/dist/infrastructure/protocols/probe-port.js +0 -36
  80. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  81. package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
  82. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  83. package/dist/infrastructure/protocols/verify-page.js +0 -983
  84. package/dist/infrastructure/security/file-collection.js +0 -118
  85. package/dist/infrastructure/security/input-validator.js +0 -152
  86. package/dist/infrastructure/security/permissions-config.js +0 -234
  87. package/dist/infrastructure/security/trust-scorer.js +0 -657
  88. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  89. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  90. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  91. package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
  92. package/dist/infrastructure/storage/project-thread-reader.js +0 -172
  93. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  94. package/dist/infrastructure/storage/task-repository.js +0 -206
  95. package/dist/monitoring.js +0 -212
  96. package/dist/presentation/cli/commands/audit-command.js +0 -126
  97. package/dist/presentation/cli/commands/budget-command.js +0 -79
  98. package/dist/presentation/cli/commands/config-command.js +0 -125
  99. package/dist/presentation/cli/commands/context-source-command.js +0 -198
  100. package/dist/presentation/cli/commands/hook-command.js +0 -265
  101. package/dist/presentation/cli/commands/init-command.js +0 -578
  102. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  103. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  104. package/dist/presentation/cli/commands/permission-command.js +0 -116
  105. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  106. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  107. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  108. package/dist/presentation/cli/commands/report-command.js +0 -387
  109. package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
  110. package/dist/presentation/cli/commands/setup-command.js +0 -307
  111. package/dist/presentation/cli/commands/status-command.js +0 -661
  112. package/dist/presentation/cli/commands/thread-command.js +0 -288
  113. package/dist/presentation/cli/commands/trust-command.js +0 -306
  114. package/dist/presentation/cli/commands/verify-command.js +0 -233
  115. package/dist/presentation/cli/error-display.js +0 -336
  116. package/dist/presentation/cli/repl.js +0 -797
  117. package/dist/presentation/cli/task-deliverable.js +0 -127
  118. package/dist/testing/cassette.js +0 -125
  119. package/dist/testing/replay-adapter.js +0 -166
@@ -1,110 +0,0 @@
1
- /**
2
- * Per-domain trust grade for W-PASS write targets · v0.9 must-ship.
3
- *
4
- * Demo v12 + locked v1 tier scope: "linear.app A · *.atlassian.net B ·
5
- * unknown F · first-write confirmation per domain."
6
- *
7
- * Two pieces of state:
8
- * 1. WELL_KNOWN_GRADES · hardcoded · the platforms we've vetted ourselves
9
- * ship with grade A. Subdomain wildcards get B. Everything else
10
- * defaults to F (forces explicit user confirmation on first write).
11
- * 2. USER_TRUSTED_DOMAINS · ~/.codehere/keys/trusted-domains.json ·
12
- * domains the user has explicitly trusted after a first-write prompt.
13
- * Domains here promote from F → B.
14
- *
15
- * Pure logic. CLI + HTTP wrap it.
16
- */
17
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
18
- import { homedir } from 'os';
19
- import { dirname, join } from 'path';
20
- const WELL_KNOWN_GRADES = [
21
- // Grade A · platforms codehere has shipped+tested adapters for
22
- { pattern: /^api\.linear\.app$/i, grade: 'A', reason: 'official Linear API · codehere ships a vetted adapter' },
23
- { pattern: /^api\.github\.com$/i, grade: 'A', reason: 'official GitHub API · codehere ships a vetted adapter' },
24
- { pattern: /^api\.figma\.com$/i, grade: 'A', reason: 'official Figma API · codehere ships a vetted adapter' },
25
- // Grade B · same vendor, surrounding subdomains
26
- { pattern: /^[a-z0-9-]+\.linear\.app$/i, grade: 'B', reason: 'Linear subdomain · adjacent to the vetted API' },
27
- { pattern: /^[a-z0-9-]+\.github\.com$/i, grade: 'B', reason: 'GitHub subdomain · adjacent to the vetted API' },
28
- { pattern: /^[a-z0-9-]+\.figma\.com$/i, grade: 'B', reason: 'Figma subdomain · adjacent to the vetted API' },
29
- {
30
- pattern: /^[a-z0-9-]+\.atlassian\.net$/i,
31
- grade: 'B',
32
- reason: 'Atlassian-hosted instance · vendor known but instance not vetted',
33
- },
34
- // Grade C · public hosts known to be commonly used
35
- { pattern: /^raw\.githubusercontent\.com$/i, grade: 'C', reason: 'GitHub-hosted raw content · static, read-mostly' },
36
- ];
37
- function trustedDomainsPath() {
38
- const home = process.env.CODEHERE_HOME || homedir();
39
- return join(home, '.codehere', 'keys', 'trusted-domains.json');
40
- }
41
- export function loadTrustedDomains() {
42
- const path = trustedDomainsPath();
43
- if (!existsSync(path))
44
- return { schema: 1, domains: {} };
45
- try {
46
- const raw = JSON.parse(readFileSync(path, 'utf-8'));
47
- if (raw && raw.schema === 1 && typeof raw.domains === 'object')
48
- return raw;
49
- }
50
- catch {
51
- /* fall through · empty trust store */
52
- }
53
- return { schema: 1, domains: {} };
54
- }
55
- export function trustDomain(host, note) {
56
- const file = loadTrustedDomains();
57
- file.domains[host.toLowerCase()] = { addedAt: Date.now(), note };
58
- mkdirSync(dirname(trustedDomainsPath()), { recursive: true });
59
- writeFileSync(trustedDomainsPath(), JSON.stringify(file, null, 2) + '\n', 'utf-8');
60
- }
61
- export function untrustDomain(host) {
62
- const file = loadTrustedDomains();
63
- const key = host.toLowerCase();
64
- if (!file.domains[key])
65
- return false;
66
- delete file.domains[key];
67
- writeFileSync(trustedDomainsPath(), JSON.stringify(file, null, 2) + '\n', 'utf-8');
68
- return true;
69
- }
70
- export function gradeDomain(host) {
71
- const userFile = loadTrustedDomains();
72
- const userTrusted = !!userFile.domains[host.toLowerCase()];
73
- for (const rule of WELL_KNOWN_GRADES) {
74
- if (rule.pattern.test(host)) {
75
- return {
76
- host,
77
- grade: rule.grade,
78
- reason: rule.reason,
79
- userTrusted,
80
- requiresFirstWriteConfirmation: false,
81
- };
82
- }
83
- }
84
- if (userTrusted) {
85
- return {
86
- host,
87
- grade: 'B',
88
- reason: 'previously trusted by you · stored in trusted-domains.json',
89
- userTrusted: true,
90
- requiresFirstWriteConfirmation: false,
91
- };
92
- }
93
- return {
94
- host,
95
- grade: 'F',
96
- reason: 'unknown domain · not in well-known list · not previously trusted',
97
- userTrusted: false,
98
- requiresFirstWriteConfirmation: true,
99
- };
100
- }
101
- /** Extract the host from a fully-resolved HTTPS URL. */
102
- export function hostOf(url) {
103
- try {
104
- return new URL(url).host;
105
- }
106
- catch {
107
- return '';
108
- }
109
- }
110
- //# sourceMappingURL=domain-trust.js.map
@@ -1,134 +0,0 @@
1
- /**
2
- * Final Report — the headline deliverable. At task exit, produce a plain-English
3
- * summary of what the agent did.
4
- *
5
- * Engine (locked 2026-06-03, after the real dogfood found NO report on a real
6
- * conductor run): REUSE THE AGENT SESSION. The agent already narrates what it
7
- * did (kind:'say' steps — e.g. "Added subtract(a,b) to utils.js:5"); we use that
8
- * directly. No fresh summarizer spawn.
9
- *
10
- * Why the old path was replaced: it spawned a SEPARATE provider binary chain
11
- * (`claude-free` → `claude`) to summarize. `claude-free` (the FCC/NIM proxy
12
- * binary) is on PATH but its proxy was down → 30s hang → null; the `claude`
13
- * fallback also returned null in the conductor exit-handler. Net: a completed
14
- * real task showed "⚠ Final Report unavailable" — the headline deliverable
15
- * missing. Reusing the agent's own words is cheaper, has full context, and can't
16
- * be taken down by an external proxy.
17
- *
18
- * Fallback (locked): when the agent left no usable summary (or it looks like a
19
- * provider failure), build a DETERMINISTIC report from the recorded steps +
20
- * artifacts. It states ONLY verified facts, says plainly that no written summary
21
- * was available, and NEVER implies success — so a completed task always shows a
22
- * deliverable, and it's honest about which kind it is (see `source`).
23
- */
24
- const MAX_SUMMARY_CHARS = 1600;
25
- /**
26
- * UAT-003 — patterns indicating the agent's own text is actually a network /
27
- * auth / quota failure, not a real summary. Such "summaries" must NOT be shown
28
- * as the report; they fall through to the deterministic fallback.
29
- */
30
- const NETWORK_FAILURE_PATTERNS = /(?:all connection attempts failed|ECONNREFUSED|ENOTFOUND|fetch failed|network error|request id:\s*req_|connection timed out|getaddrinfo)/i;
31
- function isLikelyNetworkFailureSummary(text) {
32
- return NETWORK_FAILURE_PATTERNS.test(text);
33
- }
34
- /**
35
- * ENGINE: the agent's own narration. kind:'say' steps are the words the agent
36
- * already emitted (reasoning + wrap-up). Joined newest-context-last, they ARE
37
- * the summary — no extra spawn. Returns null if the agent said nothing usable.
38
- */
39
- function extractAgentSummary(steps) {
40
- const says = steps
41
- .filter((s) => s.kind === 'say' && typeof s.label === 'string' && s.label.trim().length > 0)
42
- .map((s) => s.label.trim());
43
- if (!says.length)
44
- return null;
45
- const text = says.join('\n\n').slice(0, MAX_SUMMARY_CHARS);
46
- // Too short to be a real summary (e.g. a one-word ack) → let the fallback give facts.
47
- return text.length >= 12 ? text : null;
48
- }
49
- /**
50
- * FALLBACK: deterministic, honest. Facts only, from steps + artifacts. Says
51
- * plainly that no written summary was available. Never implies success.
52
- */
53
- function buildDeterministicReport(input) {
54
- const files = input.artifacts?.files ?? [];
55
- const toolSteps = input.steps.filter((s) => s.kind !== 'say');
56
- const noGit = input.artifacts?.noGitTracking;
57
- const agentName = input.agent === 'conductor' ? 'the agent codehere chose' : input.agent;
58
- 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.`,
60
- `The session ran ${input.steps.length} step${input.steps.length === 1 ? '' : 's'}` +
61
- (toolSteps.length
62
- ? ` (${toolSteps.slice(0, 5).map((s) => s.label).join(', ')}${toolSteps.length > 5 ? ', …' : ''})`
63
- : '') +
64
- `.`,
65
- ];
66
- if (noGit) {
67
- parts.push(`This folder isn't a git repository, so codehere couldn't track which files changed — check the working tree yourself.`);
68
- }
69
- else if (files.length) {
70
- parts.push(`Files changed: ` +
71
- files
72
- .map((f) => `${f.path} (+${f.added}/-${f.removed}${f.trust !== undefined ? `, trust ${f.trust}` : ''})`)
73
- .join('; ') +
74
- `.`);
75
- }
76
- else {
77
- parts.push(`No files were changed.`);
78
- }
79
- return { summary: parts.join('\n\n'), generatedAt: Date.now(), durationMs: 0, source: 'deterministic' };
80
- }
81
- function buildErrorRecoveryReport(input, durationMs) {
82
- 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
- };
94
- }
95
- /**
96
- * Generate a Final Report. Reuses the agent's own summary; falls back to a
97
- * deterministic, honest facts report. A completed task ALWAYS gets a report
98
- * (never null for non-error terminal states) — no external dependency.
99
- */
100
- export async function generateFinalReport(input) {
101
- // No mock short-circuit: under CODEHERE_MOCK the SAME engine runs, so the
102
- // narration gap is reproduced faithfully — claude/aider (emit `say`) get a
103
- // rich agent-summary; codex/gemini/opencode (no `say`) get the honest
104
- // deterministic report, exactly as on a real run. (A prior mock-only
105
- // short-circuit synthesized a report from steps for every agent, hiding the
106
- // cross-vendor gap — you couldn't dogfood "codex's report is thin" under mock.)
107
- // Errored session — deterministic recovery hint, no summarization of nothing.
108
- if (input.status === 'error') {
109
- return buildErrorRecoveryReport(input, 0);
110
- }
111
- // ENGINE: reuse the agent's own narration (no fresh spawn).
112
- const agentSummary = extractAgentSummary(input.steps);
113
- if (agentSummary && !isLikelyNetworkFailureSummary(agentSummary)) {
114
- return { summary: agentSummary, generatedAt: Date.now(), durationMs: 0, source: 'agent-summary' };
115
- }
116
- // FALLBACK: deterministic, honest — always a deliverable, never a fake ✓.
117
- return buildDeterministicReport(input);
118
- }
119
- /**
120
- * Pretty-print to terminal. Used by session-manager when a spawned task exits.
121
- * Wrapped in try/catch upstream — never let a print failure surface.
122
- */
123
- export function printFinalReportToTerminal(input, output) {
124
- const banner = '─'.repeat(60);
125
- process.stdout.write('\n');
126
- process.stdout.write(`${banner}\n`);
127
- process.stdout.write(`Final Report · ${input.agent} · ${input.sessionId.slice(0, 8)}\n`);
128
- process.stdout.write(`${banner}\n`);
129
- process.stdout.write(`${output.summary}\n`);
130
- process.stdout.write(`${banner}\n`);
131
- process.stdout.write(`(generated in ${output.durationMs}ms)\n`);
132
- process.stdout.write('\n');
133
- }
134
- //# sourceMappingURL=generate-final-report.js.map
@@ -1,128 +0,0 @@
1
- /**
2
- * PM Narration — v0.6 M3 mid-task summary.
3
- *
4
- * While a task is in flight, periodically generates a one-paragraph
5
- * plain-English summary of what the agent has done so far. Fires on
6
- * step-count or elapsed-time thresholds (caller decides when).
7
- *
8
- * Per claim_user_persona HIGH: non-technical users primary. They don't
9
- * read tool calls; they read narration. Mid-task narration tells them
10
- * "the agent is currently editing your auth file and ran the tests"
11
- * instead of forcing them to interpret a list of Bash/Edit/Read steps.
12
- *
13
- * Per feedback_overconfidence_guard.md: narration is an LLM call.
14
- * Hallucination is possible. Surface MUST frame it as "narrator says…"
15
- * not authoritative truth. The PROMPT_TEMPLATE includes the same
16
- * anti-hallucination rule as generate-final-report (ground every claim
17
- * in the structured data).
18
- *
19
- * Provider strategy: same as generate-final-report (try claude-free
20
- * first, fall back to claude). Returns null if neither is installed.
21
- */
22
- import { spawn } from 'child_process';
23
- import { loadTemplate, renderTemplate } from './narrator-prompt-template.js';
24
- import { isMockMode } from '../../infrastructure/orchestration/mocks/resolve-agent-spawn.js';
25
- import { mockNarration } from '../../infrastructure/orchestration/mocks/mock-synthesis.js';
26
- /** v0.6 S6 — prompt template lifted into ~/.codehere/narrator-prompt.md so
27
- * the user can edit the agent's voice. loadTemplate() falls back to the
28
- * baked-in default on missing file or missing required placeholders, so
29
- * the prompt is never silently broken. See narrator-prompt-template.ts. */
30
- function buildPrompt(input) {
31
- const { template } = loadTemplate();
32
- return renderTemplate(template, input);
33
- }
34
- function selectProviderBinary() {
35
- // Same precedence as generate-final-report: env override → claude-free → claude.
36
- const override = process.env.CODEHERE_NARRATOR_BINARY?.trim();
37
- if (override)
38
- return [override];
39
- return ['claude-free', 'claude'];
40
- }
41
- export async function generateNarration(input) {
42
- if (isMockMode()) {
43
- const m = mockNarration({ steps: input.steps });
44
- return {
45
- summary: m.summary,
46
- generatedAt: Date.now(),
47
- durationMs: m.durationMs,
48
- stepCountAtSnapshot: input.steps.length,
49
- };
50
- }
51
- const prompt = buildPrompt(input);
52
- const candidates = selectProviderBinary();
53
- for (const binary of candidates) {
54
- const result = await tryProvider(binary, prompt, input.sessionId, input.steps.length);
55
- if (result)
56
- return result;
57
- }
58
- return null;
59
- }
60
- function tryProvider(binary, prompt, sessionId, stepCount) {
61
- return new Promise((resolve) => {
62
- const startedAt = Date.now();
63
- let stdout = '';
64
- let stderr = '';
65
- let resolved = false;
66
- const finish = (val) => {
67
- if (resolved)
68
- return;
69
- resolved = true;
70
- resolve(val);
71
- };
72
- let child;
73
- try {
74
- // Same shape as generate-final-report's tryProvider — claude --bare -p.
75
- child = spawn(binary, ['--bare', '-p', prompt], {
76
- stdio: ['ignore', 'pipe', 'pipe'],
77
- });
78
- }
79
- catch {
80
- finish(null);
81
- return;
82
- }
83
- // 20s timeout — narration should be SNAPPIER than Final Report
84
- // (which is 60s). Mid-task = the user is actively waiting.
85
- const timeout = setTimeout(() => {
86
- try {
87
- child.kill();
88
- }
89
- catch {
90
- /* ignore */
91
- }
92
- finish(null);
93
- }, 20_000);
94
- child.stdout?.on('data', (chunk) => {
95
- stdout += chunk.toString('utf-8');
96
- });
97
- child.stderr?.on('data', (chunk) => {
98
- stderr += chunk.toString('utf-8');
99
- });
100
- child.on('close', (code) => {
101
- clearTimeout(timeout);
102
- if (code !== 0) {
103
- // Non-zero usually means binary missing OR the model refused. Either
104
- // way silent fail; caller treats null as "no narration this cycle".
105
- void stderr;
106
- void sessionId;
107
- finish(null);
108
- return;
109
- }
110
- const summary = stdout.trim();
111
- if (!summary || summary.length < 10) {
112
- finish(null);
113
- return;
114
- }
115
- finish({
116
- summary,
117
- generatedAt: Date.now(),
118
- durationMs: Date.now() - startedAt,
119
- stepCountAtSnapshot: stepCount,
120
- });
121
- });
122
- child.on('error', () => {
123
- clearTimeout(timeout);
124
- finish(null);
125
- });
126
- });
127
- }
128
- //# sourceMappingURL=generate-narration.js.map
@@ -1,178 +0,0 @@
1
- /**
2
- * Hidden Title agent — v0.6.0 M9.
3
- *
4
- * Generates a 4-6 word title for a project thread from its first user prompt.
5
- * Hidden = users don't pick it from any picker; fires automatically when the
6
- * project thread gets its first task entry. The title surfaces in the browser
7
- * tab + future thread switcher UI.
8
- *
9
- * Companion to generate-final-report.ts:
10
- * - Same provider chain pattern (claude-free → claude → graceful skip)
11
- * - Same single-shot, no streaming, returns string
12
- * - Different prompt (terse title generation, NOT 3-paragraph narrative)
13
- * - Cheaper budget (~50 tokens per title vs 200 per Final Report paragraph)
14
- *
15
- * Triggering: project-thread-mirror invokes this when seeding the project-meta
16
- * header for a NEW project (i.e., this cwd's thread.jsonl didn't exist before).
17
- * Idempotent: subsequent task entries do NOT regenerate the title.
18
- *
19
- * Cost: ~1 LLM call per NEW project (not per task). At a typical 1-2 new
20
- * projects per week per user, the cost is negligible. FCC-routed when
21
- * available; degrades silently to "(untitled)" when no provider responds.
22
- */
23
- import { spawn } from 'child_process';
24
- import { isMockMode } from '../../infrastructure/orchestration/mocks/resolve-agent-spawn.js';
25
- import { mockTitle } from '../../infrastructure/orchestration/mocks/mock-synthesis.js';
26
- const TITLE_TIMEOUT_MS = 15_000; // titles are short; abort fast if stuck
27
- const PROMPT_TEMPLATE = (firstPrompt, cwd) => `Generate a 4-6 word project title from this first user prompt.
28
-
29
- Project working directory: ${cwd}
30
- First prompt: ${firstPrompt}
31
-
32
- Rules:
33
- - 4 to 6 words MAX
34
- - No quotes, no punctuation, no trailing period
35
- - Title-case (e.g. "Auth Bug Fix Sprint")
36
- - Ground the title in the prompt + cwd ONLY. Do NOT invent project subject
37
- matter you can't derive from the data above (e.g. don't infer "Thread
38
- Interface Updates" from a "list files" prompt — say what the user
39
- actually asked, not what you imagine the project is about).
40
- - If the prompt is too generic to title meaningfully, prefer naming the
41
- cwd's last path segment (e.g. "Taste V0.6 Workspace") over invention.
42
- - Output ONLY the title — no preamble, no explanation
43
-
44
- Title:`;
45
- function selectProviderBinary() {
46
- const override = process.env.CODEHERE_TITLE_BINARY;
47
- if (override)
48
- return [override];
49
- return ['claude-free', 'claude'];
50
- }
51
- function tryProvider(binary, prompt) {
52
- return new Promise((resolve) => {
53
- let stdout = '';
54
- let settled = false;
55
- const finish = (result) => {
56
- if (settled)
57
- return;
58
- settled = true;
59
- resolve(result);
60
- };
61
- const child = spawn(binary, ['--bare', '-p', prompt], {
62
- stdio: ['ignore', 'pipe', 'pipe'],
63
- });
64
- const timeout = setTimeout(() => {
65
- child.kill('SIGTERM');
66
- if (process.env.CODEHERE_DEBUG === '1') {
67
- console.error(`[title-agent] ${binary} timeout after ${TITLE_TIMEOUT_MS}ms`);
68
- }
69
- finish(null);
70
- }, TITLE_TIMEOUT_MS);
71
- child.on('error', () => {
72
- clearTimeout(timeout);
73
- finish(null); // ENOENT etc. — silent fallback to next provider
74
- });
75
- child.stdout?.on('data', (chunk) => {
76
- stdout += chunk.toString('utf-8');
77
- });
78
- child.on('exit', (code) => {
79
- clearTimeout(timeout);
80
- if (code !== 0) {
81
- finish(null);
82
- return;
83
- }
84
- const title = sanitizeTitle(stdout);
85
- if (!title) {
86
- finish(null);
87
- return;
88
- }
89
- finish(title);
90
- });
91
- });
92
- }
93
- /**
94
- * Strip whitespace / quotes / trailing punctuation. Cap at 80 chars.
95
- *
96
- * Garbage rejection (added 2026-05-06 after dogfood Bug A4): the underlying
97
- * `claude --bare` call can return error-shaped text on stdout with exit 0
98
- * (rate-limit messages, auth errors, refusals). Pre-fix those flowed straight
99
- * into the project sidebar as titles like "Request ID: req_xyz...". We now
100
- * detect obvious error / refusal patterns and return '' so the caller falls
101
- * back to the cwd-derived title.
102
- */
103
- export function sanitizeTitle(raw) {
104
- const lines = raw
105
- .split('\n')
106
- .map((l) => l.trim())
107
- .filter(Boolean);
108
- if (lines.length === 0)
109
- return '';
110
- let title = lines[lines.length - 1]; // last non-empty line is the answer
111
- // Strip surrounding quotes
112
- title = title.replace(/^["'`]+|["'`]+$/g, '');
113
- // Strip trailing punctuation
114
- title = title.replace(/[.!?,;:]+$/, '');
115
- // Cap at 80 chars
116
- if (title.length > 80)
117
- title = title.slice(0, 77) + '...';
118
- title = title.trim();
119
- if (!title)
120
- return '';
121
- // Garbage rejection — patterns that indicate an error response, NOT a title.
122
- // Each pattern matches a real failure mode observed in dogfood:
123
- // - "Request ID: req_..." → API error envelope leaking through
124
- // - "Error:" / "ERROR" / "error " → generic error prefix
125
- // - "I cannot" / "I can't" → model refusal
126
- // - "I apologize" / "I'm sorry" → model refusal
127
- // - "Rate limit" / "rate-limited" → quota error
128
- // - "Authentication" / "Unauthorized" / "401" / "403" → auth failure
129
- // - Long sentences (>10 words) → not a title; model ignored the rules
130
- // - Contains http:// or https:// → URL leakage from error envelope
131
- const lower = title.toLowerCase();
132
- const garbageMarkers = [
133
- /^request\s+id\s*[:=]/i,
134
- /^error\b/i,
135
- /\berror[:\s]/i,
136
- /^i\s+(cannot|can'?t|am\s+unable|won'?t)\b/i,
137
- /^i\s+(apologi[sz]e|am\s+sorry)\b/i,
138
- /^i'?m\s+sorry\b/i,
139
- /\brate\s*[-_]?\s*limit/i,
140
- /\b(authentication|unauthori[sz]ed|forbidden)\b/i,
141
- /\b(401|403|429|500|502|503)\b/i,
142
- /https?:\/\//i,
143
- /api[_-]?key/i,
144
- ];
145
- if (garbageMarkers.some((re) => re.test(lower))) {
146
- if (process.env.CODEHERE_DEBUG === '1') {
147
- console.error(`[title-agent] rejected garbage title: ${title.slice(0, 60)}`);
148
- }
149
- return '';
150
- }
151
- // Length sanity: a real title is 4-6 words per the prompt; allow up to 10
152
- // for slack. Anything longer is the model ignoring instructions.
153
- const wordCount = title.split(/\s+/).filter(Boolean).length;
154
- if (wordCount > 10) {
155
- if (process.env.CODEHERE_DEBUG === '1') {
156
- console.error(`[title-agent] rejected long title (${wordCount} words): ${title.slice(0, 60)}`);
157
- }
158
- return '';
159
- }
160
- return title;
161
- }
162
- /**
163
- * Generate a thread title from the first user prompt + cwd context.
164
- * Returns the title string, or null if no provider is reachable.
165
- * Never throws — failure modes return null.
166
- */
167
- export async function generateThreadTitle(firstPrompt, cwd) {
168
- if (isMockMode())
169
- return sanitizeTitle(mockTitle(firstPrompt, cwd)) || null;
170
- const prompt = PROMPT_TEMPLATE(firstPrompt, cwd);
171
- for (const binary of selectProviderBinary()) {
172
- const result = await tryProvider(binary, prompt);
173
- if (result)
174
- return result;
175
- }
176
- return null;
177
- }
178
- //# sourceMappingURL=generate-thread-title.js.map