codehere 0.8.0 → 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 (134) 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/plan-auto-review.js +57 -0
  11. package/dist/application/use-cases/reconcile-audit.js +100 -0
  12. package/dist/application/use-cases/rerun-tests.js +226 -0
  13. package/dist/application/use-cases/search-sessions.js +70 -0
  14. package/dist/application/use-cases/should-auto-review.js +71 -0
  15. package/dist/application/use-cases/thread-export.js +90 -0
  16. package/dist/application/use-cases/verify-claim.js +187 -5
  17. package/dist/domain/audit.js +11 -0
  18. package/dist/domain/services/codebase-detector.js +0 -6
  19. package/dist/domain/task.js +91 -0
  20. package/dist/error-handler.js +3 -115
  21. package/dist/index.js +269 -25
  22. package/dist/infrastructure/completion/bash-completion.js +7 -3
  23. package/dist/infrastructure/completion/fish-completion.js +7 -12
  24. package/dist/infrastructure/completion/zsh-completion.js +7 -12
  25. package/dist/infrastructure/config/config-manager.js +4 -3
  26. package/dist/infrastructure/config/product-context.js +1 -19
  27. package/dist/infrastructure/context/adapters/figma-source.js +37 -2
  28. package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
  29. package/dist/infrastructure/context/github-api.js +110 -0
  30. package/dist/infrastructure/context/github-device-flow.js +65 -0
  31. package/dist/infrastructure/context/oauth-store.js +5 -2
  32. package/dist/infrastructure/context/registry-bootstrap.js +0 -4
  33. package/dist/infrastructure/cost/budget-config.js +3 -2
  34. package/dist/infrastructure/cost/cost-router.js +16 -23
  35. package/dist/infrastructure/cost/spend-aggregator.js +90 -15
  36. package/dist/infrastructure/cost/team-ledger.js +355 -0
  37. package/dist/infrastructure/discovery/discovery-log.js +85 -0
  38. package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
  39. package/dist/infrastructure/hooks/claude-sdk-hook.js +133 -3
  40. package/dist/infrastructure/hooks/hook-mode.js +20 -4
  41. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
  42. package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
  43. package/dist/infrastructure/observability/execution-tracer.js +1 -1
  44. package/dist/infrastructure/orchestration/agents-config-seed.js +22 -10
  45. package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
  46. package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
  47. package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
  48. package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
  49. package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
  50. package/dist/infrastructure/orchestration/context-handoff.js +32 -14
  51. package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
  52. package/dist/infrastructure/orchestration/failover-config.js +0 -10
  53. package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
  54. package/dist/infrastructure/orchestration/generic-cli-adapter.js +132 -1
  55. package/dist/infrastructure/orchestration/graphify-context.js +14 -59
  56. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +164 -9
  57. package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
  58. package/dist/infrastructure/orchestration/progress-aggregator.js +238 -21
  59. package/dist/infrastructure/orchestration/session-manager.js +593 -94
  60. package/dist/infrastructure/orchestration/session-persistence.js +177 -41
  61. package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
  62. package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
  63. package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
  64. package/dist/infrastructure/protocols/mcp-server-impl.js +1462 -380
  65. package/dist/infrastructure/protocols/mcp-tools.js +401 -0
  66. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
  67. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
  68. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
  69. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
  70. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
  71. package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
  72. package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
  73. package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
  74. package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
  75. package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
  76. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
  77. package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
  78. package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
  79. package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
  80. package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
  81. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
  82. package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
  83. package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
  84. package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
  85. package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
  86. package/dist/infrastructure/protocols/ui/styles.js +2147 -0
  87. package/dist/infrastructure/protocols/ui/tokens.js +96 -0
  88. package/dist/infrastructure/protocols/ui-page-v2.js +116 -4214
  89. package/dist/infrastructure/protocols/verify-page.js +849 -155
  90. package/dist/infrastructure/security/fix-code.js +144 -0
  91. package/dist/infrastructure/security/fix-secret.js +89 -0
  92. package/dist/infrastructure/security/permissions-config.js +3 -2
  93. package/dist/infrastructure/security/scan-types.js +10 -0
  94. package/dist/infrastructure/security/ship-scan.js +195 -0
  95. package/dist/infrastructure/security/trust-scorer.js +70 -3
  96. package/dist/infrastructure/security/vibe-checks.js +99 -0
  97. package/dist/infrastructure/storage/atomic-write.js +86 -0
  98. package/dist/infrastructure/storage/project-thread-mirror.js +150 -3
  99. package/dist/infrastructure/storage/project-thread-reader.js +261 -24
  100. package/dist/monitoring.js +2 -2
  101. package/dist/presentation/cli/commands/audit-command.js +79 -4
  102. package/dist/presentation/cli/commands/budget-command.js +2 -2
  103. package/dist/presentation/cli/commands/check-command.js +96 -0
  104. package/dist/presentation/cli/commands/context-source-command.js +20 -1
  105. package/dist/presentation/cli/commands/help-command.js +5 -5
  106. package/dist/presentation/cli/commands/hook-command.js +5 -5
  107. package/dist/presentation/cli/commands/init-command.js +26 -14
  108. package/dist/presentation/cli/commands/memory-command.js +28 -28
  109. package/dist/presentation/cli/commands/permission-command.js +1 -12
  110. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  111. package/dist/presentation/cli/commands/protocols-command.js +4 -4
  112. package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
  113. package/dist/presentation/cli/commands/report-command.js +48 -32
  114. package/dist/presentation/cli/commands/scan-command.js +52 -0
  115. package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
  116. package/dist/presentation/cli/commands/search-command.js +51 -0
  117. package/dist/presentation/cli/commands/setup-command.js +31 -8
  118. package/dist/presentation/cli/commands/status-command.js +39 -86
  119. package/dist/presentation/cli/commands/team-command.js +256 -0
  120. package/dist/presentation/cli/commands/thread-command.js +87 -5
  121. package/dist/presentation/cli/commands/trust-command.js +53 -31
  122. package/dist/presentation/cli/commands/verify-command.js +74 -9
  123. package/dist/presentation/cli/error-display.js +0 -8
  124. package/dist/presentation/cli/repl.js +15 -7
  125. package/dist/presentation/cli/task-deliverable.js +36 -10
  126. package/dist/testing/replay-adapter.js +0 -19
  127. package/dist/ui.js +51 -239
  128. package/dist/utils/version.js +0 -6
  129. package/package.json +30 -10
  130. package/dist/domain/entities/task.js +0 -61
  131. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  132. package/dist/infrastructure/security/input-validator.js +0 -152
  133. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  134. package/dist/infrastructure/storage/task-repository.js +0 -206
