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,51 @@
1
+ /**
2
+ * `codehere search "<query>"` — REQUEST_METHOD stage 7 (Learn) / #19.
3
+ * Search past agent sessions with PROVENANCE: which agent ran it, whether it
4
+ * succeeded, in which project. Local-first (reads on-disk records, no network).
5
+ * Follows the CLI color economy (DESIGN.md §5.2): colour carries meaning, and
6
+ * the output stays legible under NO_COLOR (glyph + word carry the status).
7
+ */
8
+ import { searchSessions } from '../../../application/use-cases/search-sessions.js';
9
+ import { colors, symbols, newline } from '../../../ui.js';
10
+ function statusGlyph(status) {
11
+ const s = status.toLowerCase();
12
+ if (/completed|done/.test(s))
13
+ return colors.verified(symbols.success); // ✓ verified
14
+ if (/error|caught|stopped|failed/.test(s))
15
+ return colors.caught(symbols.error); // ✕
16
+ return colors.dim('·'); // · pending / other
17
+ }
18
+ export function executeSearchCommand(query, options) {
19
+ const limit = options.limit ? Math.max(1, parseInt(options.limit, 10) || 10) : 10;
20
+ const hits = searchSessions(query, { limit });
21
+ if (options.json) {
22
+ process.stdout.write(JSON.stringify(hits, null, 2) + '\n');
23
+ return 0;
24
+ }
25
+ if (!hits.length) {
26
+ process.stdout.write(colors.dim(`No past sessions match "${query}".\n`));
27
+ return 0;
28
+ }
29
+ newline();
30
+ process.stdout.write(colors.heading(`${hits.length} past session${hits.length === 1 ? '' : 's'} for "${query}"`) +
31
+ colors.dim(' (provenance: which agent · whether it succeeded)') +
32
+ '\n\n');
33
+ for (const h of hits) {
34
+ // provenance line: agent · verdict · status · id · project
35
+ process.stdout.write(' ' +
36
+ colors.identity(h.agent.padEnd(12)) +
37
+ ' ' +
38
+ statusGlyph(h.status) +
39
+ ' ' +
40
+ colors.dim(h.status.padEnd(10)) +
41
+ ' ' +
42
+ colors.evidence(h.sessionId.slice(0, 8)) +
43
+ ' ' +
44
+ colors.dim(h.project) +
45
+ '\n');
46
+ process.stdout.write(' ' + colors.dim(h.snippet) + '\n\n');
47
+ }
48
+ process.stdout.write(colors.dim('Open one: ') + colors.bold('codehere verify <id>') + '\n\n');
49
+ return 0;
50
+ }
51
+ //# sourceMappingURL=search-command.js.map
@@ -39,7 +39,11 @@ function printAgentLines(all) {
39
39
  console.log(warning(`${a.label}: installed, needs sign-in`) + hint);
40
40
  }
41
41
  else if (a.state === 'billed_key_required') {
42
- console.log(colors.dim(`○ ${a.label}: needs a billed API key (paid)`));
42
+ // Surface the adapter's own authHint instead of a hardcoded "paid" label —
43
+ // some agents (Aider via LiteLLM) actually have a $0 path the detail names.
44
+ console.log(colors.dim(`○ ${a.label}: installed, needs a model + key`));
45
+ if (a.authHint?.detail)
46
+ console.log(colors.dim(` ${a.authHint.detail}`));
43
47
  }
44
48
  else {
45
49
  const hint = a.installHint?.command ? colors.dim(` (install: ${a.installHint.command})`) : '';
@@ -47,6 +51,27 @@ function printAgentLines(all) {
47
51
  }
48
52
  }
49
53
  }
