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
@@ -215,12 +215,12 @@ function executeMemoryGraduate(options) {
215
215
  }
216
216
  const r = appendLessonIfNew(lessonsPath, text, 'manual');
217
217
  if (r.result === 'added') {
218
- console.log(` ${success('added ')} lesson ${colors.dim(r.lesson.id)} → ${lessonsPath}`);
218
+ console.log(` ${success('added ')} lesson ${colors.dim(r.lesson.id)} › ${lessonsPath}`);
219
219
  console.log(` ${colors.dim('"' + r.lesson.text + '"')}`);
220
220
  }
221
221
  else {
222
222
  console.log(` ${info('skipped')} duplicate of existing lesson ${colors.dim(r.lesson.id)}`);
223
- console.log(` ${colors.dim('(already in lessons.jsonl — graduate is a no-op)')}`);
223
+ console.log(` ${colors.dim('(already in lessons.jsonl · graduate is a no-op)')}`);
224
224
  }
225
225
  newline();
226
226
  return;
@@ -240,7 +240,7 @@ function executeMemoryGraduate(options) {
240
240
  // Either way, the user decided — delete the candidate.
241
241
  rmSync(join(candidatesDir, candidateId + '.json'));
242
242
  if (r.result === 'added') {
243
- console.log(` ${success('graduated')} candidate ${candidateId} → lesson ${colors.dim(r.lesson.id)}`);
243
+ console.log(` ${success('graduated')} candidate ${candidateId} › lesson ${colors.dim(r.lesson.id)}`);
244
244
  }
245
245
  else {
246
246
  console.log(` ${info('skipped ')} candidate ${candidateId} text already in lessons (lesson ${colors.dim(r.lesson.id)})`);
@@ -320,19 +320,19 @@ function gatherLayerSummary(codehereDir, projectRoot) {
320
320
  out.push({ layer: 'working', line: `working · session.md (${fmtSize(sz)})` });
321
321
  }
322
322
  else {
323
- out.push({ layer: 'working', line: `working · (empty — no session.md)` });
323
+ out.push({ layer: 'working', line: `working · (empty · no session.md)` });
324
324
  }
325
325
  // episodic — symlink + repo_map
326
326
  const episodicLearnings = join(codehereDir, 'memory/episodic/AGENT_LEARNINGS.jsonl');
327
327
  const repoMapPath = join(codehereDir, 'memory/episodic/repo_map.json');
328
328
  const learningsLine = existsSync(episodicLearnings) || isSymlink(episodicLearnings)
329
329
  ? isSymlink(episodicLearnings)
330
- ? `AGENT_LEARNINGS.jsonl → ${readlinkSync(episodicLearnings)}`
330
+ ? `AGENT_LEARNINGS.jsonl › ${readlinkSync(episodicLearnings)}`
331
331
  : 'AGENT_LEARNINGS.jsonl (real file)'
332
332
  : '(no AGENT_LEARNINGS.jsonl)';
333
333
  const repoMapLine = existsSync(repoMapPath)
334
334
  ? `repo_map.json (${fmtSize(statSync(repoMapPath).size)})`
335
- : 'no repo_map.json — run `codehere memory repo-map`';
335
+ : 'no repo_map.json · run `codehere memory repo-map`';
336
336
  out.push({ layer: 'episodic', line: `episodic · ${learningsLine} · ${repoMapLine}` });
337
337
  // semantic/lessons.jsonl
338
338
  const lessonsPath = join(codehereDir, 'memory/semantic/lessons.jsonl');
@@ -383,7 +383,7 @@ function gatherLayerSummary(codehereDir, projectRoot) {
383
383
  }
384
384
  }
385
385
  else {
386
- out.push({ layer: 'protocols', line: `protocols · (no hook_patterns.json — run \`codehere protocols export\`)` });
386
+ out.push({ layer: 'protocols', line: `protocols · (no hook_patterns.json · run \`codehere protocols export\`)` });
387
387
  }
388
388
  // injection — codex Round 1 fix: every entry in SHOW_LAYERS must appear
389
389
  // in the overview, including the special-case `injection` layer.
@@ -393,7 +393,7 @@ function gatherLayerSummary(codehereDir, projectRoot) {
393
393
  // setup.
394
394
  const inj = buildMemoryPrefix(projectRoot);
395
395
  if (inj.prefix === '') {
396
- out.push({ layer: 'injection', line: `injection · (nothing — empty / placeholder layers)` });
396
+ out.push({ layer: 'injection', line: `injection · (nothing · empty / placeholder layers)` });
397
397
  }
398
398
  else {
399
399
  const truncNote = inj.truncated ? ' TRUNCATED' : '';
@@ -488,7 +488,7 @@ function executeMemoryShow(layerArg) {
488
488
  const repoMap = join(codehereDir, 'memory/episodic/repo_map.json');
489
489
  if (isSymlink(learnings)) {
490
490
  const target = readlinkSync(learnings);
491
- console.log(` ${success('linked ')} AGENT_LEARNINGS.jsonl → ${target}`);
491
+ console.log(` ${success('linked ')} AGENT_LEARNINGS.jsonl › ${target}`);
492
492
  try {
493
493
  const body = readFileSync(learnings, 'utf8');
494
494
  const count = body.trim().length === 0 ? 0 : body.trim().split('\n').length;
@@ -514,7 +514,7 @@ function executeMemoryShow(layerArg) {
514
514
  return acc + (Array.isArray(symbols) ? symbols.length : 0);
515
515
  }, 0);
516
516
  const topCount = Array.isArray(m.topSymbols) ? m.topSymbols.length : 0;
517
- console.log(` ${success('present')} repo_map.json — ${fileCount} files, ${symbolCount} symbols, top ${topCount} ranked`);
517
+ console.log(` ${success('present')} repo_map.json · ${fileCount} files, ${symbolCount} symbols, top ${topCount} ranked`);
518
518
  if (Array.isArray(m.topSymbols) && m.topSymbols.length > 0) {
519
519
  console.log(` ${colors.dim('top 5:')}`);
520
520
  for (const s of m.topSymbols.slice(0, 5)) {
@@ -527,7 +527,7 @@ function executeMemoryShow(layerArg) {
527
527
  }
528
528
  }
529
529
  else {
530
- console.log(` ${info('absent ')} no repo_map.json — run ${colors.cyan('codehere memory repo-map')}`);
530
+ console.log(` ${info('absent ')} no repo_map.json · run ${colors.cyan('codehere memory repo-map')}`);
531
531
  }
532
532
  break;
533
533
  }
@@ -542,7 +542,7 @@ function executeMemoryShow(layerArg) {
542
542
  .split('\n')
543
543
  .filter((l) => l.length > 0);
544
544
  if (lines.length === 0) {
545
- console.log(` ${info('empty ')} lessons.jsonl is empty — run ${colors.cyan('codehere memory graduate')} (coming in v0.5)`);
545
+ console.log(` ${info('empty ')} lessons.jsonl is empty · run ${colors.cyan('codehere memory graduate')} (coming in v0.5)`);
546
546
  break;
547
547
  }
548
548
  console.log(` ${success('present')} ${lines.length} lesson${lines.length === 1 ? '' : 's'}:`);
@@ -590,7 +590,7 @@ function executeMemoryShow(layerArg) {
590
590
  case 'protocols': {
591
591
  const path = join(codehereDir, 'protocols/hook_patterns.json');
592
592
  if (!existsSync(path)) {
593
- console.log(` ${info('empty ')} no hook_patterns.json — run ${colors.cyan('codehere protocols export')}`);
593
+ console.log(` ${info('empty ')} no hook_patterns.json · run ${colors.cyan('codehere protocols export')}`);
594
594
  break;
595
595
  }
596
596
  try {
@@ -874,44 +874,44 @@ function executeMemoryInit(options) {
874
874
  console.log(` ${success('migrated')} ${label}${extra ? ' ' + colors.dim(extra) : ''}`);
875
875
  break;
876
876
  case 'skipped':
877
- console.log(` ${warning('skipped ')} ${label} ${colors.dim('(target already exists — use --force)')}`);
877
+ console.log(` ${warning('skipped ')} ${label} ${colors.dim('(target already exists · use --force)')}`);
878
878
  break;
879
879
  case 'missing':
880
880
  console.log(` ${info('skipped ')} ${label} ${colors.dim('(source not found)')}`);
881
881
  break;
882
882
  case 'already-migrated':
883
- console.log(` ${info('exists ')} ${label} ${colors.dim('(already migrated — symlink in place)')}`);
883
+ console.log(` ${info('exists ')} ${label} ${colors.dim('(already migrated · symlink in place)')}`);
884
884
  break;
885
885
  }
886
886
  };
887
- report('CONTEXT.md → AGENTS.md', contextOutcome);
888
- report('HANDOVER.md → memory/working/session.md', handoverOutcome);
887
+ report('CONTEXT.md › AGENTS.md', contextOutcome);
888
+ report('HANDOVER.md › memory/working/session.md', handoverOutcome);
889
889
  // PROMPTS.md gets a richer report because of the split semantics.
890
890
  if (promptsResult.outcome === 'migrated') {
891
891
  if (promptsResult.legacyFallback) {
892
- console.log(` ${success('migrated')} PROMPTS.md → skills/_legacy.md ${colors.dim('(no ^## sections found — preserved as-is for manual split)')}`);
892
+ console.log(` ${success('migrated')} PROMPTS.md › skills/_legacy.md ${colors.dim('(no ^## sections found · preserved as-is for manual split)')}`);
893
893
  }
894
894
  else {
895
- console.log(` ${success('migrated')} PROMPTS.md → ${promptsResult.skillsCreated} skill folder${promptsResult.skillsCreated === 1 ? '' : 's'} ${colors.dim('(skills/<slug>/SKILL.md per ^## section)')}`);
895
+ console.log(` ${success('migrated')} PROMPTS.md › ${promptsResult.skillsCreated} skill folder${promptsResult.skillsCreated === 1 ? '' : 's'} ${colors.dim('(skills/<slug>/SKILL.md per ^## section)')}`);
896
896
  }
897
897
  }
898
898
  else {
899
- report('PROMPTS.md → skills/', promptsResult.outcome);
899
+ report('PROMPTS.md › skills/', promptsResult.outcome);
900
900
  }
901
901
  // F3 Task 3 — episodic symlink result.
902
902
  if (episodicResult) {
903
903
  switch (episodicResult.result) {
904
904
  case 'created':
905
- console.log(` ${success('linked ')} memory/episodic/AGENT_LEARNINGS.jsonl → ${episodicResult.targetPath}`);
905
+ console.log(` ${success('linked ')} memory/episodic/AGENT_LEARNINGS.jsonl › ${episodicResult.targetPath}`);
906
906
  break;
907
907
  case 're-linked':
908
- console.log(` ${success('re-linked')} memory/episodic/AGENT_LEARNINGS.jsonl → ${episodicResult.targetPath}`);
908
+ console.log(` ${success('re-linked')} memory/episodic/AGENT_LEARNINGS.jsonl › ${episodicResult.targetPath}`);
909
909
  break;
910
910
  case 'refused':
911
- console.log(` ${error('refused ')} memory/episodic/AGENT_LEARNINGS.jsonl ${colors.dim('(real file or symlinked ancestor — remove first)')}`);
911
+ console.log(` ${error('refused ')} memory/episodic/AGENT_LEARNINGS.jsonl ${colors.dim('(real file or symlinked ancestor · remove first)')}`);
912
912
  break;
913
913
  case 'root-skipped':
914
- console.log(` ${warning('skipped ')} episodic symlink ${colors.dim('(running as root — re-run as your normal user)')}`);
914
+ console.log(` ${warning('skipped ')} episodic symlink ${colors.dim('(running as root · re-run as your normal user)')}`);
915
915
  break;
916
916
  case 'failed':
917
917
  console.log(` ${warning('skipped ')} episodic symlink ${colors.dim('(HOME read-only?)')}`);
@@ -929,9 +929,9 @@ function executeMemoryInit(options) {
929
929
  }
930
930
  newline();
931
931
  console.log(colors.bold('Next:'));
932
- console.log(` 1. Open ${colors.cyan('.codehere/AGENTS.md')} — your project briefing (renamed from CONTEXT.md)`);
933
- console.log(` 2. Edit ${colors.cyan('.codehere/memory/working/session.md')} as you work — supersedes HANDOVER.md`);
934
- console.log(` 3. Each ${colors.cyan('.codehere/skills/<name>/SKILL.md')} is a task-shape template — copy and adapt`);
932
+ console.log(` 1. Open ${colors.cyan('.codehere/AGENTS.md')} · your project briefing (renamed from CONTEXT.md)`);
933
+ console.log(` 2. Edit ${colors.cyan('.codehere/memory/working/session.md')} as you work · supersedes HANDOVER.md`);
934
+ console.log(` 3. Each ${colors.cyan('.codehere/skills/<name>/SKILL.md')} is a task-shape template · copy and adapt`);
935
935
  if (!noSymlinks && anyMigrated) {
936
936
  console.log(` ${colors.dim('(Old paths kept as symlinks for back-compat; safe to delete once teammates have pulled.)')}`);
937
937
  }
@@ -1024,7 +1024,7 @@ codehere memory <action> — F3 project memory operations
1024
1024
  }
1025
1025
  catch (e) {
1026
1026
  const msg = e instanceof Error ? e.message : String(e);
1027
- console.error(`\n ${error('failed')} codehere memory ${action ?? ''} — ${msg}\n`);
1027
+ console.error(`\n ${error('failed')} codehere memory ${action ?? ''} · ${msg}\n`);
1028
1028
  process.exitCode = 1;
1029
1029
  }
1030
1030
  }
@@ -15,8 +15,6 @@
15
15
  * the slice-2 web-UI panel (forthcoming) wraps the same primitive for
16
16
  * non-technical users. The YAML format is the source of truth either way.
17
17
  */
18
- import { existsSync, readFileSync } from 'fs';
19
- import { join } from 'path';
20
18
  import { loadPermissions, addPermissionEntry, removePermissionEntry, } from '../../../infrastructure/security/permissions-config.js';
21
19
  import { colors, success, error, warning, info, newline } from '../../../ui.js';
22
20
  export async function executePermissionCommand(subcommand, pattern, options = {}) {
@@ -48,9 +46,6 @@ export async function executePermissionCommand(subcommand, pattern, options = {}
48
46
  process.exit(1);
49
47
  }
50
48
  }
51
- function permissionsPath() {
52
- return join(process.cwd(), '.codehere', 'permissions.yaml');
53
- }
54
49
  function listPermissions() {
55
50
  const cfg = loadPermissions(process.cwd());
56
51
  if (cfg.allow.length === 0 && cfg.deny.length === 0) {
@@ -70,7 +65,7 @@ function listPermissions() {
70
65
  }
71
66
  }
72
67
  if (cfg.deny.length > 0) {
73
- console.log(warning('deny (reserved — currently no-op):'));
68
+ console.log(warning('deny (reserved · currently no-op):'));
74
69
  for (const e of cfg.deny) {
75
70
  console.log(` • ${e.pattern}`);
76
71
  if (e.reason)
@@ -107,10 +102,4 @@ function removeEntry(pattern) {
107
102
  console.log(success(`Removed ${removed} entr${removed === 1 ? 'y' : 'ies'} for:`), pattern);
108
103
  newline();
109
104
  }
110
- // Ensure file is read on disk after writes (smoke test in unit tests)
111
- export function _readRaw() {
112
- if (!existsSync(permissionsPath()))
113
- return '';
114
- return readFileSync(permissionsPath(), 'utf-8');
115
- }
116
105
  //# sourceMappingURL=permission-command.js.map
@@ -121,8 +121,8 @@ export function buildHookPatternsExport() {
121
121
  generatedAt: new Date().toISOString(),
122
122
  source: 'agent/src/infrastructure/security/trust-scorer.ts',
123
123
  _note: "Exported from codehere's trust-scorer pattern bank via 'codehere protocols export'. " +
124
- "DO NOT hand-edit `patterns_builtin[]` — it's regenerated on every export. " +
125
- 'Add project-specific patterns to `patterns_project[]` — those are preserved across re-exports. ' +
124
+ "DO NOT hand-edit `patterns_builtin[]` · it's regenerated on every export. " +
125
+ 'Add project-specific patterns to `patterns_project[]` · those are preserved across re-exports. ' +
126
126
  'Pattern ids are stable slugs of category + description; reference them deterministically.',
127
127
  patterns_builtin: builtin,
128
128
  patterns_project: [],
@@ -19,7 +19,7 @@ function executeProtocolsExport(options) {
19
19
  const codehereDir = join(root, '.codehere');
20
20
  newline();
21
21
  console.log(colors.bold('codehere protocols export'));
22
- console.log(colors.dim(` exporting trust-scorer pattern bank → ${codehereDir}/protocols/hook_patterns.json`));
22
+ console.log(colors.dim(` exporting trust-scorer pattern bank › ${codehereDir}/protocols/hook_patterns.json`));
23
23
  newline();
24
24
  if (!existsSync(codehereDir)) {
25
25
  console.log(` ${error('refused')} .codehere/ not found at this root.`);
@@ -48,8 +48,8 @@ function executeProtocolsExport(options) {
48
48
  newline();
49
49
  console.log(colors.bold('Next:'));
50
50
  console.log(` - Inspect ${colors.cyan('.codehere/protocols/hook_patterns.json')} to see what the safety hook matches`);
51
- console.log(` - Add project-specific patterns under ${colors.cyan('patterns_project[]')} — they survive re-exports`);
52
- console.log(` ${colors.dim('(Edit patterns_builtin[] is a no-op — re-exports overwrite it from trust-scorer.ts.)')}`);
51
+ console.log(` - Add project-specific patterns under ${colors.cyan('patterns_project[]')} · they survive re-exports`);
52
+ console.log(` ${colors.dim('(Edit patterns_builtin[] is a no-op · re-exports overwrite it from trust-scorer.ts.)')}`);
53
53
  newline();
54
54
  }
55
55
  export function executeProtocolsCommand(action, options = {}) {
@@ -77,7 +77,7 @@ Coming later:
77
77
  }
78
78
  catch (e) {
79
79
  const msg = e instanceof Error ? e.message : String(e);
80
- console.error(`\n ${error('failed')} codehere protocols ${action ?? ''} — ${msg}\n`);
80
+ console.error(`\n ${error('failed')} codehere protocols ${action ?? ''} · ${msg}\n`);
81
81
  process.exitCode = 1;
82
82
  }
83
83
  }
@@ -205,7 +205,7 @@ export function buildRepoMap(repoRoot) {
205
205
  version: 'v0.5',
206
206
  generatedAt: new Date().toISOString(),
207
207
  rootPath: repoRoot,
208
- _note: 'TS-native regex MVP — KNOWN MISSES (v0.5, addressed in v0.6 tree-sitter port): ' +
208
+ _note: 'TS-native regex MVP · KNOWN MISSES (v0.5, addressed in v0.6 tree-sitter port): ' +
209
209
  '`export default function/class` (no name on the export line), ' +
210
210
  '`export { foo, bar }` blocks, `export { foo } from "..."` re-exports, ' +
211
211
  '`export * from "..."` star re-exports, multi-line `export { ... }` blocks, ' +
@@ -20,21 +20,22 @@
20
20
  import { writeFileSync, readFileSync, existsSync } from 'fs';
21
21
  import { basename, relative, join, isAbsolute } from 'path';
22
22
  import { colors, success, error, newline, info } from '../../../ui.js';
23
- import { scoreFile } from '../../../infrastructure/security/trust-scorer.js';
23
+ import { scoreFile, aggregateTrust } from '../../../infrastructure/security/trust-scorer.js';
24
24
  import { collectCodeFiles } from '../../../infrastructure/security/file-collection.js';
25
25
  import { loadProductContext, adjustRecommendation } from '../../../infrastructure/config/product-context.js';
26
26
  // ── Markdown generation ───────────────────────────────────────────────────────
27
- function gradeEmoji(grade) {
28
- return grade === 'A' ? '🟢' : grade === 'B' ? '🟡' : grade === 'C' ? '🟠' : grade === 'D' ? '🔴' : '🚨';
27
+ // S19 glyph language: grades carry the sanctioned geometric set, never emoji.
28
+ function gradeGlyph(grade) {
29
+ return grade === 'A' ? '✓' : grade === 'B' ? '·' : grade === 'C' ? '!' : grade === 'D' ? '✕' : '✕✕';
29
30
  }
30
31
  function severityLabel(severity) {
31
32
  return severity === 'critical'
32
- ? '🚨 Critical'
33
+ ? '✕ Critical'
33
34
  : severity === 'high'
34
- ? '⚠️ High'
35
+ ? '! High'
35
36
  : severity === 'medium'
36
- ? '🔵 Medium'
37
- : '⬇️ Low';
37
+ ? '· Medium'
38
+ : '· Low';
38
39
  }
39
40
  function badgeUrl(score, grade) {
40
41
  const color = grade === 'A'
@@ -57,7 +58,7 @@ function narrativeSummary(report) {
57
58
  ? `${mediumCount} medium-severity pattern${mediumCount !== 1 ? 's' : ''} worth reviewing in a future sprint.`
58
59
  : 'No significant findings.'));
59
60
  }
60
- return `Solid codebase with some medium/low findings. ${overallScore}/100 — worth a review pass but not blocking.`;
61
+ return `Solid codebase with some medium/low findings. ${overallScore}/100 · worth a review pass but not blocking.`;
61
62
  }
62
63
  if (criticalCount > 0) {
63
64
  return (`${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} found that should be resolved before this merges. ` +
@@ -75,13 +76,13 @@ function recommendationsMarkdown(report) {
75
76
  lines.push(`${lines.length + 1}. **Address the ${highCount} high-severity pattern${highCount !== 1 ? 's' : ''} in this sprint.** Not blocking, but worth fixing while the code is fresh.`);
76
77
  }
77
78
  if (mediumCount > 0) {
78
- lines.push(`${lines.length + 1}. **${mediumCount} medium-severity item${mediumCount !== 1 ? 's' : ''} can be queued for a future cleanup pass.** Mostly defensive hygiene — redirect hardening, input validation gaps.`);
79
+ lines.push(`${lines.length + 1}. **${mediumCount} medium-severity item${mediumCount !== 1 ? 's' : ''} can be queued for a future cleanup pass.** Mostly defensive hygiene · redirect hardening, input validation gaps.`);
79
80
  }
80
81
  if (lowCount > 0) {
81
82
  lines.push(`${lines.length + 1}. **${lowCount} low-severity note${lowCount !== 1 ? 's' : ''} for informational purposes.** These rarely cause issues but keep the score from being perfect.`);
82
83
  }
83
84
  if (lines.length === 0) {
84
- lines.push('1. **Nothing blocking.** Keep it up — this is what a healthy codebase looks like.');
85
+ lines.push('1. **Nothing blocking.** Keep it up · this is what a healthy codebase looks like.');
85
86
  }
86
87
  return lines.join('\n\n');
87
88
  }
@@ -116,7 +117,7 @@ function generateMarkdown(report, target, cwd) {
116
117
  const dateStr = new Date(scannedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
117
118
  const badge = badgeUrl(overallScore, grade);
118
119
  const summary = narrativeSummary(report);
119
- const gradeIcon = gradeEmoji(grade);
120
+ const gradeIcon = gradeGlyph(grade);
120
121
  const productCtx = loadProductContext(cwd);
121
122
  const contextLine = productCtx
122
123
  ? `\nProject: **${productCtx.name}** · Risk level: **${productCtx.riskLevel}**${productCtx.productionApp ? ' · Production' : ''}\n`
@@ -138,8 +139,8 @@ ${summary}
138
139
  | | |
139
140
  |---|---|
140
141
  | Files scanned | ${files.length} |
141
- | Critical | ${criticalCount > 0 ? `**${criticalCount}** 🚨` : criticalCount} |
142
- | High | ${highCount > 0 ? `**${highCount}** ⚠️` : highCount} |
142
+ | Critical | ${criticalCount > 0 ? `**${criticalCount}** ✕` : criticalCount} |
143
+ | High | ${highCount > 0 ? `**${highCount}** !` : highCount} |
143
144
  | Medium | ${mediumCount} |
144
145
  | Low | ${lowCount} |
145
146
 
@@ -196,7 +197,7 @@ function generateSimpleReport(report, target) {
196
197
  : grade === 'B'
197
198
  ? 'Good'
198
199
  : grade === 'C'
199
- ? 'Fair — some issues to address'
200
+ ? 'Fair · some issues to address'
200
201
  : grade === 'D'
201
202
  ? 'Needs work'
202
203
  : 'Urgent attention needed';
@@ -207,13 +208,13 @@ function generateSimpleReport(report, target) {
207
208
  if (criticalCount > 0 || highCount > 0 || mediumCount > 0) {
208
209
  lines.push('What was found:');
209
210
  if (criticalCount > 0) {
210
- lines.push(` • ${criticalCount} serious issue${criticalCount !== 1 ? 's' : ''} — things like exposed passwords, unsafe commands, or code that could be exploited`);
211
+ lines.push(` • ${criticalCount} serious issue${criticalCount !== 1 ? 's' : ''} · things like exposed passwords, unsafe commands, or code that could be exploited`);
211
212
  }
212
213
  if (highCount > 0) {
213
- lines.push(` • ${highCount} notable issue${highCount !== 1 ? 's' : ''} — worth fixing, but not an emergency`);
214
+ lines.push(` • ${highCount} notable issue${highCount !== 1 ? 's' : ''} · worth fixing, but not an emergency`);
214
215
  }
215
216
  if (mediumCount > 0) {
216
- lines.push(` • ${mediumCount} minor thing${mediumCount !== 1 ? 's' : ''} — good to know about, low urgency`);
217
+ lines.push(` • ${mediumCount} minor thing${mediumCount !== 1 ? 's' : ''} · good to know about, low urgency`);
217
218
  }
218
219
  lines.push('');
219
220
  }
@@ -231,12 +232,12 @@ function generateSimpleReport(report, target) {
231
232
  lines.push(' 2. This code is safe to continue working on in the meantime.');
232
233
  }
233
234
  else {
234
- lines.push(' 1. Keep going — this code is in good shape.');
235
+ lines.push(' 1. Keep going · this code is in good shape.');
235
236
  lines.push(' 2. Run this scan again before your next release.');
236
237
  }
237
238
  lines.push('');
238
239
  lines.push('---');
239
- lines.push('Generated by codehere — know what your AI agent did before your tech lead asks.');
240
+ lines.push('Generated by codehere · know what your AI agent did before your tech lead asks.');
240
241
  return lines.join('\n');
241
242
  }
242
243
  /**
@@ -259,7 +260,7 @@ function writeReportWithFallback(targetPath, content, label) {
259
260
  catch (e) {
260
261
  const code = e.code ?? '';
261
262
  if (['EROFS', 'EACCES', 'EPERM', 'ENOENT'].includes(code)) {
262
- console.error(error(`Could not write to ${targetPath} (${code}). Printing to stdout instead — pass --out <path> to a writable location.`));
263
+ console.error(error(`Could not write to ${targetPath} (${code}). Printing to stdout instead · pass --out <path> to a writable location.`));
263
264
  process.stdout.write(content + '\n');
264
265
  return false;
265
266
  }
@@ -308,27 +309,42 @@ export async function executeReportCommand(target, options = {}) {
308
309
  if (!effectiveOut && paths.length > 20 && process.stderr.isTTY) {
309
310
  process.stderr.write(colors.dim(` Scanning ${paths.length} files...`));
310
311
  }
311
- const results = paths.map((p) => scoreFile(relative(cwd, p), readFileSync(p, 'utf-8')));
312
+ // S8 · same read semantics as `trust`: unreadable files are skipped, not
313
+ // crashed on — a different read-set was the other way the report could see
314
+ // a different score than the badge for the same target.
315
+ const results = [];
316
+ for (const p of paths) {
317
+ let code;
318
+ try {
319
+ code = readFileSync(p, 'utf-8');
320
+ }
321
+ catch {
322
+ continue;
323
+ }
324
+ results.push(scoreFile(relative(cwd, p), code));
325
+ }
312
326
  // Clear progress line
313
327
  if (!effectiveOut && paths.length > 20 && process.stderr.isTTY) {
314
328
  process.stderr.write('\r\x1b[K');
315
329
  }
316
- const totalFindings = results.reduce((s, r) => s + r.findings.length, 0);
317
- const criticalCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'critical').length, 0);
318
- const highCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'high').length, 0);
319
- const mediumCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'medium').length, 0);
320
- const lowCount = results.reduce((s, r) => s + r.findings.filter((f) => f.severity === 'low').length, 0);
321
- const overallScore = results.length > 0 ? Math.round(results.reduce((s, r) => s + r.score, 0) / results.length) : 100;
322
- const grade = overallScore >= 90 ? 'A' : overallScore >= 75 ? 'B' : overallScore >= 60 ? 'C' : overallScore >= 40 ? 'D' : 'F';
330
+ // S8 · score + grade come from the ONE shared aggregation (trust-scorer's
331
+ // aggregateTrust) — this block used to duplicate trust-command's reduce-and-
332
+ // cap and the badge could disagree with the report for the same target.
333
+ const { overallScore, grade, criticalCount, highCount, mediumCount, lowCount, totalFindings } = aggregateTrust(results);
334
+ // Same rule as trust-command: a score aggregated over zero files is not a clean scan.
335
+ const scanned = results.length > 0;
323
336
  const recommendation = criticalCount > 0
324
337
  ? `${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} need${criticalCount === 1 ? 's' : ''} attention before this merges.`
325
338
  : highCount > 0
326
339
  ? `Nothing critical, but ${highCount} high-severity pattern${highCount !== 1 ? 's' : ''} worth addressing.`
327
- : overallScore >= 75
328
- ? 'Clean scan. Nothing blocking a merge.'
329
- : 'A few things to look at — nothing critical.';
340
+ : !scanned
341
+ ? 'No files scanned — nothing was graded.'
342
+ : overallScore >= 75
343
+ ? 'Clean scan. Nothing blocking a merge.'
344
+ : 'A few things to look at · nothing critical.';
330
345
  const report = {
331
346
  files: results,
347
+ scanned,
332
348
  overallScore,
333
349
  grade,
334
350
  totalFindings,
@@ -372,7 +388,7 @@ export async function executeReportCommand(target, options = {}) {
372
388
  newline();
373
389
  console.log(colors.dim(` ${paths.length} files · score ${overallScore}/100 · grade ${grade}`));
374
390
  if (criticalCount > 0) {
375
- console.log(colors.red(` ${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} — check the report`));
391
+ console.log(colors.red(` ${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} · check the report`));
376
392
  }
377
393
  newline();
378
394
  console.log(colors.dim(' Share it: paste it in your PR, add the badge to your README.'));
@@ -0,0 +1,52 @@
1
+ /**
2
+ * CLI: scan — point codehere at an app, get the ONE thing that will get you hacked, in plain words.
3
+ * Deterministic (orchestrates gitleaks + semgrep), zero model tokens. The 60-second ship-readiness hook
4
+ * for non-experts (docs/PRODUCT_10X.md). Exit 1 when a critical blocker is found (CI-usable).
5
+ */
6
+ import { resolve } from 'node:path';
7
+ import { colors, symbols } from '../../../ui.js';
8
+ import { shipScan } from '../../../infrastructure/security/ship-scan.js';
9
+ export async function executeScanCommand(target, options = {}) {
10
+ const dir = resolve(target || '.');
11
+ const r = shipScan(dir);
12
+ if (options.json) {
13
+ process.stdout.write(JSON.stringify(r, null, 2) + '\n');
14
+ return r.top && r.top.tier === 'critical' ? 1 : 0;
15
+ }
16
+ if (r.missing.length) {
17
+ process.stdout.write(colors.dim(`(${r.missing.join(' + ')} not installed — coverage reduced. brew install ${r.missing.join(' ')})\n`));
18
+ }
19
+ if (r.failed.length) {
20
+ process.stdout.write(colors.dim(`(${r.failed.join(' + ')} did not finish — that part of the app went unscanned.)\n`));
21
+ }
22
+ if (!r.top) {
23
+ // Name the scanners that ACTUALLY ran. The old copy hardcoded "gitleaks + semgrep" and printed
24
+ // "Observed, not assumed" even when semgrep was absent or had timed out — the product asserting
25
+ // a scan it never performed, which is the exact failure it exists to catch elsewhere.
26
+ const ran = [r.ran.gitleaks && 'gitleaks', r.ran.semgrep && 'semgrep', 'codehere’s own checks'].filter(Boolean);
27
+ const partial = r.missing.length > 0 || r.failed.length > 0;
28
+ process.stdout.write('\n' +
29
+ // Green is for a clean pass at full coverage. A hole in coverage is not a pass, so it does not
30
+ // get the green — it gets a plain line that says what was and wasn't looked at.
31
+ (partial
32
+ ? `${symbols.info} No ship-blockers found in what was scanned`
33
+ : colors.success(`${symbols.success} No ship-blockers found`)) +
34
+ colors.dim(` — ${ran.join(' + ')} ran on this app and nothing critical surfaced.\n` +
35
+ (partial ? `Coverage was incomplete, so this is not an all-clear.\n` : '') +
36
+ `Observed, not assumed. (Fast pass on what actually gets apps hacked — not a full audit.)\n\n`));
37
+ return 0;
38
+ }
39
+ const t = r.top;
40
+ const crit = r.findings.filter((f) => f.tier === 'critical').length;
41
+ const rest = r.findings.length - 1;
42
+ // THE ONE — consequence first, plain language (the EPAM lesson: surface + don't bury it).
43
+ process.stdout.write('\n' + colors.error(`${symbols.error} 1 thing will get you hacked:`) + '\n\n');
44
+ process.stdout.write(` ${t.what}\n`);
45
+ process.stdout.write(colors.dim(` → ${t.file}:${t.line} (found by ${t.tool}: ${t.ruleId})\n\n`));
46
+ if (rest > 0) {
47
+ const alsoCrit = crit > 1 ? ` (${crit - 1} also critical)` : '';
48
+ process.stdout.write(colors.dim(`Plus ${rest} more issue${rest > 1 ? 's' : ''}${alsoCrit} — fix the one above first.\n\n`));
49
+ }
50
+ return 1; // not safe to ship
51
+ }
52
+ //# sourceMappingURL=scan-command.js.map
@@ -16,8 +16,24 @@
16
16
  * Usage (by hook system, not user):
17
17
  * echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | codehere scan-tool
18
18
  */
19
- import { processHookEvent, appendOverrideAudit, } from '../../../infrastructure/hooks/claude-sdk-hook.js';
19
+ import { processHookEvent, appendOverrideAudit, enableHookAuditQueue, flushHookAuditQueue, } from '../../../infrastructure/hooks/claude-sdk-hook.js';
20
+ /** R1.2 · every exit path flushes the queued audit entries first: each is
21
+ * offered to the daemon (250ms cap) and falls back to a direct append marked
22
+ * writer:'hook-direct'. The flush itself never throws — the user's tool call
23
+ * must never break or hang on audit plumbing (core value #5). */
24
+ async function flushAndExit(code) {
25
+ try {
26
+ await flushHookAuditQueue();
27
+ }
28
+ catch {
29
+ /* fail-open */
30
+ }
31
+ process.exit(code);
32
+ }
20
33
  export async function executeScanToolCommand(options) {
34
+ // R1.2 · queue mode: appends in this hook process buffer instead of writing,
35
+ // so the flush can offer them to the daemon (the sole-writer path).
36
+ enableHookAuditQueue();
21
37
  // Read all stdin
22
38
  let raw = '';
23
39
  try {
@@ -29,10 +45,10 @@ export async function executeScanToolCommand(options) {
29
45
  // If stdin read fails, allow the tool call — but log the override so
30
46
  // the audit trail records that the scanner didn't get a chance to run.
31
47
  appendOverrideAudit('stdin_read_failure');
32
- process.exit(0);
48
+ return flushAndExit(0);
33
49
  }
34
50
  if (!raw.trim()) {
35
- process.exit(0);
51
+ return flushAndExit(0);
36
52
  }
37
53
  // Parse event JSON
38
54
  let event;
@@ -49,7 +65,7 @@ export async function executeScanToolCommand(options) {
49
65
  if (options.debug) {
50
66
  process.stderr.write('[codehere] Warning: could not parse hook event JSON\n');
51
67
  }
52
- process.exit(0);
68
+ return flushAndExit(0);
53
69
  }
54
70
  try {
55
71
  const decision = processHookEvent(event);
@@ -67,17 +83,17 @@ export async function executeScanToolCommand(options) {
67
83
  process.stderr.write(`[codehere] Would block: ${decision.reason}\n`);
68
84
  }
69
85
  }
70
- process.exit(0);
86
+ return flushAndExit(0);
71
87
  }
72
88
  if (decision?.decision === 'block') {
73
89
  // Output block decision as JSON (Claude Code hook contract)
74
90
  process.stdout.write(JSON.stringify(decision) + '\n');
75
91
  // Also write to stderr for user visibility
76
92
  process.stderr.write(`\n${decision.reason}\n`);
77
- process.exit(2); // non-zero exit = Claude Code treats as block signal
93
+ return flushAndExit(2); // non-zero exit = Claude Code treats as block signal
78
94
  }
79
95
  // Allow — exit 0 with no output
80
- process.exit(0);
96
+ return flushAndExit(0);
81
97
  }
82
98
  catch {
83
99
  // Any scan failure: fail open (allow), never break developer workflow.
@@ -88,7 +104,7 @@ export async function executeScanToolCommand(options) {
88
104
  sessionId: event.session_id,
89
105
  toolName: event.tool_name,
90
106
  });
91
- process.exit(0);
107
+ return flushAndExit(0);
92
108
  }
93
109
  }
94
110
  //# sourceMappingURL=scan-tool-command.js.map