package/dist/index.js CHANGED
@@ -15,15 +15,67 @@ import { config } from 'dotenv';
15
15
  import { join, dirname } from 'path';
16
16
  import { fileURLToPath } from 'url';
17
17
  import { homedir } from 'os';
18
+ import { readFileSync } from 'fs';
18
19
  // ── Global crash safety ───────────────────────────────────────────────────
19
- // Unhandled rejections and exceptions must never silently kill the daemon.
20
- process.on('unhandledRejection', (reason) => {
21
- console.error('[codehere] unhandledRejection:', reason instanceof Error ? reason.stack : reason);
22
- });
20
+ // The old comment here said these "must never silently kill the daemon", and the
21
+ // code delivered the opposite of safety: `process.exitCode = 1` is only honoured
22
+ // once the event loop DRAINS, which for a daemon is never. Measured 2026-08-08 —
23
+ // after an uncaught exception the process was still running and still serving a
24
+ // second later, and would have gone on appending to a hash chain from a state
25
+ // Node itself considers undefined. Staying up is not resilience when the thing
26
+ // you are protecting is a record of what happened.
27
+ //
28
+ // One copy-pasteable block, no secrets: version/platform/node/state-dir are what
29
+ // a bug report needs, and none of them is sensitive. The error's own message may
30
+ // contain a path, which is why the state dir is named rather than dumped.
31
+ // Read from package.json rather than the getVersion() imported further down:
32
+ // the crash guard installs before most module init, and must not itself throw
33
+ // while reporting a throw.
34
+ function safeVersion() {
35
+ try {
36
+ const here = dirname(fileURLToPath(import.meta.url));
37
+ return JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf-8')).version ?? 'unknown';
38
+ }
39
+ catch {
40
+ return 'unknown';
41
+ }
42
+ }
43
+ function crashReport(kind, err) {
44
+ const e = err instanceof Error ? err : new Error(String(err));
45
+ return [
46
+ '',
47
+ `[codehere] ${kind} — this process is stopping.`,
48
+ ` version : ${safeVersion()}`,
49
+ ` node : ${process.version} on ${process.platform} ${process.arch}`,
50
+ ` state : ${process.env.CODEHERE_HOME ? '$CODEHERE_HOME' : '~/.codehere'}`,
51
+ ` pid : ${process.pid}`,
52
+ '',
53
+ (e.stack ?? e.message)
54
+ .split('\n')
55
+ .map((l) => ` ${l}`)
56
+ .join('\n'),
57
+ '',
58
+ ' Report at https://github.com/muhammadegaa/codehere/issues with the block above.',
59
+ '',
60
+ ].join('\n');
61
+ }
23
62
  process.on('uncaughtException', (err) => {
24
- console.error('[codehere] uncaughtException:', err.stack);
25
- // persist what we can before dying
26
- process.exitCode = 1;
63
+ // Node's own guidance: after this the process state is undefined. Exiting is
64
+ // the honest move — a restarted daemon re-reads the chain from disk and is
65
+ // correct again; a surviving one is silently guessing.
66
+ console.error(crashReport('uncaughtException', err));
67
+ process.exit(1);
68
+ });
69
+ process.on('unhandledRejection', (reason) => {
70
+ // Deliberately NOT fatal, unlike the above. The codebase has 17 explicitly
71
+ // `void`-marked fire-and-forget promises (re-persist, enrichment) whose
72
+ // rejection does not mean the process is corrupt, and killing the daemon on
73
+ // one would be a worse regression than the bug being fixed. It is made loud
74
+ // and attributed instead, so it cannot hide.
75
+ // The residual risk is real and worth naming: a rejected audit-append would be
76
+ // logged here and the process would continue. Closing that belongs at the
77
+ // write site, not in a global handler.
78
+ console.error(crashReport('unhandledRejection (non-fatal, continuing)', reason).replace(' — this process is stopping.', ' — continuing, but something is wrong.'));
27
79
  });