54
+ /**
55
+ * The free-tier setup recipe (Phase 1 — free-tier-first). One compact,
56
+ * copy-pasteable line per vendor showing the CHEAPEST way to get it running —
57
+ * so a user testing all 5 vendors knows exactly how to do it for ~$0. Shown
58
+ * when no agent is ready yet (the moment the user needs it). Mirrors the
59
+ * per-agent readiness authHints; kept here as one consolidated cheatsheet
60
+ * because the readiness hints surface one-at-a-time, not as a plan.
61
+ */
62
+ function printFreeTierRecipe() {
63
+ console.log(colors.bold('Set up any agent for free (or near-free):'));
64
+ console.log(colors.cyan(' Claude') +
65
+ colors.dim(' run `claude` › /login · uses your Claude subscription (no API key, no per-token bill)'));
66
+ console.log(colors.cyan(' Gemini') +
67
+ colors.dim(' export GEMINI_API_KEY=… free tier › https://aistudio.google.com/apikey'));
68
+ console.log(colors.cyan(' Codex') +
69
+ colors.dim(' run `codex login` with a ChatGPT Plus/Pro account · flat, no per-token charge'));
70
+ console.log(colors.cyan(' OpenCode') +
71
+ colors.dim(' run `opencode auth` · point at a local Ollama model or a free provider ($0)'));
72
+ console.log(colors.cyan(' Aider') +
73
+ colors.dim(' reuse the free Gemini key: `--model gemini/gemini-2.5-flash` (routes via LiteLLM, $0)'));
74
+ }
50
75
  /**
51
76
  * Check if any API key is configured
52
77
  */
@@ -162,9 +187,9 @@ export async function executeSetupCommand(force = false) {
162
187
  console.log(colors.dim(' Next: codehere start (open the dashboard)'));
163
188
  }
164
189
  else {
165
- console.log(warning('No coding agent is set up yet. Install or sign in to one:'));
166
- console.log(colors.cyan(' npm i -g @anthropic-ai/claude-code') + colors.dim(' then run: claude (sign in)'));
167
- console.log(colors.dim(' Already use one by API key? export ANTHROPIC_API_KEY=sk-ant-...'));
190
+ console.log(warning('No coding agent is set up yet.'));
191
+ console.log();
192
+ printFreeTierRecipe();
168
193
  }
169
194
  return;
170
195
  }
