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.
- package/README.md +22 -13
- package/dist/application/services/verdict-service.js +154 -0
- package/dist/application/use-cases/assess-cross-vendor.js +126 -0
- package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
- package/dist/application/use-cases/export-aat.js +107 -0
- package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
- package/dist/application/use-cases/generate-final-report.js +140 -15
- package/dist/application/use-cases/generate-narration.js +5 -3
- package/dist/application/use-cases/generate-thread-title.js +5 -2
- package/dist/application/use-cases/plan-auto-review.js +57 -0
- package/dist/application/use-cases/reconcile-audit.js +100 -0
- package/dist/application/use-cases/rerun-tests.js +226 -0
- package/dist/application/use-cases/search-sessions.js +70 -0
- package/dist/application/use-cases/should-auto-review.js +71 -0
- package/dist/application/use-cases/thread-export.js +90 -0
- package/dist/application/use-cases/verify-claim.js +187 -5
- package/dist/domain/audit.js +11 -0
- package/dist/domain/services/codebase-detector.js +0 -6
- package/dist/domain/task.js +91 -0
- package/dist/error-handler.js +3 -115
- package/dist/index.js +269 -25
- package/dist/infrastructure/completion/bash-completion.js +7 -3
- package/dist/infrastructure/completion/fish-completion.js +7 -12
- package/dist/infrastructure/completion/zsh-completion.js +7 -12
- package/dist/infrastructure/config/config-manager.js +4 -3
- package/dist/infrastructure/config/product-context.js +1 -19
- package/dist/infrastructure/context/adapters/figma-source.js +37 -2
- package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
- package/dist/infrastructure/context/github-api.js +110 -0
- package/dist/infrastructure/context/github-device-flow.js +65 -0
- package/dist/infrastructure/context/oauth-store.js +5 -2
- package/dist/infrastructure/context/registry-bootstrap.js +0 -4
- package/dist/infrastructure/cost/budget-config.js +3 -2
- package/dist/infrastructure/cost/cost-router.js +16 -23
- package/dist/infrastructure/cost/spend-aggregator.js +90 -15
- package/dist/infrastructure/cost/team-ledger.js +355 -0
- package/dist/infrastructure/discovery/discovery-log.js +85 -0
- package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +133 -3
- package/dist/infrastructure/hooks/hook-mode.js +20 -4
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
- package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
- package/dist/infrastructure/observability/execution-tracer.js +1 -1
- package/dist/infrastructure/orchestration/agents-config-seed.js +22 -10
- package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
- package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
- package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
- package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
- package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
- package/dist/infrastructure/orchestration/context-handoff.js +32 -14
- package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
- package/dist/infrastructure/orchestration/failover-config.js +0 -10
- package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +132 -1
- package/dist/infrastructure/orchestration/graphify-context.js +14 -59
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +164 -9
- package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
- package/dist/infrastructure/orchestration/progress-aggregator.js +238 -21
- package/dist/infrastructure/orchestration/session-manager.js +593 -94
- package/dist/infrastructure/orchestration/session-persistence.js +177 -41
- package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
- package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +1462 -380
- package/dist/infrastructure/protocols/mcp-tools.js +401 -0
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
- package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
- package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
- package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
- package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
- package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
- package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
- package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
- package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
- package/dist/infrastructure/protocols/ui/styles.js +2147 -0
- package/dist/infrastructure/protocols/ui/tokens.js +96 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +116 -4214
- package/dist/infrastructure/protocols/verify-page.js +849 -155
- package/dist/infrastructure/security/fix-code.js +144 -0
- package/dist/infrastructure/security/fix-secret.js +89 -0
- package/dist/infrastructure/security/permissions-config.js +3 -2
- package/dist/infrastructure/security/scan-types.js +10 -0
- package/dist/infrastructure/security/ship-scan.js +195 -0
- package/dist/infrastructure/security/trust-scorer.js +70 -3
- package/dist/infrastructure/security/vibe-checks.js +99 -0
- package/dist/infrastructure/storage/atomic-write.js +86 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +150 -3
- package/dist/infrastructure/storage/project-thread-reader.js +261 -24
- package/dist/monitoring.js +2 -2
- package/dist/presentation/cli/commands/audit-command.js +79 -4
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/check-command.js +96 -0
- package/dist/presentation/cli/commands/context-source-command.js +20 -1
- package/dist/presentation/cli/commands/help-command.js +5 -5
- package/dist/presentation/cli/commands/hook-command.js +5 -5
- package/dist/presentation/cli/commands/init-command.js +26 -14
- package/dist/presentation/cli/commands/memory-command.js +28 -28
- package/dist/presentation/cli/commands/permission-command.js +1 -12
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/protocols-command.js +4 -4
- package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
- package/dist/presentation/cli/commands/report-command.js +48 -32
- package/dist/presentation/cli/commands/scan-command.js +52 -0
- package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
- package/dist/presentation/cli/commands/search-command.js +51 -0
- package/dist/presentation/cli/commands/setup-command.js +31 -8
- package/dist/presentation/cli/commands/status-command.js +39 -86
- package/dist/presentation/cli/commands/team-command.js +256 -0
- package/dist/presentation/cli/commands/thread-command.js +87 -5
- package/dist/presentation/cli/commands/trust-command.js +53 -31
- package/dist/presentation/cli/commands/verify-command.js +74 -9
- package/dist/presentation/cli/error-display.js +0 -8
- package/dist/presentation/cli/repl.js +15 -7
- package/dist/presentation/cli/task-deliverable.js +36 -10
- package/dist/testing/replay-adapter.js +0 -19
- package/dist/ui.js +51 -239
- package/dist/utils/version.js +0 -6
- package/package.json +30 -10
- package/dist/domain/entities/task.js +0 -61
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- 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)}
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
883
|
+
console.log(` ${info('exists ')} ${label} ${colors.dim('(already migrated · symlink in place)')}`);
|
|
884
884
|
break;
|
|
885
885
|
}
|
|
886
886
|
};
|
|
887
|
-
report('CONTEXT.md
|
|
888
|
-
report('HANDOVER.md
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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')}
|
|
933
|
-
console.log(` 2. Edit ${colors.cyan('.codehere/memory/working/session.md')} as you work
|
|
934
|
-
console.log(` 3. Each ${colors.cyan('.codehere/skills/<name>/SKILL.md')} is a task-shape template
|
|
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 ?? ''}
|
|
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
|
|
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[]`
|
|
125
|
-
'Add project-specific patterns to `patterns_project[]`
|
|
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
|
|
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[]')}
|
|
52
|
-
console.log(` ${colors.dim('(Edit patterns_builtin[] is a no-op
|
|
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 ?? ''}
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
? '
|
|
33
|
+
? '✕ Critical'
|
|
33
34
|
: severity === 'high'
|
|
34
|
-
? '
|
|
35
|
+
? '! High'
|
|
35
36
|
: severity === 'medium'
|
|
36
|
-
? '
|
|
37
|
-
: '
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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}**
|
|
142
|
-
| High | ${highCount > 0 ? `**${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
|
|
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' : ''}
|
|
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' : ''}
|
|
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' : ''}
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
const
|
|
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
|
-
:
|
|
328
|
-
? '
|
|
329
|
-
:
|
|
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' : ''}
|
|
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
|
-
|
|
48
|
+
return flushAndExit(0);
|
|
33
49
|
}
|
|
34
50
|
if (!raw.trim()) {
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
+
return flushAndExit(2); // non-zero exit = Claude Code treats as block signal
|
|
78
94
|
}
|
|
79
95
|
// Allow — exit 0 with no output
|
|
80
|
-
|
|
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
|
-
|
|
107
|
+
return flushAndExit(0);
|
|
92
108
|
}
|
|
93
109
|
}
|
|
94
110
|
//# sourceMappingURL=scan-tool-command.js.map
|