28
80
  import { executeSetupCommand } from './presentation/cli/commands/setup-command.js';
29
81
  import { executeInitCommand } from './presentation/cli/commands/init-command.js';
@@ -35,11 +87,15 @@ import { executeStatusCommand } from './presentation/cli/commands/status-command
35
87
  import { executeHookCommand } from './presentation/cli/commands/hook-command.js';
36
88
  import { executePermissionCommand } from './presentation/cli/commands/permission-command.js';
37
89
  import { executeBudgetCommand } from './presentation/cli/commands/budget-command.js';
90
+ import { executeTeamCommand } from './presentation/cli/commands/team-command.js';
38
91
  import { executeNarratorCommand } from './presentation/cli/commands/narrator-command.js';
39
92
  import { executeTrustCommand } from './presentation/cli/commands/trust-command.js';
93
+ import { executeCheckCommand } from './presentation/cli/commands/check-command.js';
94
+ import { executeScanCommand } from './presentation/cli/commands/scan-command.js';
40
95
  import { executeReportCommand } from './presentation/cli/commands/report-command.js';
41
96
  import { executeScanToolCommand } from './presentation/cli/commands/scan-tool-command.js';
42
97
  import { executeVerifyCommand } from './presentation/cli/commands/verify-command.js';
98
+ import { executeSearchCommand } from './presentation/cli/commands/search-command.js';
43
99
  import { executeAuditCommand } from './presentation/cli/commands/audit-command.js';
44
100
  import { executeContextSourceCommand } from './presentation/cli/commands/context-source-command.js';
45
101
  import { executeThreadCommand } from './presentation/cli/commands/thread-command.js';
@@ -62,7 +118,7 @@ import { checkTypo, looksLikeUnknownCommand } from './utils/typo-guard.js';
62
118
  const program = new Command();
63
119
  program
64
120
  .name('codehere')
65
- .description('A PM for your AI coding agents. Delegate a task, watch it run, and get a plain-English report with a trust score on every file it changed.')
121
+ .description('Your AI coding agents: audited, cost-bounded, cross-vendor. Delegate a task, watch it run, and get a plain-English report with a trust score on every file it changed.')
66
122
  .version(getVersion())
67
123
  .configureHelp({ helpWidth: 80, sortSubcommands: true })
68
124
  // rc.18: many users hit `codehere --help` first and get Commander's flat
@@ -272,7 +328,6 @@ program
272
328
  .command('status')
273
329
  .description('Security dashboard — hook activity, blocked calls, cost routing')
274
330
  .option('--json', 'Machine-readable JSON')
275
- .option('--tasks', 'Show recent tasks (experimental)')
276
331
  .option('--sessions', 'Show per-session audit trail (multi-agent analysis)')
277
332
  .option('--session <id>', 'Drill into a specific session (first 8+ chars of session ID)')
278
333
  .option('--trust', 'Include a quick trust scan of the current directory')
@@ -330,6 +385,37 @@ program
330
385
  process.exit(1);
331
386
  }
332
387
  });
388
+ // ── check ────────────────────────────────────────────────────────────────────
389
+ program
390
+ .command('check')
391
+ .description('Re-run your tests independently — proof of what the AI actually did, not what it claimed')
392
+ .argument('[target]', 'Repo or directory to check (default: current directory)')
393
+ .option('--json', 'Machine-readable output')
394
+ .option('--proof', 'Emit a paste-ready proof block for a PR description or review comment')
395
+ .action(async (target, options) => {
396
+ try {
397
+ process.exitCode = await executeCheckCommand(target, { json: options?.json, proof: options?.proof });
398
+ }
399
+ catch (err) {
400
+ console.error(error('Error:'), err instanceof Error ? err.message : String(err));
401
+ process.exit(1);
402
+ }
403
+ });
404
+ // ── scan ───────────────────────────────────────────────────────────────────────
405
+ program
406
+ .command('scan')
407
+ .description('Find the ONE thing that will get your app hacked — fast pass (gitleaks + semgrep), no API key')
408
+ .argument('[target]', 'App directory to scan (default: current directory)')
409
+ .option('--json', 'Machine-readable output')
410
+ .action(async (target, options) => {
411
+ try {
412
+ process.exitCode = await executeScanCommand(target, { json: options?.json });
413
+ }
414
+ catch (err) {
415
+ console.error(error('Error:'), err instanceof Error ? err.message : String(err));
416
+ process.exit(1);
417
+ }
418
+ });
333
419
  // ── report ───────────────────────────────────────────────────────────────────
