great-cto 3.60.0 → 3.62.0
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/board/.claude-plugin/plugin.json +23 -1
- package/board/packages/board/lib/config.mjs +11 -4
- package/board/packages/board/lib/data-readers.mjs +4 -6
- package/board/packages/board/lib/projects.mjs +48 -17
- package/board/packages/board/lib/routes.mjs +2 -1
- package/board/packages/board/push-adapter.mjs +3 -3
- package/board/packages/cli/src/private-state.mjs +43 -0
- package/board/packages/cli/src/state-home.mjs +15 -0
- package/board/scripts/codex-pipeline.mjs +34 -5
- package/board/scripts/hooks/auto-attach-reviewers.mjs +449 -0
- package/board/scripts/lib/adaptive-benchmark-collector.mjs +202 -0
- package/board/scripts/lib/adaptive-benchmark-protocol.mjs +75 -0
- package/board/scripts/lib/agent-execution-budget.mjs +143 -0
- package/board/scripts/lib/approval-level.mjs +174 -0
- package/board/scripts/lib/benchmark-review-dispatch.mjs +52 -0
- package/board/scripts/lib/benchmark-scorer-signature.mjs +64 -0
- package/board/scripts/lib/claude-exec.mjs +11 -3
- package/board/scripts/lib/codex-checks.mjs +77 -6
- package/board/scripts/lib/codex-exec.mjs +13 -2
- package/board/scripts/lib/codex-host-state.mjs +2 -0
- package/board/scripts/lib/codex-pipeline.mjs +281 -38
- package/board/scripts/lib/codex-release.mjs +9 -5
- package/board/scripts/lib/codex-role-profiles.mjs +46 -1
- package/board/scripts/lib/controlled-specialists.mjs +150 -0
- package/board/scripts/lib/controller-dispatch-evidence.mjs +60 -0
- package/board/scripts/lib/pinned-benchmark-scorer.mjs +132 -0
- package/board/scripts/lib/receipt.mjs +31 -6
- package/board/scripts/lib/review-graph-floor.mjs +54 -0
- package/board/scripts/lib/runtime-gate-policy.mjs +84 -0
- package/board/scripts/lib/scoped-review-reuse.mjs +154 -0
- package/board/scripts/lib/specialist-plan.mjs +102 -0
- package/board/scripts/lib/turn-snapshot.mjs +24 -4
- package/dist/codex-host.js +1 -1
- package/dist/private-state.mjs +53 -0
- package/dist/serve.js +10 -5
- package/dist/state-home.mjs +15 -0
- package/dist/task-queue.js +2 -2
- package/dist/update-check.js +2 -5
- package/dist/webhook-cli.js +2 -2
- package/dist/webhook-config.js +18 -14
- package/dist/webhook-dispatch.js +5 -8
- package/dist/worker.js +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SessionStart hook — auto-attach reviewers.
|
|
4
|
+
*
|
|
5
|
+
* Scans git diff since the last session and emits a list of reviewers that
|
|
6
|
+
* should be invoked for the changed files. Writes a short markdown block to
|
|
7
|
+
* stdout that the SessionStart bash hook pipes into Claude's context.
|
|
8
|
+
*
|
|
9
|
+
* Why this exists
|
|
10
|
+
* ---------------
|
|
11
|
+
* In real sessions, users dispatch Agent calls with
|
|
12
|
+
* `subagent_type: general-purpose` and never invoke the 24 specialist
|
|
13
|
+
* reviewers — even when migrations / auth / payment code changed.
|
|
14
|
+
* This hook makes the right reviewers visible at session start, so Claude
|
|
15
|
+
* picks the matching `subagent_type` proactively without `/start`.
|
|
16
|
+
*
|
|
17
|
+
* Pattern-to-reviewer map below. Add patterns conservatively — over-firing
|
|
18
|
+
* burns context. Default: emit only if diff > 0 files match a pattern.
|
|
19
|
+
*
|
|
20
|
+
* Exit codes: 0 always. Silent if no matches or no .great_cto dir.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { execFileSync } from "node:child_process";
|
|
24
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { adaptiveSpecialistPlan, planNotice } from '../lib/specialist-plan.mjs';
|
|
27
|
+
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
const greatCtoDir = join(cwd, ".great_cto");
|
|
30
|
+
|
|
31
|
+
// Imports expose rules without exiting the caller outside a project.
|
|
32
|
+
|
|
33
|
+
// Pattern → reviewer mapping. Ordered specific-first.
|
|
34
|
+
// Each pattern is matched against the changed-file paths via simple substring.
|
|
35
|
+
// `match` may be a function returning a count of matched files.
|
|
36
|
+
// EXCLUDE (below) drops every `.md` path, so a rule token that can only match a
|
|
37
|
+
// markdown file can never fire. Three did: `EVAL-.*\.md$`, `ssp\.md`, and an
|
|
38
|
+
// `.api.md` token — removed rather than left as rules that read as coverage.
|
|
39
|
+
// `tests/eval/` already routes the eval files that are not excluded for another
|
|
40
|
+
// reason. The test `no rule can only match an excluded path` keeps this true.
|
|
41
|
+
export const RULES = [
|
|
42
|
+
{ reviewer: "db-migration-reviewer", pattern: /(migrations\/|schema\.sql$|alembic\/|knex\/migrations|prisma\/migrations|\.room\.|Migration\d+\.kt$|Migration\d+\.swift$)/i,
|
|
43
|
+
cases: {
|
|
44
|
+
match: ["db/migrations/0007_add_index.sql", "prisma/migrations/20260801_init/migration.sql", "app/Migration12.kt"],
|
|
45
|
+
noMatch: ["src/db/query.ts", "docs/schema-notes.txt"],
|
|
46
|
+
} },
|
|
47
|
+
{ reviewer: "pci-reviewer", pattern: /(stripe|webhook.*sig|payment|refund|paypal|adyen|braintree|saq-a|pci-dss)/i,
|
|
48
|
+
cases: {
|
|
49
|
+
match: ["src/billing/stripe-webhook.ts", "api/refund.ts", "lib/adyen-client.ts"],
|
|
50
|
+
noMatch: ["src/user/profile.ts", "tests/fixtures/cart.json"],
|
|
51
|
+
} },
|
|
52
|
+
{ reviewer: "security-officer", pattern: /(auth\/|oauth|saml|jwt|password|login|session|csrf|cors\.|crypto\.|secret)/i,
|
|
53
|
+
cases: {
|
|
54
|
+
match: ["src/auth/session.ts", "api/oauth-callback.ts", "lib/jwt-verify.ts"],
|
|
55
|
+
noMatch: ["src/ui/button.tsx", "docs/onboarding.txt"],
|
|
56
|
+
} },
|
|
57
|
+
{ reviewer: "ai-security-reviewer", pattern: /(prompts?\/|system_prompt|tool_definitions|rag|embeddings?\/|jailbreak)/i,
|
|
58
|
+
cases: {
|
|
59
|
+
match: ["prompts/system.txt", "src/rag/retriever.ts", "lib/embeddings/index.ts"],
|
|
60
|
+
noMatch: ["src/api/users.ts"],
|
|
61
|
+
} },
|
|
62
|
+
{ reviewer: "ai-eval-engineer", pattern: /(tests\/eval\/|golden_set|prompt_regression)/i,
|
|
63
|
+
cases: {
|
|
64
|
+
match: ["tests/eval/runner.mjs", "data/golden_set.jsonl"],
|
|
65
|
+
noMatch: ["tests/unit/parser.test.ts"],
|
|
66
|
+
} },
|
|
67
|
+
{ reviewer: "mobile-store-reviewer", pattern: /(play[_-]?store|app[_-]?store|fastlane|iap|in_app_purchase|store_listing|aab$|ipa$)/i,
|
|
68
|
+
cases: {
|
|
69
|
+
match: ["fastlane/Fastfile", "ios/iap-manager.swift", "build/app.aab"],
|
|
70
|
+
noMatch: ["src/api/client.ts"],
|
|
71
|
+
} },
|
|
72
|
+
{ reviewer: "api-platform-reviewer", pattern: /(openapi\.|swagger|graphql\.schema|webhooks?\.|rfc7807|sunset_header)/i,
|
|
73
|
+
cases: {
|
|
74
|
+
match: ["api/openapi.yaml", "src/webhooks.ts", "schema/graphql.schema"],
|
|
75
|
+
noMatch: ["src/db/pool.ts"],
|
|
76
|
+
} },
|
|
77
|
+
{ reviewer: "voice-ai-reviewer", pattern: /(twilio|vonage|livekit|deepgram|elevenlabs|ivr|tcpa|stir.shaken)/i,
|
|
78
|
+
cases: {
|
|
79
|
+
match: ["integrations/twilio-handler.ts", "src/ivr/menu.ts", "lib/deepgram-stream.ts"],
|
|
80
|
+
noMatch: ["src/audio/player.ts"],
|
|
81
|
+
} },
|
|
82
|
+
{ reviewer: "hr-ai-reviewer", pattern: /(aedt|ny.?city.?ll.?144|resume_screen|hiring|interview_ai)/i,
|
|
83
|
+
cases: {
|
|
84
|
+
match: ["src/hiring/rank.ts", "lib/resume_screen.py"],
|
|
85
|
+
noMatch: ["src/hr/payroll.ts"],
|
|
86
|
+
} },
|
|
87
|
+
{ reviewer: "edtech-reviewer", pattern: /(coppa|ferpa|student[_-]?(data|pii)|sopipa)/i,
|
|
88
|
+
cases: {
|
|
89
|
+
match: ["src/coppa-consent.ts", "lib/student_data.py"],
|
|
90
|
+
noMatch: ["src/course/player.tsx"],
|
|
91
|
+
} },
|
|
92
|
+
{ reviewer: "gov-reviewer", pattern: /(fedramp|nist.?800.?53|cjis|fips.?140|fisma|vpat)/i,
|
|
93
|
+
cases: {
|
|
94
|
+
match: ["config/fedramp-boundary.yaml", "src/fisma-controls.ts"],
|
|
95
|
+
noMatch: ["src/gov/ui.tsx"],
|
|
96
|
+
} },
|
|
97
|
+
{ reviewer: "game-reviewer", pattern: /(loot[_-]?box|esrb|pegi|iarc|gacha)/i,
|
|
98
|
+
cases: {
|
|
99
|
+
match: ["src/loot_box.ts", "config/esrb.json"],
|
|
100
|
+
noMatch: ["src/game/physics.ts"],
|
|
101
|
+
} },
|
|
102
|
+
// general.?ledger / \bgaap\b appended for accounting-vertical coverage — there is no
|
|
103
|
+
// dedicated accounting-reviewer agent (only project-auditor, which is unrelated), so
|
|
104
|
+
// core accounting-controls signals route to the SOX-ITGC surface enterprise-saas-reviewer
|
|
105
|
+
// already owns. "1099"/"e-file" deliberately excluded: too loose as bare substrings
|
|
106
|
+
// (collide with port numbers, filenames, version strings) without a path anchor.
|
|
107
|
+
{ reviewer: "enterprise-saas-reviewer",pattern: /(scim|tenant_id|row.?level.?security|sso\/|saml\/|sox.itgc)/i,
|
|
108
|
+
cases: {
|
|
109
|
+
match: ["src/scim/provision.ts", "db/row-level-security.sql", "src/sso/handler.ts"],
|
|
110
|
+
noMatch: ["src/ui/table.tsx"],
|
|
111
|
+
} },
|
|
112
|
+
// procurement-reviewer: purchasing / source-to-pay tokens only — deliberately avoids
|
|
113
|
+
// bare "purchase"/"order"/"vendor" (each collides with e-commerce/checkout code).
|
|
114
|
+
// Each token below is a procurement-specific compound or acronym:
|
|
115
|
+
// purchase.?order — the PO artifact itself, requires the compound
|
|
116
|
+
// three.?way.?match — the PO/receipt/invoice reconciliation control, unambiguous
|
|
117
|
+
// \brfp\b — Request for Proposal (word-boundaried: short acronym)
|
|
118
|
+
// \bofac\b — sanctions-screening authority (word-boundaried)
|
|
119
|
+
// punchout — e-procurement catalog-launch protocol, unambiguous
|
|
120
|
+
// cxml — the punchout XML dialect name, unambiguous
|
|
121
|
+
// requisition — procurement-specific request artifact (not generic "request")
|
|
122
|
+
{ reviewer: "procurement-reviewer", pattern: /(purchase.?order|three.?way.?match|\brfp\b|\bofac\b|punchout|cxml|requisition)/i,
|
|
123
|
+
cases: {
|
|
124
|
+
match: ["src/purchase-order.ts", "lib/punchout.ts"],
|
|
125
|
+
noMatch: ["src/store/cart.ts"],
|
|
126
|
+
} },
|
|
127
|
+
// accounting-reviewer: bookkeeping / GL / close-cycle tokens — MOVED from
|
|
128
|
+
// enterprise-saas-reviewer (was a stop-gap before this reviewer existed, see
|
|
129
|
+
// great_cto-k0uf). general.?ledger and \bgaap\b now route here exclusively so
|
|
130
|
+
// accounting signals hit the domain-correct reviewer instead of the generic
|
|
131
|
+
// enterprise-controls one.
|
|
132
|
+
{ reviewer: "accounting-reviewer", pattern: /(asc.?606|journal.?entry|general.?ledger|\bgaap\b|1099|month.?end.?close|chart.?of.?accounts)/i,
|
|
133
|
+
cases: {
|
|
134
|
+
match: ["src/journal-entry.ts", "lib/general-ledger.ts"],
|
|
135
|
+
noMatch: ["src/reports/chart.tsx"],
|
|
136
|
+
} },
|
|
137
|
+
// msp-reviewer: managed-service-provider / RMM-PSA tokens only — deliberately avoids
|
|
138
|
+
// bare "tenant"/"service" (both collide with generic SaaS/microservice code; tenant_id
|
|
139
|
+
// already belongs to enterprise-saas-reviewer above). Each token below is MSP-specific:
|
|
140
|
+
// \bmsa\b / \bsla\b — Master Service Agreement / Service Level Agreement acronyms
|
|
141
|
+
// (word-boundaried: both are short and common as substrings)
|
|
142
|
+
// \brmm\b / \bpsa\b — Remote Monitoring & Management / Professional Services
|
|
143
|
+
// Automation, the two dominant MSP tool categories
|
|
144
|
+
// multi.?tenant — MSP cross-client blast-radius framing (distinct from
|
|
145
|
+
// enterprise-saas-reviewer's bare "tenant_id" data-isolation signal)
|
|
146
|
+
// managed.?service — "managed service provider" / "managed services", unambiguous
|
|
147
|
+
// credential.?vault — MSP's core liability surface (client credential storage)
|
|
148
|
+
{ reviewer: "msp-reviewer", pattern: /(\bmsa\b|\bsla\b|\brmm\b|\bpsa\b|multi.?tenant|managed.?service|credential.?vault)/i,
|
|
149
|
+
cases: {
|
|
150
|
+
match: ["src/rmm-agent.ts", "lib/credential-vault.ts"],
|
|
151
|
+
noMatch: ["src/ui/nav.tsx"],
|
|
152
|
+
} },
|
|
153
|
+
// tax-reviewer: tax-prep / IRS e-file tokens only — deliberately avoids bare "tax"
|
|
154
|
+
// (collides with sales-tax/tax-rate code in commerce/pci contexts) and bare "irs"
|
|
155
|
+
// alone as unanchored substring was considered too loose; each token below is a
|
|
156
|
+
// tax-prep-specific compound or acronym:
|
|
157
|
+
// \bptin\b — Preparer Tax Identification Number (word-boundaried)
|
|
158
|
+
// circular.?230 — IRS preparer-practice regulations, unambiguous
|
|
159
|
+
// form.?8879 — the e-file signature authorization form number
|
|
160
|
+
// \bmef\b — Modernized e-File, the IRS e-file system (word-boundaried:
|
|
161
|
+
// short acronym)
|
|
162
|
+
// pub.?4557 — IRS Publication 4557 (taxpayer data safeguards)
|
|
163
|
+
// section.?7216 — IRC §7216 consent-to-disclose statute
|
|
164
|
+
// tax.?prep — "tax prep"/"tax preparation", the domain itself
|
|
165
|
+
{ reviewer: "tax-reviewer", pattern: /(\bptin\b|circular.?230|form.?8879|\bmef\b|pub.?4557|section.?7216|tax.?prep)/i,
|
|
166
|
+
cases: {
|
|
167
|
+
match: ["src/form-8879.ts", "lib/tax-prep.py"],
|
|
168
|
+
noMatch: ["src/finance/invoice.ts"],
|
|
169
|
+
} },
|
|
170
|
+
{ reviewer: "insurance-reviewer", pattern: /(naic|solvency|ifrs.?17|acord|actuarial)/i,
|
|
171
|
+
cases: {
|
|
172
|
+
match: ["src/naic-filing.ts", "lib/actuarial-model.py"],
|
|
173
|
+
noMatch: ["src/policy/ui.tsx"],
|
|
174
|
+
} },
|
|
175
|
+
// legal-reviewer: legal-services / legal-tech domain tokens only — deliberately
|
|
176
|
+
// avoids bare "legal"/"law"/"case"/"trust" (each collides with generic code:
|
|
177
|
+
// "legal" appears in license headers, "law" in unrelated words, "case" in
|
|
178
|
+
// switch/test-case terminology, "trust" in security/crypto trust-boundary code).
|
|
179
|
+
// Each token below is a domain-specific signal:
|
|
180
|
+
// iolta — Interest on Lawyers' Trust Accounts, the client-trust
|
|
181
|
+
// accounting regime; unambiguous legal-practice term
|
|
182
|
+
// trust.?account — trust-accounting artifact, requires the compound so
|
|
183
|
+
// bare "trust" (crypto/security) doesn't match
|
|
184
|
+
// matter.?number — legal "matter" identifier; requires the compound so
|
|
185
|
+
// bare "matter" (generic English word) doesn't match
|
|
186
|
+
// conflict.?check — conflict-of-interest screening, legal-ethics specific
|
|
187
|
+
// \bpacer\b — federal court public e-filing system (word-boundaried:
|
|
188
|
+
// avoids matching inside unrelated identifiers)
|
|
189
|
+
// \becf\b — Electronic Case Filing (word-boundaried: "ecf" alone
|
|
190
|
+
// is short enough to risk false positives without \b)
|
|
191
|
+
// \bcm\/ecf\b — CM/ECF, the federal court e-filing platform name
|
|
192
|
+
// retainer.?agreement — engagement/retainer contract, legal-practice specific
|
|
193
|
+
// attorney.?client — attorney-client privilege, unambiguous legal-ethics term
|
|
194
|
+
// \bupl\b — Unauthorized Practice of Law, word-boundaried (short token)
|
|
195
|
+
// docket — court docket / case-management term, legal-specific
|
|
196
|
+
{ reviewer: "legal-reviewer", pattern: /(iolta|trust.?account|matter.?number|conflict.?check|\bpacer\b|\becf\b|\bcm\/ecf\b|retainer.?agreement|attorney.?client|\bupl\b|docket)/i,
|
|
197
|
+
cases: {
|
|
198
|
+
match: ["src/iolta-ledger.ts", "lib/conflict-check.ts", "src/docket-sync.ts"],
|
|
199
|
+
noMatch: ["src/legal/ui.tsx"],
|
|
200
|
+
} },
|
|
201
|
+
// healthcare-reviewer: HIPAA/PHI/clinical-transport tokens only — deliberately
|
|
202
|
+
// avoids bare "health"/"care"/"claim" (collide with insurance, wellness apps,
|
|
203
|
+
// generic support-ticket code). Each token below is a domain-specific signal:
|
|
204
|
+
// hipaa — the statute itself
|
|
205
|
+
// phi[_-] — Protected Health Information as an identifier/var prefix
|
|
206
|
+
// (require the separator so it doesn't match unrelated "phi" substrings)
|
|
207
|
+
// hl7 / fhir — the two dominant clinical-data transport standards
|
|
208
|
+
// hitech — HIPAA's breach-notification amendment
|
|
209
|
+
// \bbaa\b — Business Associate Agreement (word-boundaried: "baa" alone
|
|
210
|
+
// is a common false-positive token otherwise)
|
|
211
|
+
// (^|\/)ehr\/ — electronic-health-record integration directories (Epic/Cerner/
|
|
212
|
+
// athenahealth); path-anchored so it doesn't match inside unrelated
|
|
213
|
+
// words like "behr" or "cheerful"
|
|
214
|
+
// superbill — clinical billing artifact, healthcare-specific (not "invoice")
|
|
215
|
+
// icd-?10 — the diagnosis coding standard
|
|
216
|
+
// soap.?note — clinical documentation format (Subjective/Objective/Assessment/Plan)
|
|
217
|
+
// cdt-?code — Current Dental Terminology billing code (dental-specific,
|
|
218
|
+
// parallels icd-?10 for the dental vertical)
|
|
219
|
+
// odontogram — dental charting artifact (tooth-by-tooth condition map),
|
|
220
|
+
// unambiguous dental-clinical term
|
|
221
|
+
// perio-chart — periodontal charting artifact, dental-specific
|
|
222
|
+
// dental.?claim — dental insurance claim, requires the compound so bare
|
|
223
|
+
// "claim" (generic/insurance-collision) doesn't match
|
|
224
|
+
{ reviewer: "healthcare-reviewer", pattern: /(hipaa|phi[_-]|hl7|fhir|hitech|\bbaa\b|(^|\/)ehr\/|superbill|icd-?10|soap.?note|cdt-?code|odontogram|perio-chart|dental.?claim)/i,
|
|
225
|
+
cases: {
|
|
226
|
+
match: ["src/hipaa-audit.ts", "integrations/fhir/client.ts", "src/soap-note.ts"],
|
|
227
|
+
noMatch: ["src/patient/avatar.tsx"],
|
|
228
|
+
} },
|
|
229
|
+
// rcm-reviewer: medical-billing / revenue-cycle tokens only — deliberately excludes
|
|
230
|
+
// bare "claim" (generic/insurance-collision, already avoided by healthcare-reviewer's
|
|
231
|
+
// "dental.?claim" compound) and bare "modifier"/"denial" (too generic on their own).
|
|
232
|
+
// "icd-?10" and "superbill" stay on healthcare-reviewer (clinical/chart surface);
|
|
233
|
+
// the tokens below are billing/claims-submission specific:
|
|
234
|
+
// cms-?1500 / ub-?04 — the professional/institutional claim form names
|
|
235
|
+
// hcpcs — HCPCS Level II billing code set
|
|
236
|
+
// \b835\b — ERA remittance transaction number (word-boundaried:
|
|
237
|
+
// short numeric token needs a boundary to avoid false hits)
|
|
238
|
+
// remittance.?advice — the 835's plain-English name
|
|
239
|
+
// prior.?auth — prior-authorization workflow, billing-specific compound
|
|
240
|
+
// denial.?code — CARC/RARC denial-code workflow, requires the compound so
|
|
241
|
+
// bare "denial" doesn't fire on unrelated rejection/reject code
|
|
242
|
+
// npi\b — National Provider Identifier (word-boundaried)
|
|
243
|
+
{ reviewer: "rcm-reviewer", pattern: /(cms-?1500|ub-?04|hcpcs|\b835\b|remittance.?advice|prior.?auth|denial.?code|npi\b)/i,
|
|
244
|
+
cases: {
|
|
245
|
+
match: ["src/cms-1500.ts", "lib/prior-auth.ts", "src/denial-code-map.ts"],
|
|
246
|
+
noMatch: ["src/billing/ui.tsx"],
|
|
247
|
+
} },
|
|
248
|
+
// regulated-reviewer: DORA/NIS2/ISO27001 only — its other two frameworks (SOX ITGC,
|
|
249
|
+
// HIPAA) are already claimed by enterprise-saas-reviewer's `sox.itgc` and the
|
|
250
|
+
// healthcare-reviewer pattern above respectively, so re-adding those tokens here
|
|
251
|
+
// would double-attach two reviewers on the same file for no added signal. DORA/NIS2/
|
|
252
|
+
// ISO27001 are regulated-reviewer's exclusive surface — no other rule covers them.
|
|
253
|
+
{ reviewer: "regulated-reviewer", pattern: /(\bdora\b|dora.?ict|nis2|iso.?27001)/i,
|
|
254
|
+
cases: {
|
|
255
|
+
match: ["config/dora-ict-register.yaml", "src/nis2-controls.ts"],
|
|
256
|
+
noMatch: ["src/app/main.ts"],
|
|
257
|
+
} },
|
|
258
|
+
{ reviewer: "infra-reviewer", pattern: /(\.tf$|terraform\/|helm\/|kustomize\/|cdk\/|pulumi\/|\.bicep$)/i,
|
|
259
|
+
cases: {
|
|
260
|
+
match: ["infra/main.tf", "helm/values.yaml", "cdk/stack.ts"],
|
|
261
|
+
noMatch: ["src/server.ts"],
|
|
262
|
+
} },
|
|
263
|
+
{ reviewer: "web-store-reviewer", pattern: /(manifest\.json$|mv3|chrome_extension|firefox_addon)/i,
|
|
264
|
+
cases: {
|
|
265
|
+
match: ["extension/manifest.json", "src/mv3-worker.ts"],
|
|
266
|
+
noMatch: ["package.json"],
|
|
267
|
+
} },
|
|
268
|
+
{ reviewer: "performance-engineer", pattern: /(perf|p99|latency_budget|k6\/|locust\/|gatling\/|benchmark)/i,
|
|
269
|
+
cases: {
|
|
270
|
+
match: ["tests/k6/load.js", "src/perf-budget.ts", "bench/benchmark.ts"],
|
|
271
|
+
noMatch: ["src/ui/form.tsx"],
|
|
272
|
+
} },
|
|
273
|
+
// This rule never fired. It required a manifest name AND the word "version" or
|
|
274
|
+
// "public api" in the SAME string, but the string it is tested against is a
|
|
275
|
+
// FILE PATH — no path contains both, so library-reviewer was never attached to
|
|
276
|
+
// anything since the rule was written. The `version` half was written as if it
|
|
277
|
+
// would see the diff.
|
|
278
|
+
//
|
|
279
|
+
// Bare `package.json` is deliberately not a trigger: every project has one, and
|
|
280
|
+
// a reviewer that attaches to everything is one people learn to ignore. What is
|
|
281
|
+
// matched instead are the manifests and artifacts that only a PUBLISHED library
|
|
282
|
+
// has — a declared public API surface, or a packaging manifest for an ecosystem
|
|
283
|
+
// where the file's presence already implies distribution.
|
|
284
|
+
{ reviewer: "library-reviewer", pattern: /(api-extractor\.json|(^|\/)(pyproject\.toml|Cargo\.toml|setup\.py|go\.mod)$|\.npmignore$)/i,
|
|
285
|
+
cases: {
|
|
286
|
+
match: ["Cargo.toml", "pyproject.toml", "sdk/api-extractor.json", "go.mod"],
|
|
287
|
+
noMatch: ["package.json", "src/index.ts"],
|
|
288
|
+
} },
|
|
289
|
+
{ reviewer: "cli-reviewer", pattern: /(bin\/|cli\/main|argv|exit.?code)/i,
|
|
290
|
+
cases: {
|
|
291
|
+
match: ["bin/great-cto", "src/cli/main.ts", "lib/argv-parse.ts"],
|
|
292
|
+
noMatch: ["src/server/routes.ts"],
|
|
293
|
+
} },
|
|
294
|
+
// Building an MCP server, not consuming one: `.mcp.json` and `.playwright-mcp/`
|
|
295
|
+
// are what USING a server leaves behind, and neither is ours to review.
|
|
296
|
+
{ reviewer: "mcp-server-reviewer", pattern: /(mcp[-_]servers?[/.]|[/.]mcp[-_]server\.|(^|\/)mcp\.(ts|js|mjs|py)$|modelcontextprotocol)/i,
|
|
297
|
+
cases: {
|
|
298
|
+
match: ["mcp-servers/llm-router/server.py", "src/mcp.ts", "lib/modelcontextprotocol-adapter.ts"],
|
|
299
|
+
noMatch: [".mcp.json", ".playwright-mcp/session.json"],
|
|
300
|
+
} },
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
// Known limit, found by giving every rule its own cases: `^docs/` and `\.md$`
|
|
304
|
+
// are excluded wholesale, so a compliance artefact that lives as a document —
|
|
305
|
+
// a FedRAMP boundary, a DORA ICT register, a system prompt in markdown — never
|
|
306
|
+
// reaches gov-reviewer, regulated-reviewer or ai-security-reviewer. Those rules
|
|
307
|
+
// fire only on config and source. Widening EXCLUDE would make every rule fire on
|
|
308
|
+
// every doc, so it is recorded here rather than changed in passing.
|
|
309
|
+
//
|
|
310
|
+
// Files we deliberately ignore for reviewer routing — these are metadata,
|
|
311
|
+
// templates, or test fixtures whose path matches a pattern by accident but
|
|
312
|
+
// they're not actual product code that needs review.
|
|
313
|
+
const EXCLUDE = [
|
|
314
|
+
/^tests\/fixtures\//, // fixtures for our own tests
|
|
315
|
+
/^docs\//, // analysis docs, ADRs, marketing
|
|
316
|
+
/^commands\//, // slash-command definitions
|
|
317
|
+
/^agents\//, // agent prompt files (great_cto-repo only)
|
|
318
|
+
/^skills\/great_cto\//, // plugin skill files
|
|
319
|
+
/^\.claude\//, // claude config
|
|
320
|
+
/\.md$/, // any markdown — docs talking about X ≠ doing X
|
|
321
|
+
/^node_modules\//,
|
|
322
|
+
/^\.git\//,
|
|
323
|
+
// What USING an MCP server leaves behind, not code we wrote. The
|
|
324
|
+
// mcp-server-reviewer rule already says these are not ours to review; they
|
|
325
|
+
// were still reaching every OTHER rule, and `.playwright-mcp/session.json`
|
|
326
|
+
// matched security-officer's `session` token on every run that used the
|
|
327
|
+
// browser. A reviewer flagged by a tool's own cache file is noise, and noise
|
|
328
|
+
// is what teaches people to skim the reviewer list.
|
|
329
|
+
/^\.playwright-mcp\//,
|
|
330
|
+
/^\.mcp\.json$/,
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
export function shouldExclude(path) {
|
|
334
|
+
return EXCLUDE.some(rx => rx.test(path));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Get changed files since last session.
|
|
338
|
+
// Strategy: files touched in the last 7 days (broader than `since last commit`,
|
|
339
|
+
// captures uncommitted edits + recent work). Falls back to last 50 commits.
|
|
340
|
+
function getChangedFiles() {
|
|
341
|
+
try {
|
|
342
|
+
// execFileSync, not execSync: no shell at all. Nothing here is interpolated
|
|
343
|
+
// today, but this runs on EVERY tool use, and a hook that builds a command
|
|
344
|
+
// as a string is one careless edit away from running whatever a path
|
|
345
|
+
// contains. `sort -u` was the only reason a shell was needed; a Set does it.
|
|
346
|
+
const recent = execFileSync(
|
|
347
|
+
"git", ["log", "--since=@{7.days.ago}", "--name-only", "--pretty=format:"],
|
|
348
|
+
{ cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
349
|
+
).split("\n").filter(Boolean);
|
|
350
|
+
// Plus currently uncommitted
|
|
351
|
+
const uncommitted = execFileSync("git", ["status", "--porcelain"], {
|
|
352
|
+
cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"]
|
|
353
|
+
}).split("\n").map(l => l.slice(3)).filter(Boolean);
|
|
354
|
+
return [...new Set([...recent, ...uncommitted])];
|
|
355
|
+
} catch {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Read verdicts log to skip reviewers that have ALREADY run recently on these files.
|
|
361
|
+
// Conservative: dedupe by reviewer name + day. If a reviewer ran in the last 24h,
|
|
362
|
+
// don't re-flag it.
|
|
363
|
+
function recentlyInvokedReviewers() {
|
|
364
|
+
try {
|
|
365
|
+
// log-verdict.sh writes to the PROJECT-local .great_cto/verdicts/ — read
|
|
366
|
+
// that first; the HOME-level dir only exists if a global aggregator syncs
|
|
367
|
+
// logs there. Reading only HOME meant the dedupe never fired (bug).
|
|
368
|
+
const candidateDirs = [
|
|
369
|
+
join(greatCtoDir, "verdicts"),
|
|
370
|
+
join(process.env.HOME || "~", ".great_cto", "verdicts"),
|
|
371
|
+
];
|
|
372
|
+
const day24Ago = Date.now() - 24 * 3600 * 1000;
|
|
373
|
+
const seen = new Set();
|
|
374
|
+
for (const verdictDir of candidateDirs) {
|
|
375
|
+
if (!existsSync(verdictDir)) continue;
|
|
376
|
+
scanVerdictDir(verdictDir, day24Ago, seen);
|
|
377
|
+
}
|
|
378
|
+
return seen;
|
|
379
|
+
} catch {
|
|
380
|
+
return new Set();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function scanVerdictDir(verdictDir, day24Ago, seen) {
|
|
385
|
+
for (const f of readdirSync(verdictDir)) {
|
|
386
|
+
const fp = join(verdictDir, f);
|
|
387
|
+
try {
|
|
388
|
+
const stat = statSync(fp);
|
|
389
|
+
if (stat.mtimeMs < day24Ago) continue;
|
|
390
|
+
const lines = readFileSync(fp, "utf8").split("\n").slice(-30);
|
|
391
|
+
for (const line of lines) {
|
|
392
|
+
const m = line.match(/\|?\s*([\w-]+-reviewer|security-officer|qa-engineer|architect|pm|senior-dev|devops|l3-support|ai-eval-engineer|ai-prompt-architect)\s*\|/);
|
|
393
|
+
if (!m) continue;
|
|
394
|
+
// Lines start with an ISO-8601 UTC timestamp (verdict-format.md).
|
|
395
|
+
// Compare it against the 24h window instead of matching a calendar
|
|
396
|
+
// date string (the old check only matched yesterday's date, so
|
|
397
|
+
// verdicts written today were never deduped).
|
|
398
|
+
const ts = Date.parse(line.split(" ")[0]);
|
|
399
|
+
if (!Number.isNaN(ts) && ts >= day24Ago) seen.add(m[1]);
|
|
400
|
+
}
|
|
401
|
+
} catch { /* skip unreadable */ }
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function main() {
|
|
406
|
+
if (!existsSync(greatCtoDir)) process.exit(0);
|
|
407
|
+
const plan = adaptiveSpecialistPlan(cwd, RULES);
|
|
408
|
+
if (plan) { console.log(planNotice(plan)); return; }
|
|
409
|
+
const files = getChangedFiles();
|
|
410
|
+
if (files.length === 0) process.exit(0);
|
|
411
|
+
|
|
412
|
+
const recent = recentlyInvokedReviewers();
|
|
413
|
+
const matches = new Map(); // reviewer → [matched files]
|
|
414
|
+
|
|
415
|
+
// Filter out fixtures/docs/templates — they accidentally match patterns
|
|
416
|
+
// but don't represent product code that needs review.
|
|
417
|
+
const productFiles = files.filter(f => !shouldExclude(f));
|
|
418
|
+
if (productFiles.length === 0) process.exit(0);
|
|
419
|
+
|
|
420
|
+
for (const rule of RULES) {
|
|
421
|
+
if (recent.has(rule.reviewer)) continue;
|
|
422
|
+
const matchedFiles = productFiles.filter(f => rule.pattern.test(f));
|
|
423
|
+
if (matchedFiles.length === 0) continue;
|
|
424
|
+
matches.set(rule.reviewer, matchedFiles);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (matches.size === 0) process.exit(0);
|
|
428
|
+
|
|
429
|
+
// Cap output: emit at most 5 reviewers (most-matched first), summarize rest.
|
|
430
|
+
const sorted = [...matches.entries()].sort((a, b) => b[1].length - a[1].length);
|
|
431
|
+
const top = sorted.slice(0, 5);
|
|
432
|
+
const overflow = sorted.slice(5);
|
|
433
|
+
|
|
434
|
+
console.log("\n📋 REVIEWERS FLAGGED — match `subagent_type` to changed files");
|
|
435
|
+
for (const [reviewer, mf] of top) {
|
|
436
|
+
const preview = mf.slice(0, 2).join(", ");
|
|
437
|
+
const extra = mf.length > 2 ? ` (+${mf.length - 2})` : "";
|
|
438
|
+
console.log(` • ${reviewer} — ${preview}${extra}`);
|
|
439
|
+
}
|
|
440
|
+
if (overflow.length > 0) {
|
|
441
|
+
console.log(` • +${overflow.length} more: ${overflow.map(([r]) => r).join(", ")}`);
|
|
442
|
+
}
|
|
443
|
+
console.log(` Use: Agent({ subagent_type: "<reviewer>", description, prompt })`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Guarded so reviewer-nudge.mjs can import RULES/shouldExclude without running the scan.
|
|
447
|
+
import { fileURLToPath } from "node:url";
|
|
448
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
449
|
+
if (isMain) main();
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/** Read-only operator-pinned observation collection, not provider attestation. */
|
|
2
|
+
import { lstatSync, realpathSync, openSync, fstatSync, readSync, closeSync, constants } from 'node:fs';
|
|
3
|
+
import { resolve, relative, isAbsolute, sep } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { describeMatchedObservations } from './adaptive-benchmark-protocol.mjs';
|
|
6
|
+
import { dispatchEvidenceSummary } from './controller-dispatch-evidence.mjs';
|
|
7
|
+
import { treeReceipt } from './receipt.mjs';
|
|
8
|
+
import { scorerAuthority, verifyScorerReport } from './benchmark-scorer-signature.mjs';
|
|
9
|
+
import { baselineInputDigest, readPinnedScorerOracle } from './pinned-benchmark-scorer.mjs';
|
|
10
|
+
import { assertBenchmarkReviewDispatches } from './benchmark-review-dispatch.mjs';
|
|
11
|
+
|
|
12
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
13
|
+
const digest = value => hash(JSON.stringify(value));
|
|
14
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
15
|
+
const mandatory = ['code-reviewer', 'qa-engineer', 'security-officer'];
|
|
16
|
+
const hex = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
17
|
+
function document(raw, label) {
|
|
18
|
+
try { return JSON.parse(raw); }
|
|
19
|
+
catch { throw Error(`invalid benchmark ${label} JSON`); } // Never echo private input bytes.
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function benchmarkPolicySnapshot(state, { entry = state.queue[0] } = {}) {
|
|
23
|
+
// Stand-down audit grows during execution; it is not a policy configuration.
|
|
24
|
+
const gatePolicy = state.gatePolicy ? { ...state.gatePolicy } : null;
|
|
25
|
+
if (gatePolicy) delete gatePolicy.skipped;
|
|
26
|
+
return structuredClone({ graphHash: state.graphHash, allowed: state.allowed, intent: state.intent,
|
|
27
|
+
entry, hostRoutes: state.hostRoutes || {}, maxAttempts: state.maxAttempts,
|
|
28
|
+
checkPolicy: state.checkPolicy || null, gatePolicy, executionBudget: state.executionBudget || null,
|
|
29
|
+
specialistPolicy: state.specialistPolicy || null });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function registrationIdentity(registration) {
|
|
33
|
+
const { protocol, trial } = registration || {};
|
|
34
|
+
describeMatchedObservations(protocol, []); // Checks the entire registered protocol digest.
|
|
35
|
+
const block = protocol.blocks.find(b => b.task === trial?.task && b.repetition === trial?.repetition);
|
|
36
|
+
if (registration.version !== 1 || !block || !['legacy', 'adaptive'].includes(trial.arm)
|
|
37
|
+
|| typeof registration.scorer?.id !== 'string' || !registration.scorer.id.trim() || !hex(registration.scorer.sha256)
|
|
38
|
+
|| !Array.isArray(registration.requiredRoles) || registration.requiredRoles.some(r => typeof r !== 'string' || !r)
|
|
39
|
+
|| new Set(registration.requiredRoles).size !== registration.requiredRoles.length
|
|
40
|
+
|| mandatory.some(r => !registration.requiredRoles.includes(r))) throw Error('invalid benchmark trial registration or mandatory roles');
|
|
41
|
+
const scenario = protocol.scenarios.find(s => s.id === block.task);
|
|
42
|
+
scorerAuthority(registration.scorer);
|
|
43
|
+
return { registrationDigest: digest(registration), protocolDigest: protocol.digest,
|
|
44
|
+
task: block.task, repetition: block.repetition, arm: trial.arm,
|
|
45
|
+
specificationDigest: block.specificationDigest, acceptanceDigest: block.acceptanceDigest,
|
|
46
|
+
artifact: protocol.artifacts[trial.arm], policyDigest: digest(protocol.policies[trial.arm]),
|
|
47
|
+
scorer: registration.scorer, requiredRoles: registration.requiredRoles, scenario };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Called only on a newly constructed controller state, before its first dispatch. */
|
|
51
|
+
export function bindBenchmarkTrial(state, registration) {
|
|
52
|
+
if (state.benchmarkBinding || state.status !== 'ready' || state.active || state.steps !== 0
|
|
53
|
+
|| state.attempts.length || state.approvals.length || state.dispatchEvidence?.records.length
|
|
54
|
+
|| state.releasePolicy) throw Error('benchmark binding requires a fresh no-release run');
|
|
55
|
+
const identity = registrationIdentity(registration);
|
|
56
|
+
if (state.prompt !== identity.scenario.task || !same(state.acceptance, identity.scenario.checks)
|
|
57
|
+
|| digest(benchmarkPolicySnapshot(state)) !== identity.policyDigest) throw Error('benchmark task, criteria or policy differs from registration');
|
|
58
|
+
const initialReceipt = treeReceipt(state.root);
|
|
59
|
+
if (!initialReceipt || initialReceipt.truncated) throw Error('benchmark requires a readable complete initial Git receipt');
|
|
60
|
+
state.benchmarkBinding = structuredClone({ ...identity, initialReceipt });
|
|
61
|
+
return state.benchmarkBinding;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function outside(root, file) {
|
|
65
|
+
const rel = relative(root, realpathSync(file));
|
|
66
|
+
if (!rel || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))) throw Error('benchmark evidence must be external to the worker project');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function boundedFile(file, { privateFile = true, maxBytes = 65536 } = {}) {
|
|
70
|
+
const path = resolve(file);
|
|
71
|
+
if (realpathSync(path) !== path || lstatSync(path).isSymbolicLink()) throw Error('unsafe noncanonical benchmark evidence path');
|
|
72
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
|
|
73
|
+
try {
|
|
74
|
+
const info = fstatSync(fd);
|
|
75
|
+
if (!info.isFile() || (privateFile && (info.mode & 0o077)) || info.size > maxBytes) throw Error('unsafe or oversized bounded benchmark evidence file');
|
|
76
|
+
// Allocate only the observed bounded size; one extra byte detects growth.
|
|
77
|
+
const bytes = Buffer.alloc(info.size + 1); let used = 0, count;
|
|
78
|
+
while (used < bytes.length && (count = readSync(fd, bytes, used, bytes.length - used, null)) > 0) used += count;
|
|
79
|
+
if (used > info.size) throw Error('benchmark evidence grew during bounded read');
|
|
80
|
+
const raw = bytes.subarray(0, used);
|
|
81
|
+
return { raw, sha256: hash(raw) };
|
|
82
|
+
} finally { closeSync(fd); }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function externalFile(root, file, options) {
|
|
86
|
+
outside(root, file);
|
|
87
|
+
return boundedFile(file, options);
|
|
88
|
+
}
|
|
89
|
+
export function privateBenchmarkEvidence(root, file) { return externalFile(root, file); }
|
|
90
|
+
|
|
91
|
+
export function readBenchmarkRegistration(root, file) {
|
|
92
|
+
const evidence = externalFile(realpathSync(root), file);
|
|
93
|
+
const registration = document(evidence.raw, 'registration');
|
|
94
|
+
registrationIdentity(registration);
|
|
95
|
+
return registration;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function assertArtifacts(state) {
|
|
99
|
+
for (const [name, expected] of Object.entries(state.writes || {})) {
|
|
100
|
+
if (!name || isAbsolute(name) || name.includes('\\') || name.split('/').some(p => !p || p === '..' || p === '.')) throw Error('invalid benchmark artifact path');
|
|
101
|
+
const path = resolve(state.root, name), actual = realpathSync(path), info = lstatSync(path);
|
|
102
|
+
if (actual !== path || !info.isFile() || !state.allowed.some(p => name === p || name.startsWith(`${p}/`))
|
|
103
|
+
|| boundedFile(path, { privateFile: false, maxBytes: 1024 * 1024 }).sha256 !== expected) throw Error('benchmark artifact drift or unsafe path');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function assertScorable(state, registration, receipt) {
|
|
108
|
+
if (state.status !== 'done' || state.active || state.pending || state.wave || state.rework || state.queue.length
|
|
109
|
+
|| state.releasePolicy || !receipt || receipt.truncated || !state.attempts.length || !same(state.attempts.at(-1).receipt, receipt)) throw Error('controller is not settled on the scored receipt');
|
|
110
|
+
assertArtifacts(state);
|
|
111
|
+
if (state.dispatchEvidence?.records.some(r => !r || r.outcome === 'intent' || !r.finishedAt)) throw Error('unfinished controller invocation cannot be assessed as settled');
|
|
112
|
+
const required = new Set(registration.requiredRoles);
|
|
113
|
+
// Do not omit reviewers that the actual adaptive controller selected.
|
|
114
|
+
for (const role of state.specialistReview?.roles || []) required.add(role);
|
|
115
|
+
for (const role of required) {
|
|
116
|
+
const result = state.results[role], attempt = state.attempts.find(a => a.id === result?.attemptId);
|
|
117
|
+
const observed = state.dispatchEvidence?.records.find(r => r.id === `${attempt?.id}:verifier`);
|
|
118
|
+
if (!result || !attempt || attempt.role !== role || attempt.status !== 'verified'
|
|
119
|
+
|| result.verification?.state !== 'verified' || !result.verification.checks?.length
|
|
120
|
+
|| attempt.verification?.state !== 'verified' || !same(result.verification, attempt.verification)
|
|
121
|
+
|| observed?.kind !== 'verifier' || observed.outcome !== 'returned') throw Error('missing independently verified required role');
|
|
122
|
+
}
|
|
123
|
+
for (const approval of state.approvals) {
|
|
124
|
+
if (state.results[approval.role]?.digest !== approval.result) throw Error('approval refers to stale result');
|
|
125
|
+
}
|
|
126
|
+
assertBenchmarkReviewDispatches(state, [...required]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Shared read-only pre/post execution context; not proof of executing this package. */
|
|
130
|
+
export function benchmarkScoringContext({ registrationFile, stateFile, stateSha256, artifactFile,
|
|
131
|
+
scorerFile, oracleFile = null }, { settled = false } = {}) {
|
|
132
|
+
if (!hex(stateSha256)) throw Error('exact state byte pin required');
|
|
133
|
+
// Root is not trusted until the bounded private state has been parsed and checked.
|
|
134
|
+
const evidence = boundedFile(stateFile, { maxBytes: 8 * 1024 * 1024 });
|
|
135
|
+
if (evidence.sha256 !== stateSha256) throw Error('controller state byte pin mismatch');
|
|
136
|
+
const state = document(evidence.raw, 'state'), root = realpathSync(state.root);
|
|
137
|
+
if (state.version !== 1 || !state.id || state.root !== root) throw Error('invalid benchmark controller identity');
|
|
138
|
+
outside(root, stateFile);
|
|
139
|
+
const registration = readBenchmarkRegistration(root, registrationFile), identity = registrationIdentity(registration);
|
|
140
|
+
const binding = state.benchmarkBinding;
|
|
141
|
+
if (!binding || !same({ ...binding, initialReceipt: undefined }, { ...identity, initialReceipt: undefined })
|
|
142
|
+
|| !binding.initialReceipt || state.prompt !== identity.scenario.task || !same(state.acceptance, identity.scenario.checks)) throw Error('controller trial binding mismatch or absent pre-dispatch binding');
|
|
143
|
+
if (digest(benchmarkPolicySnapshot(state, { entry: registration.protocol.policies[identity.arm].entry })) !== identity.policyDigest) throw Error('controller execution policy drift');
|
|
144
|
+
const artifact = externalFile(root, artifactFile, { privateFile: false, maxBytes: 100 * 1024 * 1024 });
|
|
145
|
+
const scorer = externalFile(root, scorerFile, { privateFile: false, maxBytes: 1024 * 1024 });
|
|
146
|
+
if (artifact.sha256 !== identity.artifact.artifactSha256 || scorer.sha256 !== identity.scorer.sha256) throw Error('package or scorer code pin mismatch');
|
|
147
|
+
const receipt = treeReceipt(root);
|
|
148
|
+
let oracle = null;
|
|
149
|
+
if (identity.scorer.authority) {
|
|
150
|
+
if (!oracleFile) throw Error('registered signed scorer requires pinned oracle evidence');
|
|
151
|
+
oracle = readPinnedScorerOracle(root, oracleFile, identity.scorer.oracleSha256);
|
|
152
|
+
if (oracle.scenario !== identity.task) throw Error('scorer oracle task differs from registered trial');
|
|
153
|
+
}
|
|
154
|
+
if (settled) assertScorable(state, registration, receipt);
|
|
155
|
+
return { state, root, registration, identity, artifact, scorer, receipt, oracle };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Binds raw state bytes, package bytes and pinned scorer code to the supplied report. */
|
|
159
|
+
export function collectBenchmarkObservation(options) {
|
|
160
|
+
const { stateSha256, scoreFile = null, scoreSha256 = null } = options;
|
|
161
|
+
if (!hex(stateSha256) || (scoreFile && !hex(scoreSha256)) || (!scoreFile && scoreSha256)) throw Error('exact state/score byte pins required');
|
|
162
|
+
const { state, root, registration, identity, artifact, scorer, receipt, oracle } = benchmarkScoringContext(options);
|
|
163
|
+
const telemetry = dispatchEvidenceSummary(state);
|
|
164
|
+
let status = ['blocked', 'cancelled'].includes(state.status) ? (state.status === 'blocked' ? 'blocked' : 'failed') : 'unassessed';
|
|
165
|
+
let accepted = null, scoreDigest = null, trustedScorerProcessSignatureVerified = false;
|
|
166
|
+
if (scoreFile) {
|
|
167
|
+
assertScorable(state, registration, receipt);
|
|
168
|
+
const score = externalFile(root, scoreFile);
|
|
169
|
+
if (score.sha256 !== scoreSha256) throw Error('score byte pin mismatch');
|
|
170
|
+
let report = document(score.raw, 'score');
|
|
171
|
+
if (identity.scorer.authority) {
|
|
172
|
+
const payload = verifyScorerReport(report, identity.scorer);
|
|
173
|
+
if (payload.artifactSha256 !== artifact.sha256 || payload.scorerSha256 !== scorer.sha256
|
|
174
|
+
|| payload.oracleSha256 !== identity.scorer.oracleSha256
|
|
175
|
+
|| payload.candidateInputDigest !== baselineInputDigest(root, Object.keys(oracle.baseline).sort())) throw Error('signed scorer artifact/oracle/input binding mismatch');
|
|
176
|
+
report = { ...payload, version: 1, source: 'operator-attestation', scorer: identity.scorer };
|
|
177
|
+
trustedScorerProcessSignatureVerified = true;
|
|
178
|
+
}
|
|
179
|
+
if (report.version !== 1 || report.source !== 'operator-attestation' || report.runId !== state.id
|
|
180
|
+
|| report.stateSha256 !== stateSha256 || report.registrationDigest !== identity.registrationDigest
|
|
181
|
+
|| !same(report.scorer, identity.scorer) || !same(report.receipt, receipt)
|
|
182
|
+
|| !Array.isArray(report.criteria) || report.criteria.length !== state.acceptance.length
|
|
183
|
+
|| report.criteria.some((c, i) => c?.text !== state.acceptance[i] || !['passed', 'failed'].includes(c.state)
|
|
184
|
+
|| typeof c.evidence !== 'string' || !c.evidence.trim())) throw Error('independent score report identity/criteria mismatch');
|
|
185
|
+
status = 'completed'; accepted = report.criteria.every(c => c.state === 'passed'); scoreDigest = score.sha256;
|
|
186
|
+
}
|
|
187
|
+
const row = { task: identity.task, repetition: identity.repetition, arm: identity.arm,
|
|
188
|
+
protocolDigest: identity.protocolDigest, specificationDigest: identity.specificationDigest,
|
|
189
|
+
acceptanceDigest: identity.acceptanceDigest, artifactSha256: artifact.sha256, status, accepted,
|
|
190
|
+
activeMs: telemetry.activeMs, workerCalls: telemetry.workerCalls, verifierCalls: telemetry.verifierCalls,
|
|
191
|
+
humanPauses: null, actionableFindings: null, escapedDefects: null, actualCostUsd: null,
|
|
192
|
+
evidence: { level: trustedScorerProcessSignatureVerified
|
|
193
|
+
? 'trusted-launcher-signed-scorer-and-pinned-controller-not-provider-attested'
|
|
194
|
+
: 'operator-pinned-controller-and-score-not-provider-attested', runId: state.id,
|
|
195
|
+
controllerStatus: state.status, stateSha256, registrationDigest: identity.registrationDigest,
|
|
196
|
+
scorerSha256: scorer.sha256, scoreSha256: scoreDigest, receipt,
|
|
197
|
+
trustedScorerProcessSignatureVerified,
|
|
198
|
+
graphFloorCoverageVerified: false, executionArtifactProvenanceVerified: false,
|
|
199
|
+
benchmarkEligible: false } };
|
|
200
|
+
describeMatchedObservations(registration.protocol, [row]);
|
|
201
|
+
return row;
|
|
202
|
+
}
|