codehere 0.8.0 → 1.0.2
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 +37 -184
- 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 +329 -0
- package/dist/index.js +74 -1004
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
- package/dist/presentation/cli/commands/check-command.js +251 -0
- package/dist/presentation/cli/commands/help-command.js +22 -286
- package/dist/ui.js +19 -438
- package/dist/utils/version.js +0 -12
- package/package.json +18 -27
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/generate-final-report.js +0 -134
- package/dist/application/use-cases/generate-narration.js +0 -128
- package/dist/application/use-cases/generate-thread-title.js +0 -178
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/thread-export.js +0 -155
- package/dist/application/use-cases/verify-claim.js +0 -274
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/entities/task.js +0 -61
- package/dist/domain/services/codebase-detector.js +0 -91
- package/dist/error-handler.js +0 -251
- package/dist/infrastructure/completion/bash-completion.js +0 -25
- package/dist/infrastructure/completion/fish-completion.js +0 -22
- package/dist/infrastructure/completion/zsh-completion.js +0 -30
- package/dist/infrastructure/config/config-manager.js +0 -173
- package/dist/infrastructure/config/product-context.js +0 -67
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -163
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
- 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/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -119
- package/dist/infrastructure/context/registry-bootstrap.js +0 -99
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -113
- package/dist/infrastructure/cost/cost-router.js +0 -104
- package/dist/infrastructure/cost/spend-aggregator.js +0 -147
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
- package/dist/infrastructure/hooks/hook-mode.js +0 -71
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
- 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 -56
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
- package/dist/infrastructure/orchestration/context-handoff.js +0 -144
- 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/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -77
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
- package/dist/infrastructure/orchestration/graphify-context.js +0 -254
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
- 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 -510
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1091
- package/dist/infrastructure/orchestration/session-persistence.js +0 -258
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
- 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 -2853
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/protocols/verify-page.js +0 -983
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/security/permissions-config.js +0 -234
- package/dist/infrastructure/security/trust-scorer.js +0 -657
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
- package/dist/infrastructure/storage/project-thread-reader.js +0 -172
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/infrastructure/storage/task-repository.js +0 -206
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -126
- 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 -198
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -578
- 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 -116
- 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 -387
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
- package/dist/presentation/cli/commands/setup-command.js +0 -307
- package/dist/presentation/cli/commands/status-command.js +0 -661
- package/dist/presentation/cli/commands/thread-command.js +0 -288
- package/dist/presentation/cli/commands/trust-command.js +0 -306
- package/dist/presentation/cli/commands/verify-command.js +0 -233
- package/dist/presentation/cli/error-display.js +0 -336
- package/dist/presentation/cli/repl.js +0 -797
- package/dist/presentation/cli/task-deliverable.js +0 -127
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -166
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v0.6 S6 — editable PM narrator prompt template.
|
|
3
|
-
*
|
|
4
|
-
* The mid-task narration (M3) used to ship a hard-coded prompt baked into
|
|
5
|
-
* generate-narration.ts. That made the agent's "voice" un-tunable: a user
|
|
6
|
-
* who wanted shorter / longer / more-jargony / less-jargony narration
|
|
7
|
-
* had no lever short of forking the source.
|
|
8
|
-
*
|
|
9
|
-
* S6 lifts the template into ~/.codehere/narrator-prompt.md, a plain
|
|
10
|
-
* Markdown file the user edits with whatever editor they like. The file
|
|
11
|
-
* uses `{{placeholder}}` substitution for the runtime values:
|
|
12
|
-
* {{prompt}} — the user's original task ask (truncated 500ch)
|
|
13
|
-
* {{agent}} — which adapter is running ("claude-code" etc.)
|
|
14
|
-
* {{elapsed_seconds}} — how long the task has been running
|
|
15
|
-
* {{step_count}} — how many steps completed/in-progress
|
|
16
|
-
* {{steps}} — last 15 steps, " N. [state] label"
|
|
17
|
-
*
|
|
18
|
-
* If the file is missing OR the user-edited file is missing required
|
|
19
|
-
* placeholders, we fall back to the baked-in DEFAULT_TEMPLATE so a typo
|
|
20
|
-
* never breaks narration silently.
|
|
21
|
-
*
|
|
22
|
-
* Per claim_user_persona HIGH: the narrator IS the product voice for
|
|
23
|
-
* non-technical users. Letting them control it is a v1 promise.
|
|
24
|
-
*
|
|
25
|
-
* Per feedback_overconfidence_guard.md: the DEFAULT_TEMPLATE keeps the
|
|
26
|
-
* anti-hallucination rule + "frame as narrator not authority" cue. Users
|
|
27
|
-
* editing the template can drop those rules at their own risk; the file
|
|
28
|
-
* comment warns about it.
|
|
29
|
-
*/
|
|
30
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
31
|
-
import { dirname, join } from 'path';
|
|
32
|
-
import { homedir } from 'os';
|
|
33
|
-
/** The required placeholders. If a user-edited template is missing ANY of
|
|
34
|
-
* these we fall back to the default so the rendered prompt is never
|
|
35
|
-
* silently broken. {{step_count}} is omitted from the required list — it's
|
|
36
|
-
* derivable from {{steps}} and a custom template might intentionally drop it. */
|
|
37
|
-
const REQUIRED_PLACEHOLDERS = ['{{prompt}}', '{{agent}}', '{{elapsed_seconds}}', '{{steps}}'];
|
|
38
|
-
export const DEFAULT_TEMPLATE = `<!--
|
|
39
|
-
codehere narrator prompt — drives the mid-task summary the user sees as
|
|
40
|
-
"narrator says…" while their AI coding agent is working. Edit freely.
|
|
41
|
-
|
|
42
|
-
Available placeholders (substituted at render time):
|
|
43
|
-
{{prompt}} user's original task ask
|
|
44
|
-
{{agent}} which adapter is running
|
|
45
|
-
{{elapsed_seconds}} how long the task has been running
|
|
46
|
-
{{step_count}} number of steps completed/in-progress
|
|
47
|
-
{{steps}} last 15 steps, formatted as " N. [state] label"
|
|
48
|
-
|
|
49
|
-
REQUIRED: {{prompt}}, {{agent}}, {{elapsed_seconds}}, {{steps}}.
|
|
50
|
-
Missing any of those → codehere falls back to its baked-in default.
|
|
51
|
-
|
|
52
|
-
Reset to default any time with: codehere narrator reset
|
|
53
|
-
-->
|
|
54
|
-
|
|
55
|
-
You are a PM giving a 1-sentence-or-2 mid-task update to a non-technical user
|
|
56
|
-
about what their AI coding agent is doing RIGHT NOW. Write ONE short paragraph
|
|
57
|
-
(2-3 sentences). No headers, no bullet lists, no markdown.
|
|
58
|
-
|
|
59
|
-
CRITICAL anti-hallucination rule: ground every claim in the structured data
|
|
60
|
-
below. Do not invent file names, status, or actions not in the steps list.
|
|
61
|
-
If you don't know what something does, say "the agent is working on …" with
|
|
62
|
-
the literal target. Better to under-claim than to over-claim.
|
|
63
|
-
|
|
64
|
-
Tone: like a colleague checking in. "It's reading the auth file and ran the
|
|
65
|
-
tests — looks like 2 failed so far." Not robotic, not breathless.
|
|
66
|
-
|
|
67
|
-
Reference frame for the user (not for the LLM): the user did NOT see this
|
|
68
|
-
list directly; you're translating it for them. The user only sees your
|
|
69
|
-
paragraph.
|
|
70
|
-
|
|
71
|
-
────────── Structured data (the only source of truth) ──────────
|
|
72
|
-
Task: "{{prompt}}"
|
|
73
|
-
Agent: {{agent}}
|
|
74
|
-
Elapsed: {{elapsed_seconds}}s
|
|
75
|
-
Steps so far ({{step_count}}):
|
|
76
|
-
{{steps}}
|
|
77
|
-
|
|
78
|
-
Write the paragraph now. Do not preface with "Update:" or anything similar.
|
|
79
|
-
Plain prose. No "I'm currently…" — narrate the AGENT, not yourself.
|
|
80
|
-
`;
|
|
81
|
-
function templateFile() {
|
|
82
|
-
return join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'narrator-prompt.md');
|
|
83
|
-
}
|
|
84
|
-
/** Read the user-customized template if present and valid. Falls back to
|
|
85
|
-
* baked-in default on missing file, parse failure, or missing required
|
|
86
|
-
* placeholders (so a typo never silently breaks narration). */
|
|
87
|
-
export function loadTemplate() {
|
|
88
|
-
const path = templateFile();
|
|
89
|
-
if (!existsSync(path))
|
|
90
|
-
return { source: 'default', template: DEFAULT_TEMPLATE };
|
|
91
|
-
let raw;
|
|
92
|
-
try {
|
|
93
|
-
raw = readFileSync(path, 'utf-8');
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return { source: 'default', template: DEFAULT_TEMPLATE };
|
|
97
|
-
}
|
|
98
|
-
for (const ph of REQUIRED_PLACEHOLDERS) {
|
|
99
|
-
if (!raw.includes(ph)) {
|
|
100
|
-
return { source: 'default', template: DEFAULT_TEMPLATE };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return { source: 'user', template: raw };
|
|
104
|
-
}
|
|
105
|
-
/** Persist the default template to disk so the user has a starting point
|
|
106
|
-
* to edit. Used by `codehere narrator edit`. Doesn't overwrite an existing
|
|
107
|
-
* user-edited file unless `force` is set. */
|
|
108
|
-
export function writeDefaultTemplate(force = false) {
|
|
109
|
-
const path = templateFile();
|
|
110
|
-
if (!force && existsSync(path))
|
|
111
|
-
return { wrote: false, path };
|
|
112
|
-
const dir = dirname(path);
|
|
113
|
-
if (!existsSync(dir))
|
|
114
|
-
mkdirSync(dir, { recursive: true });
|
|
115
|
-
writeFileSync(path, DEFAULT_TEMPLATE, 'utf-8');
|
|
116
|
-
return { wrote: true, path };
|
|
117
|
-
}
|
|
118
|
-
/** Delete the user-customized template. Future loads use the baked-in
|
|
119
|
-
* default. Idempotent: missing file is a no-op success. */
|
|
120
|
-
export function resetTemplate() {
|
|
121
|
-
const path = templateFile();
|
|
122
|
-
if (!existsSync(path))
|
|
123
|
-
return { removed: false, path };
|
|
124
|
-
try {
|
|
125
|
-
unlinkSync(path);
|
|
126
|
-
return { removed: true, path };
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
return { removed: false, path };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
/** Substitute the runtime values into the template. Same {{placeholder}}
|
|
133
|
-
* protocol as the template-file comments document. */
|
|
134
|
-
export function renderTemplate(template, input) {
|
|
135
|
-
const stepsBlock = input.steps
|
|
136
|
-
.slice(-15)
|
|
137
|
-
.map((s, idx) => ` ${idx + 1}. [${s.state}] ${(s.label ?? '').slice(0, 120)}`)
|
|
138
|
-
.join('\n');
|
|
139
|
-
const replacements = {
|
|
140
|
-
'{{prompt}}': input.prompt.slice(0, 500),
|
|
141
|
-
'{{agent}}': input.agent,
|
|
142
|
-
'{{elapsed_seconds}}': String(Math.round(input.elapsedMs / 1000)),
|
|
143
|
-
'{{step_count}}': String(input.steps.length),
|
|
144
|
-
'{{steps}}': stepsBlock,
|
|
145
|
-
};
|
|
146
|
-
let out = template;
|
|
147
|
-
for (const [k, v] of Object.entries(replacements)) {
|
|
148
|
-
// Replace ALL occurrences. Use split/join to avoid regex escaping
|
|
149
|
-
// pain on the curly braces.
|
|
150
|
-
out = out.split(k).join(v);
|
|
151
|
-
}
|
|
152
|
-
return out;
|
|
153
|
-
}
|
|
154
|
-
/** Path to the user-customized template (for CLI status display). */
|
|
155
|
-
export function templatePath() {
|
|
156
|
-
return templateFile();
|
|
157
|
-
}
|
|
158
|
-
//# sourceMappingURL=narrator-prompt-template.js.map
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thread export · v0.8 S4 · signed-payload "GitHub for agent sessions"
|
|
3
|
-
*
|
|
4
|
-
* Produces a self-contained, Ed25519-signed bundle of one PersistedSession
|
|
5
|
-
* that a colleague can verify and replay locally. Local-first by design:
|
|
6
|
-
* never relayed through codehere's infrastructure · the user shares the
|
|
7
|
-
* file (or signed URL) by their preferred channel (Slack, email, PR
|
|
8
|
-
* description) and the recipient runs `codehere thread import <file>`.
|
|
9
|
-
*
|
|
10
|
-
* The signature covers a CANONICAL JSON form of the bundle body so two
|
|
11
|
-
* machines that re-serialize the same data get the same hash. Verification
|
|
12
|
-
* recomputes the canonical form, re-derives the hash, checks the signature
|
|
13
|
-
* against the included public key, and returns one of:
|
|
14
|
-
* - 'valid' · signature matches · safe to replay
|
|
15
|
-
* - 'tampered' · signature does NOT match · refuse
|
|
16
|
-
* - 'malformed' · bundle is structurally wrong · refuse
|
|
17
|
-
* - 'unknown_key' · signature valid but public key not in user's trust
|
|
18
|
-
* store · prompt user to trust+continue or refuse
|
|
19
|
-
*
|
|
20
|
-
* Pure logic. The CLI + HTTP routes wrap this and do I/O.
|
|
21
|
-
*/
|
|
22
|
-
import { sign as cryptoSign, verify as cryptoVerify, generateKeyPairSync, createPrivateKey, createPublicKey, } from 'crypto';
|
|
23
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
24
|
-
/* Canonicalization · deterministic JSON form so signatures match */
|
|
25
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
26
|
-
/**
|
|
27
|
-
* Stable JSON: object keys sorted, no whitespace, primitives preserved.
|
|
28
|
-
* Two machines that build the same body MUST produce the same bytes for
|
|
29
|
-
* the signature to verify. We deliberately don't use JSON.stringify with
|
|
30
|
-
* a replacer because Node's serializer doesn't guarantee key order across
|
|
31
|
-
* versions/realms.
|
|
32
|
-
*/
|
|
33
|
-
export function canonicalize(value) {
|
|
34
|
-
if (value === null)
|
|
35
|
-
return 'null';
|
|
36
|
-
if (typeof value === 'number') {
|
|
37
|
-
if (!Number.isFinite(value))
|
|
38
|
-
throw new Error(`cannot canonicalize non-finite number: ${value}`);
|
|
39
|
-
return JSON.stringify(value);
|
|
40
|
-
}
|
|
41
|
-
if (typeof value === 'string' || typeof value === 'boolean')
|
|
42
|
-
return JSON.stringify(value);
|
|
43
|
-
if (Array.isArray(value)) {
|
|
44
|
-
return '[' + value.map(canonicalize).join(',') + ']';
|
|
45
|
-
}
|
|
46
|
-
if (typeof value === 'object') {
|
|
47
|
-
const obj = value;
|
|
48
|
-
const keys = Object.keys(obj).sort();
|
|
49
|
-
const parts = keys
|
|
50
|
-
.filter((k) => obj[k] !== undefined) // undefined → drop the key (JSON convention)
|
|
51
|
-
.map((k) => JSON.stringify(k) + ':' + canonicalize(obj[k]));
|
|
52
|
-
return '{' + parts.join(',') + '}';
|
|
53
|
-
}
|
|
54
|
-
if (value === undefined)
|
|
55
|
-
return 'null';
|
|
56
|
-
throw new Error(`cannot canonicalize type: ${typeof value}`);
|
|
57
|
-
}
|
|
58
|
-
/** Generate a fresh Ed25519 keypair · used for first-time setup. */
|
|
59
|
-
export function generateKeypair() {
|
|
60
|
-
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
61
|
-
return { publicKey, privateKey };
|
|
62
|
-
}
|
|
63
|
-
/** Sign a bundle body and produce the full ThreadBundle.
|
|
64
|
-
* Ed25519 is NOT a hash-then-sign scheme · pass the message directly to
|
|
65
|
-
* crypto.sign(null, msg, key) · the algorithm is implied by the key. */
|
|
66
|
-
export function signBundle(body, keys) {
|
|
67
|
-
const canonical = canonicalize(body);
|
|
68
|
-
const sig = cryptoSign(null, Buffer.from(canonical, 'utf-8'), keys.privateKey);
|
|
69
|
-
const pubRaw = keys.publicKey.export({ format: 'der', type: 'spki' });
|
|
70
|
-
return {
|
|
71
|
-
body,
|
|
72
|
-
signature: sig.toString('base64'),
|
|
73
|
-
publicKey: pubRaw.toString('base64'),
|
|
74
|
-
alg: 'ed25519',
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Verify a bundle. Returns one of:
|
|
79
|
-
* - valid · signature matches AND the publicKey is in trustedKeys
|
|
80
|
-
* - valid-untrusted · signature matches but the key isn't in trustedKeys
|
|
81
|
-
* (returned with status='valid' but trusted=false)
|
|
82
|
-
* - tampered · signature doesn't match the recomputed body bytes
|
|
83
|
-
* - malformed · bundle structure broken
|
|
84
|
-
*/
|
|
85
|
-
export function verifyBundle(bundle, trustedKeys = new Set()) {
|
|
86
|
-
if (!isBundle(bundle))
|
|
87
|
-
return { status: 'malformed', reason: 'bundle shape is invalid' };
|
|
88
|
-
if (bundle.alg !== 'ed25519')
|
|
89
|
-
return { status: 'malformed', reason: `unsupported alg: ${bundle.alg}` };
|
|
90
|
-
try {
|
|
91
|
-
const canonical = canonicalize(bundle.body);
|
|
92
|
-
const pubBuf = Buffer.from(bundle.publicKey, 'base64');
|
|
93
|
-
const sigBuf = Buffer.from(bundle.signature, 'base64');
|
|
94
|
-
const pubKey = createPublicKey({ key: pubBuf, format: 'der', type: 'spki' });
|
|
95
|
-
const ok = cryptoVerify(null, Buffer.from(canonical, 'utf-8'), pubKey, sigBuf);
|
|
96
|
-
if (!ok)
|
|
97
|
-
return { status: 'tampered', reason: 'signature does not match body' };
|
|
98
|
-
const trusted = trustedKeys.has(bundle.publicKey);
|
|
99
|
-
if (!trusted) {
|
|
100
|
-
return { status: 'unknown_key', bundle, publicKey: bundle.publicKey };
|
|
101
|
-
}
|
|
102
|
-
return { status: 'valid', bundle, trusted: true };
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
return { status: 'malformed', reason: err instanceof Error ? err.message : String(err) };
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
function isBundle(x) {
|
|
109
|
-
if (typeof x !== 'object' || x === null)
|
|
110
|
-
return false;
|
|
111
|
-
const obj = x;
|
|
112
|
-
if (typeof obj.signature !== 'string')
|
|
113
|
-
return false;
|
|
114
|
-
if (typeof obj.publicKey !== 'string')
|
|
115
|
-
return false;
|
|
116
|
-
if (typeof obj.alg !== 'string')
|
|
117
|
-
return false;
|
|
118
|
-
const body = obj.body;
|
|
119
|
-
if (typeof body !== 'object' || body === null)
|
|
120
|
-
return false;
|
|
121
|
-
const b = body;
|
|
122
|
-
if (b.schema !== 1)
|
|
123
|
-
return false;
|
|
124
|
-
if (b.kind !== 'thread')
|
|
125
|
-
return false;
|
|
126
|
-
if (typeof b.sessionId !== 'string')
|
|
127
|
-
return false;
|
|
128
|
-
if (typeof b.session !== 'object' || b.session === null)
|
|
129
|
-
return false;
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
133
|
-
/* Key derivation helpers · PEM encoding for storage */
|
|
134
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
135
|
-
export function exportPrivatePem(key) {
|
|
136
|
-
return key.export({ format: 'pem', type: 'pkcs8' }).toString();
|
|
137
|
-
}
|
|
138
|
-
export function exportPublicPem(key) {
|
|
139
|
-
return key.export({ format: 'pem', type: 'spki' }).toString();
|
|
140
|
-
}
|
|
141
|
-
export function importPrivatePem(pem) {
|
|
142
|
-
return createPrivateKey({ key: pem, format: 'pem', type: 'pkcs8' });
|
|
143
|
-
}
|
|
144
|
-
export function importPublicPem(pem) {
|
|
145
|
-
return createPublicKey({ key: pem, format: 'pem', type: 'spki' });
|
|
146
|
-
}
|
|
147
|
-
/** Compute the base64 publicKey string for a KeyObject · same encoding as in bundles. */
|
|
148
|
-
export function publicKeyToBase64(key) {
|
|
149
|
-
return key.export({ format: 'der', type: 'spki' }).toString('base64');
|
|
150
|
-
}
|
|
151
|
-
/** Short fingerprint used in human-readable output. */
|
|
152
|
-
export function shortFingerprint(publicKeyB64) {
|
|
153
|
-
return publicKeyB64.slice(0, 8) + '...' + publicKeyB64.slice(-4);
|
|
154
|
-
}
|
|
155
|
-
//# sourceMappingURL=thread-export.js.map
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v0.7 · verify-claim use case
|
|
3
|
-
*
|
|
4
|
-
* Pure function that compares what the Final Report TEXT claims against
|
|
5
|
-
* the structured reality (artifacts + steps) captured during the session.
|
|
6
|
-
*
|
|
7
|
-
* Codehere's #1 differentiator (per claim_market_position v4 + landing copy):
|
|
8
|
-
* agents round to optimism, the verifier catches the rounding.
|
|
9
|
-
*
|
|
10
|
-
* Examples this catches today:
|
|
11
|
-
* - Report says "18 tests passed" but artifacts.tests.passed = 17 → MISMATCH
|
|
12
|
-
* - Report says "all tests green" but artifacts.tests.failed > 0 → MISMATCH
|
|
13
|
-
* - Report says "no files changed" but artifacts.files has entries → MISMATCH
|
|
14
|
-
* - Report says "5 files modified" but artifacts.files.length === 3 → MISMATCH
|
|
15
|
-
*
|
|
16
|
-
* NOT a substitute for human review · this is a tripwire for the easy lies.
|
|
17
|
-
* The verifier is intentionally conservative: only flag claims it can
|
|
18
|
-
* compare against structured data. Unverifiable claims (prose-only,
|
|
19
|
-
* intent-only) pass through silently.
|
|
20
|
-
*
|
|
21
|
-
* I/O is deferred to the CLI/UI surface. This file is pure: same input,
|
|
22
|
-
* same output, no side-effects, no clock, no filesystem.
|
|
23
|
-
*/
|
|
24
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
25
|
-
/* Main verifier */
|
|
26
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
27
|
-
export function verifyClaim(input) {
|
|
28
|
-
if (!input.summary || !input.summary.trim()) {
|
|
29
|
-
return {
|
|
30
|
-
status: 'no-report',
|
|
31
|
-
findings: [
|
|
32
|
-
{
|
|
33
|
-
severity: 'warn',
|
|
34
|
-
kind: 'no-report',
|
|
35
|
-
claim: '(no Final Report generated)',
|
|
36
|
-
reality: 'nothing to verify',
|
|
37
|
-
message: 'No Final Report available for this session. Nothing to verify against.',
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
summary: 'No Final Report to verify.',
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
const findings = [
|
|
44
|
-
...verifyTestsClaim(input.summary, input.artifacts),
|
|
45
|
-
...verifyFilesClaim(input.summary, input.artifacts),
|
|
46
|
-
];
|
|
47
|
-
const hasFail = findings.some((f) => f.severity === 'fail');
|
|
48
|
-
const hasNon = findings.length > 0;
|
|
49
|
-
const status = hasFail ? 'broken' : hasNon ? 'partial' : 'verified';
|
|
50
|
-
const summary = buildVerdictSummary(status, findings);
|
|
51
|
-
return { status, findings, summary };
|
|
52
|
-
}
|
|
53
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
54
|
-
/* Tests claim verification */
|
|
55
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
56
|
-
/**
|
|
57
|
-
* Detect tests-related claims in the report and cross-check against
|
|
58
|
-
* artifacts.tests. Catches "X tests passed" exaggeration AND "all tests
|
|
59
|
-
* green" when failed > 0.
|
|
60
|
-
*/
|
|
61
|
-
export function verifyTestsClaim(summary, artifacts) {
|
|
62
|
-
const out = [];
|
|
63
|
-
const lower = summary.toLowerCase();
|
|
64
|
-
const tests = artifacts?.tests;
|
|
65
|
-
// Extract numeric "X tests passed" / "X passed" / "X of Y" patterns.
|
|
66
|
-
// We deliberately keep the regex tight to avoid false positives on prose
|
|
67
|
-
// like "this is the 5th attempt".
|
|
68
|
-
const countClaim = matchTestCountClaim(summary);
|
|
69
|
-
if (countClaim !== null) {
|
|
70
|
-
if (!tests) {
|
|
71
|
-
// The agent claimed a test count but the adapter recorded no test
|
|
72
|
-
// artifact. Suspicious but not necessarily wrong (the adapter may not
|
|
73
|
-
// capture test results yet). Flag as warn.
|
|
74
|
-
out.push({
|
|
75
|
-
severity: 'warn',
|
|
76
|
-
kind: 'tests',
|
|
77
|
-
claim: countClaim.quote,
|
|
78
|
-
reality: 'no test artifact captured',
|
|
79
|
-
message: `Final Report claims "${countClaim.quote}" but no test results were recorded by the agent.`,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
const actualPassed = tests.passed;
|
|
84
|
-
const actualFailed = tests.failed;
|
|
85
|
-
const actualTotal = actualPassed + actualFailed;
|
|
86
|
-
// Per codex Round 1: "X of Y tests passed" must check BOTH numbers.
|
|
87
|
-
// A claim like "18 of 18 tests passed" with reality {passed: 17, failed: 1}
|
|
88
|
-
// has matching total (18 ran) but mismatched passed (17 ≠ 18) · we flag.
|
|
89
|
-
if (countClaim.passedCount !== undefined && countClaim.passedCount !== actualPassed) {
|
|
90
|
-
out.push({
|
|
91
|
-
severity: 'fail',
|
|
92
|
-
kind: 'tests',
|
|
93
|
-
claim: countClaim.quote,
|
|
94
|
-
reality: `${actualPassed} passed, ${actualFailed} failed (${actualTotal} ran)`,
|
|
95
|
-
message: `Final Report claims ${countClaim.passedCount} tests passed but reality is ${actualPassed} passed and ${actualFailed} failed.`,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
if (countClaim.totalCount !== undefined && countClaim.totalCount !== actualTotal) {
|
|
99
|
-
out.push({
|
|
100
|
-
severity: 'fail',
|
|
101
|
-
kind: 'tests',
|
|
102
|
-
claim: countClaim.quote,
|
|
103
|
-
reality: `${actualTotal} ran (${actualPassed} passed, ${actualFailed} failed)`,
|
|
104
|
-
message: `Final Report claims ${countClaim.totalCount} tests ran but reality is ${actualTotal} (${actualPassed} passed, ${actualFailed} failed).`,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// Universal "all tests pass" / "all green" claims. Requires explicit
|
|
110
|
-
// universal qualifier ("all", "every", "each", "no failures") so that
|
|
111
|
-
// count claims like "18 tests passed" don't also match — count
|
|
112
|
-
// mismatches are caught above by matchTestCountClaim.
|
|
113
|
-
const claimsAllPass = /\b(?:all|every|each)\s+tests?\s+(?:pass(?:ed|ing)?|green|succeed(?:ed)?)\b/i.test(lower) ||
|
|
114
|
-
/\btests?\s+all\s+(?:pass(?:ed|ing)?|green)\b/i.test(lower) ||
|
|
115
|
-
/\ball\s+(?:tests?\s+)?(?:green|passing)\b/i.test(lower) ||
|
|
116
|
-
/\bno\s+(?:test\s+)?(?:failures|regressions?)\b/i.test(lower);
|
|
117
|
-
if (claimsAllPass && tests && tests.failed > 0) {
|
|
118
|
-
out.push({
|
|
119
|
-
severity: 'fail',
|
|
120
|
-
kind: 'tests',
|
|
121
|
-
claim: 'all tests pass / green / no failures',
|
|
122
|
-
reality: `${tests.failed} test${tests.failed === 1 ? '' : 's'} failed`,
|
|
123
|
-
message: `Final Report claims all tests pass but ${tests.failed} actually failed.`,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
return out;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Parse the first explicit test-count claim from the summary.
|
|
130
|
-
* Returns null if no specific count is mentioned.
|
|
131
|
-
*
|
|
132
|
-
* Patterns matched (a single claim can assert BOTH a passed-count AND a total):
|
|
133
|
-
* "18 tests passed" → { passedCount: 18 }
|
|
134
|
-
* "18 of 18 tests passed" → { passedCount: 18, totalCount: 18 }
|
|
135
|
-
* "passed 18 tests" → { passedCount: 18 }
|
|
136
|
-
* "ran 25 tests" → { totalCount: 25 }
|
|
137
|
-
*
|
|
138
|
-
* Per codex Round 1 review on commit 8481412: "X of Y tests passed" must
|
|
139
|
-
* check BOTH numbers, not just total, otherwise "18 of 18 passed" with
|
|
140
|
-
* reality {passed: 17, failed: 1} slips through (total still equals 18).
|
|
141
|
-
*/
|
|
142
|
-
function matchTestCountClaim(summary) {
|
|
143
|
-
// "X of Y tests passed/green/succeeded" → X is passed AND Y is total.
|
|
144
|
-
// Per codex Round 2: require explicit pass-context to populate passedCount
|
|
145
|
-
// so negative phrasing ("1 of 3 tests failed") doesn't become a false-positive
|
|
146
|
-
// "1 passed" claim.
|
|
147
|
-
const ofPassMatch = summary.match(/\b(\d+)\s+(?:of|\/)\s+(\d+)\s+tests?\s+(?:pass(?:ed|ing)?|green|succeed(?:ed)?)\b/i);
|
|
148
|
-
if (ofPassMatch) {
|
|
149
|
-
return {
|
|
150
|
-
quote: ofPassMatch[0],
|
|
151
|
-
passedCount: parseInt(ofPassMatch[1], 10),
|
|
152
|
-
totalCount: parseInt(ofPassMatch[2], 10),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
// "X of Y tests" without pass-context → only Y is a total-count claim. X
|
|
156
|
-
// is ambient (failed / ran / partial / etc) and we don't pretend to know.
|
|
157
|
-
// Per codex Round 3: this branch preserves total-count detection for
|
|
158
|
-
// bare X-of-Y phrasing so the verifier still catches mismatched totals.
|
|
159
|
-
const ofMatch = summary.match(/\b(\d+)\s+(?:of|\/)\s+(\d+)\s+tests?\b/i);
|
|
160
|
-
if (ofMatch) {
|
|
161
|
-
return {
|
|
162
|
-
quote: ofMatch[0],
|
|
163
|
-
totalCount: parseInt(ofMatch[2], 10),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
// "X tests passed" / "X tests pass"
|
|
167
|
-
const passedMatch = summary.match(/\b(\d+)\s+tests?\s+(?:pass(?:ed|ing)?|green)\b/i);
|
|
168
|
-
if (passedMatch) {
|
|
169
|
-
return { quote: passedMatch[0], passedCount: parseInt(passedMatch[1], 10) };
|
|
170
|
-
}
|
|
171
|
-
// "passed X tests" / "ran X tests"
|
|
172
|
-
const reverseMatch = summary.match(/\b(?:pass(?:ed)?|ran)\s+(\d+)\s+tests?\b/i);
|
|
173
|
-
if (reverseMatch) {
|
|
174
|
-
const isPass = /pass/i.test(reverseMatch[0]);
|
|
175
|
-
return {
|
|
176
|
-
quote: reverseMatch[0],
|
|
177
|
-
passedCount: isPass ? parseInt(reverseMatch[1], 10) : undefined,
|
|
178
|
-
totalCount: isPass ? undefined : parseInt(reverseMatch[1], 10),
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
184
|
-
/* Files claim verification */
|
|
185
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
186
|
-
/**
|
|
187
|
-
* Detect files-related claims in the report and cross-check against
|
|
188
|
-
* artifacts.files. Catches "no files changed" when files DID change,
|
|
189
|
-
* and "X files changed" count mismatches.
|
|
190
|
-
*/
|
|
191
|
-
export function verifyFilesClaim(summary, artifacts) {
|
|
192
|
-
const out = [];
|
|
193
|
-
// Claim: "no files were changed" / "no files modified" / similar
|
|
194
|
-
const claimsNoFiles = /\bno\s+files?\s+(were\s+)?(changed|modified|touched|written|created)\b/i.test(summary) ||
|
|
195
|
-
/\bsession\s+was\s+read-only\b/i.test(summary) ||
|
|
196
|
-
/\bnothing\s+(was\s+)?(written|changed|modified)\b/i.test(summary);
|
|
197
|
-
// Claim: "X files changed" / "modified X files" / "wrote X files"
|
|
198
|
-
const countClaim = matchFilesCountClaim(summary);
|
|
199
|
-
// Per codex Round 1: if the adapter didn't capture file artifacts at all,
|
|
200
|
-
// we can't verify file claims · warn rather than fail. Mirrors the tests
|
|
201
|
-
// path (missing tests artifact → warn). Distinguishes from `files: []`
|
|
202
|
-
// which is a valid "explicitly captured zero files" reality.
|
|
203
|
-
if (artifacts?.files === undefined) {
|
|
204
|
-
if (claimsNoFiles) {
|
|
205
|
-
out.push({
|
|
206
|
-
severity: 'warn',
|
|
207
|
-
kind: 'files',
|
|
208
|
-
claim: 'no files changed / read-only',
|
|
209
|
-
reality: 'no file artifact captured',
|
|
210
|
-
message: 'Final Report claims no files changed but no file results were recorded by the agent.',
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
if (countClaim !== null) {
|
|
214
|
-
out.push({
|
|
215
|
-
severity: 'warn',
|
|
216
|
-
kind: 'files',
|
|
217
|
-
claim: countClaim.quote,
|
|
218
|
-
reality: 'no file artifact captured',
|
|
219
|
-
message: `Final Report claims "${countClaim.quote}" but no file results were recorded by the agent.`,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
return out;
|
|
223
|
-
}
|
|
224
|
-
const actualCount = artifacts.files.length;
|
|
225
|
-
if (claimsNoFiles && actualCount > 0) {
|
|
226
|
-
out.push({
|
|
227
|
-
severity: 'fail',
|
|
228
|
-
kind: 'files',
|
|
229
|
-
claim: 'no files changed / read-only',
|
|
230
|
-
reality: `${actualCount} file${actualCount === 1 ? '' : 's'} changed`,
|
|
231
|
-
message: `Final Report claims no files were changed but ${actualCount} actually were.`,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
if (countClaim !== null && countClaim.value !== actualCount) {
|
|
235
|
-
out.push({
|
|
236
|
-
severity: 'fail',
|
|
237
|
-
kind: 'files',
|
|
238
|
-
claim: countClaim.quote,
|
|
239
|
-
reality: `${actualCount} file${actualCount === 1 ? '' : 's'} changed`,
|
|
240
|
-
message: `Final Report claims ${countClaim.value} file${countClaim.value === 1 ? '' : 's'} were changed but ${actualCount} actually were.`,
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
return out;
|
|
244
|
-
}
|
|
245
|
-
function matchFilesCountClaim(summary) {
|
|
246
|
-
// "X files changed" / "X files modified" / "X files were touched"
|
|
247
|
-
const m1 = summary.match(/\b(\d+)\s+files?\s+(were\s+)?(?:changed|modified|touched|written|created|added|updated)\b/i);
|
|
248
|
-
if (m1)
|
|
249
|
-
return { quote: m1[0], value: parseInt(m1[1], 10) };
|
|
250
|
-
// "changed X files" / "modified X files"
|
|
251
|
-
const m2 = summary.match(/\b(?:changed|modified|touched|wrote|created|added|updated)\s+(\d+)\s+files?\b/i);
|
|
252
|
-
if (m2)
|
|
253
|
-
return { quote: m2[0], value: parseInt(m2[1], 10) };
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
257
|
-
/* Verdict summary */
|
|
258
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
259
|
-
function buildVerdictSummary(status, findings) {
|
|
260
|
-
if (status === 'verified')
|
|
261
|
-
return 'Claims match the recorded reality. No gaps detected.';
|
|
262
|
-
if (status === 'no-report')
|
|
263
|
-
return 'No Final Report to verify.';
|
|
264
|
-
const failCount = findings.filter((f) => f.severity === 'fail').length;
|
|
265
|
-
const warnCount = findings.filter((f) => f.severity === 'warn').length;
|
|
266
|
-
if (failCount > 0 && warnCount > 0) {
|
|
267
|
-
return `${failCount} claim${failCount === 1 ? '' : 's'} contradict the recorded reality, plus ${warnCount} unverifiable claim${warnCount === 1 ? '' : 's'}.`;
|
|
268
|
-
}
|
|
269
|
-
if (failCount > 0) {
|
|
270
|
-
return `${failCount} claim${failCount === 1 ? '' : 's'} contradict the recorded reality.`;
|
|
271
|
-
}
|
|
272
|
-
return `${warnCount} unverifiable claim${warnCount === 1 ? '' : 's'}.`;
|
|
273
|
-
}
|
|
274
|
-
//# sourceMappingURL=verify-claim.js.map
|