334
420
  program
335
421
  .command('report')
@@ -395,6 +481,21 @@ Exit codes:
395
481
  process.exitCode = 1;
396
482
  }
397
483
  });
484
+ program
485
+ .command('search')
486
+ .description('Search your past agent sessions — with provenance (which agent, whether it succeeded)')
487
+ .argument('<query>', 'What to look for across your past sessions')
488
+ .option('--json', 'Machine-readable JSON')
489
+ .option('--limit <n>', 'Max results (default 10)')
490
+ .action((query, options) => {
491
+ try {
492
+ process.exitCode = executeSearchCommand(query, options);
493
+ }
494
+ catch (err) {
495
+ console.error(error('Error:'), err instanceof Error ? err.message : String(err));
496
+ process.exitCode = 1;
497
+ }
498
+ });
398
499
  // ── audit ────────────────────────────────────────────────────────────────────
399
500
  // F-05 of the demo v12 backlog · expose the hash-chain verifier as a CLI.
400
501
  program
@@ -564,6 +665,22 @@ program
564
665
  process.exit(1);
565
666
  }
566
667
  });
668
+ // ── team (local-first team spend ledger) ─────────────────────────────────────
669
+ program
670
+ .command('team')
671
+ .description("Merge each machine's daily spend into one team total (local-first, no cloud)")
672
+ .argument('[subcommand]', 'sync | set-dir | budget')
673
+ .argument('[value]', 'path (for set-dir) or amount in USD (for budget)')
674
+ .option('--strict', 'exit non-zero if any share is tampered or its audit chain is broken (for CI / enterprise gating)')
675
+ .action(async (subcommand, value, options) => {
676
+ try {
677
+ await executeTeamCommand(subcommand, value, { strict: Boolean(options?.strict) });
678
+ }
679
+ catch (err) {
680
+ console.error(error('Error:'), err instanceof Error ? err.message : String(err));
681
+ process.exit(1);
682
+ }
683
+ });
567
684
  // ── serve (MCP server) ───────────────────────────────────────────────────────
568
685
  program
569
686
  .command('serve')
@@ -577,25 +694,42 @@ program
577
694
  const host = options?.host ?? '127.0.0.1';
578
695
  const token = options?.token;
579
696
  if (options?.showConfig) {
697
+ const url = `http://${host}:${port}/mcp`;
698
+ const authHeader = token ? ` --header "Authorization: Bearer ${token}"` : '';
580
699
  const configBlock = {
581
700
  mcpServers: {
582
701
  codehere: {
583
- url: `http://${host}:${port}/mcp`,
702
+ url,
584
703
  ...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
585
704
  },
586
705
  },
587
706
  };
588
- console.log('\nAdd to Claude Desktop or Cursor MCP settings:\n');
707
+ newline();
708
+ console.log(colors.bold('Connect codehere to your coding agent') + colors.dim(' (codehere must be running)'));
709
+ newline();
710
+ console.log(colors.bold('Claude Code:'));
711
+ console.log(' claude mcp add --transport http codehere ' + url + authHeader);
712
+ newline();
713
+ console.log(colors.bold('Cursor') + colors.dim(' (.cursor/mcp.json in the project, or ~/.cursor/mcp.json):'));
589
714
  console.log(JSON.stringify(configBlock, null, 2));
590
715
  newline();
591
- console.log(colors.dim('Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json'));
592
- console.log(colors.dim('Cursor: Settings > MCP Servers'));
716
+ console.log(colors.bold('Claude Desktop') +
717
+ colors.dim(' (~/Library/Application Support/Claude/claude_desktop_config.json):'));
718
+ console.log(JSON.stringify(configBlock, null, 2));
593
719
  newline();
594
720
  return;
595
721
  }
