codehere 1.0.1 → 1.0.3
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 +35 -193
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +110 -7
- package/dist/index.js +38 -1212
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
- package/dist/presentation/cli/commands/check-command.js +178 -23
- package/dist/presentation/cli/commands/help-command.js +21 -285
- package/dist/ui.js +5 -236
- package/dist/utils/version.js +0 -6
- package/package.json +9 -38
- package/dist/application/services/verdict-service.js +0 -154
- package/dist/application/use-cases/assess-cross-vendor.js +0 -126
- package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/export-aat.js +0 -107
- package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
- package/dist/application/use-cases/generate-final-report.js +0 -259
- package/dist/application/use-cases/generate-narration.js +0 -130
- package/dist/application/use-cases/generate-thread-title.js +0 -181
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/plan-auto-review.js +0 -57
- package/dist/application/use-cases/reconcile-audit.js +0 -100
- package/dist/application/use-cases/search-sessions.js +0 -70
- package/dist/application/use-cases/should-auto-review.js +0 -71
- package/dist/application/use-cases/thread-export.js +0 -245
- package/dist/application/use-cases/verify-claim.js +0 -456
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/audit.js +0 -11
- package/dist/domain/services/codebase-detector.js +0 -85
- package/dist/domain/task.js +0 -91
- package/dist/error-handler.js +0 -139
- package/dist/infrastructure/completion/bash-completion.js +0 -29
- package/dist/infrastructure/completion/fish-completion.js +0 -17
- package/dist/infrastructure/completion/zsh-completion.js +0 -25
- package/dist/infrastructure/config/config-manager.js +0 -174
- package/dist/infrastructure/config/product-context.js +0 -49
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -198
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/github-api.js +0 -110
- package/dist/infrastructure/context/github-device-flow.js +0 -65
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -122
- package/dist/infrastructure/context/registry-bootstrap.js +0 -95
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -114
- package/dist/infrastructure/cost/cost-router.js +0 -97
- package/dist/infrastructure/cost/spend-aggregator.js +0 -222
- package/dist/infrastructure/cost/team-ledger.js +0 -355
- package/dist/infrastructure/discovery/discovery-log.js +0 -85
- package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
- package/dist/infrastructure/hooks/hook-mode.js +0 -87
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
- package/dist/infrastructure/orchestration/context-handoff.js +0 -162
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -67
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
- package/dist/infrastructure/orchestration/graphify-context.js +0 -209
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1590
- package/dist/infrastructure/orchestration/session-persistence.js +0 -394
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
- package/dist/infrastructure/protocols/mcp-tools.js +0 -401
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
- package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
- package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
- package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
- package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
- package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
- package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
- package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
- package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
- package/dist/infrastructure/protocols/ui/styles.js +0 -2147
- package/dist/infrastructure/protocols/ui/tokens.js +0 -96
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
- package/dist/infrastructure/protocols/verify-page.js +0 -1677
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/fix-code.js +0 -144
- package/dist/infrastructure/security/fix-secret.js +0 -89
- package/dist/infrastructure/security/permissions-config.js +0 -235
- package/dist/infrastructure/security/scan-types.js +0 -10
- package/dist/infrastructure/security/ship-scan.js +0 -195
- package/dist/infrastructure/security/trust-scorer.js +0 -724
- package/dist/infrastructure/security/vibe-checks.js +0 -99
- package/dist/infrastructure/storage/atomic-write.js +0 -86
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
- package/dist/infrastructure/storage/project-thread-reader.js +0 -409
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -201
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -217
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -590
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -105
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -403
- package/dist/presentation/cli/commands/scan-command.js +0 -52
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
- package/dist/presentation/cli/commands/search-command.js +0 -51
- package/dist/presentation/cli/commands/setup-command.js +0 -330
- package/dist/presentation/cli/commands/status-command.js +0 -614
- package/dist/presentation/cli/commands/team-command.js +0 -256
- package/dist/presentation/cli/commands/thread-command.js +0 -370
- package/dist/presentation/cli/commands/trust-command.js +0 -328
- package/dist/presentation/cli/commands/verify-command.js +0 -298
- package/dist/presentation/cli/error-display.js +0 -328
- package/dist/presentation/cli/repl.js +0 -805
- package/dist/presentation/cli/task-deliverable.js +0 -153
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -147
package/README.md
CHANGED
|
@@ -1,223 +1,65 @@
|
|
|
1
1
|
# codehere
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
>
|
|
3
|
+
> **Re-runs the tests an AI says passed, and tells you whether they did.**
|
|
4
|
+
> `codehere check` on your machine, or the GitHub Action on every pull request. No account.
|
|
5
5
|
|
|
6
|
-
[](../LICENSE)
|
|
8
|
-
[]()
|
|
9
|
-
[](../docs/PRODUCT_STATE.md)
|
|
6
|
+
[](https://github.com/muhammadegaa/codehere/blob/main/LICENSE)
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
`codehere check` re-runs your project's tests itself and reports the real result, so you do not have to take an agent's "all tests pass" on trust. It detects `npm test`, `node --test`, `pytest`, `go test`, `cargo test` and vitest or jest configs.
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
## Install
|
|
14
11
|
|
|
15
|
-
> **Honest status** — 1371 internal tests verify codehere's code against codehere's own assumptions about what each adapter emits, plus an end-to-end suite (`npm run e2e`) that drives the real product in a browser. The current verified / planned / blocked state of every surface lives in [`docs/PRODUCT_STATE.md`](../docs/PRODUCT_STATE.md) and the capability scoreboard [`docs/CAPABILITIES.md`](../docs/CAPABILITIES.md).
|
|
16
|
-
|
|
17
|
-
The enterprise tier is owned by [OpenAI Frontier](https://openai.com/index/introducing-openai-frontier/) (8-14 weeks of deploy prereqs, sales-team gated) and [Anthropic Cowork](https://www.anthropic.com/product/claude-cowork) (paid Claude subscription, single-vendor). Codehere fills the gap they structurally can't reach: **the individual / small-team / no-IT-department tier, vendor-neutral**.
|
|
18
|
-
|
|
19
|
-
> See [`docs/STRATEGIC_VISION.md`](../docs/STRATEGIC_VISION.md) Appendix C and [`docs/figures/positioning-matrix.svg`](../docs/figures/positioning-matrix.svg) for the full positioning chart.
|
|
20
|
-
|
|
21
|
-
5 vendors supported, 2 proven on a real billed run. Your record, your machine, nobody's vendor.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## Recipe — setup to first audit in 30 minutes
|
|
26
|
-
|
|
27
|
-
> One folder convention, one prompt formula, four ready-to-use prompts.
|
|
28
|
-
|
|
29
|
-
### Setup (once)
|
|
30
|
-
|
|
31
|
-
| | | |
|
|
32
|
-
|---|---|---|
|
|
33
|
-
| 1 | **Install** | `npm install -g codehere` (Node 20+) — v0.8.0 is the current published stable (`@latest`) |
|
|
34
|
-
| 2 | **Detect environment** | `codehere setup` — checks API key, finds your repo |
|
|
35
|
-
| 3 | **Install the safety hook** | `codehere hook install` — every Claude Code tool call now audited |
|
|
36
|
-
| 4 | **Install at least one specialist** | Claude Code: `npm i -g @anthropic-ai/claude-code` (Codex / Aider / OpenCode / Gemini also supported · see [docs/integrations/](../docs/integrations/) for Cursor MCP setup) |
|
|
37
|
-
| 5 | **Scaffold this repo** | `cd /your/repo && codehere init` — creates `.codehere/` with the F3 memory tree (working / episodic / semantic / personal / candidates) plus `AGENTS.md` (project briefing) and `skills/` (prompt templates) |
|
|
38
|
-
| 6 | **Fill in AGENTS.md** | 5 minutes. The agent will follow what's written there. |
|
|
39
|
-
| 7 | **Build the symbol map (optional)** | `codehere memory repo-map` — JSON map of every top-level export in your repo, ranked by reference count. Spawned agents read it on entry instead of cold-grepping. |
|
|
40
|
-
|
|
41
|
-
You're done. `codehere start` opens the web UI at `:3773`; you delegate from there.
|
|
42
|
-
|
|
43
|
-
### The prompt formula
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
Context + End State + Constraints
|
|
47
12
|
```
|
|
48
|
-
|
|
49
|
-
- **Context** — *"Read .codehere/CONTEXT.md"* + any specific files
|
|
50
|
-
- **End State** — the user-visible change, the test that passes, the file that exists
|
|
51
|
-
- **Constraints** — files not to touch, dependencies not to add, patterns not to use
|
|
52
|
-
|
|
53
|
-
### Folder convention (what `codehere init` writes)
|
|
54
|
-
|
|
55
|
-
The v0.6 layout — F3 project memory:
|
|
56
|
-
|
|
57
|
-
| Path | Purpose | Committed? |
|
|
58
|
-
|---|---|---|
|
|
59
|
-
| `.codehere/AGENTS.md` *(or legacy `CONTEXT.md`)* | What this repo is. Conventions. Style. Constraints. | yes |
|
|
60
|
-
| `.codehere/memory/working/session.md` *(or legacy `HANDOVER.md`)* | What's in flight in the current session. | **no** |
|
|
61
|
-
| `.codehere/memory/episodic/AGENT_LEARNINGS.jsonl` | Symlink to your full session history at `~/.codehere/sessions.jsonl`. | (symlink) |
|
|
62
|
-
| `.codehere/memory/episodic/repo_map.json` | Ranked symbol map. Built by `codehere memory repo-map`. | yes |
|
|
63
|
-
| `.codehere/memory/semantic/lessons.jsonl` | Project-specific lessons promoted from episodic patterns. | yes |
|
|
64
|
-
| `.codehere/memory/personal/preferences.json` | Per-developer prefs. | **no** |
|
|
65
|
-
| `.codehere/skills/<name>/SKILL.md` *(or legacy flat `PROMPTS.md`)* | Prompt templates — one folder per skill. | yes |
|
|
66
|
-
| `.codehere/protocols/permissions.md` + `hook_patterns.json` | Safety policy for the PreToolUse hook. | yes |
|
|
67
|
-
|
|
68
|
-
Upgrading from v0.4.1? Run `codehere memory init` once — it migrates `CONTEXT/HANDOVER/PROMPTS` into the new layout and leaves symlinks at the old paths so back-compat holds. `--dry-run` shows what would move first.
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## What you see when you delegate a task
|
|
73
|
-
|
|
74
|
-
`codehere start` opens `http://127.0.0.1:3773`. Pick an agent, type a task, hit **Assign**. codehere:
|
|
75
|
-
|
|
76
|
-
1. Spawns the agent with the right headless flags
|
|
77
|
-
2. Streams the steps back as readable progress · *"Read src/auth.ts · Edit src/auth.ts · Run npm test"*
|
|
78
|
-
3. For Claude Code tasks: routes every tool call through the codehere safety hook — you see *"🛡 5 safe · 0 blocked"* grow live. For Codex / Aider / OpenCode / Gemini tasks: emits the agent's native event stream (no per-tool blocking yet)
|
|
79
|
-
4. On completion, hands you a **Final report**: files changed with `+/-` lines, trust score per file, test pass/fail, token usage
|
|
80
|
-
|
|
81
|
-
Claude Code tool calls are logged to `~/.codehere/audit/hook-events.jsonl`; spawned-task lifecycle (any adapter) is logged to `~/.codehere/sessions.jsonl`. Open-source MIT. Node 20+.
|
|
82
|
-
|
|
83
|
-
### Just the safety hook (no agents required)
|
|
84
|
-
|
|
85
|
-
If you only want the audit-and-block layer for AI tools you already use:
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
codehere hook install # activates the PreToolUse hook in your global ~/.claude/settings.json
|
|
89
|
-
codehere trust . # static trust scan of the current directory
|
|
90
|
-
codehere report # generates a shareable markdown audit report
|
|
91
|
-
codehere status # quick dashboard · sessions, blocks, trust trends
|
|
13
|
+
npx codehere check
|
|
92
14
|
```
|
|
93
15
|
|
|
94
|
-
The
|
|
95
|
-
|
|
96
|
-
## Memory · F3 (v0.6)
|
|
97
|
-
|
|
98
|
-
Once you've run `codehere init` in a project, every spawned agent — Claude Code, Codex, Aider, OpenCode, or Gemini — automatically reads the project's memory on entry. The agent starts warm with the briefing, recent lessons, and the most-referenced symbols in your codebase, instead of cold-grepping every session.
|
|
99
|
-
|
|
100
|
-
**What gets injected, in priority order:**
|
|
101
|
-
1. **`.codehere/AGENTS.md`** — your project briefing (conventions, style, constraints)
|
|
102
|
-
2. **`.codehere/memory/working/session.md`** — what's in flight right now (per-machine, gitignored)
|
|
103
|
-
3. **`.codehere/memory/semantic/lessons.jsonl`** — graduated project-wide lessons
|
|
104
|
-
4. **`.codehere/memory/episodic/repo_map.json`** — top-N most-referenced symbols (run `codehere memory repo-map` to build / refresh)
|
|
105
|
-
|
|
106
|
-
The injection is token-budget-aware (default ~3000 tokens, drops lower-priority sections first to fit; AGENTS.md is never dropped). Pass `--no-memory-prefix` if you want the bare prompt for a one-off task.
|
|
16
|
+
## The four commands
|
|
107
17
|
|
|
108
|
-
**Inspect what would inject** before spawning anything:
|
|
109
|
-
```bash
|
|
110
|
-
codehere memory show injection
|
|
111
18
|
```
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
codehere
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
**See everything codehere has accumulated:**
|
|
119
|
-
```bash
|
|
120
|
-
codehere memory show # one-line summary per layer
|
|
121
|
-
codehere memory show semantic # numbered lesson list
|
|
122
|
-
codehere memory show episodic # session count + top symbols
|
|
123
|
-
codehere memory show protocols # safety patterns by category
|
|
19
|
+
codehere check # re-run the tests here
|
|
20
|
+
codehere check --base origin/main # also read everything this branch changed
|
|
21
|
+
codehere check --claim "<the agent's summary>" # flag test files it named that do not exist
|
|
22
|
+
codehere check --proof # a receipt you can paste into a pull request
|
|
124
23
|
```
|
|
125
24
|
|
|
126
|
-
|
|
127
|
-
- `codehere memory init` — migrate the v0.4.1 layout (`CONTEXT.md` / `HANDOVER.md` / `PROMPTS.md`) into the F3 layout (one shot, leaves back-compat symlinks; `--dry-run` previews)
|
|
128
|
-
- `codehere memory repo-map [--rebuild]` — build the symbol map; currently TS-native MVP
|
|
129
|
-
- `codehere protocols export [--rebuild]` — write the trust-scorer pattern bank to `.codehere/protocols/hook_patterns.json` for inspection / cross-tool ingestion
|
|
25
|
+
`codehere help check` lists the options. `--json` adds a `verdict` (`pass`, `fail`, `unverified` or `skipped`) and the receipt text.
|
|
130
26
|
|
|
131
|
-
|
|
27
|
+
Exit codes:
|
|
132
28
|
|
|
133
|
-
|
|
29
|
+
- **0**: the tests ran and passed, no test was weakened, no named test file is missing, and the weakened-test scan finished.
|
|
30
|
+
- **1**: the tests failed; the runner reported no test that ran (0 tests, or every test skipped); the change made a test check nothing (an assertion swapped for `assert True` or `assert.ok(true)`, a skip marker, an assertion commented out, or a `return` above it); or a test file named in `--claim` is not in the repository. Usage errors (an unknown `--base`, a missing folder, an unknown option) also exit 1, with the message on stderr and nothing on stdout, and so does an internal error. Pass the claim as `--claim=<text>`: in `--claim --json` the claim is the text `--json`.
|
|
31
|
+
- **2**: nothing could be verified: the run timed out, no test command was recognized, the runner is not installed, the re-run was turned off, or the weakened-test scan could not run (git is not installed, git could not read the repository, or the diff could not be read).
|
|
134
32
|
|
|
135
|
-
|
|
136
|
-
|---|---|
|
|
137
|
-
| [`docs/BLUEPRINT.md`](../docs/BLUEPRINT.md) | The HOW — system overview, module map, data flow, adapter contracts, audit chain, failover, F3 memory, cost-bounded execution |
|
|
138
|
-
| [`docs/THREAT_MODEL.md`](../docs/THREAT_MODEL.md) | Every attack surface, current mitigation, residual risk, planned hardening — npm supply chain, hook bypass, symlink redirect, audit log tampering, MCP server, prompt injection |
|
|
139
|
-
| [`docs/CAPABILITIES.md`](../docs/CAPABILITIES.md) | Honest matrix of what each adapter can and cannot do today — Claude Code, Codex, Aider, OpenCode, Gemini, Generic CLI — across spawn, stream, per-tool block, audit, F3 memory, auth |
|
|
140
|
-
| [`agent/REPO_MAP.md`](./REPO_MAP.md) | One-line-per-file index of the 54 source files with their top-level exports — generated, regenerated via `npm run repomap` after renames |
|
|
33
|
+
Settings, as environment variables: `CODEHERE_TEST_CMD` replaces the detected test command (split on whitespace, no shell); `CODEHERE_RERUN_TIMEOUT_MS` sets the time limit in whole milliseconds (default 120000, two minutes, also used for a value that is not a whole number from 1 to 2147483647); `CODEHERE_RERUN_TESTS=0` turns the re-run off. Detection covers `npm test` (a `test` script in `package.json`), `go test ./...`, `cargo test`, `pytest` (with `pytest.ini`, `conftest.py` or a pytest section in `pyproject.toml` or `setup.cfg`), `vitest.config.ts`/`.js`, `jest.config.js`/`.ts`, and `node --test` for `*.test.js` files.
|
|
141
34
|
|
|
142
|
-
|
|
35
|
+
"No test ran" is read from the runner's own summary for node:test, pytest, jest, vitest and Go's `[no test files]`. A Go package with test files but no tests (`[no tests to run]`), `cargo test` with 0 tests, jest with `--passWithNoTests`, mocha, and a test script that only prints "No tests found" read as a pass. A test script that leaves a background process holding its output open is reported as timed out, even when the tests passed.
|
|
143
36
|
|
|
144
|
-
|
|
145
|
-
codehere start · launch the task intake web UI at :3773
|
|
146
|
-
codehere task <prompt> · spawn a task from the CLI (requires `start` running)
|
|
147
|
-
codehere kill <sessionId> · kill a running task
|
|
148
|
-
codehere serve · start as MCP server for Claude Desktop, Cursor, etc.
|
|
149
|
-
codehere status · dashboard · sessions, blocked calls, audit trends
|
|
150
|
-
codehere trust [path] · static trust scan · score 0-100, grade A-F
|
|
151
|
-
codehere report · generate a shareable markdown audit report
|
|
152
|
-
codehere hook · install/uninstall/inspect the PreToolUse safety hook
|
|
153
|
-
codehere permission · manage this folder's allow-list (skip false-positive blocks)
|
|
154
|
-
codehere budget · daily-spend ceiling · `set <usd>` blocks paid spawns at the cap
|
|
155
|
-
codehere narrator · customize the PM narrator prompt (~/.codehere/narrator-prompt.md)
|
|
156
|
-
codehere setup · interactive first-run setup wizard
|
|
157
|
-
codehere config · manage configuration and preferences
|
|
158
|
-
codehere health · system health check
|
|
159
|
-
codehere help · show help (use --all for hidden commands)
|
|
160
|
-
codehere completion <shell> · generate bash/zsh/fish completion script
|
|
161
|
-
codehere scan-tool · internal · used by the hook system, not by users
|
|
162
|
-
```
|
|
37
|
+
## GitHub Action
|
|
163
38
|
|
|
164
|
-
|
|
39
|
+
A GitHub Action that posts the receipt on each pull request is in [`action/`](https://github.com/muhammadegaa/codehere/tree/main/action). It runs `npx codehere@<version> check --json --base origin/<base> --claim=<PR body>`. Its default version is 1.0.2. The 120-second default time limit applies, and the Action has no input to change it. With the default `fail-on-unverified: false`, a pull request that makes the suite unrunnable (removes the test script, breaks the runner) gets `unverified` and a green job with a warning, and a `skipped` verdict (the re-run turned off in the workflow) gets a green job with no warning. Set `fail-on-unverified: true` to fail the job on `unverified`. It is not published to the marketplace.
|
|
165
40
|
|
|
166
|
-
## What it
|
|
41
|
+
## What it does not catch
|
|
167
42
|
|
|
168
|
-
The
|
|
43
|
+
- **Weakened tests.** The detector reads Python (`test_*.py`, `*_test.py`), JavaScript and TypeScript (`*.test.*`, `*.spec.*`), Go (`*_test.go`) and PHP (`*Test.php`) test files, and files under `test`, `tests` or `__tests__`. It flags an assertion replaced by an always-true one (`assert True`, `assert.ok(true)`, `assertTrue(True)`, a value compared with itself), a skip marker (including `skipif(True)`, `it.only(`, `test.only(` or `describe.only(` outside strings and `//` comments (including `test.describe.only(`), and in JavaScript, TypeScript and Vue files `fit(` or `fdescribe(` at the start of a statement, after `=>`, or after `await`, `return` or `void`; variants such as `test.only.each` are not flagged, a JavaScript helper or method named `fit` at the start of a statement is flagged, and `.vue` files are read only under `test`, `tests` or `__tests__`), a commented-out assertion and an early `return`. It does not flag a deleted assertion, a deleted test or test file, a loosened expectation, an always-true check written another way, or an edited test script. A very small test file that is renamed and changed in the same commit can read to git as a new file, and a new file is not scanned. Without `--base` it sees only uncommitted changes, so a weakened test that is already committed passes; with `--base <ref>` it sees everything since the branch left that ref. Only test files are diffed; if their diff cannot be read (over 64 MB), the run is reported as unverified. In a folder with no git repository and no `--base`, or before the first commit, the scan is skipped and the output says so. When git is missing or fails on a repository that is there, or `--base` is given outside a repository, the run is reported as unverified.
|
|
44
|
+
- **`--claim`** finds test files named in the summary in plain text, in quotes or backticks, in markdown bold or italics, in brackets, in `path::test` form and with a `:line` suffix. It ignores absolute paths, Windows backslash paths and Java `*Test.java` names. A file counts as present when any file with the same name exists in the checkout (outside `node_modules`, `dist`, `build`, `vendor`, `target` and virtual environments), so a file in the wrong folder is not reported. The search goes 8 folders deep and stops after 50,000 entries, so a named file deeper than that, or in a larger checkout, is reported as missing.
|
|
45
|
+
- **A passing suite with a broken build, and code the tests do not cover.** A receipt says what the test suite showed, not that the software is correct.
|
|
46
|
+
- **Who controls the tests.** `check` runs the project's own test command, so whoever controls that command controls the result, and whoever controls the environment can point the weakened-test scan at another tree (`GIT_DIR`, `GIT_WORK_TREE`). For a pull request from someone you do not trust, a receipt is not evidence. In the Action, use the `pull_request` trigger, not `pull_request_target`.
|
|
169
47
|
|
|
170
|
-
|
|
171
|
-
- Hardcoded secrets · API keys · weak crypto · JWT bypass
|
|
172
|
-
- Eval injection · prompt injection · credential theft
|
|
173
|
-
- Destructive shell operations · supply-chain patterns
|
|
48
|
+
## What has been measured
|
|
174
49
|
|
|
175
|
-
|
|
50
|
+
- **False-done eval, 22 cases.** 14 false "tests pass" or "all green" claims (6 reconstructed from public posts, 8 planted) and 8 true ones. `codehere check` caught 12 of the 14 false claims, missed 2 (a green suite with a broken build, and green tests with a broken production job), and raised 0 false alarms on the 8 true ones. Result: [`agent/eval/false-done-result.json`](https://github.com/muhammadegaa/codehere/blob/main/agent/eval/false-done-result.json), produced by `node eval/false-done.mjs`. The cases were written to fit the detectors, so this is a small set and not a general rate.
|
|
51
|
+
- **How often agent pull requests carry a failing claim: not measured.** A pre-registered study (S11) had two models label 299 public agent pull requests. They disagreed (Cohen's kappa 0.092), so the study reports no rate. The report, with the method and its limits: [`agent/eval/promise-verification/studies/S11-demand/REPORT.md`](https://github.com/muhammadegaa/codehere/blob/main/agent/eval/promise-verification/studies/S11-demand/REPORT.md).
|
|
176
52
|
|
|
177
|
-
##
|
|
53
|
+
## Development
|
|
178
54
|
|
|
179
|
-
```bash
|
|
180
|
-
codehere serve
|
|
181
55
|
```
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
- `codehere_review` · scan a directory and return riskiest files
|
|
188
|
-
- `codehere_audit` · read the local hook audit log
|
|
189
|
-
- `codehere_spend` · today's estimated spend by agent and project, vs your budget
|
|
190
|
-
- `codehere_team` · the team's merged cost and record across machines
|
|
191
|
-
|
|
192
|
-
Connect it to **Claude Code** in one command (the daemon must be running):
|
|
193
|
-
|
|
56
|
+
cd agent
|
|
57
|
+
npm ci
|
|
58
|
+
npm test # build, then vitest
|
|
59
|
+
npm run lint
|
|
60
|
+
node eval/false-done.mjs
|
|
194
61
|
```
|
|
195
|
-
claude mcp add --transport http codehere http://127.0.0.1:3773/mcp
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Then from inside a Claude Code session you can ask things like "what did we spend today?" or "what did each agent do on this repo?" and it answers inline, no switching to the browser. Remove with `claude mcp remove codehere`.
|
|
199
|
-
|
|
200
|
-
`codehere serve --show-config` prints the JSON block to paste into Claude Desktop's MCP settings.
|
|
201
|
-
|
|
202
|
-
## Architecture, story, and contribution
|
|
203
|
-
|
|
204
|
-
For depth beyond this README:
|
|
205
|
-
|
|
206
|
-
- **[docs/STRATEGIC_VISION.md](../docs/STRATEGIC_VISION.md)** · 20-year invariants + v4 positioning (individual-tier vendor-neutral) · figures in `docs/figures/`
|
|
207
|
-
- **[docs/PRD.md](../docs/PRD.md)** · master decision document · features, architecture, roadmap
|
|
208
|
-
- **[docs/THREAT_MODEL.md](../docs/THREAT_MODEL.md)** · security posture · OWASP Top 10 + STRIDE · trust boundaries · hook-bypass analysis
|
|
209
|
-
- **[docs/STRATEGY.md](../docs/STRATEGY.md)** · current strategy · competitive analysis (Conductor / Whim / Hermes), positioning, the wedge
|
|
210
|
-
- **[CONTRIBUTING.md](../CONTRIBUTING.md)** · how to contribute · setup, commit discipline, testing philosophy
|
|
211
|
-
- **[SECURITY.md](../SECURITY.md)** · responsible disclosure · `security@codehere.uk`
|
|
212
|
-
- **[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)** · community standards
|
|
213
|
-
- **[CHANGELOG.md](../CHANGELOG.md)** · per-version release notes
|
|
214
|
-
|
|
215
|
-
## Requirements
|
|
216
|
-
|
|
217
|
-
- **Node.js 20+** (pinned in `.nvmrc` and `engines.node`)
|
|
218
|
-
- **macOS or Linux** · Windows via WSL works for development
|
|
219
|
-
- **Optional but recommended:** at least one supported agent (Claude Code, Codex, Aider, OpenCode, or Gemini) installed before running `codehere start`
|
|
220
62
|
|
|
221
63
|
## License
|
|
222
64
|
|
|
223
|
-
MIT
|
|
65
|
+
MIT. See [`LICENSE`](https://github.com/muhammadegaa/codehere/blob/main/LICENSE).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Did the agent name a test file that is not in the repo?
|
|
3
|
+
*
|
|
4
|
+
* "The webhook tests passed" while the file never existed is a lie a green re-run cannot see:
|
|
5
|
+
* the suite passes because it never contained the test. Given the agent's own words, this lists
|
|
6
|
+
* the test files it named that exist nowhere in the checkout. A file counts as present when any
|
|
7
|
+
* file with the same name exists (so a wrong folder is not flagged). Deterministic, no model.
|
|
8
|
+
* It cannot tell that a named test never ran, only that it is not there.
|
|
9
|
+
*/
|
|
10
|
+
import { readdirSync } from 'node:fs';
|
|
11
|
+
import { basename, join } from 'node:path';
|
|
12
|
+
const TEST_FILE = /^(?:[\w.@-]+\/)*(?:test_[\w.-]+\.py|[\w.-]+_test\.(?:py|go)|[\w.-]+\.(?:test|spec)\.[cm]?[jt]sx?|[\w-]+Test\.php)$/;
|
|
13
|
+
const SKIP_DIRS = new Set([
|
|
14
|
+
'node_modules',
|
|
15
|
+
'.git',
|
|
16
|
+
'dist',
|
|
17
|
+
'build',
|
|
18
|
+
'.venv',
|
|
19
|
+
'venv',
|
|
20
|
+
'__pycache__',
|
|
21
|
+
'vendor',
|
|
22
|
+
'target',
|
|
23
|
+
]);
|
|
24
|
+
const MAX_DEPTH = 8;
|
|
25
|
+
const MAX_ENTRIES = 50_000;
|
|
26
|
+
/** Separators: whitespace, quotes, backticks, brackets of every kind, markdown emphasis and table pipes. */
|
|
27
|
+
const SEPARATORS = /[\s`'"(),;[\]<>{}|*]+/;
|
|
28
|
+
/** `tests/test_x.py::test_y` → `tests/test_x.py`; `a.spec.ts:42:3` → `a.spec.ts`; `_a.test.js_` → `a.test.js`; trailing punctuation dropped. */
|
|
29
|
+
function cleanToken(raw) {
|
|
30
|
+
let t = raw.replace(/^\.\//, '').replace(/::.*$/, '');
|
|
31
|
+
t = t.replace(/[.:…!?]+$/, '').replace(/(:\d+)+$/, '');
|
|
32
|
+
if (t.length > 2 && t.startsWith('_') && t.endsWith('_'))
|
|
33
|
+
t = t.slice(1, -1);
|
|
34
|
+
return t;
|
|
35
|
+
}
|
|
36
|
+
export function extractClaimedTestFiles(text) {
|
|
37
|
+
const found = [];
|
|
38
|
+
for (const raw of text.split(SEPARATORS)) {
|
|
39
|
+
const token = cleanToken(raw);
|
|
40
|
+
if (token.startsWith('/') || token.split('/').includes('..'))
|
|
41
|
+
continue;
|
|
42
|
+
if (TEST_FILE.test(token) && !found.includes(token))
|
|
43
|
+
found.push(token);
|
|
44
|
+
}
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
function namesInRepo(cwd) {
|
|
48
|
+
const names = new Set();
|
|
49
|
+
let seen = 0;
|
|
50
|
+
const walk = (dir, depth) => {
|
|
51
|
+
if (depth > MAX_DEPTH || seen > MAX_ENTRIES)
|
|
52
|
+
return;
|
|
53
|
+
let entries;
|
|
54
|
+
try {
|
|
55
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (const e of entries) {
|
|
61
|
+
seen++;
|
|
62
|
+
if (e.isDirectory()) {
|
|
63
|
+
if (!SKIP_DIRS.has(e.name))
|
|
64
|
+
walk(join(dir, e.name), depth + 1);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
names.add(e.name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
walk(cwd, 0);
|
|
72
|
+
return names;
|
|
73
|
+
}
|
|
74
|
+
export function findMissingClaimedTests(cwd, claim) {
|
|
75
|
+
const claimed = extractClaimedTestFiles(claim);
|
|
76
|
+
if (!claimed.length)
|
|
77
|
+
return [];
|
|
78
|
+
const present = namesInRepo(cwd);
|
|
79
|
+
return claimed.filter((p) => !present.has(basename(p)));
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=check-claimed-tests.js.map
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Did the change make a test pass without checking anything?
|
|
3
|
+
*
|
|
4
|
+
* A green re-run proves nothing when the agent got there by editing the test: swapping a failing
|
|
5
|
+
* assertion for `assert True`, skipping the test, commenting the assertion out, or returning above
|
|
6
|
+
* it. This reads `git diff` against HEAD and flags such lines added to test files that already
|
|
7
|
+
* existed. It is a deterministic diff read (no model), so it is measured evidence (DESIGN.md §1).
|
|
8
|
+
* By default it reads uncommitted changes against HEAD. With a base ref it reads everything since
|
|
9
|
+
* the branch left that base (committed or not), which is the view a pull request needs.
|
|
10
|
+
*/
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join, resolve } from 'node:path';
|
|
14
|
+
const TEST_PATH = /(^|\/)(test_[^/]*\.py|[^/]*_test\.py|[^/]*\.(test|spec)\.[cm]?[jt]sx?|[^/]*_test\.go|[^/]*Test\.php)$|(^|\/)(__tests__|tests?)\//;
|
|
15
|
+
const TRIVIAL = [
|
|
16
|
+
// `assert True`, `assert(True)`, `assert True, "message"`
|
|
17
|
+
/^assert\s*\(?\s*(True|1|not\s+False)\s*\)?\s*(,.*)?(#.*)?$/,
|
|
18
|
+
/^assert\s+([\w.'"]+)\s*==\s*\1\s*(#.*)?$/,
|
|
19
|
+
/\bexpect\(\s*(true|1)\s*\)\s*\.\s*(toBe|toEqual|toStrictEqual)\(\s*(true|1)\s*\)/,
|
|
20
|
+
/\bexpect\(\s*([\w.'"]+)\s*\)\s*\.\s*(toBe|toEqual|toStrictEqual)\(\s*\1\s*\)/,
|
|
21
|
+
/\bexpect\(\s*true\s*\)\s*\.\s*toBeTruthy\(\)/,
|
|
22
|
+
/\bassert(\.ok)?\(\s*true\s*\)/,
|
|
23
|
+
/\bassert\.(equal|strictEqual|deepEqual|deepStrictEqual)\(\s*([\w.'"]+)\s*,\s*\2\s*\)/,
|
|
24
|
+
/\bassert(Equal|Equals|Same)\(\s*([\w.'"]+)\s*,\s*\2\s*\)/,
|
|
25
|
+
/\bassertTrue\(\s*(true|True)\s*\)/,
|
|
26
|
+
];
|
|
27
|
+
const SKIP = [
|
|
28
|
+
/@pytest\.mark\.(skip|xfail)\b/,
|
|
29
|
+
/@pytest\.mark\.skipif\(\s*True\b/,
|
|
30
|
+
/\bpytest\.(skip|xfail)\(/,
|
|
31
|
+
/@unittest\.skip/,
|
|
32
|
+
/\b(it|test|describe)\.(skip|todo)\(/,
|
|
33
|
+
/\b(xit|xtest|xdescribe)\(/,
|
|
34
|
+
/\bmarkTest(Skipped|Incomplete)\(/,
|
|
35
|
+
/\bt\.Skip(f|Now)?\(/,
|
|
36
|
+
// node:test options: { skip: true } / { skip: 'reason' } / { todo: true }; a conditional skip is left alone
|
|
37
|
+
/\b(skip|todo)\s*:\s*(true\b|['"`])/,
|
|
38
|
+
];
|
|
39
|
+
/** `.only` makes jest, mocha and Playwright skip every other test: the original pattern, on any test file. */
|
|
40
|
+
const ONLY = /\b(it|test|describe)\.only\(/;
|
|
41
|
+
/** `fit` and `fdescribe` do the same, but `fit` is also an ordinary function name: JS, TS and Vue files, at a statement
|
|
42
|
+
* start, after `=>`, or after `void`, `await` or `return`. */
|
|
43
|
+
const JS_FILE = /\.([cm]?[jt]sx?|vue)$/i;
|
|
44
|
+
const JS_FOCUS = /(^|[;{}]\s*|=>\s*|\b(void|await|return)\s+)(fit|fdescribe)\(/;
|
|
45
|
+
/** The line with quoted text emptied and a trailing `//` comment dropped, so neither can pose as a call. */
|
|
46
|
+
const jsCode = (text) => text.replace(/(["'`])(?:\\.|(?!\1).)*\1/g, '""').replace(/\/\/.*$/, '');
|
|
47
|
+
const ASSERTION = /\b(assert\w*|expect|should|self\.assert\w*|this->assert\w*|t\.(Error|Fatal)\w*)\s*[.(]|^\s*assert\s+\S/;
|
|
48
|
+
const BARE_RETURN = /^return\s*;?$/;
|
|
49
|
+
/** A line that opens a branch: a return under it is conditional, not an unconditional skip. */
|
|
50
|
+
const CONDITIONAL = /^(\}\s*)?(if|else|for|while|switch|case|default|catch|try|finally)\b/;
|
|
51
|
+
/** How far below a bare return an assertion still counts as the one it hides. */
|
|
52
|
+
const RETURN_LOOKAHEAD = 8;
|
|
53
|
+
function uncomment(text) {
|
|
54
|
+
const m = /^(?:\/\/+|#+|\/\*+|\*)\s?(.*?)\s*(?:\*\/)?$/.exec(text);
|
|
55
|
+
return m ? m[1].trim() : null;
|
|
56
|
+
}
|
|
57
|
+
/** The commented text is an assertion that the same change removed (alone on its line, or inside a one-line test). */
|
|
58
|
+
function wasCommentedOut(body, removed) {
|
|
59
|
+
const core = body.replace(/;$/, '');
|
|
60
|
+
return core.length >= 12 && ASSERTION.test(core) && removed.some((r) => r.includes(core));
|
|
61
|
+
}
|
|
62
|
+
/** `b/path` or git's C-quoted `"b/p\"ath"` form → `path`. */
|
|
63
|
+
function headerPath(raw) {
|
|
64
|
+
// git appends a tab to the header when the path contains a space.
|
|
65
|
+
const v = raw.slice(4).replace(/\t$/, '');
|
|
66
|
+
if (v.startsWith('b/'))
|
|
67
|
+
return v.slice(2);
|
|
68
|
+
if (v.startsWith('"b/') && v.endsWith('"')) {
|
|
69
|
+
return v.slice(3, -1).replace(/\\(["\\tn])/g, (_, c) => (c === 't' ? '\t' : c === 'n' ? '\n' : c));
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
export function parseTestWeakening(diff, readLines) {
|
|
74
|
+
const found = [];
|
|
75
|
+
let file = null;
|
|
76
|
+
let existing = false;
|
|
77
|
+
let line = 0;
|
|
78
|
+
let removed = [];
|
|
79
|
+
// File headers (`--- a/...`, `+++ b/...`) only appear between `diff --git` and the first hunk, so an added line
|
|
80
|
+
// whose text starts with `++ ` cannot pose as a header.
|
|
81
|
+
let inHeader = false;
|
|
82
|
+
for (const raw of diff.split('\n')) {
|
|
83
|
+
if (raw.startsWith('diff --git ')) {
|
|
84
|
+
file = null;
|
|
85
|
+
existing = true;
|
|
86
|
+
removed = [];
|
|
87
|
+
inHeader = true;
|
|
88
|
+
}
|
|
89
|
+
else if (inHeader && raw.startsWith('--- ')) {
|
|
90
|
+
existing = raw !== '--- /dev/null';
|
|
91
|
+
}
|
|
92
|
+
else if (inHeader && raw.startsWith('+++ ')) {
|
|
93
|
+
const path = headerPath(raw);
|
|
94
|
+
file = path && existing && TEST_PATH.test(path) ? path : null;
|
|
95
|
+
}
|
|
96
|
+
else if (raw.startsWith('@@')) {
|
|
97
|
+
inHeader = false;
|
|
98
|
+
const m = /\+(\d+)/.exec(raw);
|
|
99
|
+
line = m ? Number(m[1]) : 0;
|
|
100
|
+
removed = [];
|
|
101
|
+
}
|
|
102
|
+
else if (raw.startsWith('-')) {
|
|
103
|
+
removed.push(raw.slice(1).trim());
|
|
104
|
+
}
|
|
105
|
+
else if (raw.startsWith('+') && file) {
|
|
106
|
+
const text = raw.slice(1).trim();
|
|
107
|
+
const body = uncomment(text);
|
|
108
|
+
let why = null;
|
|
109
|
+
if (TRIVIAL.some((re) => re.test(text)))
|
|
110
|
+
why = 'trivial-assertion';
|
|
111
|
+
else if (SKIP.some((re) => re.test(text)) ||
|
|
112
|
+
ONLY.test(jsCode(text)) ||
|
|
113
|
+
(JS_FILE.test(file) && JS_FOCUS.test(jsCode(text))))
|
|
114
|
+
why = 'skipped';
|
|
115
|
+
else if (body !== null && wasCommentedOut(body, removed))
|
|
116
|
+
why = 'commented-out';
|
|
117
|
+
else if (BARE_RETURN.test(text)) {
|
|
118
|
+
const lines = readLines?.(file);
|
|
119
|
+
const above = (lines?.[line - 2] ?? '').trim();
|
|
120
|
+
const below = lines?.slice(line, line + RETURN_LOOKAHEAD) ?? [];
|
|
121
|
+
if (!CONDITIONAL.test(above) && below.some((l) => ASSERTION.test(l)))
|
|
122
|
+
why = 'unreachable-assertion';
|
|
123
|
+
}
|
|
124
|
+
if (why)
|
|
125
|
+
found.push({ file, line, why, text: text.slice(0, 80) });
|
|
126
|
+
line++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return found;
|
|
130
|
+
}
|
|
131
|
+
/** The commit the current branch left `base` at, so changes made on `base` since are not blamed on the branch. */
|
|
132
|
+
function resolveBase(cwd, base) {
|
|
133
|
+
if (base.startsWith('-'))
|
|
134
|
+
throw new Error(`Invalid --base "${base}": a ref cannot start with "-".`);
|
|
135
|
+
try {
|
|
136
|
+
return execFileSync('git', ['merge-base', base, 'HEAD'], {
|
|
137
|
+
cwd,
|
|
138
|
+
encoding: 'utf8',
|
|
139
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
140
|
+
}).trim();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
throw new Error(`Unknown --base "${base}": not a ref in this repository.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const MAX_DIFF = 64 * 1024 * 1024;
|
|
147
|
+
/** git with literal paths and unquoted non-ASCII names. */
|
|
148
|
+
function git(cwd, args) {
|
|
149
|
+
// GIT_DIFF_OPTS overrides --unified=0, and context lines would shift every line number after them.
|
|
150
|
+
const env = { ...process.env };
|
|
151
|
+
delete env.GIT_DIFF_OPTS;
|
|
152
|
+
return execFileSync('git', ['-c', 'core.quotepath=false', '--literal-pathspecs', ...args], {
|
|
153
|
+
cwd,
|
|
154
|
+
env,
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
157
|
+
maxBuffer: MAX_DIFF,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function hasGitAbove(cwd) {
|
|
161
|
+
for (let d = resolve(cwd);; d = dirname(d)) {
|
|
162
|
+
if (existsSync(join(d, '.git')))
|
|
163
|
+
return true;
|
|
164
|
+
if (dirname(d) === d)
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Changes in `cwd` against HEAD, or against where the branch left `base` when it is given. Only test files are
|
|
170
|
+
* diffed, so a large change elsewhere cannot push the test diff past the read limit.
|
|
171
|
+
*/
|
|
172
|
+
export function scanTestWeakening(cwd, base) {
|
|
173
|
+
try {
|
|
174
|
+
git(cwd, ['rev-parse', '--is-inside-work-tree']);
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
if (e.code === 'ENOENT')
|
|
178
|
+
return { findings: [], error: 'git is not installed' };
|
|
179
|
+
// Skipped only when there is no repository at all. Any other git failure (GIT_DIR, ownership checks, a broken
|
|
180
|
+
// .git) means the scan should have run and did not; with --base a repository is required.
|
|
181
|
+
if (!base && !hasGitAbove(cwd))
|
|
182
|
+
return { findings: [], skipped: 'not a git repository' };
|
|
183
|
+
return { findings: [], error: 'git could not read this repository; run `git status` here to see why' };
|
|
184
|
+
}
|
|
185
|
+
if (!base) {
|
|
186
|
+
try {
|
|
187
|
+
git(cwd, ['rev-parse', '--verify', '--quiet', 'HEAD']);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return { findings: [], skipped: 'no commit to compare against yet' };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const from = base ? resolveBase(cwd, base) : 'HEAD';
|
|
194
|
+
// Fixed diff options, so a user's or the change's own git config cannot alter what the parser reads: paths
|
|
195
|
+
// relative to `cwd` (a package folder inside a repository), a/ and b/ prefixes, no external diff or textconv,
|
|
196
|
+
// binary attributes ignored, renames paired.
|
|
197
|
+
const opts = ['--relative', '--no-ext-diff', '--no-textconv', '--text', '--src-prefix=a/', '--dst-prefix=b/', '-M'];
|
|
198
|
+
let tests;
|
|
199
|
+
try {
|
|
200
|
+
// -z name-status: status, path (and for renames and copies a second path), NUL-separated. Both paths of a
|
|
201
|
+
// rename go into the pathspec so git can pair them; a test renamed and weakened is still an existing test.
|
|
202
|
+
const fields = git(cwd, ['diff', ...opts, '--name-status', '-z', from]).split('\0');
|
|
203
|
+
tests = [];
|
|
204
|
+
for (let i = 0; i < fields.length - 1;) {
|
|
205
|
+
const status = fields[i++];
|
|
206
|
+
const paths = /^[RC]/.test(status) ? [fields[i++], fields[i++]] : [fields[i++]];
|
|
207
|
+
if (paths.some((p) => p && TEST_PATH.test(p)))
|
|
208
|
+
tests.push(...paths.filter(Boolean));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return { findings: [], error: 'could not read the diff (the list of changed files)' };
|
|
213
|
+
}
|
|
214
|
+
if (!tests.length)
|
|
215
|
+
return { findings: [] };
|
|
216
|
+
let diff;
|
|
217
|
+
try {
|
|
218
|
+
diff = git(cwd, ['diff', ...opts, '--unified=0', '--no-color', from, '--', ...tests]);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return {
|
|
222
|
+
findings: [],
|
|
223
|
+
error: `could not read the diff of ${tests.length} changed test file(s); it may be larger than 64 MB`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
findings: parseTestWeakening(diff, (f) => {
|
|
228
|
+
try {
|
|
229
|
+
return readFileSync(join(cwd, f), 'utf8').split('\n');
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=detect-test-weakening.js.map
|