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.
Files changed (119) hide show
  1. package/README.md +37 -184
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +329 -0
  5. package/dist/index.js +74 -1004
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
  7. package/dist/presentation/cli/commands/check-command.js +251 -0
  8. package/dist/presentation/cli/commands/help-command.js +22 -286
  9. package/dist/ui.js +19 -438
  10. package/dist/utils/version.js +0 -12
  11. package/package.json +18 -27
  12. package/dist/application/use-cases/domain-trust.js +0 -110
  13. package/dist/application/use-cases/generate-final-report.js +0 -134
  14. package/dist/application/use-cases/generate-narration.js +0 -128
  15. package/dist/application/use-cases/generate-thread-title.js +0 -178
  16. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  17. package/dist/application/use-cases/thread-export.js +0 -155
  18. package/dist/application/use-cases/verify-claim.js +0 -274
  19. package/dist/application/use-cases/write-pass.js +0 -277
  20. package/dist/domain/entities/task.js +0 -61
  21. package/dist/domain/services/codebase-detector.js +0 -91
  22. package/dist/error-handler.js +0 -251
  23. package/dist/infrastructure/completion/bash-completion.js +0 -25
  24. package/dist/infrastructure/completion/fish-completion.js +0 -22
  25. package/dist/infrastructure/completion/zsh-completion.js +0 -30
  26. package/dist/infrastructure/config/config-manager.js +0 -173
  27. package/dist/infrastructure/config/product-context.js +0 -67
  28. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  29. package/dist/infrastructure/context/adapters/figma-source.js +0 -163
  30. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
  31. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  32. package/dist/infrastructure/context/context-resolver.js +0 -144
  33. package/dist/infrastructure/context/context-source.js +0 -114
  34. package/dist/infrastructure/context/oauth-flow.js +0 -207
  35. package/dist/infrastructure/context/oauth-store.js +0 -119
  36. package/dist/infrastructure/context/registry-bootstrap.js +0 -99
  37. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  38. package/dist/infrastructure/cost/budget-config.js +0 -113
  39. package/dist/infrastructure/cost/cost-router.js +0 -104
  40. package/dist/infrastructure/cost/spend-aggregator.js +0 -147
  41. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
  42. package/dist/infrastructure/hooks/hook-mode.js +0 -71
  43. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
  44. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  45. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  46. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  47. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  48. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  49. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
  50. package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
  51. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  52. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
  53. package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
  54. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
  55. package/dist/infrastructure/orchestration/context-handoff.js +0 -144
  56. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  57. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  58. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  59. package/dist/infrastructure/orchestration/failover-config.js +0 -77
  60. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  61. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
  62. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
  63. package/dist/infrastructure/orchestration/graphify-context.js +0 -254
  64. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  65. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
  66. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  67. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  68. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  69. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
  70. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
  71. package/dist/infrastructure/orchestration/readiness.js +0 -103
  72. package/dist/infrastructure/orchestration/session-manager.js +0 -1091
  73. package/dist/infrastructure/orchestration/session-persistence.js +0 -258
  74. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  75. package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
  76. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  77. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  78. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
  79. package/dist/infrastructure/protocols/probe-port.js +0 -36
  80. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  81. package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
  82. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  83. package/dist/infrastructure/protocols/verify-page.js +0 -983
  84. package/dist/infrastructure/security/file-collection.js +0 -118
  85. package/dist/infrastructure/security/input-validator.js +0 -152
  86. package/dist/infrastructure/security/permissions-config.js +0 -234
  87. package/dist/infrastructure/security/trust-scorer.js +0 -657
  88. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  89. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  90. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  91. package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
  92. package/dist/infrastructure/storage/project-thread-reader.js +0 -172
  93. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  94. package/dist/infrastructure/storage/task-repository.js +0 -206
  95. package/dist/monitoring.js +0 -212
  96. package/dist/presentation/cli/commands/audit-command.js +0 -126
  97. package/dist/presentation/cli/commands/budget-command.js +0 -79
  98. package/dist/presentation/cli/commands/config-command.js +0 -125
  99. package/dist/presentation/cli/commands/context-source-command.js +0 -198
  100. package/dist/presentation/cli/commands/hook-command.js +0 -265
  101. package/dist/presentation/cli/commands/init-command.js +0 -578
  102. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  103. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  104. package/dist/presentation/cli/commands/permission-command.js +0 -116
  105. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  106. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  107. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  108. package/dist/presentation/cli/commands/report-command.js +0 -387
  109. package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
  110. package/dist/presentation/cli/commands/setup-command.js +0 -307
  111. package/dist/presentation/cli/commands/status-command.js +0 -661
  112. package/dist/presentation/cli/commands/thread-command.js +0 -288
  113. package/dist/presentation/cli/commands/trust-command.js +0 -306
  114. package/dist/presentation/cli/commands/verify-command.js +0 -233
  115. package/dist/presentation/cli/error-display.js +0 -336
  116. package/dist/presentation/cli/repl.js +0 -797
  117. package/dist/presentation/cli/task-deliverable.js +0 -127
  118. package/dist/testing/cassette.js +0 -125
  119. 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