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.
- 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/narrator-prompt-template.js +1 -1
- 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 +279 -31
- 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 +4 -3
- package/dist/infrastructure/cost/cost-router.js +16 -23
- package/dist/infrastructure/cost/spend-aggregator.js +91 -16
- 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 +139 -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 +23 -11
- package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
- package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
- package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
- 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 +1 -11
- package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
- package/dist/infrastructure/orchestration/graphify-context.js +14 -59
- package/dist/infrastructure/orchestration/memory-injector.js +4 -1
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
- package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
- package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
- 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/cloud-sync.js +2 -2
- package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
- 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 -4126
- package/dist/infrastructure/protocols/verify-page.js +884 -148
- 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 +151 -4
- package/dist/infrastructure/storage/project-thread-reader.js +262 -25
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- 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 +89 -66
- 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
package/README.md
CHANGED
|
@@ -1,37 +1,36 @@
|
|
|
1
1
|
# codehere
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
>
|
|
3
|
+
> **One owned, signed record of what your AI coding agents did, and what they cost.**
|
|
4
|
+
> For a team running more than one coding agent: an independent, cross-vendor record on your own machine. One `npm install`, no account.
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/codehere)
|
|
7
7
|
[](../LICENSE)
|
|
8
|
-
[]()
|
|
9
|
+
[](../docs/PRODUCT_STATE.md)
|
|
10
10
|
|
|
11
|
-
**Codehere is the vendor-neutral layer between you and the coding agents you already use** — Claude Code, Codex, Aider, Gemini, OpenCode. One `npm install`. Local-first, OSS, no account, no IAM, no compliance prereqs. Switch agent, keep your memory. End the day with one Final Report covering
|
|
11
|
+
**Codehere is the vendor-neutral layer between you and the coding agents you already use** — Claude Code, Codex, Aider, Gemini, OpenCode. One `npm install`. Local-first, OSS, no account, no IAM, no compliance prereqs. Switch agent, keep your memory. End the day with one Final Report covering what they did.
|
|
12
12
|
|
|
13
13
|
All 5 adapters (Claude Code, Codex, Aider, Gemini, OpenCode) ship adapter code for session-tracing, Final Reports, and trust scoring. Claude Code additionally gets full pre-execution safety-hook enforcement on every tool call via the Claude Agent SDK. The *memory*, *audit log*, *failover*, and *Final Report* surfaces are wired across all 5 adapters today.
|
|
14
14
|
|
|
15
|
-
> **Honest status
|
|
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
16
|
|
|
17
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
18
|
|
|
19
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
20
|
|
|
21
|
-
|
|
21
|
+
5 vendors supported, 2 proven on a real billed run. Your record, your machine, nobody's vendor.
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
25
|
## Recipe — setup to first audit in 30 minutes
|
|
26
26
|
|
|
27
27
|
> One folder convention, one prompt formula, four ready-to-use prompts.
|
|
28
|
-
> Full version: **[docs/RECIPE.md](../docs/RECIPE.md)**.
|
|
29
28
|
|
|
30
29
|
### Setup (once)
|
|
31
30
|
|
|
32
31
|
| | | |
|
|
33
32
|
|---|---|---|
|
|
34
|
-
| 1 | **Install** | `npm install -g codehere` (Node 20+) — v0.
|
|
33
|
+
| 1 | **Install** | `npm install -g codehere` (Node 20+) — v0.8.0 is the current published stable (`@latest`) |
|
|
35
34
|
| 2 | **Detect environment** | `codehere setup` — checks API key, finds your repo |
|
|
36
35
|
| 3 | **Install the safety hook** | `codehere hook install` — every Claude Code tool call now audited |
|
|
37
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) |
|
|
@@ -135,7 +134,7 @@ For technical readers — the living documents that describe codehere as a syste
|
|
|
135
134
|
|
|
136
135
|
| Doc | What it is |
|
|
137
136
|
|---|---|
|
|
138
|
-
| [`docs/
|
|
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 |
|
|
139
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 |
|
|
140
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 |
|
|
141
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 |
|
|
@@ -181,12 +180,22 @@ Every blocked call is logged with the reason; every allowed call is logged with
|
|
|
181
180
|
codehere serve
|
|
182
181
|
```
|
|
183
182
|
|
|
184
|
-
Exposes
|
|
183
|
+
Exposes 6 read-only tools to any MCP client (Claude Desktop, Cursor, Zed, Continue, Cline, etc.):
|
|
185
184
|
|
|
186
185
|
- `codehere_trust` · score a file or code snippet
|
|
187
186
|
- `codehere_scan` · scan text for injection / threat patterns
|
|
188
187
|
- `codehere_review` · scan a directory and return riskiest files
|
|
189
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
|
+
|
|
194
|
+
```
|
|
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`.
|
|
190
199
|
|
|
191
200
|
`codehere serve --show-config` prints the JSON block to paste into Claude Desktop's MCP settings.
|
|
192
201
|
|
|
@@ -196,8 +205,8 @@ For depth beyond this README:
|
|
|
196
205
|
|
|
197
206
|
- **[docs/STRATEGIC_VISION.md](../docs/STRATEGIC_VISION.md)** · 20-year invariants + v4 positioning (individual-tier vendor-neutral) · figures in `docs/figures/`
|
|
198
207
|
- **[docs/PRD.md](../docs/PRD.md)** · master decision document · features, architecture, roadmap
|
|
199
|
-
- **[docs/
|
|
200
|
-
- **[docs/
|
|
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
|
|
201
210
|
- **[CONTRIBUTING.md](../CONTRIBUTING.md)** · how to contribute · setup, commit discipline, testing philosophy
|
|
202
211
|
- **[SECURITY.md](../SECURITY.md)** · responsible disclosure · `security@codehere.uk`
|
|
203
212
|
- **[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)** · community standards
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VerdictService — the single owner of how a session's honesty verdict is
|
|
3
|
+
* ASSESSED from its parts (R11 / ARCHITECTURE.md M2).
|
|
4
|
+
*
|
|
5
|
+
* Before this, every surface re-derived the verdict inputs inline: the HTTP
|
|
6
|
+
* `/api/tasks` route did it twice (spawned + persisted records) and the
|
|
7
|
+
* `codehere verify` CLI did it a third time, each independently mapping a session
|
|
8
|
+
* to `verifyClaim({ summary, artifacts, steps })`. A change to what feeds the
|
|
9
|
+
* verdict meant editing 3+ sites in 2 layers — the "verdict has no owner" finding.
|
|
10
|
+
*
|
|
11
|
+
* This is the one entry point. It COMPOSES the pure use-cases (today `verifyClaim`;
|
|
12
|
+
* `reconcileAudit`, composed into the single `SessionVerdict`) — it does
|
|
13
|
+
* NOT re-implement their logic. Surfaces render what this returns; they never
|
|
14
|
+
* recompute a verdict rule.
|
|
15
|
+
*/
|
|
16
|
+
import { verifyClaim } from '../use-cases/verify-claim.js';
|
|
17
|
+
/**
|
|
18
|
+
* Assess a session's claim-vs-reality verdict. The single owner of the
|
|
19
|
+
* session → `verifyClaim` input mapping. Pure (delegates to the pure use-case).
|
|
20
|
+
*/
|
|
21
|
+
export function assessSession(session) {
|
|
22
|
+
return verifyClaim({
|
|
23
|
+
summary: session.finalReport?.summary,
|
|
24
|
+
artifacts: session.artifacts,
|
|
25
|
+
steps: session.steps,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Compose the single verdict object every surface consumes. */
|
|
29
|
+
export function composeVerdict(session, audit, crossVendor) {
|
|
30
|
+
const verdict = {
|
|
31
|
+
claim: assessSession(session),
|
|
32
|
+
...(audit !== undefined ? { audit } : {}),
|
|
33
|
+
...(crossVendor !== undefined ? { crossVendor } : {}),
|
|
34
|
+
};
|
|
35
|
+
// The one owner attaches the grade so no surface re-derives "is this green".
|
|
36
|
+
verdict.grade = gradeComposite(verdict);
|
|
37
|
+
return verdict;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The one canonical verdict vocabulary. Every surface (web app + CLI) reads its
|
|
41
|
+
* verdict word from here, so the same state reads the same word everywhere
|
|
42
|
+
* instead of each surface re-deriving its own ("Proven" vs "Verified" vs
|
|
43
|
+
* "verified" for the flagship cross-vendor case was a real three-way split).
|
|
44
|
+
* Wording locked with Ega 2026-06-23, refined 2026-07-17: a green composite grade
|
|
45
|
+
* reads "Verified" on EVERY surface — a single other-vendor review CORROBORATES,
|
|
46
|
+
* it doesn't prove, so the seal expands it as "Corroborated by <vendor>" and no
|
|
47
|
+
* headline says "Proven" for a lone reviewer. "Proven" stays in the vocab
|
|
48
|
+
* (reserved) but is not rendered today. "Caught" = a contradicted claim.
|
|
49
|
+
*
|
|
50
|
+
* The first seven are the canonical headline words. `partial` is a residual the
|
|
51
|
+
* web still emits for two narrow "couldn't fully verify" branches (not-a-git-repo
|
|
52
|
+
* and the catch-all); it is kept here so nothing is silently dropped, pending a
|
|
53
|
+
* decision to fold it into `unverified`.
|
|
54
|
+
*/
|
|
55
|
+
export const VERDICT_WORDS = {
|
|
56
|
+
proven: 'Proven',
|
|
57
|
+
verified: 'Verified',
|
|
58
|
+
reported: 'Reported',
|
|
59
|
+
stopped: 'Stopped',
|
|
60
|
+
caught: 'Caught',
|
|
61
|
+
unverified: 'Unverified',
|
|
62
|
+
practice: 'Practice',
|
|
63
|
+
partial: 'Partial',
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* The green-verdict corroboration clause, scoped to the ACTUAL independent
|
|
67
|
+
* signal(s) that earned the green. A LONE cross-vendor pass is one reviewer's
|
|
68
|
+
* corroboration — "corroborated by one other vendor" — never "independently
|
|
69
|
+
* corroborated", which would overstate a single reviewer. Disk-observed writes
|
|
70
|
+
* and multi-signal greens keep the "independently corroborated" wording. The
|
|
71
|
+
* web mirrors this inline (app-script `indepClause`) so both surfaces name the
|
|
72
|
+
* same signal for the same state.
|
|
73
|
+
*/
|
|
74
|
+
export function corroborationClause(grade) {
|
|
75
|
+
const pass = grade.signals.filter((s) => s.provenance !== 'self-reported' && s.state === 'pass');
|
|
76
|
+
if (pass.length === 1 && pass[0].provenance === 'adversarial')
|
|
77
|
+
return 'corroborated by one other vendor.';
|
|
78
|
+
const hasDisk = pass.some((s) => s.provenance === 'observed');
|
|
79
|
+
return hasDisk ? 'independently corroborated · file writes confirmed on disk.' : 'independently corroborated.';
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Grade a composed `SessionVerdict` into the headline the surface may show.
|
|
83
|
+
* Pure — no I/O. The single owner of "what is the ✓ allowed to say."
|
|
84
|
+
*/
|
|
85
|
+
export function gradeComposite(verdict) {
|
|
86
|
+
const signals = [];
|
|
87
|
+
// 1 · self-reported — the claim vs its own artifacts.
|
|
88
|
+
const cs = verdict.claim.status;
|
|
89
|
+
const claimState = cs === 'verified' ? 'pass' : cs === 'broken' ? 'fail' : cs === 'no-report' ? 'absent' : 'neutral';
|
|
90
|
+
signals.push({
|
|
91
|
+
name: 'self-report',
|
|
92
|
+
provenance: 'self-reported',
|
|
93
|
+
state: claimState,
|
|
94
|
+
detail: `the agent's report ${cs === 'verified' ? 'matches' : cs === 'broken' ? 'contradicts' : 'makes no checkable claim against'} its own recorded work`,
|
|
95
|
+
});
|
|
96
|
+
// 2 · observed — the audit's declared writes vs the real git diff.
|
|
97
|
+
const av = verdict.audit?.verdict;
|
|
98
|
+
if (av !== undefined) {
|
|
99
|
+
const auditState = av === 'consistent' ? 'pass' : 'neutral'; // 'unconfirmed' is honest under-count, never a fail
|
|
100
|
+
signals.push({
|
|
101
|
+
name: 'audit-on-disk',
|
|
102
|
+
provenance: 'observed',
|
|
103
|
+
state: auditState,
|
|
104
|
+
detail: av === 'consistent'
|
|
105
|
+
? 'declared file writes confirmed on disk'
|
|
106
|
+
: av === 'no-writes'
|
|
107
|
+
? 'no file writes to confirm'
|
|
108
|
+
: av === 'unconfirmed'
|
|
109
|
+
? "couldn't confirm every declared write"
|
|
110
|
+
: 'on-disk effect unobservable',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// 3 · adversarial — a different vendor independently reviewed it.
|
|
114
|
+
const cv = verdict.crossVendor;
|
|
115
|
+
if (cv !== undefined) {
|
|
116
|
+
const cvState = cv.proven ? 'pass' : 'neutral'; // not-proven ⇒ shown, never a fail (codehere doesn't adjudicate)
|
|
117
|
+
signals.push({
|
|
118
|
+
name: 'cross-vendor',
|
|
119
|
+
provenance: 'adversarial',
|
|
120
|
+
state: cvState,
|
|
121
|
+
detail: cv.proven
|
|
122
|
+
? `${cv.reviewer.agent} independently reviewed ${cv.writer.agent}'s work, integrity-checked`
|
|
123
|
+
: cv.crossVendor
|
|
124
|
+
? `cross-vendor review ran but isn't a clean proof — ${cv.caveats[0] ?? 'see caveats'}`
|
|
125
|
+
: 'not a cross-vendor review',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const independentPass = signals.some((s) => s.provenance !== 'self-reported' && s.state === 'pass');
|
|
129
|
+
const selfFailed = claimState === 'fail';
|
|
130
|
+
let headline;
|
|
131
|
+
if (cs === 'no-report')
|
|
132
|
+
headline = 'no-report';
|
|
133
|
+
else if (selfFailed)
|
|
134
|
+
headline = 'broken';
|
|
135
|
+
else if (cs === 'nothing-to-verify' && !independentPass)
|
|
136
|
+
headline = 'nothing-to-verify';
|
|
137
|
+
else if (independentPass)
|
|
138
|
+
headline = 'proven';
|
|
139
|
+
else
|
|
140
|
+
headline = 'reported';
|
|
141
|
+
const green = headline === 'proven';
|
|
142
|
+
const clause = corroborationClause({ signals });
|
|
143
|
+
const summary = headline === 'proven'
|
|
144
|
+
? clause.charAt(0).toUpperCase() + clause.slice(1)
|
|
145
|
+
: headline === 'broken'
|
|
146
|
+
? 'The report contradicts its own recorded work.'
|
|
147
|
+
: headline === 'reported'
|
|
148
|
+
? 'Self-reported only — no independent signal has corroborated it yet.'
|
|
149
|
+
: headline === 'nothing-to-verify'
|
|
150
|
+
? 'No checkable claim to verify.'
|
|
151
|
+
: 'No report to grade.';
|
|
152
|
+
return { headline, green, signals, summary };
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=verdict-service.js.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-vendor verdict — the receipt a model lab structurally cannot sell.
|
|
3
|
+
*
|
|
4
|
+
* When one vendor wrote code and a DIFFERENT vendor reviewed it in the same
|
|
5
|
+
* room, this produces the proof-of-process: who wrote, who reviewed, what the
|
|
6
|
+
* audit chain attributes to each, and the combined cost. It is the output that
|
|
7
|
+
* turns the wedge from "two sessions happened to share a room" into a stated,
|
|
8
|
+
* checkable claim.
|
|
9
|
+
*
|
|
10
|
+
* HONESTY (the trap the wedge test already flagged, refuter-pinned):
|
|
11
|
+
* codehere does NOT adjudicate whether the reviewer found a REAL bug — it can't
|
|
12
|
+
* judge code correctness, and a generic "found a flaw" tripwire would read green
|
|
13
|
+
* on a review that found nothing. So this proves the PROCESS (a different vendor
|
|
14
|
+
* independently reviewed and recorded findings, integrity-checked) and surfaces
|
|
15
|
+
* the reviewer's findings verbatim for the human to judge. `proven` is true ONLY
|
|
16
|
+
* when it really is a cross-vendor review, the chain verifies, both vendors are
|
|
17
|
+
* attributed in it, and the review actually produced output. Anything short of
|
|
18
|
+
* that is stated plainly, never laundered into a green check.
|
|
19
|
+
*
|
|
20
|
+
* Wording stays "integrity-checked", never "tamper-evident" (domain/audit.ts:
|
|
21
|
+
* the unkeyed SHA-256 chain detects naive edits, not a determined re-walk).
|
|
22
|
+
*/
|
|
23
|
+
import { reconcileAudit } from './reconcile-audit.js';
|
|
24
|
+
import { estimateCostUsd } from '../../infrastructure/cost/spend-aggregator.js';
|
|
25
|
+
/** Read-ish tool names across adapters — used to report "reviewer read N files".
|
|
26
|
+
* Conservative: only names that are unambiguously a read. A reviewer SHOULD
|
|
27
|
+
* have only reads; any write is surfaced as a caveat, not hidden. */
|
|
28
|
+
const READ_TOOLS = new Set(['read', 'glob', 'grep', 'ls', 'cat', 'view', 'search_file', 'list_files'].map((t) => t.toLowerCase()));
|
|
29
|
+
/** Same WRITE_TOOLS spirit as reconcile-audit, kept local + minimal: we only
|
|
30
|
+
* need "did the reviewer mutate anything" (it shouldn't). */
|
|
31
|
+
const WRITE_TOOLS = new Set([
|
|
32
|
+
'write',
|
|
33
|
+
'edit',
|
|
34
|
+
'multiedit',
|
|
35
|
+
'notebookedit',
|
|
36
|
+
'delete',
|
|
37
|
+
'write_file',
|
|
38
|
+
'edit_file',
|
|
39
|
+
'create_file',
|
|
40
|
+
'str_replace',
|
|
41
|
+
'apply_patch',
|
|
42
|
+
].map((t) => t.toLowerCase()));
|
|
43
|
+
function countTools(entries, set) {
|
|
44
|
+
return entries.filter((e) => set.has((e.toolName || '').toLowerCase())).length;
|
|
45
|
+
}
|
|
46
|
+
function fmtUsd(n) {
|
|
47
|
+
return n === 0 ? '$0' : `$${n.toFixed(2)}`;
|
|
48
|
+
}
|
|
49
|
+
export function assessCrossVendor(input) {
|
|
50
|
+
const { writer, reviewer, entries, chain } = input;
|
|
51
|
+
const crossVendor = writer.agent !== reviewer.agent;
|
|
52
|
+
// Attribute by the sessionId the tool-call entries carry. For a direct run
|
|
53
|
+
// that's the run's own sessionId; for a conductor-routed run the audit hook
|
|
54
|
+
// keyed the entries by the SPECIALIST's runtime id (delegateSessionId), not
|
|
55
|
+
// the conductor task's id — so match EITHER (1.3). Without the delegate leg,
|
|
56
|
+
// every Auto-routed task reads "couldn't attribute" though its writes are in
|
|
57
|
+
// the chain. pre_prompt scans use a separate scan id and carry no write/read
|
|
58
|
+
// tool, so they never inflate counts; sessionId scoping stays precise and
|
|
59
|
+
// immune to mixing other rooms' same-vendor sessions.
|
|
60
|
+
const matchesRun = (e, run) => e.sessionId === run.sessionId || (run.delegateSessionId !== undefined && e.sessionId === run.delegateSessionId);
|
|
61
|
+
const writerEntries = entries.filter((e) => matchesRun(e, writer));
|
|
62
|
+
const reviewerEntries = entries.filter((e) => matchesRun(e, reviewer));
|
|
63
|
+
const writerRecon = reconcileAudit(writerEntries, writer.artifacts);
|
|
64
|
+
const reviewerRead = countTools(reviewerEntries, READ_TOOLS);
|
|
65
|
+
const reviewerWrote = countTools(reviewerEntries, WRITE_TOOLS);
|
|
66
|
+
const reviewHasFindings = !!(reviewer.reviewText && reviewer.reviewText.trim().length > 0);
|
|
67
|
+
const writerAttributed = writerEntries.length > 0;
|
|
68
|
+
const reviewerAttributed = reviewerEntries.length > 0;
|
|
69
|
+
const totalCostUsd = estimateCostUsd(writer.agent, writer.artifacts?.usage) + estimateCostUsd(reviewer.agent, reviewer.artifacts?.usage);
|
|
70
|
+
const caveats = [];
|
|
71
|
+
if (!crossVendor)
|
|
72
|
+
caveats.push(`Same vendor on both sides (${writer.agent}) — not a cross-vendor proof.`);
|
|
73
|
+
if (!chain.valid)
|
|
74
|
+
caveats.push("Audit chain didn't verify — the proof is withheld until it does.");
|
|
75
|
+
if (crossVendor && !writerAttributed)
|
|
76
|
+
caveats.push(`Couldn't attribute ${writer.agent}'s work in the audit chain.`);
|
|
77
|
+
if (crossVendor && !reviewerAttributed)
|
|
78
|
+
caveats.push(`Couldn't attribute ${reviewer.agent}'s review in the audit chain.`);
|
|
79
|
+
if (reviewerWrote > 0)
|
|
80
|
+
caveats.push(`${reviewer.agent} also changed ${reviewerWrote} file(s) — this wasn't a read-only review.`);
|
|
81
|
+
if (!reviewHasFindings)
|
|
82
|
+
caveats.push(`${reviewer.agent}'s review recorded no findings text.`);
|
|
83
|
+
// A clean cross-vendor REVIEW proof is read-only — if the reviewer also wrote,
|
|
84
|
+
// the receipt can't honestly say "changed nothing", so it isn't `proven`
|
|
85
|
+
// (the reviewerWrote caveat above explains why). Refuter 2026-06-13.
|
|
86
|
+
const proven = crossVendor && chain.valid && writerAttributed && reviewerAttributed && reviewHasFindings && reviewerWrote === 0;
|
|
87
|
+
const summary = proven
|
|
88
|
+
? `${reviewer.agent} reviewed ${writer.agent}'s work in this room. ` +
|
|
89
|
+
`${writer.agent} ${writerRecon.verdict === 'consistent' ? `wrote ${writerRecon.filesObserved} file(s), confirmed on disk` : `recorded ${writerRecon.writeToolCalls} write(s)`}; ` +
|
|
90
|
+
`${reviewer.agent} read ${reviewerRead}, changed nothing. ` +
|
|
91
|
+
`Audit chain: ${chain.chainedEntries}/${chain.totalEntries} integrity-checked, both vendors attributed. ` +
|
|
92
|
+
`Combined cost: ${fmtUsd(totalCostUsd)}.`
|
|
93
|
+
: crossVendor
|
|
94
|
+
? `${reviewer.agent} reviewed ${writer.agent}'s work, but this isn't a clean cross-vendor proof yet — ${caveats[0]}`
|
|
95
|
+
: caveats[0];
|
|
96
|
+
return {
|
|
97
|
+
crossVendor,
|
|
98
|
+
proven,
|
|
99
|
+
writer: {
|
|
100
|
+
agent: writer.agent,
|
|
101
|
+
sessionId: writer.sessionId,
|
|
102
|
+
wrote: writerRecon.writeToolCalls,
|
|
103
|
+
reconciliation: writerRecon.verdict,
|
|
104
|
+
reconNote: writerRecon.note,
|
|
105
|
+
},
|
|
106
|
+
reviewer: {
|
|
107
|
+
agent: reviewer.agent,
|
|
108
|
+
sessionId: reviewer.sessionId,
|
|
109
|
+
read: reviewerRead,
|
|
110
|
+
wrote: reviewerWrote,
|
|
111
|
+
hasFindings: reviewHasFindings,
|
|
112
|
+
},
|
|
113
|
+
chain: {
|
|
114
|
+
totalEntries: chain.totalEntries,
|
|
115
|
+
chainedEntries: chain.chainedEntries,
|
|
116
|
+
valid: chain.valid,
|
|
117
|
+
writerAttributed,
|
|
118
|
+
reviewerAttributed,
|
|
119
|
+
},
|
|
120
|
+
totalCostUsd,
|
|
121
|
+
summary,
|
|
122
|
+
caveats,
|
|
123
|
+
reviewText: reviewer.reviewText,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=assess-cross-vendor.js.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gather the live data a cross-vendor verdict needs (room sessions, audit
|
|
3
|
+
* entries, chain facts) and run the pure `assessCrossVendor`. Shared by the CLI
|
|
4
|
+
* (`codehere verify`) and the daemon (GET /api/task/:id/cross-vendor) so both
|
|
5
|
+
* surfaces compute the receipt the same way — one path, no drift.
|
|
6
|
+
*
|
|
7
|
+
* The pure judgment lives in assess-cross-vendor.ts; this is the (disk-reading)
|
|
8
|
+
* glue around it.
|
|
9
|
+
*
|
|
10
|
+
* PAIRING is OBSERVED, not inferred: a reviewer records the writer it reviewed
|
|
11
|
+
* (parentSessionId, stamped at spawn). Older / manual reviews that predate that
|
|
12
|
+
* ground truth fall back to a STRICT inference — a candidate is only treated as
|
|
13
|
+
* a reviewer if it changed NO files (a read-only review). A session that wrote
|
|
14
|
+
* files is a writer, never a reviewer; inferring otherwise mispaired in a
|
|
15
|
+
* multi-run room (a later writer read as a reviewer of an earlier one).
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync } from 'fs';
|
|
18
|
+
import { loadRecent, loadSessionById, } from '../../infrastructure/orchestration/session-persistence.js';
|
|
19
|
+
import { AUDIT_FILE, verifyAuditChain } from '../../infrastructure/hooks/claude-sdk-hook.js';
|
|
20
|
+
import { assessCrossVendor } from './assess-cross-vendor.js';
|
|
21
|
+
/** Assemble the verdict for a KNOWN writer↔reviewer pair (the role decision is
|
|
22
|
+
* the caller's; this just reads the chain + runs the pure assessment). */
|
|
23
|
+
function assembleVerdict(writer, reviewer) {
|
|
24
|
+
let entries = [];
|
|
25
|
+
if (existsSync(AUDIT_FILE)) {
|
|
26
|
+
entries = readFileSync(AUDIT_FILE, 'utf-8')
|
|
27
|
+
.split('\n')
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.map((l) => {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(l);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
.filter((e) => !!e);
|
|
38
|
+
}
|
|
39
|
+
const chain = verifyAuditChain(AUDIT_FILE);
|
|
40
|
+
const reviewText = reviewer.finalReport?.summary?.trim() ||
|
|
41
|
+
(reviewer.steps ?? [])
|
|
42
|
+
.filter((s) => s.kind === 'say')
|
|
43
|
+
.map((s) => s.label)
|
|
44
|
+
.join('\n')
|
|
45
|
+
.trim() ||
|
|
46
|
+
undefined;
|
|
47
|
+
return assessCrossVendor({
|
|
48
|
+
writer: {
|
|
49
|
+
agent: writer.agent,
|
|
50
|
+
sessionId: writer.sessionId,
|
|
51
|
+
delegateSessionId: writer.delegateSessionId,
|
|
52
|
+
artifacts: writer.artifacts,
|
|
53
|
+
},
|
|
54
|
+
reviewer: {
|
|
55
|
+
agent: reviewer.agent,
|
|
56
|
+
sessionId: reviewer.sessionId,
|
|
57
|
+
delegateSessionId: reviewer.delegateSessionId,
|
|
58
|
+
artifacts: reviewer.artifacts,
|
|
59
|
+
reviewText,
|
|
60
|
+
},
|
|
61
|
+
entries,
|
|
62
|
+
chain: { totalEntries: chain.totalEntries, chainedEntries: chain.chainedEntries, valid: chain.valid },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Build the cross-vendor verdict for a session that REVIEWED an earlier writer
|
|
66
|
+
* in the same room. Returns undefined when there's no such pair (the session
|
|
67
|
+
* isn't a cross-vendor review). */
|
|
68
|
+
export function buildCrossVendorVerdict(reviewer) {
|
|
69
|
+
if (!reviewer.cwd)
|
|
70
|
+
return undefined;
|
|
71
|
+
// OBSERVED link: an auto-review records the exact writer it reviewed. Gate on
|
|
72
|
+
// isReview, NOT parentSessionId alone — parentSessionId is overloaded (the room
|
|
73
|
+
// composer also sets it when a goal is HANDED OFF to a different vendor to
|
|
74
|
+
// CONTINUE the work, which is not a review). Only isReview means "this session
|
|
75
|
+
// reviewed its parent." Without this gate a continuation would be falsely
|
|
76
|
+
// labeled "X reviewed Y's work."
|
|
77
|
+
if (reviewer.isReview && reviewer.parentSessionId) {
|
|
78
|
+
const writer = loadSessionById(reviewer.parentSessionId);
|
|
79
|
+
if (!writer || writer.agent === reviewer.agent)
|
|
80
|
+
return undefined;
|
|
81
|
+
return assembleVerdict(writer, reviewer);
|
|
82
|
+
}
|
|
83
|
+
// LEGACY / manual: no recorded link. Only a READ-ONLY session (changed no
|
|
84
|
+
// files) can be a reviewer — a session that wrote files is a writer. This is
|
|
85
|
+
// the guard that stops a later writer being mispaired as a reviewer.
|
|
86
|
+
if ((reviewer.artifacts?.files?.length ?? 0) > 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
const writer = loadRecent(50)
|
|
89
|
+
.filter((s) => s.cwd === reviewer.cwd &&
|
|
90
|
+
s.sessionId !== reviewer.sessionId &&
|
|
91
|
+
s.startedAt < reviewer.startedAt &&
|
|
92
|
+
s.agent !== reviewer.agent &&
|
|
93
|
+
(s.artifacts?.files?.length ?? 0) > 0)
|
|
94
|
+
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
95
|
+
if (!writer)
|
|
96
|
+
return undefined;
|
|
97
|
+
return assembleVerdict(writer, reviewer);
|
|
98
|
+
}
|
|
99
|
+
/** Convenience for the daemon route: load the session by id, then build. */
|
|
100
|
+
export function buildCrossVendorVerdictById(sessionId) {
|
|
101
|
+
const reviewer = loadSessionById(sessionId);
|
|
102
|
+
return reviewer ? buildCrossVendorVerdict(reviewer) : undefined;
|
|
103
|
+
}
|
|
104
|
+
/** The WRITER-keyed mirror: given a writer, find the reviewer that reviewed IT
|
|
105
|
+
* and build the verdict. A user opens the WRITER's run and wants to see "a
|
|
106
|
+
* different vendor reviewed this"; the reviewer recorded the link, so we read
|
|
107
|
+
* it directly (parentSessionId === this writer + isReview) — no inference, no
|
|
108
|
+
* mispairing. Returns undefined when no review of this writer exists. */
|
|
109
|
+
export function buildCrossVendorVerdictForWriter(writerId) {
|
|
110
|
+
const writer = loadSessionById(writerId);
|
|
111
|
+
if (!writer || !writer.cwd)
|
|
112
|
+
return undefined;
|
|
113
|
+
const reviewer = loadRecent(50).find((s) => s.isReview === true && s.parentSessionId === writerId);
|
|
114
|
+
if (!reviewer || reviewer.agent === writer.agent)
|
|
115
|
+
return undefined;
|
|
116
|
+
return assembleVerdict(writer, reviewer);
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=build-cross-vendor-verdict.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* export-aat — map codehere's hash-chained audit log to the IETF
|
|
3
|
+
* draft-sharif-agent-audit-trail-00 (AAT) record structure, JSONL (the
|
|
4
|
+
* draft's RECOMMENDED primary export format).
|
|
5
|
+
*
|
|
6
|
+
* Why: the 2026 wave-4 swarm discourse converged on audit-trail
|
|
7
|
+
* standardization (EU AI Act Art. 12 logging lands Aug 2026; the AAT draft
|
|
8
|
+
* maps to SOC 2 / ISO 42001). codehere's chain already carries the
|
|
9
|
+
* substance — agent identity, action, outcome, risk, hash-chained
|
|
10
|
+
* SHA-256 linking — so speaking the emerging standard is a mapping, not a
|
|
11
|
+
* rebuild.
|
|
12
|
+
*
|
|
13
|
+
* HONEST MAPPING NOTES (each also stated in the export header record):
|
|
14
|
+
* - trust_level is "L0" for every record: AAT's levels describe the
|
|
15
|
+
* AGENT's cryptographic identity, and codehere agents carry none (the
|
|
16
|
+
* DAEMON's keystore signs the chain tip — that is recorder identity,
|
|
17
|
+
* not agent identity; claiming L1 would overstate).
|
|
18
|
+
* - prev_hash/record hashing is codehere's sorted-key JSON canon over the
|
|
19
|
+
* original entries — same SHA-256 chain idea as the draft, but NOT
|
|
20
|
+
* RFC 8785 JCS, and the hashes commit to the ORIGINAL codehere entries,
|
|
21
|
+
* not to the AAT projection. Verify against the source chain.
|
|
22
|
+
* - input_hash is SHA-256 of the recorded toolInput; the raw input stays
|
|
23
|
+
* local (the draft's privacy-by-design posture).
|
|
24
|
+
* - record_id is the entry's own content hash formatted as a UUID-shaped id —
|
|
25
|
+
* deterministic, no invented identity (UUID-shaped, not an RFC 4122 UUID).
|
|
26
|
+
*/
|
|
27
|
+
import { createHash } from 'crypto';
|
|
28
|
+
import { existsSync, readFileSync } from 'fs';
|
|
29
|
+
function hashToUuid(hexHash) {
|
|
30
|
+
const h = hexHash.padEnd(32, '0').slice(0, 32);
|
|
31
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
32
|
+
}
|
|
33
|
+
function agentOf(entry) {
|
|
34
|
+
try {
|
|
35
|
+
const ti = JSON.parse(entry.toolInput);
|
|
36
|
+
if (typeof ti.agent === 'string' && ti.agent)
|
|
37
|
+
return ti.agent;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* non-JSON toolInput */
|
|
41
|
+
}
|
|
42
|
+
return 'unrecorded';
|
|
43
|
+
}
|
|
44
|
+
export function exportAat(auditFile, now = new Date().toISOString()) {
|
|
45
|
+
const out = {
|
|
46
|
+
header: {
|
|
47
|
+
format: 'codehere-aat-export',
|
|
48
|
+
source_format: 'draft-sharif-agent-audit-trail-00',
|
|
49
|
+
exported_at: now,
|
|
50
|
+
record_count: 0,
|
|
51
|
+
notes: [
|
|
52
|
+
'trust_level is L0 for all records: AAT trust levels describe the AGENT’s cryptographic identity and codehere agents carry none — the daemon’s keystore signs the chain tip (recorder identity, not agent identity).',
|
|
53
|
+
'prev_hash values are codehere’s SHA-256 chain over the ORIGINAL entries (sorted-key JSON canonicalization, not RFC 8785 JCS); verify against the source chain, not this projection.',
|
|
54
|
+
'input_hash is SHA-256 of the recorded tool input; raw inputs stay local (privacy by design).',
|
|
55
|
+
'record_id is the source entry’s content hash formatted as a UUID-shaped id — deterministic, not an invented identity.',
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
records: [],
|
|
59
|
+
skippedLegacy: 0,
|
|
60
|
+
};
|
|
61
|
+
if (!existsSync(auditFile))
|
|
62
|
+
return out;
|
|
63
|
+
for (const line of readFileSync(auditFile, 'utf-8').split('\n')) {
|
|
64
|
+
const raw = line.trim();
|
|
65
|
+
if (!raw)
|
|
66
|
+
continue;
|
|
67
|
+
let e;
|
|
68
|
+
try {
|
|
69
|
+
e = JSON.parse(raw);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
out.skippedLegacy++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (typeof e.seq !== 'number' || typeof e.entryHash !== 'string' || typeof e.prevHash !== 'string') {
|
|
76
|
+
out.skippedLegacy++; // pre-chain legacy — no hash-chained position to export
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const rec = {
|
|
80
|
+
record_id: hashToUuid(e.entryHash),
|
|
81
|
+
timestamp: e.timestamp,
|
|
82
|
+
agent_id: `urn:agent:${agentOf(e)}.codehere.local`,
|
|
83
|
+
session_id: e.sessionId,
|
|
84
|
+
action_type: e.toolName === 'pre_prompt' ? 'decision' : 'tool_call',
|
|
85
|
+
action_detail: {
|
|
86
|
+
tool_name: e.toolName,
|
|
87
|
+
input_hash: createHash('sha256')
|
|
88
|
+
.update(e.toolInput ?? '')
|
|
89
|
+
.digest('hex'),
|
|
90
|
+
...(e.writer ? { writer: e.writer } : {}),
|
|
91
|
+
},
|
|
92
|
+
outcome: e.blocked ? 'blocked' : 'success',
|
|
93
|
+
trust_level: 'L0',
|
|
94
|
+
prev_hash: e.prevHash,
|
|
95
|
+
...(typeof e.riskScore === 'number' && e.riskScore > 0 ? { risk_score: Math.min(1, e.riskScore / 100) } : {}),
|
|
96
|
+
};
|
|
97
|
+
out.records.push(rec);
|
|
98
|
+
}
|
|
99
|
+
out.header.record_count = out.records.length;
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
/** Serialize per the draft's recommended JSONL: header line first, then one
|
|
103
|
+
* record per line. */
|
|
104
|
+
export function aatToJsonl(exp) {
|
|
105
|
+
return [JSON.stringify(exp.header), ...exp.records.map((r) => JSON.stringify(r))].join('\n') + '\n';
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=export-aat.js.map
|