596
722
  try {
597
723
  const { startMCPServer } = await import('./infrastructure/protocols/mcp-server-impl.js');
598
724
  const server = startMCPServer({ port, host, token });
725
+ // Wait until the port is actually bound before claiming success — otherwise
726
+ // a listen() failure (e.g. port in use) crashes with a raw stack trace
727
+ // AFTER the "running" banner already printed. The catch below then renders
728
+ // a friendly message instead.
729
+ await new Promise((resolve, reject) => {
730
+ server.once('listening', () => resolve());
731
+ server.once('error', reject);
732
+ });
599
733
  newline();
600
734
  console.log(success(`codehere MCP server running`));
601
735
  newline();
@@ -607,6 +741,8 @@ program
607
741
  console.log(' codehere_scan — scan text for injection/threats');
608
742
  console.log(' codehere_review — scan a directory, return riskiest files');
609
743
  console.log(' codehere_audit — read hook audit log');
744
+ console.log(" codehere_spend — today's spend by agent and project, vs budget");
745
+ console.log(' codehere_team — merged team cost and record across machines');
610
746
  newline();
611
747
  console.log(colors.dim(' Run with --show-config to get the MCP client config block'));
612
748
  console.log(colors.dim(' Press Ctrl+C to stop'));
@@ -633,11 +769,18 @@ program
633
769
  .option('-p, --port <number>', 'Port to listen on', '3773')
634
770
  .option('--host <host>', 'Host to bind to', '127.0.0.1')
635
771
  .option('--no-open', "Don't auto-open the browser")
772
+ .option('--token <token>', 'Require a Bearer token for every request (use when binding to a non-localhost host)')
636
773
  .option('--cloud', 'Enable cloud sync — push session state to codehere.uk/app for remote monitoring')
637
774
  .action(async (options) => {
638
775
  const port = parseInt(options?.port ?? '3773', 10);
639
776
  const host = options?.host ?? '127.0.0.1';
640
- const shouldOpen = options?.open !== false;
777
+ const token = options?.token;
778
+ // Auto-open only for a real human at a terminal. A daemon spawned with
779
+ // piped stdio (tests, CI, scripts, nohup) must NEVER pop a browser —
780
+ // suite runs were opening 2 tabs per run at the test daemons' ports
781
+ // (Ega, 2026-06-07: 6 stray tabs from 3 runs). Machine-guard, not a
782
+ // convention the next test has to remember.
783
+ const shouldOpen = options?.open !== false && process.stdout.isTTY === true;
641
784
  const cloud = options?.cloud === true;
642
785
  const url = `http://${host}:${port}/`;
643
786
  const openBrowser = async () => {
@@ -690,8 +833,12 @@ program
690
833
  }
691
834
  })();
692
835
  }
836
+ const localhostOnly = host === '127.0.0.1' || host === 'localhost' || host === '::1';
837
+ if (!localhostOnly && !token) {
838
+ console.warn(colors.warning(` Exposed on ${host} with no token — anyone who can reach this host can drive your agents. Add --token <secret> to require auth.`));
839
+ }
693
840
  const { startMCPServer } = await import('./infrastructure/protocols/mcp-server-impl.js');
694
- const server = startMCPServer({ port, host });
841
+ const server = startMCPServer({ port, host, token });
695
842
  // Wait for the server to either start listening or fail. The default
696
843
  // Node behavior on listen() failure is an unhandled 'error' event which
697
844
  // crashes the process with a stack trace. We catch it here so the user
@@ -704,7 +851,7 @@ program
704
851
  console.log(success('codehere is running'));
705
852
  newline();
706
853
  console.log(colors.bold('Tasks:') + ' ' + url);
707
- console.log(colors.bold('History:') + ' ' + `http://${host}:${port}/history`);
854
+ console.log(colors.bold('Verify:') + ' ' + `http://${host}:${port}/verify`);
708
855
  console.log(colors.bold('MCP:') + ' ' + `http://${host}:${port}/mcp`);
709
856
  if (cloud) {
710
857
  console.log(colors.bold('Cloud:') + ' ' + 'https://codehere.uk/app');
@@ -781,6 +928,83 @@ program
781
928
  process.exit(1);
782
929
  }
783
930
  });
