@event4u/agent-config 14.8.0 → 14.9.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +165 -237
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +34 -4
- package/dist/agent-src/commands/pr/merge.md +9 -5
- package/dist/agent-src/commands/review/changes.md +95 -14
- package/dist/agent-src/commands/roadmap/process-full.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +77 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +36 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
- package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
- package/dist/agent-src/skills/humanizer/SKILL.md +39 -0
- package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +57 -0
- package/dist/agent-src/skills/humanizer/references/fixtures.md +54 -0
- package/dist/agent-src/skills/judge-spec-compliance/SKILL.md +154 -0
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +45 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +8 -0
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-MMSGVZKV.js → chunk-77GR3VD4.js} +65 -40
- package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
- package/dist/cli-delegate/{chunk-EEJUMCDZ.js → chunk-PLSG6UL5.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +89 -13
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +37 -15
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +562 -204
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +808 -331
- package/dist/scripts/_lib/claude_plugin.js +162 -0
- package/dist/scripts/_lib/claude_plugin.js.map +1 -0
- package/dist/scripts/_lib/environment_detector.js +397 -0
- package/dist/scripts/_lib/environment_detector.js.map +1 -0
- package/dist/scripts/_lib/spawn_env.js +129 -0
- package/dist/scripts/_lib/spawn_env.js.map +1 -0
- package/dist/scripts/ai_council/modes.js +199 -0
- package/dist/scripts/ai_council/modes.js.map +1 -0
- package/dist/scripts/ai_council/transport_resolver.js +418 -0
- package/dist/scripts/ai_council/transport_resolver.js.map +1 -0
- package/dist/server/routes/wizard.js +3 -2
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/ui/assets/index-Cxgy63kz.js +26 -0
- package/dist/ui/assets/index-Cxgy63kz.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +17 -0
- package/package.json +7 -3
- package/src/config/estate-count-budget.json +13 -744
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +247 -9
- package/src/config/gate-violation-baselines.json +72 -72
- package/src/config/publish-surface.json +60 -0
- package/src/config/requirements-trace-budget.json +32 -0
- package/src/config/rule-stub-ceilings.json +8 -1
- package/src/scripts/_dispatch.bash +7 -0
- package/src/scripts/_lib/base_tree.ts +175 -0
- package/src/scripts/_lib/billing_grant.ts +165 -0
- package/src/scripts/_lib/billing_grant_cli.ts +123 -0
- package/src/scripts/_lib/council_fallback_posture.ts +53 -6
- package/src/scripts/_lib/council_fallback_wiring.ts +61 -1
- package/src/scripts/_lib/host_capability.ts +59 -5
- package/src/scripts/_lib/quota_parked.ts +133 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +62 -0
- package/src/scripts/_lib/review_telemetry.ts +196 -0
- package/src/scripts/_lib/subagent_response.ts +92 -0
- package/src/scripts/adr/regenerate_index.ts +14 -7
- package/src/scripts/ai_council/clients.ts +12 -1
- package/src/scripts/ai_council/config.ts +72 -115
- package/src/scripts/ai_council/fallback_config.ts +19 -7
- package/src/scripts/ai_council/mid_flight_fallback.ts +88 -7
- package/src/scripts/ai_council/orchestrator.ts +2 -2
- package/src/scripts/ai_council/py_format.ts +124 -0
- package/src/scripts/ai_council/transport_resolver.ts +65 -2
- package/src/scripts/audit_adr_coverage.ts +33 -6
- package/src/scripts/check_council_pin_staleness.ts +300 -0
- package/src/scripts/check_estate_count.ts +420 -293
- package/src/scripts/check_gate_coverage.ts +91 -0
- package/src/scripts/check_no_stub_inventory_table.ts +292 -0
- package/src/scripts/check_pack_size.ts +234 -1
- package/src/scripts/check_publish_surface.ts +297 -0
- package/src/scripts/check_requirements_trace.ts +378 -0
- package/src/scripts/check_secret_leak.ts +166 -12
- package/src/scripts/code_graph/cli.ts +44 -5
- package/src/scripts/code_graph/extract.ts +87 -0
- package/src/scripts/council_cli.ts +12 -16
- package/src/scripts/detect_ai_tells.ts +87 -0
- package/src/scripts/dispatch_r2_reviewer.ts +42 -0
- package/src/scripts/generate_capabilities_index.ts +41 -0
- package/src/scripts/git_authorization_hook.ts +54 -4
- package/src/scripts/hooks/block_unauthorized_git.ts +19 -1
- package/src/scripts/hooks/run_continuation_hook.ts +3 -1
- package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
- package/src/scripts/injection_scan_hook.ts +120 -10
- package/src/scripts/lint_adr_sweep_routing.ts +365 -0
- package/src/scripts/lint_workflow_security.ts +53 -10
- package/src/scripts/lint_workflow_security_allowlist.json +2 -18
- package/src/scripts/report_envelope_rate.ts +166 -0
- package/src/scripts/review_axis_report.ts +218 -0
- package/src/scripts/run_supervise.ts +46 -1
- package/src/scripts/schemas/evals.schema.json +5 -0
- package/src/scripts/sync_pr_branch.ts +14 -7
- package/dist/cli/commands/commands.candidates.test.js +0 -257
- package/dist/cli/commands/commands.candidates.test.js.map +0 -1
- package/dist/cli/commands/profiles.test.js +0 -88
- package/dist/cli/commands/profiles.test.js.map +0 -1
- package/dist/cli/commands/recordTriggerEval.test.js +0 -113
- package/dist/cli/commands/recordTriggerEval.test.js.map +0 -1
- package/dist/cli/configRoot.test.js +0 -97
- package/dist/cli/configRoot.test.js.map +0 -1
- package/dist/cli/mcp/content.test.js +0 -54
- package/dist/cli/mcp/content.test.js.map +0 -1
- package/dist/cli/mcp/dispatch.test.js +0 -108
- package/dist/cli/mcp/dispatch.test.js.map +0 -1
- package/dist/server/writeRoot.test.js +0 -91
- package/dist/server/writeRoot.test.js.map +0 -1
- package/dist/shared/capabilities.test.js +0 -39
- package/dist/shared/capabilities.test.js.map +0 -1
- package/dist/ui/assets/index-C1pBRm1f.js +0 -26
- package/dist/ui/assets/index-C1pBRm1f.js.map +0 -1
- /package/dist/cli-delegate/{chunk-N7CDDGI3.js → chunk-CYH5SP6M.js} +0 -0
|
@@ -85,6 +85,68 @@ export function sanitize_text(s: string): string {
|
|
|
85
85
|
return out.length > MAX_FIELD_CHARS ? out.slice(0, MAX_FIELD_CHARS) : out;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Report the INVISIBLE layer without removing it.
|
|
90
|
+
*
|
|
91
|
+
* `sanitize_text` above strips four channels — hidden-instruction vectors
|
|
92
|
+
* (bidi, zero-width, tag, deprecated-format, private-use-area, per `_classify`),
|
|
93
|
+
* strippable control chars, and invisible fillers. Stripping is the right
|
|
94
|
+
* disposition for a retrieval path that is about to hand text to a model.
|
|
95
|
+
*
|
|
96
|
+
* It is the WRONG disposition for a reporting surface. A warn-only PostToolUse
|
|
97
|
+
* hook must not rewrite what the agent already read, so it cannot call
|
|
98
|
+
* `sanitize_text` at all — and `scan_encoding_findings` is the VISIBLE-layer
|
|
99
|
+
* twin and deliberately says nothing about these four. Measured over the frozen
|
|
100
|
+
* corpus before this function existed: `deprecated-format`, `private-use-area`,
|
|
101
|
+
* `control-char` and `invisible-filler` reported **0 of 20 each** through the
|
|
102
|
+
* reporting API while `sanitize_text` changed **20 of 20** for every one of
|
|
103
|
+
* them. So the layer had a reporting blind spot exactly where its stripping was
|
|
104
|
+
* perfect, and a consumer that may not strip was blind to a quarter of the
|
|
105
|
+
* channel set.
|
|
106
|
+
*
|
|
107
|
+
* The published 99.00 % recall figure is the STRIPPING pipeline's. The reporting
|
|
108
|
+
* half alone measured 72.33 %. Both numbers are correct about different things,
|
|
109
|
+
* and conflating them is what made "import the measured layer and get 15
|
|
110
|
+
* channels" read as true.
|
|
111
|
+
*
|
|
112
|
+
* Additive by construction: nothing that exists changes behaviour, and the
|
|
113
|
+
* predicates are the SAME ones `sanitize_text` uses — not copies — so a channel
|
|
114
|
+
* cannot become strippable without becoming reportable in the same edit.
|
|
115
|
+
*/
|
|
116
|
+
export function scan_invisible_findings(s: string): EncodingFinding[] {
|
|
117
|
+
const out: EncodingFinding[] = [];
|
|
118
|
+
const seen = new Set<string>();
|
|
119
|
+
const add = (channel: string, cp: number): void => {
|
|
120
|
+
const key = `${channel}:${cp}`;
|
|
121
|
+
if (seen.has(key)) return;
|
|
122
|
+
seen.add(key);
|
|
123
|
+
out.push({
|
|
124
|
+
channel,
|
|
125
|
+
detail: `U+${cp.toString(16).toUpperCase().padStart(4, '0')} in the invisible layer`,
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
for (const ch of s) {
|
|
129
|
+
const cp = ch.codePointAt(0);
|
|
130
|
+
if (cp === undefined) continue;
|
|
131
|
+
const classified = _classify(cp);
|
|
132
|
+
if (classified !== null) {
|
|
133
|
+
// `_classify` already names the channel; reuse its label rather than
|
|
134
|
+
// re-deriving one, so the hook's output and the authoring-time
|
|
135
|
+
// linter's speak the same vocabulary.
|
|
136
|
+
add(typeof classified === 'string' ? classified : 'hidden-instruction', cp);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (_isStrippableControl(cp)) {
|
|
140
|
+
add('control-char', cp);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (_INVISIBLE_FILLERS.has(cp)) {
|
|
144
|
+
add('invisible-filler', cp);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
88
150
|
// ---------------------------------------------------------------------------
|
|
89
151
|
// Visible-layer scanner — FLAG ONLY, never rewrites
|
|
90
152
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One structural line per review run — the review surface's only telemetry.
|
|
3
|
+
*
|
|
4
|
+
* Why it exists: before this file the review surface emitted nothing. Nothing
|
|
5
|
+
* recorded how often a review ran, which judges fired, or whether the spec
|
|
6
|
+
* axis ever changed a recommendation — so the value of adding a sixth judge
|
|
7
|
+
* was unmeasurable in either direction, and the cheapest response to a slower
|
|
8
|
+
* default path is to switch the new judge off. A number is the only defence
|
|
9
|
+
* against that, and the honest number includes "it changed nothing".
|
|
10
|
+
*
|
|
11
|
+
* PII-exclusion by CONSTRUCTION, not by scrubbing. `ReviewLine` has no field
|
|
12
|
+
* able to hold free-form content: every member is an enum, a boolean, a
|
|
13
|
+
* non-negative integer, or an ISO timestamp. There is no `payload`, `notes`,
|
|
14
|
+
* `extra`, `prompt`, `diff` or `path` member, and `buildReviewLine` REJECTS an
|
|
15
|
+
* input object carrying one rather than dropping it silently — a scrubber can
|
|
16
|
+
* fail, an absent field cannot. Same principle the artifact-engagement event
|
|
17
|
+
* and `orchestration_record`'s hex-only `payload_hash` apply.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately NOT reused from `_lib/orchestration_record.ts`: that line
|
|
20
|
+
* describes a subagent DISPATCH (tier, route, token deltas, cache hits). A
|
|
21
|
+
* review is not a dispatch, and widening `RecordInput` with review-shaped
|
|
22
|
+
* optionals would make both types describe neither thing precisely.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Was the spec axis able to run at all, and on what basis? */
|
|
26
|
+
export type SpecAxisReach = 'reachable_with_criteria' | 'reachable_no_criteria' | 'unreachable';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Did the spec axis change the recommendation the craft judges would have
|
|
30
|
+
* produced on their own?
|
|
31
|
+
*
|
|
32
|
+
* `null` is NOT "no" — it means the comparison never happened (the axis did
|
|
33
|
+
* not run, so there is no counterfactual). A `false` here would claim a
|
|
34
|
+
* comparison that was never made, which is the mis-attribution this field
|
|
35
|
+
* exists to expose. Same reasoning as `modelDivergent` in
|
|
36
|
+
* `_lib/orchestration_record.ts`.
|
|
37
|
+
*/
|
|
38
|
+
export type SpecAxisEffect = 'changed' | 'unchanged' | null;
|
|
39
|
+
|
|
40
|
+
/** The recommendation word the synthesis emitted. */
|
|
41
|
+
export type ReviewRecommendation = 'block' | 'revise' | 'proceed';
|
|
42
|
+
|
|
43
|
+
/** The criteria-source state, mirroring `judge-spec-compliance`'s three states. */
|
|
44
|
+
export type CriteriaSource = 'supplied' | 'not_provided' | 'supplied_unparseable';
|
|
45
|
+
|
|
46
|
+
/** The closed field set. A member not on this list cannot be written. */
|
|
47
|
+
export const REVIEW_LINE_FIELDS = [
|
|
48
|
+
'schema',
|
|
49
|
+
'at',
|
|
50
|
+
'judges_declared',
|
|
51
|
+
'judges_ran',
|
|
52
|
+
'spec_axis_reach',
|
|
53
|
+
'criteria_source',
|
|
54
|
+
'criteria_count',
|
|
55
|
+
'spec_missing',
|
|
56
|
+
'spec_partial',
|
|
57
|
+
'recommendation',
|
|
58
|
+
'spec_axis_effect',
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
export type ReviewLineField = (typeof REVIEW_LINE_FIELDS)[number];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Field names that must never appear on this line — the free-form shapes a
|
|
65
|
+
* reviewer would reach for first. Checked as an INPUT rejection, so adding one
|
|
66
|
+
* fails loudly at the call site instead of being stripped downstream.
|
|
67
|
+
*/
|
|
68
|
+
export const FORBIDDEN_FREEFORM_FIELDS = [
|
|
69
|
+
'payload',
|
|
70
|
+
'notes',
|
|
71
|
+
'extra',
|
|
72
|
+
'prompt',
|
|
73
|
+
'diff',
|
|
74
|
+
'path',
|
|
75
|
+
'file',
|
|
76
|
+
'criteria',
|
|
77
|
+
'findings',
|
|
78
|
+
'message',
|
|
79
|
+
'body',
|
|
80
|
+
'content',
|
|
81
|
+
'text',
|
|
82
|
+
] as const;
|
|
83
|
+
|
|
84
|
+
export interface ReviewLine {
|
|
85
|
+
readonly schema: 'review-axis-v1';
|
|
86
|
+
/** ISO-8601 UTC. Caller-supplied so the line is reproducible in a test. */
|
|
87
|
+
readonly at: string;
|
|
88
|
+
readonly judges_declared: number;
|
|
89
|
+
readonly judges_ran: number;
|
|
90
|
+
readonly spec_axis_reach: SpecAxisReach;
|
|
91
|
+
readonly criteria_source: CriteriaSource;
|
|
92
|
+
readonly criteria_count: number;
|
|
93
|
+
readonly spec_missing: number;
|
|
94
|
+
readonly spec_partial: number;
|
|
95
|
+
readonly recommendation: ReviewRecommendation;
|
|
96
|
+
readonly spec_axis_effect: SpecAxisEffect;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ReviewLineInput {
|
|
100
|
+
at: string;
|
|
101
|
+
judges_declared: number;
|
|
102
|
+
judges_ran: number;
|
|
103
|
+
spec_axis_reach: SpecAxisReach;
|
|
104
|
+
criteria_source: CriteriaSource;
|
|
105
|
+
criteria_count: number;
|
|
106
|
+
spec_missing: number;
|
|
107
|
+
spec_partial: number;
|
|
108
|
+
recommendation: ReviewRecommendation;
|
|
109
|
+
spec_axis_effect?: SpecAxisEffect;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface BuiltReviewLine {
|
|
113
|
+
line: ReviewLine | null;
|
|
114
|
+
errors: string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/;
|
|
118
|
+
const REACH: readonly SpecAxisReach[] = ['reachable_with_criteria', 'reachable_no_criteria', 'unreachable'];
|
|
119
|
+
const SOURCE: readonly CriteriaSource[] = ['supplied', 'not_provided', 'supplied_unparseable'];
|
|
120
|
+
const RECO: readonly ReviewRecommendation[] = ['block', 'revise', 'proceed'];
|
|
121
|
+
|
|
122
|
+
function isCount(n: unknown): n is number {
|
|
123
|
+
return typeof n === 'number' && Number.isInteger(n) && n >= 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Build + validate one `review-axis-v1` line.
|
|
128
|
+
*
|
|
129
|
+
* Returns `{line, errors}`; when `errors` is non-empty, `line` is null and the
|
|
130
|
+
* caller must NOT write. Never throws, never partially writes, never silently
|
|
131
|
+
* repairs an invalid input — the same contract as `buildOrchestrationLine`.
|
|
132
|
+
*/
|
|
133
|
+
export function buildReviewLine(input: ReviewLineInput): BuiltReviewLine {
|
|
134
|
+
const errors: string[] = [];
|
|
135
|
+
const raw = input as unknown as Record<string, unknown>;
|
|
136
|
+
|
|
137
|
+
for (const banned of FORBIDDEN_FREEFORM_FIELDS) {
|
|
138
|
+
if (Object.prototype.hasOwnProperty.call(raw, banned)) {
|
|
139
|
+
errors.push(
|
|
140
|
+
`'${banned}' is a free-form field and review-axis-v1 carries none — ` +
|
|
141
|
+
'privacy here is a property of the schema shape, not of a scrubbing pass',
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof input.at !== 'string' || !ISO_RE.test(input.at)) {
|
|
147
|
+
errors.push('at must be an ISO-8601 UTC timestamp (…Z)');
|
|
148
|
+
}
|
|
149
|
+
for (const k of ['judges_declared', 'judges_ran', 'criteria_count', 'spec_missing', 'spec_partial'] as const) {
|
|
150
|
+
if (!isCount(input[k])) errors.push(`${k} must be a non-negative integer`);
|
|
151
|
+
}
|
|
152
|
+
if (isCount(input.judges_ran) && isCount(input.judges_declared) && input.judges_ran > input.judges_declared) {
|
|
153
|
+
errors.push('judges_ran cannot exceed judges_declared');
|
|
154
|
+
}
|
|
155
|
+
if (!REACH.includes(input.spec_axis_reach)) errors.push(`spec_axis_reach must be one of ${REACH.join(' | ')}`);
|
|
156
|
+
if (!SOURCE.includes(input.criteria_source)) errors.push(`criteria_source must be one of ${SOURCE.join(' | ')}`);
|
|
157
|
+
if (!RECO.includes(input.recommendation)) errors.push(`recommendation must be one of ${RECO.join(' | ')}`);
|
|
158
|
+
|
|
159
|
+
const effect = input.spec_axis_effect ?? null;
|
|
160
|
+
if (effect !== null && effect !== 'changed' && effect !== 'unchanged') {
|
|
161
|
+
errors.push('spec_axis_effect must be changed | unchanged | null');
|
|
162
|
+
}
|
|
163
|
+
// A counterfactual that never ran is `null`, never `unchanged`.
|
|
164
|
+
if (input.spec_axis_reach === 'unreachable' && effect !== null) {
|
|
165
|
+
errors.push('spec_axis_effect must be null when the axis was unreachable — there was no comparison to report');
|
|
166
|
+
}
|
|
167
|
+
// The two honest pairings of reach and source.
|
|
168
|
+
if (input.spec_axis_reach === 'reachable_with_criteria' && input.criteria_source !== 'supplied') {
|
|
169
|
+
errors.push('reachable_with_criteria requires criteria_source=supplied');
|
|
170
|
+
}
|
|
171
|
+
if (input.spec_axis_reach === 'reachable_no_criteria' && input.criteria_source === 'supplied') {
|
|
172
|
+
errors.push('reachable_no_criteria contradicts criteria_source=supplied');
|
|
173
|
+
}
|
|
174
|
+
if (input.criteria_source !== 'supplied' && isCount(input.criteria_count) && input.criteria_count > 0) {
|
|
175
|
+
errors.push('criteria_count must be 0 unless criteria were supplied');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (errors.length > 0) return { line: null, errors };
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
line: {
|
|
182
|
+
schema: 'review-axis-v1',
|
|
183
|
+
at: input.at,
|
|
184
|
+
judges_declared: input.judges_declared,
|
|
185
|
+
judges_ran: input.judges_ran,
|
|
186
|
+
spec_axis_reach: input.spec_axis_reach,
|
|
187
|
+
criteria_source: input.criteria_source,
|
|
188
|
+
criteria_count: input.criteria_count,
|
|
189
|
+
spec_missing: input.spec_missing,
|
|
190
|
+
spec_partial: input.spec_partial,
|
|
191
|
+
recommendation: input.recommendation,
|
|
192
|
+
spec_axis_effect: effect,
|
|
193
|
+
},
|
|
194
|
+
errors: [],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -61,10 +61,102 @@ export interface SubagentResponse {
|
|
|
61
61
|
* the same claim as "no assumptions were made".
|
|
62
62
|
*/
|
|
63
63
|
assumptions?: CapsuleAssumption[];
|
|
64
|
+
/**
|
|
65
|
+
* Where the tests in this change were authored FROM.
|
|
66
|
+
*
|
|
67
|
+
* `from-spec` — written against the acceptance criteria, before the diff
|
|
68
|
+
* existed. `from-diff` — written after reading the implementation.
|
|
69
|
+
* `unknown` — not recorded.
|
|
70
|
+
*
|
|
71
|
+
* **`unknown` is the default and an ABSENT field resolves to it, never to
|
|
72
|
+
* `from-spec`.** That asymmetry is the whole point of the field. The claim
|
|
73
|
+
* this metric exists to keep measurable is that a suite written by the same
|
|
74
|
+
* context as the implementation inherits its blind spots — so the valuable
|
|
75
|
+
* state is `from-spec`, and a default that let absence read as the valuable
|
|
76
|
+
* state would report the claim as satisfied by silence.
|
|
77
|
+
*
|
|
78
|
+
* Structural enum only: three string values and nothing that can hold
|
|
79
|
+
* free-form content. Same exclusion-by-construction the rest of this
|
|
80
|
+
* envelope uses — a field that cannot carry a prompt or a diff has no
|
|
81
|
+
* scrubber to fail.
|
|
82
|
+
*
|
|
83
|
+
* OPTIONAL, and deliberately not in `RESPONSE_REQUIRED_FIELDS`: that list
|
|
84
|
+
* is calibrated against a recorded ledger equivalence (`error_count: 5`),
|
|
85
|
+
* and adding a sixth required field would break it. A metric is not worth
|
|
86
|
+
* invalidating a measurement for.
|
|
87
|
+
*/
|
|
88
|
+
test_authorship?: TestAuthorship;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Three states. `unknown` is one of them, not the absence of the other two.
|
|
93
|
+
*
|
|
94
|
+
* Ordered by what they license rather than alphabetically: `from-spec` is the
|
|
95
|
+
* only value that supports the independence claim, `from-diff` is the weaker
|
|
96
|
+
* form the claim says is worthless, and `unknown` licenses nothing.
|
|
97
|
+
*/
|
|
98
|
+
export type TestAuthorship = 'from-spec' | 'from-diff' | 'unknown';
|
|
99
|
+
|
|
100
|
+
export const TEST_AUTHORSHIP: ReadonlySet<string> = new Set<TestAuthorship>([
|
|
101
|
+
'from-spec',
|
|
102
|
+
'from-diff',
|
|
103
|
+
'unknown',
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the field from a decoded envelope. ABSENT and UNRECOGNISED both
|
|
108
|
+
* resolve to `unknown`.
|
|
109
|
+
*
|
|
110
|
+
* An unrecognised value resolving to `unknown` rather than throwing is
|
|
111
|
+
* deliberate: this is a metric, and a producer that emits `from_spec` with an
|
|
112
|
+
* underscore should show up in the distribution as unrecorded, not stop a
|
|
113
|
+
* dispatch. The report is what surfaces it — a spike of `unknown` IS the
|
|
114
|
+
* finding that the field is not reaching its producers.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveTestAuthorship(input: unknown): TestAuthorship {
|
|
117
|
+
if (input === null || typeof input !== 'object') return 'unknown';
|
|
118
|
+
const v = (input as Record<string, unknown>)['test_authorship'];
|
|
119
|
+
return typeof v === 'string' && TEST_AUTHORSHIP.has(v) ? (v as TestAuthorship) : 'unknown';
|
|
64
120
|
}
|
|
65
121
|
|
|
66
122
|
const CONFIDENCE: ReadonlySet<string> = new Set<Confidence>(['low', 'medium', 'high']);
|
|
67
123
|
|
|
124
|
+
/**
|
|
125
|
+
* The five fields `validateResponse` REQUIRES. Exported so the ledger's
|
|
126
|
+
* classifier counts contract fields against the validator's own list rather
|
|
127
|
+
* than a second copy that can drift from it.
|
|
128
|
+
*
|
|
129
|
+
* The optional fields (`artifact_paths`, `assumptions`) are deliberately NOT
|
|
130
|
+
* here. The boundary this list defines is "did the author aim at the envelope
|
|
131
|
+
* at all", and it is calibrated against a recorded measurement: every `fail`
|
|
132
|
+
* in the live ledger carries `error_count: 5`, which is exactly the count of
|
|
133
|
+
* required-field errors an object with none of them produces. Counting an
|
|
134
|
+
* optional field as a hit would break that equivalence, so an object carrying
|
|
135
|
+
* only `assumptions` reads as foreign — recorded here as a known edge, not as
|
|
136
|
+
* an oversight.
|
|
137
|
+
*/
|
|
138
|
+
export const RESPONSE_REQUIRED_FIELDS: readonly string[] = [
|
|
139
|
+
'summary',
|
|
140
|
+
'handoff',
|
|
141
|
+
'confidence',
|
|
142
|
+
'findings',
|
|
143
|
+
'risks',
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* How many required contract fields are PRESENT on a decoded object.
|
|
148
|
+
*
|
|
149
|
+
* Presence, never validity: a `summary: 123` is a contract attempt with a
|
|
150
|
+
* wrong type, which is a different defect from a fenced tool call that never
|
|
151
|
+
* aimed at the envelope. That distinction is the whole point of the count —
|
|
152
|
+
* `validateResponse` already reports the type errors.
|
|
153
|
+
*/
|
|
154
|
+
export function countContractFields(input: unknown): number {
|
|
155
|
+
if (input === null || typeof input !== 'object') return 0;
|
|
156
|
+
const r = input as Record<string, unknown>;
|
|
157
|
+
return RESPONSE_REQUIRED_FIELDS.filter((k) => Object.hasOwn(r, k)).length;
|
|
158
|
+
}
|
|
159
|
+
|
|
68
160
|
export interface ValidationResult {
|
|
69
161
|
valid: boolean;
|
|
70
162
|
errors: string[];
|
|
@@ -16,7 +16,7 @@ import * as fs from 'node:fs';
|
|
|
16
16
|
import * as path from 'node:path';
|
|
17
17
|
import process from 'node:process';
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
|
-
import { readAdrFrontmatterScalars } from '../_lib/adr_frontmatter.js';
|
|
19
|
+
import { readAdrAxisCells, readAdrFrontmatterScalars } from '../_lib/adr_frontmatter.js';
|
|
20
20
|
|
|
21
21
|
// ^ADR-(\d{3})-([a-z0-9-]+)\.md$
|
|
22
22
|
const NAMED = /^ADR-(\d{3})-([a-z0-9-]+)\.md$/;
|
|
@@ -29,8 +29,8 @@ const NAMED = /^ADR-(\d{3})-([a-z0-9-]+)\.md$/;
|
|
|
29
29
|
* amendments from one never touched.
|
|
30
30
|
*/
|
|
31
31
|
const HEAD =
|
|
32
|
-
'| # | Title | Status | Date | Supersedes | Superseded by | Amended by |\n' +
|
|
33
|
-
'
|
|
32
|
+
'| # | Title | Status | Date | Provenance | Evidence | Supersedes | Superseded by | Amended by |\n' +
|
|
33
|
+
'|---|---|---|---|---|---|---|---|---|';
|
|
34
34
|
|
|
35
35
|
type Meta = Record<string, string>;
|
|
36
36
|
interface Row extends Meta {
|
|
@@ -130,10 +130,16 @@ export function scan(d: string): [Row[], Row[], string[]] {
|
|
|
130
130
|
if (name === 'INDEX.md') {
|
|
131
131
|
continue;
|
|
132
132
|
}
|
|
133
|
-
const
|
|
133
|
+
const text = fs.readFileSync(p, 'utf-8');
|
|
134
|
+
const meta = fm(text);
|
|
135
|
+
// Read apart from `meta` on purpose. `provenance` and `evidence` are
|
|
136
|
+
// nested, so the scalar reader keeps them out of its map entirely —
|
|
137
|
+
// spreading `meta` could never produce these two cells, and the axis
|
|
138
|
+
// reader is where the `discovery`-rides-in-the-grade rule lives.
|
|
139
|
+
const axes = readAdrAxisCells(text);
|
|
134
140
|
const m = NAMED.exec(name);
|
|
135
141
|
if (!m) {
|
|
136
|
-
leg.push({ path: name, ...meta });
|
|
142
|
+
leg.push({ path: name, ...meta, ...axes });
|
|
137
143
|
continue;
|
|
138
144
|
}
|
|
139
145
|
const n = m[1] as string;
|
|
@@ -144,7 +150,7 @@ export function scan(d: string): [Row[], Row[], string[]] {
|
|
|
144
150
|
errs.push(`ADR-${n} duplicate: ${name} and ${seen[n]}`);
|
|
145
151
|
}
|
|
146
152
|
seen[n] = name;
|
|
147
|
-
num.push({ num: n, slug: m[2] as string, path: name, ...meta });
|
|
153
|
+
num.push({ num: n, slug: m[2] as string, path: name, ...meta, ...axes });
|
|
148
154
|
}
|
|
149
155
|
const nums = new Set(num.map((r) => r.num as string));
|
|
150
156
|
for (const r of num) {
|
|
@@ -172,7 +178,8 @@ export function row(r: Row): string {
|
|
|
172
178
|
const label = 'num' in r ? `ADR-${r.num}` : (r.path as string).slice(0, -3);
|
|
173
179
|
return (
|
|
174
180
|
`| [${label}](${r.path}) | ${title} | ${r.status ?? '—'} ` +
|
|
175
|
-
`| ${r.date ?? '—'} | ${r.
|
|
181
|
+
`| ${r.date ?? '—'} | ${r.provenance_kind ?? '—'} | ${r.evidence_grade ?? '—'} ` +
|
|
182
|
+
`| ${r.supersedes ?? '—'} | ${r.superseded_by ?? '—'} ` +
|
|
176
183
|
`| ${r.amended_by ?? '—'} |`
|
|
177
184
|
);
|
|
178
185
|
}
|
|
@@ -148,7 +148,18 @@ export const CODEX_MEASURED_UNSERVABLE: ReadonlyMap<string, string> = new Map([
|
|
|
148
148
|
]);
|
|
149
149
|
|
|
150
150
|
export const DEFAULT_OPENAI_CLI_MODEL = OPENAI_CLI_VENDOR_DEFAULT;
|
|
151
|
-
|
|
151
|
+
// A vendor ALIAS, not a version. `claude --help` documents 'fable', 'opus' and
|
|
152
|
+
// 'sonnet' as "an alias for the latest model" in their band, so an alias cannot
|
|
153
|
+
// go stale the way a dated id can — which is the whole point of the change that
|
|
154
|
+
// put it here.
|
|
155
|
+
//
|
|
156
|
+
// It was `'claude-sonnet-4-5'`, and this line is the half the roadmap that
|
|
157
|
+
// found the defect did not know about: refreshing only the template's pin would
|
|
158
|
+
// have left every member that omits `model:` falling back to the SAME stale id
|
|
159
|
+
// from code. Read from the provider's own CLI surface on 2026-08-22, never from
|
|
160
|
+
// the agent's recall — the source that road-to-council-seat-selection's own
|
|
161
|
+
// Risk 1 declares inadmissible.
|
|
162
|
+
export const DEFAULT_ANTHROPIC_CLI_MODEL = 'sonnet';
|
|
152
163
|
export const DEFAULT_GEMINI_CLI_MODEL = 'gemini-2.5-pro';
|
|
153
164
|
|
|
154
165
|
// OpenAI-API-compatible endpoints. xAI and Perplexity both expose the
|
|
@@ -144,121 +144,6 @@ export class CouncilConfigError extends Error {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
// ── Python-format helpers (byte-faithful error messages) ───────────
|
|
148
|
-
|
|
149
|
-
/** Python `repr()` for a string scalar (single-quoted, escaped). */
|
|
150
|
-
function _pyReprStr(s: string): string {
|
|
151
|
-
const hasSingle = s.includes("'");
|
|
152
|
-
const hasDouble = s.includes('"');
|
|
153
|
-
const quote = hasSingle && !hasDouble ? '"' : "'";
|
|
154
|
-
let body = s
|
|
155
|
-
.replace(/\\/g, '\\\\')
|
|
156
|
-
.replace(/\n/g, '\\n')
|
|
157
|
-
.replace(/\r/g, '\\r')
|
|
158
|
-
.replace(/\t/g, '\\t');
|
|
159
|
-
if (quote === "'") {
|
|
160
|
-
body = body.replace(/'/g, "\\'");
|
|
161
|
-
} else {
|
|
162
|
-
body = body.replace(/"/g, '\\"');
|
|
163
|
-
}
|
|
164
|
-
return `${quote}${body}${quote}`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Python `repr()` for a float value (shortest round-trip, `N.0` for ints). */
|
|
168
|
-
function _pyReprFloat(value: number): string {
|
|
169
|
-
if (Number.isInteger(value) && Number.isFinite(value)) {
|
|
170
|
-
return `${value}.0`;
|
|
171
|
-
}
|
|
172
|
-
if (value === Infinity) {
|
|
173
|
-
return 'inf';
|
|
174
|
-
}
|
|
175
|
-
if (value === -Infinity) {
|
|
176
|
-
return '-inf';
|
|
177
|
-
}
|
|
178
|
-
if (Number.isNaN(value)) {
|
|
179
|
-
return 'nan';
|
|
180
|
-
}
|
|
181
|
-
return String(value);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Python `repr()` for an arbitrary parsed value. Floats are tracked via
|
|
186
|
-
* `_FLOAT` so int-valued floats render `N.0`; bare numbers render as
|
|
187
|
-
* Python ints (no decimal). Mirrors `{value!r}` formatting.
|
|
188
|
-
*/
|
|
189
|
-
function _pyRepr(value: unknown): string {
|
|
190
|
-
if (value instanceof _Float) {
|
|
191
|
-
return _pyReprFloat(value.value);
|
|
192
|
-
}
|
|
193
|
-
if (value === null || value === undefined) {
|
|
194
|
-
return 'None';
|
|
195
|
-
}
|
|
196
|
-
if (typeof value === 'boolean') {
|
|
197
|
-
return value ? 'True' : 'False';
|
|
198
|
-
}
|
|
199
|
-
if (typeof value === 'string') {
|
|
200
|
-
return _pyReprStr(value);
|
|
201
|
-
}
|
|
202
|
-
if (typeof value === 'number') {
|
|
203
|
-
return String(value);
|
|
204
|
-
}
|
|
205
|
-
if (Array.isArray(value)) {
|
|
206
|
-
return `[${value.map((v) => _pyRepr(v)).join(', ')}]`;
|
|
207
|
-
}
|
|
208
|
-
if (typeof value === 'object') {
|
|
209
|
-
const parts: string[] = [];
|
|
210
|
-
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
211
|
-
parts.push(`${_pyReprStr(k)}: ${_pyRepr(v)}`);
|
|
212
|
-
}
|
|
213
|
-
return `{${parts.join(', ')}}`;
|
|
214
|
-
}
|
|
215
|
-
return String(value);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** Wrapper marking a number that should `repr()` as a Python float. */
|
|
219
|
-
class _Float {
|
|
220
|
-
constructor(readonly value: number) {}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Mark `n` so `_pyRepr` renders it with a Python float repr (`N.0`). */
|
|
224
|
-
function _f(n: number): _Float {
|
|
225
|
-
return new _Float(n);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Python `type(value).__name__`. */
|
|
229
|
-
function _pyTypeName(value: unknown): string {
|
|
230
|
-
if (value === null || value === undefined) {
|
|
231
|
-
return 'NoneType';
|
|
232
|
-
}
|
|
233
|
-
if (typeof value === 'boolean') {
|
|
234
|
-
return 'bool';
|
|
235
|
-
}
|
|
236
|
-
if (typeof value === 'number') {
|
|
237
|
-
return Number.isInteger(value) ? 'int' : 'float';
|
|
238
|
-
}
|
|
239
|
-
if (typeof value === 'string') {
|
|
240
|
-
return 'str';
|
|
241
|
-
}
|
|
242
|
-
if (Array.isArray(value)) {
|
|
243
|
-
return 'list';
|
|
244
|
-
}
|
|
245
|
-
if (typeof value === 'object') {
|
|
246
|
-
return 'dict';
|
|
247
|
-
}
|
|
248
|
-
return typeof value;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/** Python `sorted(set_of_strings)` rendered as a list repr `['a', 'b']`. */
|
|
252
|
-
function _sortedListRepr(items: Iterable<string>): string {
|
|
253
|
-
const sorted = [...items].sort();
|
|
254
|
-
return `[${sorted.map((s) => _pyReprStr(s)).join(', ')}]`;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** Python `oct(mode)` → `0o600`-shaped string. */
|
|
258
|
-
function _pyOct(mode: number): string {
|
|
259
|
-
return `0o${mode.toString(8)}`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
147
|
// ── Python int()/float() coercion + isinstance helpers ─────────────
|
|
263
148
|
|
|
264
149
|
/**
|
|
@@ -432,6 +317,19 @@ export interface MemberConfig {
|
|
|
432
317
|
* the falsification condition that gates ever enabling `'1h'`.
|
|
433
318
|
*/
|
|
434
319
|
readonly prompt_cache_ttl: '5m' | '1h';
|
|
320
|
+
/**
|
|
321
|
+
* Optional `YYYY-MM-DD` stamp: the date this member's `model:` pin was last
|
|
322
|
+
* checked against the provider's own surface. `null` when unset.
|
|
323
|
+
*
|
|
324
|
+
* Exists because a hard pin cannot notice its own age. The starter template
|
|
325
|
+
* shipped `claude-sonnet-4-5` on an enabled member with nothing in the file
|
|
326
|
+
* able to flag it — and `check_council_pin_staleness` reads exactly this
|
|
327
|
+
* field. A member on a vendor sentinel (`codex-default`, or an alias like
|
|
328
|
+
* `sonnet` that the provider documents as "the latest model") needs no
|
|
329
|
+
* stamp: it cannot go stale, which is why the gate exempts it rather than
|
|
330
|
+
* demanding a date nobody would refresh.
|
|
331
|
+
*/
|
|
332
|
+
readonly verified_at: string | null;
|
|
435
333
|
}
|
|
436
334
|
|
|
437
335
|
/**
|
|
@@ -635,6 +533,15 @@ export type QuorumMinPresent = number;
|
|
|
635
533
|
|
|
636
534
|
export type { FallbackConfig } from './fallback_config.js';
|
|
637
535
|
import { buildFallback, buildSecondModel, type FallbackConfig as _FallbackConfig } from './fallback_config.js';
|
|
536
|
+
import {
|
|
537
|
+
_f,
|
|
538
|
+
_pyOct,
|
|
539
|
+
_pyRepr,
|
|
540
|
+
_pyReprFloat,
|
|
541
|
+
_pyReprStr,
|
|
542
|
+
_pyTypeName,
|
|
543
|
+
_sortedListRepr,
|
|
544
|
+
} from './py_format.js';
|
|
638
545
|
|
|
639
546
|
export interface CouncilConfig {
|
|
640
547
|
readonly enabled: boolean;
|
|
@@ -1916,6 +1823,55 @@ function _build_member(
|
|
|
1916
1823
|
// untouched here) OR as a mapping carrying `ttl`. '5m' is the
|
|
1917
1824
|
// permanent default; see docs/contracts/ai-council-config.md
|
|
1918
1825
|
// § Prompt cache TTL for the falsification condition on '1h'.
|
|
1826
|
+
// A malformed date fails CLOSED. A stamp is a claim about when a human
|
|
1827
|
+
// last looked; accepting `2026-13-45` or `soon` would make the staleness
|
|
1828
|
+
// gate report on a string it cannot compare, which is worse than no stamp.
|
|
1829
|
+
const verified_at_raw = _get(cfg, 'verified_at', null);
|
|
1830
|
+
let verified_at: string | null = null;
|
|
1831
|
+
if (verified_at_raw !== null && verified_at_raw !== undefined) {
|
|
1832
|
+
// The stamp must be a QUOTED string, and the reason is a measured
|
|
1833
|
+
// fail-open rather than a style preference.
|
|
1834
|
+
//
|
|
1835
|
+
// YAML 1.1 resolves an unquoted `2026-08-22` to a Date. Accepting that
|
|
1836
|
+
// and normalising it back with `toISOString()` was the first
|
|
1837
|
+
// implementation, and it LAUNDERED impossible dates: the loader silently
|
|
1838
|
+
// rolls `2026-13-45` over to 2027-02-14, so the calendar check below saw
|
|
1839
|
+
// a valid date and the malformed input passed. Verified against a real
|
|
1840
|
+
// config, not reasoned about.
|
|
1841
|
+
//
|
|
1842
|
+
// A Date is therefore rejected with the fix in the message. That keeps
|
|
1843
|
+
// the impossible-date case failing CLOSED, which is the only direction
|
|
1844
|
+
// worth defending here: a stamp is a claim about attention, and a
|
|
1845
|
+
// silently corrected claim is worse than a rejected one.
|
|
1846
|
+
if (verified_at_raw instanceof Date) {
|
|
1847
|
+
throw new CouncilConfigError(
|
|
1848
|
+
`members.${name}.verified_at must be QUOTED — write ` +
|
|
1849
|
+
`verified_at: "YYYY-MM-DD". Unquoted, YAML parses it as a date and ` +
|
|
1850
|
+
`silently rolls impossible values over (2026-13-45 becomes 2027-02-14), ` +
|
|
1851
|
+
`which would let a malformed stamp through.`,
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
const coerced = verified_at_raw;
|
|
1855
|
+
if (typeof coerced !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(coerced)) {
|
|
1856
|
+
throw new CouncilConfigError(
|
|
1857
|
+
`members.${name}.verified_at must be a 'YYYY-MM-DD' string when set ` +
|
|
1858
|
+
`(got ${_pyRepr(verified_at_raw)}).`,
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
const [y, m, d] = coerced.split('-').map((x) => Number(x));
|
|
1862
|
+
const probe = new Date(Date.UTC(y as number, (m as number) - 1, d as number));
|
|
1863
|
+
if (
|
|
1864
|
+
probe.getUTCFullYear() !== y ||
|
|
1865
|
+
probe.getUTCMonth() + 1 !== m ||
|
|
1866
|
+
probe.getUTCDate() !== d
|
|
1867
|
+
) {
|
|
1868
|
+
throw new CouncilConfigError(
|
|
1869
|
+
`members.${name}.verified_at is not a real calendar date ` +
|
|
1870
|
+
`(got ${_pyRepr(verified_at_raw)}).`,
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
verified_at = coerced;
|
|
1874
|
+
}
|
|
1919
1875
|
const prompt_cache_raw = _get(cfg, 'prompt_cache', null);
|
|
1920
1876
|
let prompt_cache_ttl: '5m' | '1h' = '5m';
|
|
1921
1877
|
if (prompt_cache_raw !== null && prompt_cache_raw !== undefined && !_isBool(prompt_cache_raw)) {
|
|
@@ -1947,6 +1903,7 @@ function _build_member(
|
|
|
1947
1903
|
participate_low_impact: participate_raw,
|
|
1948
1904
|
tier,
|
|
1949
1905
|
prompt_cache_ttl,
|
|
1906
|
+
verified_at,
|
|
1950
1907
|
};
|
|
1951
1908
|
}
|
|
1952
1909
|
|