codehere 1.0.1 → 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 (163) hide show
  1. package/README.md +36 -192
  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 +110 -7
  5. package/dist/index.js +38 -1212
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
  7. package/dist/presentation/cli/commands/check-command.js +178 -23
  8. package/dist/presentation/cli/commands/help-command.js +21 -285
  9. package/dist/ui.js +5 -236
  10. package/dist/utils/version.js +0 -6
  11. package/package.json +9 -38
  12. package/dist/application/services/verdict-service.js +0 -154
  13. package/dist/application/use-cases/assess-cross-vendor.js +0 -126
  14. package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
  15. package/dist/application/use-cases/domain-trust.js +0 -110
  16. package/dist/application/use-cases/export-aat.js +0 -107
  17. package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
  18. package/dist/application/use-cases/generate-final-report.js +0 -259
  19. package/dist/application/use-cases/generate-narration.js +0 -130
  20. package/dist/application/use-cases/generate-thread-title.js +0 -181
  21. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  22. package/dist/application/use-cases/plan-auto-review.js +0 -57
  23. package/dist/application/use-cases/reconcile-audit.js +0 -100
  24. package/dist/application/use-cases/search-sessions.js +0 -70
  25. package/dist/application/use-cases/should-auto-review.js +0 -71
  26. package/dist/application/use-cases/thread-export.js +0 -245
  27. package/dist/application/use-cases/verify-claim.js +0 -456
  28. package/dist/application/use-cases/write-pass.js +0 -277
  29. package/dist/domain/audit.js +0 -11
  30. package/dist/domain/services/codebase-detector.js +0 -85
  31. package/dist/domain/task.js +0 -91
  32. package/dist/error-handler.js +0 -139
  33. package/dist/infrastructure/completion/bash-completion.js +0 -29
  34. package/dist/infrastructure/completion/fish-completion.js +0 -17
  35. package/dist/infrastructure/completion/zsh-completion.js +0 -25
  36. package/dist/infrastructure/config/config-manager.js +0 -174
  37. package/dist/infrastructure/config/product-context.js +0 -49
  38. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  39. package/dist/infrastructure/context/adapters/figma-source.js +0 -198
  40. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
  41. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  42. package/dist/infrastructure/context/context-resolver.js +0 -144
  43. package/dist/infrastructure/context/context-source.js +0 -114
  44. package/dist/infrastructure/context/github-api.js +0 -110
  45. package/dist/infrastructure/context/github-device-flow.js +0 -65
  46. package/dist/infrastructure/context/oauth-flow.js +0 -207
  47. package/dist/infrastructure/context/oauth-store.js +0 -122
  48. package/dist/infrastructure/context/registry-bootstrap.js +0 -95
  49. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  50. package/dist/infrastructure/cost/budget-config.js +0 -114
  51. package/dist/infrastructure/cost/cost-router.js +0 -97
  52. package/dist/infrastructure/cost/spend-aggregator.js +0 -222
  53. package/dist/infrastructure/cost/team-ledger.js +0 -355
  54. package/dist/infrastructure/discovery/discovery-log.js +0 -85
  55. package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
  56. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
  57. package/dist/infrastructure/hooks/hook-mode.js +0 -87
  58. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
  59. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  60. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  61. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  62. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  63. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  64. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
  65. package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
  66. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  67. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
  68. package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
  69. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
  70. package/dist/infrastructure/orchestration/context-handoff.js +0 -162
  71. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  72. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  73. package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
  74. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  75. package/dist/infrastructure/orchestration/failover-config.js +0 -67
  76. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  77. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
  78. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
  79. package/dist/infrastructure/orchestration/graphify-context.js +0 -209
  80. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  81. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
  82. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  83. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  84. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  85. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
  86. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
  87. package/dist/infrastructure/orchestration/readiness.js +0 -103
  88. package/dist/infrastructure/orchestration/session-manager.js +0 -1590
  89. package/dist/infrastructure/orchestration/session-persistence.js +0 -394
  90. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  91. package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
  92. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  93. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  94. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
  95. package/dist/infrastructure/protocols/mcp-tools.js +0 -401
  96. package/dist/infrastructure/protocols/probe-port.js +0 -36
  97. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  98. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
  99. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
  100. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
  101. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
  102. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
  103. package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
  104. package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
  105. package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
  106. package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
  107. package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
  108. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
  109. package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
  110. package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
  111. package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
  112. package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
  113. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
  114. package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
  115. package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
  116. package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
  117. package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
  118. package/dist/infrastructure/protocols/ui/styles.js +0 -2147
  119. package/dist/infrastructure/protocols/ui/tokens.js +0 -96
  120. package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
  121. package/dist/infrastructure/protocols/verify-page.js +0 -1677
  122. package/dist/infrastructure/security/file-collection.js +0 -118
  123. package/dist/infrastructure/security/fix-code.js +0 -144
  124. package/dist/infrastructure/security/fix-secret.js +0 -89
  125. package/dist/infrastructure/security/permissions-config.js +0 -235
  126. package/dist/infrastructure/security/scan-types.js +0 -10
  127. package/dist/infrastructure/security/ship-scan.js +0 -195
  128. package/dist/infrastructure/security/trust-scorer.js +0 -724
  129. package/dist/infrastructure/security/vibe-checks.js +0 -99
  130. package/dist/infrastructure/storage/atomic-write.js +0 -86
  131. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  132. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  133. package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
  134. package/dist/infrastructure/storage/project-thread-reader.js +0 -409
  135. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  136. package/dist/monitoring.js +0 -212
  137. package/dist/presentation/cli/commands/audit-command.js +0 -201
  138. package/dist/presentation/cli/commands/budget-command.js +0 -79
  139. package/dist/presentation/cli/commands/config-command.js +0 -125
  140. package/dist/presentation/cli/commands/context-source-command.js +0 -217
  141. package/dist/presentation/cli/commands/hook-command.js +0 -265
  142. package/dist/presentation/cli/commands/init-command.js +0 -590
  143. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  144. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  145. package/dist/presentation/cli/commands/permission-command.js +0 -105
  146. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  147. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  148. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  149. package/dist/presentation/cli/commands/report-command.js +0 -403
  150. package/dist/presentation/cli/commands/scan-command.js +0 -52
  151. package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
  152. package/dist/presentation/cli/commands/search-command.js +0 -51
  153. package/dist/presentation/cli/commands/setup-command.js +0 -330
  154. package/dist/presentation/cli/commands/status-command.js +0 -614
  155. package/dist/presentation/cli/commands/team-command.js +0 -256
  156. package/dist/presentation/cli/commands/thread-command.js +0 -370
  157. package/dist/presentation/cli/commands/trust-command.js +0 -328
  158. package/dist/presentation/cli/commands/verify-command.js +0 -298
  159. package/dist/presentation/cli/error-display.js +0 -328
  160. package/dist/presentation/cli/repl.js +0 -805
  161. package/dist/presentation/cli/task-deliverable.js +0 -153
  162. package/dist/testing/cassette.js +0 -125
  163. package/dist/testing/replay-adapter.js +0 -147