931
+ // ── reply (continue a goal — follow-up turn to an existing session) ──────────
932
+ // Multi-turn IS supported (claude --resume etc.) + context carries, but the
933
+ // only surface was the web UI; the CLI had no way to continue a goal. Also the
934
+ // message API needs the exact UUID while reports show an 8-char prefix — so this
935
+ // resolves a prefix → full id (dogfood findings 2026-06-09).
936
+ program
937
+ .command('reply <id> <text...>')
938
+ .description('Continue a goal — send a follow-up turn to an existing session (context carries).')
939
+ .option('-p, --port <number>', 'Daemon port', '3773')
940
+ .option('--host <host>', 'Daemon host', '127.0.0.1')
941
+ .addHelpText('after', `
942
+ Examples:
943
+ $ codehere reply a5a39692 "now add a clear() method and a test"
944
+ The id may be the 8-char prefix that \`codehere task\` printed.`)
945
+ .action(async (idArg, textParts, options) => {
946
+ const text = textParts.join(' ').trim();
947
+ if (!text) {
948
+ console.error(error('No message. Usage: codehere reply <id> "your follow-up"'));
949
+ process.exit(1);
950
+ }
951
+ const port = parseInt(options?.port ?? '3773', 10);
952
+ const host = options?.host ?? '127.0.0.1';
953
+ const base = `http://${host}:${port}`;
954
+ const http = await import('http');
955
+ // Resolve a prefix id → full session id (reports show 8-char prefixes).
956
+ const fullId = await new Promise((resolve) => {
957
+ http
958
+ .get(`${base}/api/tasks`, (res) => {
959
+ let b = '';
960
+ res.on('data', (c) => (b += c.toString()));
961
+ res.on('end', () => {
962
+ try {
963
+ const j = JSON.parse(b);
964
+ const all = [...(j.spawned ?? []), ...(j.persisted ?? [])];
965
+ const m = all.find((s) => s.sessionId === idArg) ?? all.find((s) => s.sessionId.startsWith(idArg));
966
+ resolve(m ? m.sessionId : null);
967
+ }
968
+ catch {
969
+ resolve(null);
970
+ }
971
+ });
972
+ })
973
+ .on('error', () => resolve(null));
974
+ });
975
+ if (!fullId) {
976
+ console.error(error(`No session matching "${idArg}". See recent ones: codehere search`));
977
+ process.exit(1);
978
+ }
979
+ console.log(colors.dim(` Continuing session ${fullId.slice(0, 8)}… (context carries; the agent keeps the prior turns)`));
980
+ const body = JSON.stringify({ text });
981
+ const result = await new Promise((resolve) => {
982
+ const r = http.request(`${base}/api/task/${fullId}/message`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, (res) => {
983
+ let b = '';
984
+ res.on('data', (c) => (b += c.toString()));
985
+ res.on('end', () => {
986
+ try {
987
+ resolve(JSON.parse(b));
988
+ }
989
+ catch {
990
+ resolve(null);
991
+ }
992
+ });
993
+ });
994
+ r.on('error', () => resolve(null));
995
+ r.write(body);
996
+ r.end();
997
+ });
998
+ if (!result) {
999
+ console.error(error(`Could not reach the daemon at ${base}. Start it with: codehere start`));
1000
+ process.exit(1);
1001
+ }
1002
+ if (result.error) {
1003
+ console.error(error(result.error));
1004
+ process.exit(1);
1005
+ }
1006
+ console.log('\n' + (result.result ?? '(the agent left no text response)') + '\n');
1007
+ });
784
1008
  // ── task (spawn an agent session via CLI) ────────────────────────────────────
785
1009
  program
786
1010
  .command('task <prompt...>')
@@ -789,6 +1013,7 @@ program
789
1013
  .option('--host <host>', 'Daemon host', '127.0.0.1')
790
1014
  .option('--agent <id>', 'Which agent to run (claude-code, codex, aider, gemini, opencode). Default: auto.')
791
1015
  .option('--watch', 'Watch it work and print the report when it finishes')