@@ -206,15 +231,13 @@ export async function executeSetupCommand(force = false) {
206
231
  else if (apiKeyCheck.configured) {
207
232
  // No agent CLI ready, but a key is present — some adapters (e.g. Aider) run off it.
208
233
  console.log(success(`${apiKeyCheck.provider} API key found.`));
209
- console.log(colors.dim(' No agent CLI detected yet — install one for the full experience.'));
234
+ console.log(colors.dim(' No agent CLI detected yet · install one for the full experience.'));
210
235
  result.apiKeyConfigured = true;
211
236
  }
212
237
  else {
213
238
  console.log(warning('No coding agent is set up yet.'));
214
239
  newline();
215
- console.log(colors.bold('Install or sign in to at least one:'));
216
- console.log(colors.cyan(' npm i -g @anthropic-ai/claude-code') + colors.dim(' then run: claude (sign in)'));
217
- console.log(colors.dim(' Already use one by API key? export ANTHROPIC_API_KEY=sk-ant-...'));
240
+ printFreeTierRecipe();
218
241
  newline();
219
242
  }
220
243
  newline();
@@ -17,13 +17,17 @@ import { getVersion } from '../../../utils/version.js';
17
17
  import { loadProductContext } from '../../../infrastructure/config/product-context.js';
18
18
  import { AUDIT_FILE } from '../../../infrastructure/hooks/claude-sdk-hook.js';
19
19
  import { analyzeSessions, detectAmplificationAlerts, formatDuration, } from '../../../infrastructure/hooks/session-analyzer.js';
20
- function readAuditLog(maxLines = 5000) {
20
+ // Reads the FULL audit log. A prior 5000-line cap fed the "All time" column —
21
+ // so with >5000 entries the dashboard showed e.g. "All time: 5000" when the
22
+ // true total was ~8k (DISCOVERED-006: a precise number that's wrong is worse
23
+ // than none). The recent-blocks list caps itself downstream via slice(-20);
24
+ // the time-window stats filter by timestamp — neither needs a read cap.
25
+ function readAuditLog() {
21
26
  if (!existsSync(AUDIT_FILE))
22
27
  return [];
23
28
  try {
24
29
  const lines = readFileSync(AUDIT_FILE, 'utf-8').split('\n').filter(Boolean);
25
- const recent = lines.slice(-maxLines);
26
- return recent
30
+ return lines
27
31
  .map((l) => {
28
32
  try {
29
33
  return JSON.parse(l);
@@ -41,6 +45,24 @@ function readAuditLog(maxLines = 5000) {
41
45
  function withinHours(timestamp, hours) {
42
46
  return Date.now() - new Date(timestamp).getTime() < hours * 3600 * 1000;
43
47
  }
48
+ // Real daemon reachability — the "Status" line was a hardcoded "Running" that
49
+ // stayed green even with no daemon up (a green-on-failure lie at the surface).
50
+ // Probe /health with a short timeout so the dashboard reports the true state.
51
+ async function probeDaemonRunning(port = parseInt(process.env.CODEHERE_PORT ?? '3773', 10) || 3773, host = '127.0.0.1') {
52
+ const http = await import('http');
53
+ return new Promise((resolve) => {
54
+ const req = http.get({ host, port, path: '/health', timeout: 500 }, (res) => {
55
+ const ok = res.statusCode === 200;
56
+ res.resume();
57
+ resolve(ok);
58
+ });
59
+ req.on('error', () => resolve(false));
60
+ req.on('timeout', () => {
61
+ req.destroy();
62
+ resolve(false);
63
+ });
64
+ });
65
+ }
44
66
  function timeAgo(timestamp) {
45
67
  const ms = Date.now() - new Date(timestamp).getTime();
46
68
  const mins = Math.floor(ms / 60000);
@@ -126,7 +148,7 @@ function renderSessionDrillDown(sessionId, entries) {
126
148
  const tool = colors.cyan(entry.toolName.padEnd(12));
127
149
  const reason = extractBlockReason(entry);
128
150
  const risk = entry.riskScore > 70 ? colors.red(`risk:${entry.riskScore}`) : colors.yellow(`risk:${entry.riskScore}`);
129
- console.log(` ${colors.red('✗')} ${time} ${tool} ${risk} ${colors.dim(reason)}`);
151
+ console.log(` ${colors.red('✕')} ${time} ${tool} ${risk} ${colors.dim(reason)}`);
130
152
  }
131
153
  newline();
132
154
  }
@@ -143,7 +165,7 @@ function renderSessionDrillDown(sessionId, entries) {
143
165
  for (const entry of toShow) {
144
166
  const time = colors.dim(new Date(entry.timestamp).toLocaleTimeString());
145
167
  const tool = (entry.blocked ? colors.red : colors.cyan)(entry.toolName.padEnd(12));
146
- const icon = entry.blocked ? colors.red('✗') : entry.criticalFindings > 0 ? colors.yellow('!') : colors.green('✓');
168
+ const icon = entry.blocked ? colors.red('✕') : entry.criticalFindings > 0 ? colors.yellow('!') : colors.green('✓');
147
169
  const reason = extractBlockReason(entry);
148
170
  console.log(` ${icon} ${time} ${tool} ${colors.dim(reason)}`);
149
171
  }
@@ -152,16 +174,16 @@ function renderSessionDrillDown(sessionId, entries) {
152
174
  // (UX_OBSERVATIONS.md P1; long attended sessions shouldn't get accused).
153
175
  if (blocked.length > 2 && sorted.length > 20 && blocked.length / sorted.length > 0.05) {
154
176
  const pct = ((blocked.length / sorted.length) * 100).toFixed(0);
155
- console.log(colors.red(` ⚠ ${pct}% block rate (${blocked.length}/${sorted.length}) — review for injection patterns.`));
177
+ console.log(colors.red(` ! ${pct}% block rate (${blocked.length}/${sorted.length}) · review for injection patterns.`));
156
178
  }
157
179
  else if (blocked.length > 0 && sorted.length > 20) {
158
- console.log(colors.yellow(` ${blocked.length} blocked / ${sorted.length} total — long session, review if unattended.`));
180
+ console.log(colors.yellow(` ${blocked.length} blocked / ${sorted.length} total · long session, review if unattended.`));
159
181
  }
160
182
  else if (blocked.length > 0) {
161
- console.log(colors.yellow(` ${blocked.length} blocked call${blocked.length !== 1 ? 's' : ''} — review the blocked entries above.`));
183
+ console.log(colors.yellow(` ${blocked.length} blocked call${blocked.length !== 1 ? 's' : ''} · review the blocked entries above.`));
162
184
  }
163
185
  else if (sorted.length > 50) {
164
- console.log(colors.yellow(` ${sorted.length} calls in one session — check for amplification.`));
186
+ console.log(colors.yellow(` ${sorted.length} calls in one session · check for amplification.`));
165
187
  }
166
188
  else {
167
189
  console.log(colors.green(' Session looks clean.'));
@@ -403,7 +425,7 @@ function exportAudit(entries, format, sinceHours) {
403
425
  console.log('|---|---|---|---:|:---:|---:|');
404
426
  for (const e of filtered) {
405
427
  const sid = (e.sessionId || '').substring(0, 8);
406
- const blocked = e.blocked ? '✗' : '✓';
428
+ const blocked = e.blocked ? '✕' : '✓';
407
429
  console.log(`| \`${e.timestamp}\` | \`${sid}\` | ${e.toolName} | ${e.riskScore} | ${blocked} | ${e.findings} |`);
408
430
  }
409
431
  return;
@@ -456,11 +478,13 @@ export async function executeStatusCommand(options = {}) {
456
478
  const productCtx = loadProductContext(cwd);
457
479
  const hookRunning = existsSync(join(homedir(), '.claude', 'settings.json'));
458
480
  const hasSkills = existsSync(join(homedir(), '.claude', 'commands', 'codehere'));
481
+ const daemonRunning = await probeDaemonRunning();
459
482
  // ── JSON mode ──────────────────────────────────────────────────────────────
460
483
  if (options.json) {
461
484
  const result = {
462
485
  version,
463
486
  status: 'ok',
487
+ daemon: { running: daemonRunning },
464
488
  hooks: {
465
489
  running: hookRunning,
466
490
  stats: { last24h: s24, last7d: s7d, allTime: sAll },
@@ -468,35 +492,6 @@ export async function executeStatusCommand(options = {}) {
468
492
  skills: { installed: hasSkills },
469
493
  product: productCtx,
470
494
  };
471
- if (options.tasks) {
472
- try {
473
- const { getTaskRepository } = await import('../../../infrastructure/storage/task-repository.js');
474
- const taskRepo = getTaskRepository();
475
- const tasks = await taskRepo.getAll();
476
- result.tasks = tasks.slice(0, 20).map((t) => ({
477
- id: t.id,
478
- mode: t.mode,
479
- status: t.status,
480
- riskTier: t.riskTier,
481
- environment: t.environment,
482
- goal: t.goal,
483
- createdAt: t.createdAt,
484
- }));
485
- result.taskCount = tasks.length;
486
- }
487
- catch (e) {
488
- // rc.25: JSON consumers reading only stdout were still being fed a
489
- // lie ([] tasks, 0 count) on real failure (Codex final review:
490
- // "degraded behavior being presented as success"). Now we surface a
491
- // structured taskLoadError + status:'degraded' so automation can
492
- // distinguish "no tasks" from "load failed."
493
- console.error('[status --tasks --json] task repository failed to load:', e);
494
- result.tasks = [];
495
- result.taskCount = 0;
496
- result.taskLoadError = e instanceof Error ? e.message : String(e);
497
- result.status = 'degraded';
498
- }
499
- }
500
495
  console.log(JSON.stringify(result, null, 2));
501
496
  return;
502
497
  }
@@ -504,8 +499,9 @@ export async function executeStatusCommand(options = {}) {
504
499
  sectionHeader('Codehere Status');
505
500
  newline();
506
501
  // Version + hooks
507
- const hookStatus = hookRunning ? colors.green('✓') + ' Hooks running' : colors.yellow('⚠') + ' Hooks not installed';
508
- console.log(` Version: ${version} · Status: Running · ${hookStatus}`);
502
+ const hookStatus = hookRunning ? colors.green('✓') + ' Hooks running' : colors.yellow('!') + ' Hooks not installed';
503
+ const daemonStatus = daemonRunning ? colors.green('●') + ' Daemon running' : colors.dim('○') + ' Daemon stopped';
504
+ console.log(` Version: ${version} · ${daemonStatus} · ${hookStatus}`);
509
505
  newline();
510
506
  // Product brain
511
507
  if (productCtx) {
@@ -557,13 +553,13 @@ export async function executeStatusCommand(options = {}) {
557
553
  const tool = colors.cyan(entry.toolName.padEnd(10));
558
554
  const time = colors.dim(timeAgo(entry.timestamp).padEnd(8));
559
555
  const reason = extractBlockReason(entry);
560
- console.log(` ${colors.red('✗')} ${time} ${tool} ${colors.dim(reason)}`);
556
+ console.log(` ${colors.red('✕')} ${time} ${tool} ${colors.dim(reason)}`);
561
557
  }
562
558
  newline();
563
559
  }
564
560
  // If nothing was ever blocked, celebrate
565
561
  if (sAll.blocked === 0) {
566
- console.log(` ${colors.green('✓')} Nothing blocked yet — all scans clean.`);
562
+ console.log(` ${colors.green('✓')} Nothing blocked yet · all scans clean.`);
567
563
  newline();
568
564
  }
569
565
  }
@@ -609,49 +605,6 @@ export async function executeStatusCommand(options = {}) {
609
605
  }
610
606
  // Removed: Cost router jargon — not meaningful to users
611
607
  // Tasks table (--tasks flag)
612
- if (options.tasks) {
613
- newline();
614
- console.log(colors.bold('Recent tasks (experimental)'));
615
- console.log(colors.dim('Task storage: .codehere/tasks.jsonl'));
616
- newline();
617
- try {
618
- const { getTaskRepository } = await import('../../../infrastructure/storage/task-repository.js');
619
- const taskRepo = getTaskRepository();
620
- const tasks = await taskRepo.getAll();
621
- if (tasks.length === 0) {
622
- console.log(colors.dim('No tasks found. Run "codehere start" to open the task intake, or "codehere task <prompt>" from the CLI.'));
623
- }
624
- else {
625
- const pad = (s, n) => (s.length >= n ? s.substring(0, n) : s + ' '.repeat(n - s.length));
626
- const trunc = (s, n) => (s.length <= n ? s : s.substring(0, n - 1) + '…');
627
- console.log(colors.dim([pad('ID', 10), pad('Mode', 8), pad('Status', 12), pad('Risk', 4), pad('Env', 6), 'Goal'].join(' ')));
628
- console.log(colors.dim(['-'.repeat(10), '-'.repeat(8), '-'.repeat(12), '-'.repeat(4), '-'.repeat(6), '-'.repeat(30)].join(' ')));
629
- tasks.slice(0, 10).forEach((task) => {
630
- console.log([
631
- pad(task.id.substring(0, 8) + '..', 10),
632
- pad(task.mode, 8),
633
- pad(task.status, 12),
634
- pad(task.riskTier.toUpperCase(), 4),
635
- pad(task.environment.substring(0, 6), 6),
636
- trunc(task.goal, 30),
637
- ].join(' '));
638
- });
639
- if (tasks.length > 10) {
640
- newline();
641
- console.log(colors.dim(`... and ${tasks.length - 10} more tasks`));
642
- }
643
- }
644
- }
645
- catch (e) {
646
- // rc.25: don't print "No tasks found" — that's a lie when load failed
647
- // (Codex final review: "degraded behavior presented as success"). Show
648
- // a clear error line; stderr already has the cause for the operator.
649
- console.error('[status] task repository failed to load:', e);
650
- const msg = e instanceof Error ? e.message : String(e);
651
- console.log(colors.red(` ✗ Could not load task repository: ${msg}`));
652
- console.log(colors.dim(' See logs above for details. Try `codehere start` to (re)init the task intake.'));
653
- }
654
- }
655
608
  // Quick trust scan (opt-in)
656
609
  if (options.trust) {
657
610
  const { executeTrustCommand } = await import('./trust-command.js');
@@ -0,0 +1,256 @@
1
+ /**
2
+ * `codehere team`, the CLI face of the local-first team cost+audit ledger.
3
+ *
4
+ * The data layer (config, snapshots, merge, git sync) lives in
5
+ * infrastructure/cost/team-ledger.ts so the CLI and the daemon (`GET /api/team`)
6
+ * read the same core. This file is just the terminal rendering + subcommands.
7
+ *
8
+ * Subcommands:
9
+ * (none) / sync write this machine's snapshot, sync the team repo, show merged view
10
+ * init [path] create a team: make + git-init a shared folder and point at it
11
+ * join <git-url> join a team: clone the shared repo and point at it
12
+ * set-dir <path> point at an existing shared team directory
13
+ * budget <usd> set the team's shared daily ceiling (off = clear)
14
+ */
15
+ import { existsSync, mkdirSync } from 'fs';
16
+ import { execFileSync } from 'child_process';
17
+ import { join as pathJoin, basename } from 'path';
18
+ import { loadTeamConfig, saveTeamConfig, codehereHome, initTeamDir, isGitRepo, gitPull, gitShareMine, writeMyShare, buildTeamView, } from '../../../infrastructure/cost/team-ledger.js';
19
+ import { success, error, info, heading, newline } from '../../../ui.js';
20
+ function fmtUsd(n) {
21
+ return n >= 0.01 || n === 0 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`;
22
+ }
23
+ export async function executeTeamCommand(subcommand, value, opts) {
24
+ const sub = subcommand ?? 'sync';
25
+ switch (sub) {
26
+ case 'sync':
27
+ case 'show':
28
+ return syncAndShow(Boolean(opts?.strict));
29
+ case 'init':
30
+ return initTeam(value);
31
+ case 'join':
32
+ if (!value) {
33
+ console.error(error('Usage:'), 'codehere team join <git-url>');
34
+ process.exit(1);
35
+ }
36
+ return joinTeam(value);
37
+ case 'set-dir':
38
+ if (!value) {
39
+ console.error(error('Usage:'), 'codehere team set-dir <path-to-shared-dir>');
40
+ process.exit(1);
41
+ }
42
+ return setDir(value);
43
+ case 'budget':
44
+ if (!value) {
45
+ console.error(error('Usage:'), 'codehere team budget <usd> (or `off`)');
46
+ process.exit(1);
47
+ }
48
+ return setBudget(value);
49
+ default:
50
+ console.error(error('Unknown subcommand:'), sub);
51
+ console.error(info('Try:'), 'codehere team [sync | init [path] | join <git-url> | set-dir <path> | budget <usd>]');
52
+ process.exit(1);
53
+ }
54
+ }
55
+ function setDir(path) {
56
+ const cfg = loadTeamConfig();
57
+ cfg.dir = path;
58
+ saveTeamConfig(cfg);
59
+ console.log(success('Team directory set:'), path);
60
+ console.log(info('Next:'), 'run `codehere team` on each machine. Keep that folder in your own git/shared drive.');
61
+ }
62
+ // Create a team: make + git-init a shared folder, point at it, write the first
63
+ // snapshot — so a lead sets up a team in one command, no hand-editing config.
64
+ function initTeam(dir) {
65
+ const target = dir || pathJoin(codehereHome(), 'team-shared');
66
+ try {
67
+ initTeamDir(target); // shared core: mkdir + git-init + README (also used by POST /api/team/init)
68
+ }
69
+ catch {
70
+ console.error(error('Could not create team dir:'), target, '— check the path and permissions.');
71
+ process.exit(1);
72
+ }
73
+ const cfg = loadTeamConfig();
74
+ cfg.dir = target;
75
+ saveTeamConfig(cfg);
76
+ try {
77
+ writeMyShare(target);
78
+ }
79
+ catch {
80
+ /* best-effort first snapshot */
81
+ }
82
+ console.log(success('Team created:'), target);
83
+ console.log(info('Share it:'), 'push this folder to a git repo, then each teammate runs `codehere team join <git-url>`.');
84
+ }
85
+ // Join a team: clone the shared repo under ~/.codehere/teams/<name>, point at it,
86
+ // write this machine's first snapshot.
87
+ function joinTeam(url) {
88
+ // A join URL is shared/pasted, so it's a real attack surface. git's ext::/fd::
89
+ // (and file::) transports execute arbitrary commands, and a leading-dash URL is
90
+ // parsed as a git option (argument injection). Allow only real remote schemes.
91
+ if (/^-/.test(url) || /^(ext|fd|file)::/i.test(url) || !/^(https?:\/\/|git:\/\/|ssh:\/\/|git@[^/]+:)/.test(url)) {
92
+ console.error(error('Unsupported git URL:'), url);
93
+ console.error(info('Use:'), 'https:// , git:// , ssh:// , or git@host:owner/repo');
94
+ process.exit(1);
95
+ }
96
+ const name = basename(url.replace(/\.git$/, '')) || 'team-shared';
97
+ const target = pathJoin(codehereHome(), 'teams', name);
98
+ if (existsSync(target)) {
99
+ console.error(error('Already exists:'), target);
100
+ console.error(info('Point at it instead:'), `codehere team set-dir ${target}`);
101
+ process.exit(1);
102
+ }
103
+ try {
104
+ mkdirSync(pathJoin(codehereHome(), 'teams'), { recursive: true });
105
+ // Belt-and-suspenders: disable the command-executing transports at the git
106
+ // level too, and `--` so the URL can never be read as an option.
107
+ execFileSync('git', ['-c', 'protocol.ext.allow=never', '-c', 'protocol.allow=user', 'clone', '-q', '--', url, target], { stdio: 'inherit' });
108
+ }
109
+ catch {
110
+ console.error(error('Clone failed:'), url, '— check the URL and your access.');
111
+ process.exit(1);
112
+ }
113
+ if (!existsSync(pathJoin(target, 'spend')))
114
+ mkdirSync(pathJoin(target, 'spend'), { recursive: true });
115
+ const cfg = loadTeamConfig();
116
+ cfg.dir = target;
117
+ saveTeamConfig(cfg);
118
+ try {
119
+ writeMyShare(target);
120
+ }
121
+ catch {
122
+ /* best-effort */
123
+ }
124
+ console.log(success('Joined team:'), target);
125
+ console.log(info('Next:'), 'run `codehere team` to sync and see the merged view.');
126
+ }
127
+ function setBudget(value) {
128
+ const cfg = loadTeamConfig();
129
+ if (value.toLowerCase() === 'off') {
130
+ cfg.dailyUsd = null;
131
+ saveTeamConfig(cfg);
132
+ console.log(success('Team daily budget cleared.'));
133
+ return;
134
+ }
135
+ const num = Number(value.replace(/^\$/, ''));
136
+ if (!Number.isFinite(num) || num <= 0) {
137
+ console.error(error('Invalid amount:'), value, '(use a positive number, e.g. `codehere team budget 50`)');
138
+ process.exit(1);
139
+ }
140
+ cfg.dailyUsd = num;
141
+ saveTeamConfig(cfg);
142
+ console.log(success('Team daily budget set:'), fmtUsd(num));
143
+ }
144
+ function syncAndShow(strict = false) {
145
+ const cfg = loadTeamConfig();
146
+ if (!cfg.dir) {
147
+ console.error(error('No team directory set.'));
148
+ console.error(info('Set one (a folder your team controls, e.g. a shared git repo):'));
149
+ console.error(' codehere team set-dir /path/to/team-shared-dir');
150
+ process.exit(1);
151
+ }
152
+ if (!existsSync(cfg.dir)) {
153
+ console.error(error('Team directory does not exist:'), cfg.dir);
154
+ console.error(info('Create it (or clone the team repo) first, then re-run.'));
155
+ process.exit(1);
156
+ }
157
+ const gitMode = isGitRepo(cfg.dir);
158
+ const pulled = gitMode ? gitPull(cfg.dir) : false;
159
+ const mine = writeMyShare(cfg.dir);
160
+ const pushed = gitMode ? gitShareMine(cfg.dir, `spend/${mine.machineId}.json`) : null;
161
+ const view = buildTeamView(cfg.dir, cfg.dailyUsd);
162
+ newline();
163
+ console.log(heading(`Team spend today (${view.date}), estimate, not a provider invoice`));
164
+ console.log(' ' + '─'.repeat(48));
165
+ for (const m of view.machines) {
166
+ const me = m.machineId === view.myMachineId ? ' (this machine)' : '';
167
+ console.log(` ${m.machineId.padEnd(28)} ${fmtUsd(m.totalUsd).padStart(9)}${me}`);
168
+ }
169
+ const agents = Object.entries(view.perAgent).sort((a, b) => b[1] - a[1]);
170
+ if (agents.length) {
171
+ console.log(' ' + '─'.repeat(48));
172
+ console.log(info(' by agent (cost · what each did)'));
173
+ for (const [agent, usd] of agents) {
174
+ const sess = view.perAgentSessions[agent] ?? 0;
175
+ console.log(` ${agent.padEnd(20)} ${fmtUsd(usd).padStart(9)} ${String(sess).padStart(3)} session${sess === 1 ? '' : 's'}`);
176
+ }
177
+ }
178
+ const projects = Object.entries(view.perProject).sort((a, b) => b[1] - a[1]);
179
+ if (projects.length) {
180
+ console.log(' ' + '─'.repeat(48));
181
+ console.log(info(' by project'));
182
+ for (const [proj, usd] of projects)
183
+ console.log(` ${proj.padEnd(28)} ${fmtUsd(usd).padStart(9)}`);
184
+ }
185
+ // Practice runs sit OUTSIDE the totals (their cost is the mock's own
186
+ // fabrication, never billed) — one quiet line, only when any exist.
187
+ if (view.practice && view.practice.sessions > 0) {
188
+ console.log(' ' + '─'.repeat(48));
189
+ console.log(info(` practice runs · ${view.practice.sessions} session${view.practice.sessions === 1 ? '' : 's'} · ${fmtUsd(view.practice.usd)} fabricated · not in the totals`));
190
+ }
191
+ console.log(' ' + '─'.repeat(48));
192
+ const totalLine = ` ${'TEAM TOTAL'.padEnd(28)} ${fmtUsd(view.teamTotal).padStart(9)}`;
193
+ if (view.dailyUsd !== null) {
194
+ const pct = Math.min(100, Math.round((view.teamTotal / view.dailyUsd) * 100));
195
+ console.log(totalLine + ` of ${fmtUsd(view.dailyUsd)} (${pct}%)`);
196
+ if (view.teamTotal >= view.dailyUsd)
197
+ console.log(error(' Over the team daily budget.'));
198
+ else if (pct >= 80)
199
+ console.log(info(' Approaching the team daily budget.'));
200
+ }
201
+ else {
202
+ console.log(totalLine);
203
+ console.log(info(' No team budget set, `codehere team budget <usd>` to add one.'));
204
+ }
205
+ if (view.totalActions > 0) {
206
+ newline();
207
+ console.log(heading('What the agents did (recorded, integrity-checked)'));
208
+ console.log(' ' + '─'.repeat(48));
209
+ for (const m of view.machines.filter((x) => x.audit).sort((a, b) => b.audit.actions - a.audit.actions)) {
210
+ const status = m.audit.chainOk ? success('chain ok') : error('chain BROKEN');
211
+ console.log(` ${m.machineId.padEnd(28)} ${String(m.audit.actions).padStart(6)} actions ${status}`);
212
+ }
213
+ console.log(' ' + '─'.repeat(48));
214
+ console.log(` ${'TEAM ACTIONS'.padEnd(28)} ${String(view.totalActions).padStart(6)}`);
215
+ if (view.brokenChains.length)
216
+ console.log(error(` Integrity broken on: ${view.brokenChains.join(', ')}. Investigate before trusting that record.`));
217
+ }
218
+ if (view.tamperedShares.length || view.unsignedShares.length) {
219
+ newline();
220
+ if (view.tamperedShares.length)
221
+ console.log(error(` ✗ Signature/key mismatch on: ${view.tamperedShares.join(', ')} — altered, or re-signed with a different key than first seen. Don't trust that share.`));
222
+ if (view.unsignedShares.length)
223
+ console.log(info(` Unsigned, can't verify: ${view.unsignedShares.join(', ')}.`));
224
+ }
225
+ else if (view.machines.length) {
226
+ newline();
227
+ console.log(success(' ✓ All shares signed; keys match the ones first seen (TOFU) — no post-write change detected.'));
228
+ }
229
+ newline();
230
+ if (gitMode) {
231
+ const pushMsg = pushed === 'pushed'
232
+ ? 'pushed yours'
233
+ : pushed === 'committed-local'
234
+ ? 'committed yours locally (no remote or offline)'
235
+ : pushed === 'nochange'
236
+ ? 'yours unchanged'
237
+ : 'could not push yours';
238
+ console.log(info('Synced:'), `git ${pulled ? 'pulled teammates' : 'pull skipped'}, ${pushMsg}. ${view.machines.length} machine(s) merged, all in your team's own repo, nothing sent to codehere.`);
239
+ }
240
+ else {
241
+ console.log(info('Local-first:'), `${view.machines.length} machine(s) merged from ${cfg.dir}, codehere sent nothing to a server.`);
242
+ console.log(info('Tip:'), 'make the team dir a git repo with a remote and codehere will pull/push it for you.');
243
+ }
244
+ // --strict: refuse to exit clean if the record can't be trusted. A tampered
245
+ // signature or a broken audit chain means the team total above is not a
246
+ // trustworthy record — fail closed so CI / a lead's gate catches it.
247
+ if (strict) {
248
+ const bad = [...view.tamperedShares, ...view.brokenChains];
249
+ if (bad.length) {
250
+ newline();
251
+ console.error(error('--strict: record integrity failed.'), `Untrustworthy: ${[...new Set(bad)].join(', ')}.`);
252
+ process.exit(1);
253
+ }
254
+ }
255
+ }
256
+ //# sourceMappingURL=team-command.js.map