@@ -1,416 +0,0 @@
1
- /**
2
- * F6 Conductor — codehere's own meta-agent.
3
- *
4
- * Plain English in, agent actions out. The Conductor is a first-class
5
- * AgentAdapter that doesn't fork a subprocess — instead it ROUTES the
6
- * incoming prompt to one of the dogfooded specialists (Claude Code, Codex,
7
- * Aider, OpenCode) based on intent, then proxies the spawned session's
8
- * events back to the caller verbatim, with a single Conductor-attributed
9
- * "delegated to X" event prepended so the audit log records the routing
10
- * decision.
11
- *
12
- * This is the v0.4-alpha SCAFFOLD. Routing is rule-based (keyword → adapter)
13
- * for now. The v0.4 stable target replaces the rule lookup with an LLM
14
- * classifier — Claude Haiku 4.5 prompted to pick the best specialist given
15
- * the user's request, the available agents, and any context from past
16
- * sessions. The seam (`pickSpecialist`) is the same; only the body changes.
17
- *
18
- * Why this matters: until Conductor exists, codehere is "a UI over other
19
- * people's agents." With Conductor, codehere becomes a participant in the
20
- * thread — the user talks to *codehere*, codehere talks to the agents, and
21
- * the user gets a single conversational surface instead of having to know
22
- * which specialist suits the task. This is the seam the v0.4+ Slack-shape
23
- * thread UX (`@conductor` mention) attaches to.
24
- *
25
- * What v0.4-alpha intentionally does NOT do:
26
- * - LLM-backed classifier (rule-based only)
27
- * - Multi-step plans (one prompt → one specialist)
28
- * - Cross-session memory (Conductor sees only the current prompt)
29
- * - Mid-session redirection (once delegated, the specialist owns the session)
30
- *
31
- * Adding any of those before v0.4-stable rc.1 violates the "rc.N is feature-
32
- * frozen, beta.N+1 may only harden or fix" rule.
33
- *
34
- * @see docs/CONDUCTOR.md for the full design + roadmap from scaffold → stable
35
- * @see memory/project_decisions_log.md Decision 05 (F6 as first-class foundation)
36
- */
37
- import { EventEmitter } from 'events';
38
- import { randomUUID } from 'crypto';
39
- import { loadBudget, isOverBudget } from '../cost/budget-config.js';
40
- import { computeSpendToday, estimateCostUsd } from '../cost/spend-aggregator.js';
41
- const ROUTING_RULES = [
42
- {
43
- triggers: ['review', 'audit', 'explain', 'understand', 'analyze', 'analyse', 'why', 'what does'],
44
- preferred: ['claude-code', 'codex', 'aider'],
45
- reason: 'review/audit/explain tasks favor Claude Code',
46
- },
47
- {
48
- triggers: ['refactor', 'restructure', 'reorganize', 'reorganise', 'rewrite the', 'extract'],
49
- preferred: ['codex', 'claude-code', 'aider'],
50
- reason: 'structural refactors favor Codex',
51
- },
52
- {
53
- triggers: ['fix', 'patch', 'bugfix', 'hotfix', 'repair', 'resolve'],
54
- preferred: ['aider', 'claude-code', 'codex'],
55
- reason: 'targeted fixes favor Aider',
56
- },
57
- {
58
- // Narrowed 2026-06-02 (root-cause fix for "randomly uses opencode"): bare
59
- // 'generate'/'set up' are far too common ("generate a report", "set up the
60
- // config") and were silently routing ordinary tasks to opencode. AND
61
- // claude-code now leads — opencode has NO mid-stream visibility (buffered
62
- // output), so it must never be the surprise default for a "watch it work"
63
- // product; it stays available via the explicit agent picker.
64
- triggers: ['scaffold', 'bootstrap', 'create a new project', 'greenfield', 'new project from scratch'],
65
- preferred: ['claude-code', 'codex', 'opencode'],
66
- reason: 'greenfield scaffolding — claude-code default (visible); opencode opt-in only',
67
- },
68
- {
69
- triggers: ['add tests', 'write tests', 'test coverage', 'unit test', 'integration test'],
70
- preferred: ['codex', 'claude-code', 'aider'],
71
- reason: 'test authoring favors Codex',
72
- },
73
- ];
74
- /** Default specialist when no routing rule matches. */
75
- const DEFAULT_SPECIALIST = 'claude-code';
76
- /**
77
- * Tokenize a prompt for trigger matching. Splits on any non-alphanumeric
78
- * boundary (whitespace, hyphens, punctuation), lowercases everything. So:
79
- *
80
- * "Bug fix" → ["bug", "fix"]
81
- * "set-up auth" → ["set", "up", "auth"] (trigger "set up" still matches via phrase below)
82
- * "unit-test it" → ["unit", "test", "it"]
83
- * "prefix matter" → ["prefix", "matter"] (does NOT match `fix`)
84
- * "hotfixes" → ["hotfixes"] (does NOT match `fix`)
85
- *
86
- * Multi-word triggers ("add tests", "create new") are matched against the
87
- * normalized prompt directly using word boundaries — so they catch
88
- * "add tests" but not "added testing harness".
89
- */
90
- function tokenize(prompt) {
91
- const tokens = new Set(prompt
92
- .toLowerCase()
93
- .split(/[^a-z0-9]+/)
94
- .filter(Boolean));
95
- // Normalized form for multi-word phrase matching: ANY non-alphanumeric char
96
- // (hyphens, underscores, slashes, dots, colons, commas, etc.) becomes a space,
97
- // then collapse runs of whitespace. This matches the tokenizer's split rule
98
- // exactly, so multi-word triggers like "add tests" catch all of:
99
- // "add tests" "add-tests" "add_tests" "add/tests" "add:tests" "add.tests"
100
- // Note: must NOT collapse the leading/trailing spaces — they're load-bearing
101
- // for the boundary check below (`.includes(' ' + t + ' ')`).
102
- const normalized = ' ' +
103
- prompt
104
- .toLowerCase()
105
- .replace(/[^a-z0-9]+/g, ' ')
106
- .trim() +
107
- ' ';
108
- return { tokens, normalized };
109
- }
110
- /**
111
- * Test whether a routing trigger matches a prompt. Single-word triggers must
112
- * appear as whole tokens (no substring matches). Multi-word triggers must
113
- * appear as a phrase in the normalized prompt (with leading/trailing spaces
114
- * to enforce word boundaries on the ends).
115
- */
116
- function triggerMatches(trigger, tokens, normalized) {
117
- const t = trigger.toLowerCase().trim();
118
- if (!t)
119
- return false;
120
- if (t.includes(' ')) {
121
- return normalized.includes(' ' + t + ' ');
122
- }
123
- return tokens.has(t);
124
- }
125
- /**
126
- * Pick the right specialist for a given prompt.
127
- *
128
- * Algorithm:
129
- * 1. Tokenize the prompt (whole-word matching, no substring false-positives).
130
- * 2. Walk routing rules in order. Stop at the first rule whose triggers match.
131
- * 3. Within that rule, return the first preferred adapter that's installed.
132
- * 4. If that rule had a match but none of its preferred adapters are
133
- * installed, return DEFAULT_SPECIALIST (or first installed) with a
134
- * HONEST reason that names the matched rule and the fallback.
135
- * 5. If no rule matched at all, return DEFAULT_SPECIALIST (or first
136
- * installed) with a "no intent matched" reason.
137
- *
138
- * Returns null if NO specialist is installed (caller must handle).
139
- *
140
- * Known limitation: a prompt with multiple intents (e.g. "fix the bug and
141
- * add tests") routes by the first matching rule, not the dominant intent.
142
- * The v0.4 LLM classifier inherently handles dominance via confidence
143
- * scoring; the rule-based path here keeps the deterministic-priority
144
- * contract instead.
145
- */
146
- export function pickSpecialist(prompt, installedAgentIds) {
147
- if (installedAgentIds.length === 0) {
148
- return null;
149
- }
150
- const installedSet = new Set(installedAgentIds);
151
- const { tokens, normalized } = tokenize(prompt);
152
- for (const rule of ROUTING_RULES) {
153
- const matches = rule.triggers.some((t) => triggerMatches(t, tokens, normalized));
154
- if (!matches)
155
- continue;
156
- // Rule matched. Walk preferred specialists in order.
157
- for (const candidate of rule.preferred) {
158
- if (installedSet.has(candidate)) {
159
- return { agentId: candidate, reason: rule.reason };
160
- }
161
- }
162
- // Rule matched but none of its preferred specialists are installed.
163
- // Fall through with an HONEST reason that names the matched rule + fallback,
164
- // so the audit log records what actually happened (Codex review caught
165
- // the prior version saying "default routing" here, which was a lie).
166
- const fallbackId = installedSet.has(DEFAULT_SPECIALIST) ? DEFAULT_SPECIALIST : installedAgentIds[0];
167
- return {
168
- agentId: fallbackId,
169
- reason: `${rule.reason} — but none of [${rule.preferred.join(', ')}] installed; falling back to ${fallbackId}`,
170
- };
171
- }
172
- // No rule matched. Default routing.
173
- if (installedSet.has(DEFAULT_SPECIALIST)) {
174
- return { agentId: DEFAULT_SPECIALIST, reason: 'default routing (no specific intent matched)' };
175
- }
176
- return { agentId: installedAgentIds[0], reason: 'fallback to first available specialist' };
177
- }
178
- /**
179
- * Build the Conductor adapter. The lookup function is injected so we can
180
- * register against the same registry without creating an import cycle.
181
- *
182
- * IMPORTANT: the lookup is called at spawn time, NOT module-load time, so
183
- * it sees the fully-populated registry including any user-config adapters.
184
- */
185
- export function buildConductorAdapter(lookup) {
186
- const adapter = {
187
- id: 'conductor',
188
- label: 'Conductor (codehere)',
189
- // Conductor doesn't run as its own OS process — there's nothing to detect
190
- // via `ps`. The processPattern returned here will never match, by design.
191
- processPattern: /__conductor_never_matches__/,
192
- isInstalled() {
193
- // Conductor is "installed" iff at least one specialist is installed.
194
- // Anything else would be a confusing UX (offer Conductor when it has
195
- // nothing to delegate to).
196
- return lookup().some((a) => a.id !== 'conductor' && a.isInstalled());
197
- },
198
- checkReadiness() {
199
- const specialists = lookup().filter((a) => a.id !== 'conductor');
200
- const installedSpecialists = specialists.filter((a) => a.isInstalled());
201
- if (installedSpecialists.length === 0) {
202
- return {
203
- id: 'conductor',
204
- label: 'Conductor (codehere)',
205
- state: 'needs_install',
206
- installed: false,
207
- installHint: {
208
- command: 'npm install -g @anthropic-ai/claude-code',
209
- url: 'https://github.com/codehere/codehere#install',
210
- },
211
- authHint: {
212
- detail: 'Conductor delegates to specialists. Install at least one (Claude Code, Codex, Aider, or OpenCode) and Conductor becomes available automatically.',
213
- },
214
- };
215
- }
216
- return {
217
- id: 'conductor',
218
- label: 'Conductor (codehere)',
219
- state: 'ready',
220
- installed: true,
221
- };
222
- },
223
- spawn(prompt, options) {
224
- return spawnConductor(prompt, options ?? {}, lookup);
225
- },
226
- };
227
- return adapter;
228
- }
229
- /**
230
- * Spawn the Conductor session. Does not fork a process — instead picks a
231
- * specialist, spawns it, and proxies its events through with a single
232
- * "delegated" event prepended to the buffer.
233
- */
234
- function spawnConductor(prompt, options, lookup) {
235
- const sessionId = randomUUID();
236
- const startedAt = Date.now();
237
- const emitter = new EventEmitter();
238
- const buffer = [];
239
- let statusValue = 'running';
240
- // Look up usable specialists (excluding Conductor itself to avoid recursion).
241
- // READY, not merely installed — user-#1 dogfood (2026-06-07): Auto routed a
242
- // task to aider whose readiness already said billed_key_required; it died on
243
- // its startup banner and the run landed as a green "completed" that did
244
- // nothing. Mock mode keeps the installed-only pool (mock spawns never hit
245
- // real vendors; CI has no real CLIs to be "ready").
246
- const conductorMockMode = process.env.CODEHERE_MOCK === '1' || process.env.CODEHERE_MOCK === 'true';
247
- const installedAgents = lookup().filter((a) => a.id !== 'conductor' && a.isInstalled());
248
- const installedAgentIds = installedAgents
249
- .filter((a) => conductorMockMode || !a.checkReadiness || a.checkReadiness().state === 'ready')
250
- .map((a) => a.id);
251
- // Cost-bounded fail-over (Phase 2 Pillar 2 · Ega-approved 2026-06-08): when
252
- // today's spend has hit the daily budget, conductor routes to a FREE specialist
253
- // (gemini/opencode) instead of burning more — the cost-bounded promise. Explicit
254
- // paid-agent picks are refused upstream (429); conductor, the auto choice, fails
255
- // over HERE, observed in the routing event. If no free agent is ready it falls
256
- // through to the honest no-eligible-agent error below — it NEVER spawns a paid
257
- // agent over budget.
258
- const budgetCfg = loadBudget();
259
- const budgetBounded = budgetCfg.dailyUsd !== null && isOverBudget(computeSpendToday().totalUsd, budgetCfg);
260
- const eligibleAgentIds = budgetBounded
261
- ? installedAgentIds.filter((id) => estimateCostUsd(id, { inputTokens: 1, outputTokens: 1 }) === 0)
262
- : installedAgentIds;
263
- const decision = pickSpecialist(prompt, eligibleAgentIds);
264
- // If the pool is empty, emit an error event and return a non-running session.
265
- // Two distinct causes deserve two distinct messages: nothing installed vs
266
- // installed-but-not-ready (e.g. every vendor unbilled/unauthed — telling that
267
- // user to "install" a vendor they already have would misdirect them).
268
- if (!decision) {
269
- statusValue = 'error';
270
- const errEvent = {
271
- type: 'error',
272
- timestamp: Date.now(),
273
- data: {
274
- message: budgetBounded
275
- ? `Daily budget reached and no FREE agent (gemini, opencode) is ready to fail over to. ` +
276
- `Raise the cap (\`codehere budget set <amount>\`) or set up a free agent (e.g. \`export GEMINI_API_KEY=…\` — free tier), then retry. ` +
277
- `codehere won't spend over your budget.`
278
- : installedAgents.length === 0
279
- ? 'Conductor has no specialists to delegate to. Install at least one of: Claude Code, Codex, Aider, OpenCode.'
280
- : 'Conductor found installed agents, but none are ready to run (' +
281
- installedAgents.map((a) => a.id).join(', ') +
282
- '). Run `codehere health` to see what each one needs — usually an API key or billing.',
283
- },
284
- };
285
- buffer.push(errEvent);
286
- return synthesizeSession({ sessionId, startedAt, prompt, statusValue, emitter, buffer });
287
- }
288
- // Emit the routing-decision event FIRST so the audit log records WHY this
289
- // specialist was chosen. Layer 7 agent-provenance — every action attributable.
290
- const routingEvent = {
291
- type: 'codehere:conductor-routing',
292
- timestamp: Date.now(),
293
- data: {
294
- conductorSessionId: sessionId,
295
- delegatedTo: decision.agentId,
296
- // Surface the cost-bounded fail-over so it's never a silent vendor switch.
297
- reason: budgetBounded ? `daily budget reached · routed to a free agent · ${decision.reason}` : decision.reason,
298
- promptExcerpt: prompt.slice(0, 200),
299
- ...(budgetBounded ? { budgetBounded: true } : {}),
300
- },
301
- };
302
- // Buffer replay on subscribe() handles delivery to the first subscriber. Do
303
- // NOT also queueMicrotask(emit) — the microtask fires AFTER subscribe attaches
304
- // its handler, so the handler would receive routingEvent twice (rc.13 bug:
305
- // duplicate "routing → ..." line in the REPL).
306
- buffer.push(routingEvent);
307
- // Find the chosen specialist adapter and spawn against it.
308
- const specialist = lookup().find((a) => a.id === decision.agentId);
309
- if (!specialist) {
310
- // Defensive: pickSpecialist returned an ID not in the registry. Shouldn't
311
- // happen if installedAgentIds was derived from lookup(), but guard anyway.
312
- statusValue = 'error';
313
- const errEvent = {
314
- type: 'error',
315
- timestamp: Date.now(),
316
- data: { message: `Conductor picked '${decision.agentId}' but no adapter with that ID is registered.` },
317
- };
318
- buffer.push(errEvent);
319
- return synthesizeSession({ sessionId, startedAt, prompt, statusValue, emitter, buffer });
320
- }
321
- // Spawn the specialist session and proxy every event through.
322
- const specialistSession = specialist.spawn(prompt, options);
323
- let stepsSnapshot = [];
324
- let artifactsSnapshot;
325
- specialistSession.subscribe((evt) => {
326
- buffer.push(evt);
327
- // Mirror progress + artifacts BEFORE re-emitting so any subscriber that
328
- // reads `session.status` / `session.steps` / `session.artifacts` from
329
- // inside its event handler sees the up-to-date conductor state.
330
- //
331
- // BUG 2 (zombie RUNNING sessions, May 1 2026, 138/155 records affected):
332
- // pre-fix this block emitted FIRST and updated status SECOND. session-
333
- // manager.ts subscribes to the conductor session and persists `session.status`
334
- // synchronously inside its on-exit handler. With emit-before-update,
335
- // every conductor session was persisted with status='running' even though
336
- // the underlying specialist had already finished. The fix reorders so the
337
- // emit() at the end sees the freshly-mirrored statusValue.
338
- if (specialistSession.steps)
339
- stepsSnapshot = specialistSession.steps;
340
- if (specialistSession.artifacts)
341
- artifactsSnapshot = specialistSession.artifacts;
342
- // Specialist adapters terminate with an 'exit' event — never emit
343
- // 'codehere:complete' / 'codehere:error'. Without mirroring on 'exit',
344
- // session.status stayed 'running' forever and the REPL printed
345
- // "✗ └─ running" (rc.13 bug). Also keep the legacy types for safety in
346
- // case a future adapter emits them.
347
- if (evt.type === 'exit' || evt.type === 'codehere:complete' || evt.type === 'codehere:error') {
348
- statusValue = specialistSession.status;
349
- }
350
- emitter.emit('event', evt);
351
- });
352
- return {
353
- sessionId,
354
- startedAt,
355
- prompt,
356
- agent: 'conductor',
357
- // 1.3 · the specialist's runtime sessionId IS the id the audit hook keys
358
- // this task's tool calls by. Persisting it lets cross-vendor attribution
359
- // join the chain for conductor-routed (Auto) tasks — without it, every such
360
- // task reads "couldn't attribute" even though its writes are in the chain.
361
- delegateSessionId: specialistSession.sessionId,
362
- get status() {
363
- return statusValue;
364
- },
365
- get steps() {
366
- return stepsSnapshot;
367
- },
368
- get artifacts() {
369
- return artifactsSnapshot;
370
- },
371
- // v0.6.0 — Slice 3 of REPL persistence. Conductor inherits appendMessage
372
- // capability from whichever specialist it routed to. With all 5 external
373
- // adapters wired in Slices 1-2c, this becomes true for every routing
374
- // decision. Follow-ups stay with the SAME specialist that handled the
375
- // original turn — no re-routing mid-thread (intentional: the user's reply
376
- // is part of an ongoing exchange with that specific specialist, not a
377
- // fresh classification opportunity).
378
- canAppendMessage: specialistSession.canAppendMessage === true,
379
- appendMessage: specialistSession.appendMessage
380
- ? (text) => specialistSession.appendMessage(text)
381
- : undefined,
382
- subscribe(handler) {
383
- for (const e of buffer)
384
- handler(e);
385
- emitter.on('event', handler);
386
- return () => emitter.off('event', handler);
387
- },
388
- kill() {
389
- specialistSession.kill();
390
- statusValue = 'killed';
391
- },
392
- };
393
- }
394
- /** Build a TaskSession that just replays an event buffer — used for error states. */
395
- function synthesizeSession(args) {
396
- const { sessionId, startedAt, prompt, statusValue, emitter, buffer } = args;
397
- return {
398
- sessionId,
399
- startedAt,
400
- prompt,
401
- agent: 'conductor',
402
- get status() {
403
- return statusValue;
404
- },
405
- subscribe(handler) {
406
- for (const e of buffer)
407
- handler(e);
408
- emitter.on('event', handler);
409
- return () => emitter.off('event', handler);
410
- },
411
- kill() {
412
- /* nothing to kill — never started */
413
- },
414
- };
415
- }
416
- //# sourceMappingURL=conductor-adapter.js.map
@@ -1,162 +0,0 @@
1
- /**
2
- * context-handoff — structured cross-session context handoff protocol (P2).
3
- *
4
- * Bridges P0's auto-failover trigger with P1's graphify-ts context packs.
5
- * When a session fails over (or a new session spawns), the handoff loader
6
- * finds the most relevant context pack from recent session logs and injects
7
- * it as a system hint so the replacement agent inherits continuity.
8
- *
9
- * Protocol:
10
- * 1. loadHandoffContext(cwd, { fromSessionId?, sessionId?, maxAgeMs? })
11
- * — scans .codehere/context-packs.jsonl for the most relevant pack
12
- * 2. enrichPromptWithHandoff(prompt, handoff) — 2-arg (LOW #9: this header used to claim a cwd arg the signature never had)
13
- * — merges the handoff context into a prompt string
14
- * 3. session handoff is automatic during failover (wired in session-manager)
15
- *
16
- * Handoff format (injected as system hint):
17
- * [handoff: previous agent=<agent>, completed=<status>, pack=<packId>]
18
- * Previous work summary: <pack.payloadPreview or compiled summary>
19
- * ---
20
- * <original prompt>
21
- */
22
- import { existsSync, readFileSync } from 'fs';
23
- import { join } from 'path';
24
- // ── Constants ──────────────────────────────────────────────────────────────────
25
- const CONTEXT_PACKS_DIR = '.codehere';
26
- const CONTEXT_PACKS_FILE = 'context-packs.jsonl';
27
- const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
28
- const DEFAULT_MIN_NODES = 5;
29
- // ── Session log scanning ───────────────────────────────────────────────────────
30
- /**
31
- * Load all recent context packs from disk, sorted by timestamp (newest first).
32
- */
33
- function loadAllPacks(cwd) {
34
- const packsFile = join(cwd, CONTEXT_PACKS_DIR, CONTEXT_PACKS_FILE);
35
- if (!existsSync(packsFile)) {
36
- return [];
37
- }
38
- try {
39
- const content = readFileSync(packsFile, 'utf-8');
40
- const lines = content.split('\n').filter(Boolean);
41
- return lines.map((line) => JSON.parse(line)).reverse();
42
- }
43
- catch {
44
- return [];
45
- }
46
- }
47
- /**
48
- * Find the most relevant handoff context from previous sessions.
49
- *
50
- * Priority:
51
- * 1. Same workspace, same prompt keywords (most recent pack)
52
- * 2. Same workspace, any pack (most recent)
53
- * 3. Fallback: no handoff
54
- */
55
- export function loadHandoffContext(cwd, { maxAgeMs = DEFAULT_MAX_AGE_MS, minNodes = DEFAULT_MIN_NODES } = {}) {
56
- const now = Date.now();
57
- const packs = loadAllPacks(cwd);
58
- // Filter by age
59
- const recent = packs.filter((p) => {
60
- const ts = p.timestamp;
61
- return ts && now - ts < maxAgeMs;
62
- });
63
- // Prefer packs with enough graph structure to be useful
64
- const structured = recent.filter((p) => {
65
- const nodes = p.nodeCount ?? 0;
66
- return nodes >= minNodes;
67
- });
68
- if (structured.length === 0)
69
- return null;
70
- const last = structured[0];
71
- return {
72
- packId: String(last.packId ?? ''),
73
- agent: String(last.agent ?? 'unknown'),
74
- prompt: String(last.prompt ?? ''),
75
- status: String(last.status ?? 'unknown'),
76
- timestamp: last.timestamp,
77
- nodeCount: last.nodeCount,
78
- edgeCount: last.edgeCount,
79
- task: String(last.task ?? 'explain'),
80
- payloadPreview: String(last.payloadPreview ?? ''),
81
- };
82
- }
83
- // ── Prompt enrichment ──────────────────────────────────────────────────────────
84
- /**
85
- * Inject handoff context into a prompt.
86
- *
87
- * Returns the enriched prompt string with the handoff as a system hint
88
- * prepended before the original prompt. Returns the original prompt
89
- * if no handoff is available.
90
- */
91
- export function enrichPromptWithHandoff(prompt, handoff) {
92
- if (!handoff)
93
- return prompt;
94
- const handoffLine = `[handoff: agent=${handoff.agent}, pack=${handoff.packId}, ${handoff.nodeCount} nodes, ${handoff.edgeCount} edges]`;
95
- const preview = handoff.payloadPreview ? handoff.payloadPreview.slice(0, 800) : '';
96
- return [
97
- `[Previous session context — ${handoffLine}]`,
98
- `This workspace has recent structural context from a prior session.`,
99
- ...(preview ? [`Recent context: ${preview}`] : []),
100
- `For details, refer to the knowledge graph at the workspace root.`,
101
- `---`,
102
- prompt,
103
- ].join('\n');
104
- }
105
- /**
106
- * Prepend a conversational hand-off preamble so a DIFFERENT agent picking up an
107
- * open thread actually receives what the prior agent was asked + what it did —
108
- * not a blind restart. This is the cross-agent hand-off the room promises
109
- * ("any agent picks up where the last left off"). The summary it injects is the
110
- * SAME one the user sees in the room hand-off render, so the agent and the human
111
- * read one truth. Pure: the caller looks up the parent turn; this just formats.
112
- * Returns the prompt unchanged when there's no usable parent context.
113
- */
114
- export function enrichPromptWithThreadHandoff(prompt, parent) {
115
- if (!parent || !parent.agent)
116
- return prompt;
117
- const cut = (s, n) => {
118
- const t = String(s ?? '')
119
- .trim()
120
- .replace(/\s+/g, ' ');
121
- return t.length > n ? t.slice(0, n - 1) + '…' : t;
122
- };
123
- const askedLine = parent.prompt ? `They were asked: "${cut(parent.prompt, 300)}"` : null;
124
- const didLine = cut(parent.summary, 600)
125
- ? `What they did: ${cut(parent.summary, 600)}`
126
- : 'What they did: (no summary was recorded — inspect the files they touched before continuing)';
127
- return [
128
- `[Continuing a thread — the agent "${parent.agent}" worked on this project immediately before you.]`,
129
- ...(askedLine ? [askedLine] : []),
130
- didLine,
131
- `Build on that work — do not redo it. Handle the request below as the next step in the same thread.`,
132
- `---`,
133
- prompt,
134
- ].join('\n');
135
- }
136
- // ── Failover handoff (P2 integration point) ────────────────────────────────────
137
- /**
138
- * Build a handoff context for a failover target.
139
- *
140
- * When session A fails and session B auto-spawns as replacement,
141
- * this loads the most recent pack from the same workspace and
142
- * merges it into the prompt so B has continuity with A's work.
143
- */
144
- export function buildFailoverHandoff(cwd, { failedAgent, failedPrompt }) {
145
- // Load most recent pack from workspace
146
- const pack = loadHandoffContext(cwd, { maxAgeMs: DEFAULT_MAX_AGE_MS, minNodes: 3 });
147
- if (!pack)
148
- return null;
149
- // Enrich the prompt with the handoff
150
- const enrichedPrompt = enrichPromptWithHandoff(failedPrompt, {
151
- ...pack,
152
- agent: failedAgent,
153
- status: 'failed',
154
- });
155
- return {
156
- ...pack,
157
- agent: failedAgent,
158
- prompt: enrichedPrompt,
159
- status: 'failed',
160
- };
161
- }
162
- //# sourceMappingURL=context-handoff.js.map
@@ -1,99 +0,0 @@
1
- /**
2
- * CodexControlSurface · drives a running codex session via the app-server
3
- * JSON-RPC protocol (newline-delimited over stdio). inject() = `turn/steer`,
4
- * which pushes a message into the running turn.
5
- *
6
- * Under CODEHERE_MOCK=1 it spawns the mock app-server (zero API, no rate-limit);
7
- * otherwise the real `codex app-server`. The protocol is identical — the real
8
- * handshake was verified 2026-05-25; the mock lets us build + test the inject
9
- * path without spending tokens or waiting on the rate-limit.
10
- */
11
- import { spawn } from 'child_process';
12
- import { fileURLToPath } from 'url';
13
- import { dirname, join } from 'path';
14
- import { isMockMode } from '../mocks/resolve-agent-spawn.js';
15
- function serverSpawn() {
16
- if (isMockMode()) {
17
- // Env override lets tests point at the compiled dist mock (import.meta.url
18
- // is the src .ts under vitest, where the .js sibling doesn't exist).
19
- const mockPath = process.env.CODEHERE_MOCK_CODEX_SERVER ||
20
- join(dirname(fileURLToPath(import.meta.url)), '..', 'mocks', 'mock-codex-app-server.js');
21
- return { command: process.execPath, args: [mockPath] };
22
- }
23
- return { command: 'codex', args: ['app-server'] };
24
- }
25
- export class CodexControlSurface {
26
- agentId = 'codex';
27
- proc = null;
28
- buf = '';
29
- nextId = 1;
30
- pending = new Map();
31
- listeners = [];
32
- threadId = '';
33
- start() {
34
- if (this.proc)
35
- return;
36
- const { command, args } = serverSpawn();
37
- this.proc = spawn(command, args, { stdio: ['pipe', 'pipe', 'inherit'] });
38
- this.proc.stdout?.on('data', (d) => this.onData(d.toString()));
39
- }
40
- onData(chunk) {
41
- this.buf += chunk;
42
- let nl;
43
- while ((nl = this.buf.indexOf('\n')) >= 0) {
44
- const line = this.buf.slice(0, nl).trim();
45
- this.buf = this.buf.slice(nl + 1);
46
- if (!line)
47
- continue;
48
- let m;
49
- try {
50
- m = JSON.parse(line);
51
- }
52
- catch {
53
- continue;
54
- }
55
- if (typeof m.id === 'number' && this.pending.has(m.id)) {
56
- this.pending.get(m.id)(m.result ?? m.error);
57
- this.pending.delete(m.id);
58
- }
59
- else if (m.method) {
60
- const evt = { method: m.method, params: m.params };
61
- for (const cb of this.listeners)
62
- cb(evt);
63
- }
64
- }
65
- }
66
- rpc(method, params) {
67
- this.start();
68
- return new Promise((resolve) => {
69
- const id = this.nextId++;
70
- this.pending.set(id, resolve);
71
- this.proc.stdin?.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
72
- });
73
- }
74
- async open(prompt) {
75
- await this.rpc('initialize', { clientInfo: { name: 'codehere', version: '0.6.2' } });
76
- const th = (await this.rpc('thread/start', { cwd: process.cwd() }));
77
- this.threadId = String(th?.thread_id ?? '');
78
- // Fire the initial turn (don't await — it streams; inject() steers it).
79
- void this.rpc('turn/start', { thread_id: this.threadId, input: prompt });
80
- return { threadId: this.threadId };
81
- }
82
- async inject(message) {
83
- const r = (await this.rpc('turn/steer', { thread_id: this.threadId, input: message }));
84
- return { accepted: r?.accepted === true };
85
- }
86
- onEvent(cb) {
87
- this.listeners.push(cb);
88
- }
89
- close() {
90
- try {
91
- this.proc?.kill();
92
- }
93
- catch {
94
- /* best-effort */
95
- }
96
- this.proc = null;
97
- }
98
- }
99
- //# sourceMappingURL=codex-control-surface.js.map