1016
+ .option('--cwd <path>', 'Repo/directory the agent works in (default: where you run this)')
792
1017
  .addHelpText('after', `
793
1018
  Examples:
794
1019
  $ codehere task "fix the totalValue bug in inventory.js" # assign, get a session id
@@ -806,9 +1031,13 @@ Needs a running daemon: start one with \`codehere start\` (or \`codehere serve\`
806
1031
  const host = options?.host ?? '127.0.0.1';
807
1032
  const base = `http://${host}:${port}`;
808
1033
  const http = await import('http');
1034
+ const { resolve: resolvePath } = await import('path');
809
1035
  const { waitForDeliverable, formatDeliverable } = await import('./presentation/cli/task-deliverable.js');
1036
+ // The agent must run in the repo the USER targets — not the daemon's dir.
1037
+ // Default to where `codehere task` was invoked; allow an explicit --cwd.
1038
+ const cwd = resolvePath(options?.cwd ?? process.cwd());
810
1039
  const spawnResult = await new Promise((resolve) => {
811
- const body = JSON.stringify({ prompt, ...(options?.agent ? { agent: options.agent } : {}) });
1040
+ const body = JSON.stringify({ prompt, cwd, ...(options?.agent ? { agent: options.agent } : {}) });
812
1041
  const r = http.request(`${base}/api/task`, {
813
1042
  method: 'POST',
814
1043
  headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
@@ -871,7 +1100,7 @@ Needs a running daemon: start one with \`codehere start\` (or \`codehere serve\`
871
1100
  if (printed.has(s.label))
872
1101
  continue; // one line per step, on first settle
873
1102
  printed.add(s.label);
874
- const mark = s.state === 'failed' ? colors.error('✗') : colors.success('✓');
1103
+ const mark = s.state === 'failed' ? colors.error('✕') : colors.success('✓');
875
1104
  const text = s.kind === 'say' ? colors.dim(s.label) : s.label;
876
1105
  console.log(` ${mark} ${text}`);
877
1106
  }
@@ -935,13 +1164,22 @@ program
935
1164
  res.on('end', () => {
936
1165
  if (res.statusCode === 200)
937
1166
  console.log(success(`Killed ${sessionId}`));
938
- else
939
- console.error(error(`Failed (${res.statusCode}): ${buf}`));
1167
+ else if (res.statusCode === 404) {
1168
+ // Plain English + nonzero exit — scripts must be able to trust us
1169
+ // (audit 2026-06-07: kill exited 0 and leaked raw JSON on failure).
1170
+ console.error(error(`No running task found for "${sessionId}". See active tasks with: codehere status`));
1171
+ process.exitCode = 1;
1172
+ }
1173
+ else {
1174
+ console.error(error(`Could not kill ${sessionId} — the daemon said ${res.statusCode}. Details: ${buf}`));
1175
+ process.exitCode = 1;
1176
+ }
940
1177
  resolve();
941
1178
  });
942
1179
  });
943
- r.on('error', (err) => {
944
- console.error(error(err.message));
1180
+ r.on('error', () => {
1181
+ console.error(error(`Could not reach the daemon at http://${host}:${port}. Start it with: codehere start`));
1182
+ process.exitCode = 1;
945
1183
  resolve();
946
1184
  });
947
1185
  r.end();
@@ -1003,20 +1241,26 @@ program
1003
1241
  .argument('<shell>', 'Shell type: bash, zsh, or fish')
1004
1242
  .action(async (shell) => {
1005
1243
  try {
1244
+ // The REAL visible command set — completions must never drift from
1245
+ // registration again (audit 2026-06-07: the generators shipped a
1246
+ // fictional alpha-era command list).
1247
+ const cmds = program.commands
1248
+ .filter((c) => !c._hidden)
1249
+ .map((c) => ({ name: c.name(), description: c.description().split('\n')[0] }));
1006
1250
  switch (shell.toLowerCase()) {
1007
1251
  case 'bash': {
1008
1252
  const { generateBashCompletion } = await import('./infrastructure/completion/bash-completion.js');
1009
- console.log(generateBashCompletion());
1253
+ console.log(generateBashCompletion(cmds));
1010
1254
  break;
1011
1255
  }
1012
1256
  case 'zsh': {
1013
1257
  const { generateZshCompletion } = await import('./infrastructure/completion/zsh-completion.js');
1014
- console.log(generateZshCompletion());
1258
+ console.log(generateZshCompletion(cmds));
1015
1259
  break;
1016
1260
  }
1017
1261
  case 'fish': {
1018
1262
  const { generateFishCompletion } = await import('./infrastructure/completion/fish-completion.js');
1019
- console.log(generateFishCompletion());
1263
+ console.log(generateFishCompletion(cmds));
1020
1264
  break;
1021
1265
  }
1022
1266
  default:
@@ -1,16 +1,20 @@
1
1
  /**
2
2
  * Bash Completion Script Generator
3
3
  *
4
- * Phase 1: v0.5.0-alpha.0
4
+ * Completions are generated from the REAL registered command list passed by
5
+ * the caller — the alpha-era hardcoded set (`ask plan orchestrate …`) shipped
6
+ * completions for commands that never existed (caught by the 2026-06-07
7
+ * v1-readiness audit, Lane C).
5
8
  */
6
- export function generateBashCompletion() {
9
+ export function generateBashCompletion(commands) {
10
+ const opts = commands.map((c) => c.name).join(' ');
7
11
  return `# Codehere bash completion
8
12
  _complete_codehere() {
9
13
  local cur prev opts
10
14
  COMPREPLY=()
11
15
  cur="\${COMP_WORDS[COMP_CWORD]}"
12
16
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
13
- opts="ask plan orchestrate fix suggest run review status help version"
17
+ opts="${opts}"
14
18
 
15
19
  if [[ \${cur} == -* ]]; then
16
20
  COMPREPLY=($(compgen -W "--help --version" -- \${cur}))
@@ -1,22 +1,17 @@
1
1
  /**
2
2
  * Fish Completion Script Generator
3
3
  *
4
- * Phase 1: v0.5.0-alpha.0
4
+ * Completions are generated from the REAL registered command list passed by
5
+ * the caller (see bash-completion.ts header for the audit history).
5
6
  */
6
- export function generateFishCompletion() {
7
+ export function generateFishCompletion(commands) {
8
+ const lines = commands
9
+ .map((c) => `complete -c codehere -n __fish_use_subcommand -a ${c.name} -d "${c.description.replace(/"/g, '')}"`)
10
+ .join('\n');
7
11
  return `# Codehere fish completion
8
12
  complete -c codehere -f
9
13
 
10
- complete -c codehere -n __fish_use_subcommand -a ask -d "Answer questions about codebase"
11
- complete -c codehere -n __fish_use_subcommand -a plan -d "Generate execution plan"
12
- complete -c codehere -n __fish_use_subcommand -a orchestrate -d "Multi-agent orchestration"
13
- complete -c codehere -n __fish_use_subcommand -a fix -d "Apply AI edits to file"
14
- complete -c codehere -n __fish_use_subcommand -a suggest -d "Read-only assistant mode"
15
- complete -c codehere -n __fish_use_subcommand -a run -d "Unified agentic execution"
16
- complete -c codehere -n __fish_use_subcommand -a review -d "Task review interface"
17
- complete -c codehere -n __fish_use_subcommand -a status -d "Show agent status"
18
- complete -c codehere -n __fish_use_subcommand -a help -d "Show help"
19
- complete -c codehere -n __fish_use_subcommand -a version -d "Show version"
14
+ ${lines}
20
15
  `;
21
16
  }
22
17
  //# sourceMappingURL=fish-completion.js.map
@@ -1,24 +1,19 @@
1
1
  /**
2
2
  * Zsh Completion Script Generator
3
3
  *
4
- * Phase 1: v0.5.0-alpha.0
4
+ * Completions are generated from the REAL registered command list passed by
5
+ * the caller (see bash-completion.ts header for the audit history).
5
6
  */
6
- export function generateZshCompletion() {
7
+ export function generateZshCompletion(commands) {
8
+ // zsh _describe entries are 'name:description' — strip colons/quotes from
9
+ // descriptions so they can't break the entry syntax.
10
+ const entries = commands.map((c) => ` '${c.name}:${c.description.replace(/['":]/g, '')}'`).join('\n');
7
11
  return `#compdef codehere
8
12
 
9
13
  _codehere() {
10
14
  local -a commands
11
15
  commands=(
12
- 'ask:Answer questions about codebase'
13
- 'plan:Generate execution plan'
14
- 'orchestrate:Multi-agent orchestration'
15
- 'fix:Apply AI edits to file'
16
- 'suggest:Read-only assistant mode'
17
- 'run:Unified agentic execution'
18
- 'review:Task review interface'
19
- 'status:Show agent status'
20
- 'help:Show help'
21
- 'version:Show version'
16
+ ${entries}
22
17
  )
23
18
 
24
19
  _describe 'codehere' commands
@@ -1,14 +1,15 @@
1
+ import { writeFileAtomic } from '../storage/atomic-write.js';
1
2
  /**
2
3
  * Configuration Manager
3
4
  * Manages user preferences and settings for Codehere
4
5
  * Stores configuration in data/config.json
5
6
  */
6
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
7
+ import { readFileSync, existsSync, mkdirSync } from 'fs';
7
8
  import { join } from 'path';
8
9
  import { getVersion } from '../../utils/version.js';
9
10
  function detectDefaultProvider() {
10
11
  if (process.env.ANTHROPIC_API_KEY)
11
- return { provider: 'anthropic', model: 'claude-sonnet-4-20250514' };
12
+ return { provider: 'anthropic', model: 'claude-fable-5' };
12
13
  if (process.env.OPENAI_API_KEY)
13
14
  return { provider: 'openai', model: 'gpt-4o-mini' };
14
15
  if (process.env.COHERE_API_KEY)
@@ -100,7 +101,7 @@ export class ConfigManager {
100
101
  if (!existsSync(dataDir)) {
101
102
  mkdirSync(dataDir, { recursive: true });
102
103
  }
103
- writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8');
104
+ writeFileAtomic(this.configPath, JSON.stringify(this.config, null, 2));
104
105
  }
105
106
  catch (error) {
106
107
  throw new Error(`Failed to save config to ${this.configPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Stored in: .codehere/product.json (per-project, gitignore-friendly)
9
9
  */
10
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
10
+ import { readFileSync, existsSync } from 'fs';
11
11
  import { join } from 'path';
12
12
  const CONFIG_DIR = '.codehere';
13
13
  const CONFIG_FILE = 'product.json';
@@ -25,24 +25,6 @@ export function loadProductContext(cwd = process.cwd()) {
25
25
  return null;
26
26
  }
27
27
  }
28
- export function saveProductContext(ctx, cwd = process.cwd()) {
29
- const dir = join(cwd, CONFIG_DIR);
30
- if (!existsSync(dir))
31
- mkdirSync(dir, { recursive: true });
32
- writeFileSync(getConfigPath(cwd), JSON.stringify(ctx, null, 2) + '\n');
33
- }
34
- /**
35
- * Returns a plain-English risk label for use in recommendations.
36
- */
37
- export function riskLabel(level) {
38
- return level === 'critical'
39
- ? 'critical-risk'
40
- : level === 'high'
41
- ? 'high-risk'
42
- : level === 'medium'
43
- ? 'production'
44
- : 'low-risk';
45
- }
46
28
  /**
47
29
  * Adjusts a trust recommendation based on product context.
48
30
  * If no context is available, returns the base recommendation unchanged.