@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
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* here; the rest is there rather than duplicated.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import type { ApiOnQuota } from './transport_resolver.js';
|
|
14
|
+
|
|
13
15
|
/**
|
|
14
16
|
* `fallback.*` — the mid-flight cli→api retry's one configurable knob.
|
|
15
17
|
*
|
|
@@ -24,8 +26,13 @@ export interface FallbackConfig {
|
|
|
24
26
|
* May an exhausted CLI quota retry on the metered api rung? Default
|
|
25
27
|
* `false`. The no-double-charge classes are always eligible and never
|
|
26
28
|
* consult this; `timeout` / 5xx are never eligible under any value.
|
|
29
|
+
*
|
|
30
|
+
* Three values, not two. `'ask'` defers the answer out of configuration
|
|
31
|
+
* time and into the round that would cross the cliff: the seat is PARKED
|
|
32
|
+
* unless a run-scoped billing grant says a human already agreed for this
|
|
33
|
+
* run. See `transport_resolver.ts` § `ApiOnQuota` and `_lib/billing_grant`.
|
|
27
34
|
*/
|
|
28
|
-
readonly api_on_quota:
|
|
35
|
+
readonly api_on_quota: ApiOnQuota;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
/**
|
|
@@ -67,12 +74,17 @@ export function buildFallback(raw: unknown, d: FallbackBuilderDeps): FallbackCon
|
|
|
67
74
|
if (!d.isDict(raw)) return { api_on_quota: false };
|
|
68
75
|
const v = raw['api_on_quota'];
|
|
69
76
|
if (v === undefined) return { api_on_quota: false };
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
if (d.isBool(v)) return { api_on_quota: v };
|
|
78
|
+
// `'ask'` is the ONLY string this accepts, and the narrowness is the
|
|
79
|
+
// point: the paragraph above refuses `"yes"` because an operator writing
|
|
80
|
+
// it is trying to authorise spend, and widening to "any truthy-looking
|
|
81
|
+
// string" to let `ask` through would readmit exactly that. A third named
|
|
82
|
+
// value is not a lenient parse.
|
|
83
|
+
if (d.isStr(v) && v === 'ask') return { api_on_quota: 'ask' };
|
|
84
|
+
throw d.error(
|
|
85
|
+
`\`fallback.api_on_quota\`=${d.repr(v)} must be \`false\`, \`true\` or \`'ask'\` ` +
|
|
86
|
+
`(got ${d.typeName(v)}).`,
|
|
87
|
+
);
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
/**
|
|
@@ -19,19 +19,28 @@
|
|
|
19
19
|
import type { ExternalAIClient, CouncilResponse } from './clients.js';
|
|
20
20
|
import {
|
|
21
21
|
classifyCliFailure,
|
|
22
|
+
effectiveApiOnQuota,
|
|
22
23
|
isFallbackEligibleUnder,
|
|
24
|
+
type ApiOnQuota,
|
|
23
25
|
type FallbackPolicy,
|
|
24
26
|
type MidFlightFallback,
|
|
25
27
|
} from './transport_resolver.js';
|
|
26
28
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
+
/**
|
|
30
|
+
* One escalation's outcome, for the event sink.
|
|
31
|
+
*
|
|
32
|
+
* `awaiting_grant` is the posture `'ask'` adds and it is neither of the two
|
|
33
|
+
* that existed: the seat was NOT switched to metered billing and it was NOT
|
|
34
|
+
* lost. It is held, and the round closes with a question instead of a silent
|
|
35
|
+
* charge or a silent absence.
|
|
36
|
+
*/
|
|
37
|
+
export type FallbackOutcome = 'retried' | 'no_twin' | 'cost_budget' | 'awaiting_grant';
|
|
29
38
|
|
|
30
39
|
export interface FallbackEvent {
|
|
31
40
|
readonly provider: string;
|
|
32
41
|
readonly failure: string;
|
|
33
42
|
readonly outcome: FallbackOutcome;
|
|
34
|
-
readonly api_on_quota:
|
|
43
|
+
readonly api_on_quota: ApiOnQuota;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
export interface CliFallbackOptions {
|
|
@@ -42,7 +51,21 @@ export interface CliFallbackOptions {
|
|
|
42
51
|
*/
|
|
43
52
|
readonly construct: (provider: string) => ExternalAIClient | null;
|
|
44
53
|
/** `ai_council.fallback.api_on_quota` — see `FallbackPolicy.apiOnQuota`. */
|
|
45
|
-
readonly api_on_quota:
|
|
54
|
+
readonly api_on_quota: ApiOnQuota;
|
|
55
|
+
/**
|
|
56
|
+
* True when a run-scoped billing grant is in force for THIS run. Read only
|
|
57
|
+
* under `api_on_quota: 'ask'`; absence means park, never proceed.
|
|
58
|
+
*/
|
|
59
|
+
readonly billing_grant?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Providers parked under `'ask'`, appended during the round.
|
|
62
|
+
*
|
|
63
|
+
* It lives on the options object rather than in the CLI because the CLI
|
|
64
|
+
* has nothing to do with it: the factory that decides a seat is parked is
|
|
65
|
+
* the only thing that can populate it, and a caller threading an array
|
|
66
|
+
* down to that factory and back is plumbing with no reader in between.
|
|
67
|
+
*/
|
|
68
|
+
readonly parked?: string[];
|
|
46
69
|
/**
|
|
47
70
|
* Called once per ESTABLISHING escalation, never per substituted call.
|
|
48
71
|
*
|
|
@@ -60,6 +83,42 @@ export interface EstablishedTwin {
|
|
|
60
83
|
readonly original_error: string;
|
|
61
84
|
}
|
|
62
85
|
|
|
86
|
+
/**
|
|
87
|
+
* A seat held back because the policy is `'ask'` and no grant is in force.
|
|
88
|
+
*
|
|
89
|
+
* The third state the boolean could not express. Before `'ask'` existed a
|
|
90
|
+
* quota-hit seat had exactly two futures — a twin (metered, silent) or `null`
|
|
91
|
+
* (lost for the round). Neither is answerable afterwards: by the time a human
|
|
92
|
+
* reads the output, the spend has happened or the seat is gone. A parked seat
|
|
93
|
+
* is still in the round's seat map, so the question the round closes with is
|
|
94
|
+
* about work that can still be done.
|
|
95
|
+
*/
|
|
96
|
+
export interface ParkedSeat {
|
|
97
|
+
readonly parked: true;
|
|
98
|
+
readonly provider: string;
|
|
99
|
+
readonly reason: string;
|
|
100
|
+
readonly original_error: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** `establishTwin`'s three outcomes: a twin, a parked seat, or nothing. */
|
|
104
|
+
export type EstablishOutcome = EstablishedTwin | ParkedSeat | null;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Narrow an `EstablishOutcome` to the twin branch.
|
|
108
|
+
*
|
|
109
|
+
* Every caller must use this rather than a truthiness test: a `ParkedSeat` is
|
|
110
|
+
* an object, so `if (twin)` retries the parked marker as though it were a
|
|
111
|
+
* client — the one bug the park branch exists to make impossible.
|
|
112
|
+
*/
|
|
113
|
+
export function isEstablishedTwin(o: EstablishOutcome): o is EstablishedTwin {
|
|
114
|
+
return o !== null && !('parked' in o);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Narrow an `EstablishOutcome` to the parked branch. */
|
|
118
|
+
export function isParkedSeat(o: EstablishOutcome): o is ParkedSeat {
|
|
119
|
+
return o !== null && 'parked' in o;
|
|
120
|
+
}
|
|
121
|
+
|
|
63
122
|
/**
|
|
64
123
|
* Why the twin map exists alongside the ledger.
|
|
65
124
|
*
|
|
@@ -99,10 +158,32 @@ export interface EstablishInput {
|
|
|
99
158
|
* budget then refused. An ineligible class emits nothing at all: there was no
|
|
100
159
|
* escalation to report.
|
|
101
160
|
*/
|
|
102
|
-
export function establishTwin(input: EstablishInput):
|
|
161
|
+
export function establishTwin(input: EstablishInput): EstablishOutcome {
|
|
103
162
|
const { member, error, fallback, ledger } = input;
|
|
104
163
|
const failure = classifyCliFailure(error);
|
|
105
|
-
const policy: FallbackPolicy = {
|
|
164
|
+
const policy: FallbackPolicy = {
|
|
165
|
+
apiOnQuota: fallback.api_on_quota,
|
|
166
|
+
billingGrant: fallback.billing_grant === true,
|
|
167
|
+
};
|
|
168
|
+
// The park branch runs BEFORE the eligibility test, and the order is the
|
|
169
|
+
// whole mechanism. `effectiveApiOnQuota` collapses an ungranted `'ask'`
|
|
170
|
+
// to `false`, so an eligibility-first reading would return `null` here —
|
|
171
|
+
// the seat lost, exactly as under `api_on_quota: false`, and the operator
|
|
172
|
+
// never asked. The ledger is deliberately NOT consulted: parking spends no
|
|
173
|
+
// fallback attempt, so the seat is still escalatable on the grant.
|
|
174
|
+
if (
|
|
175
|
+
fallback.api_on_quota === 'ask' &&
|
|
176
|
+
failure === 'quota_exhausted' &&
|
|
177
|
+
!effectiveApiOnQuota(policy)
|
|
178
|
+
) {
|
|
179
|
+
fallback.on_event?.({
|
|
180
|
+
provider: member.name,
|
|
181
|
+
failure,
|
|
182
|
+
outcome: 'awaiting_grant',
|
|
183
|
+
api_on_quota: fallback.api_on_quota,
|
|
184
|
+
});
|
|
185
|
+
return { parked: true, provider: member.name, reason: failure, original_error: error };
|
|
186
|
+
}
|
|
106
187
|
if (!isFallbackEligibleUnder(failure, policy)) return null;
|
|
107
188
|
if (ledger.attempt(member.name, failure, policy) !== 'api') return null;
|
|
108
189
|
|
|
@@ -241,7 +322,7 @@ export function escalateUnmetered(
|
|
|
241
322
|
fallback,
|
|
242
323
|
ledger,
|
|
243
324
|
});
|
|
244
|
-
if (twin
|
|
325
|
+
if (!isEstablishedTwin(twin)) return null;
|
|
245
326
|
// NOTHING is emitted here, and the previous comment justifying an emit at
|
|
246
327
|
// establishment is deleted rather than reworded because it stopped being
|
|
247
328
|
// true. It read: "on this path the twin goes through the round's ORDINARY
|
|
@@ -75,7 +75,7 @@ import {
|
|
|
75
75
|
emitOutcome,
|
|
76
76
|
escalateUnmetered,
|
|
77
77
|
refuseUnmeteredEscalation,
|
|
78
|
-
establishTwin,
|
|
78
|
+
establishTwin, isEstablishedTwin,
|
|
79
79
|
isFailedCliCall,
|
|
80
80
|
runGatedRetry,
|
|
81
81
|
stampFallback,
|
|
@@ -764,7 +764,7 @@ function _run_round(
|
|
|
764
764
|
fallback,
|
|
765
765
|
ledger,
|
|
766
766
|
});
|
|
767
|
-
if (twin
|
|
767
|
+
if (isEstablishedTwin(twin)) {
|
|
768
768
|
const out = runGatedRetry(response, member, twin, fallback, {
|
|
769
769
|
gate: (client) =>
|
|
770
770
|
table === null ||
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python-format helpers, extracted from `config.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Every error string this package emits is byte-faithful to the Python original
|
|
5
|
+
* it replaced, which is why `repr()`, `type().__name__` and `oct()` are
|
|
6
|
+
* reimplemented here instead of being approximated with template literals.
|
|
7
|
+
*
|
|
8
|
+
* Pure and self-contained: these functions call only each other, which is what
|
|
9
|
+
* made the section safe to move out whole. Extracted because config.ts is
|
|
10
|
+
* hundreds of lines above the 1500-line source ceiling and a change adding to it
|
|
11
|
+
* has to pay for the lines somewhere.
|
|
12
|
+
*/
|
|
13
|
+
/** Python `repr()` for a string scalar (single-quoted, escaped). */
|
|
14
|
+
export function _pyReprStr(s: string): string {
|
|
15
|
+
const hasSingle = s.includes("'");
|
|
16
|
+
const hasDouble = s.includes('"');
|
|
17
|
+
const quote = hasSingle && !hasDouble ? '"' : "'";
|
|
18
|
+
let body = s
|
|
19
|
+
.replace(/\\/g, '\\\\')
|
|
20
|
+
.replace(/\n/g, '\\n')
|
|
21
|
+
.replace(/\r/g, '\\r')
|
|
22
|
+
.replace(/\t/g, '\\t');
|
|
23
|
+
if (quote === "'") {
|
|
24
|
+
body = body.replace(/'/g, "\\'");
|
|
25
|
+
} else {
|
|
26
|
+
body = body.replace(/"/g, '\\"');
|
|
27
|
+
}
|
|
28
|
+
return `${quote}${body}${quote}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Python `repr()` for a float value (shortest round-trip, `N.0` for ints). */
|
|
32
|
+
export function _pyReprFloat(value: number): string {
|
|
33
|
+
if (Number.isInteger(value) && Number.isFinite(value)) {
|
|
34
|
+
return `${value}.0`;
|
|
35
|
+
}
|
|
36
|
+
if (value === Infinity) {
|
|
37
|
+
return 'inf';
|
|
38
|
+
}
|
|
39
|
+
if (value === -Infinity) {
|
|
40
|
+
return '-inf';
|
|
41
|
+
}
|
|
42
|
+
if (Number.isNaN(value)) {
|
|
43
|
+
return 'nan';
|
|
44
|
+
}
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Python `repr()` for an arbitrary parsed value. Floats are tracked via
|
|
50
|
+
* `_FLOAT` so int-valued floats render `N.0`; bare numbers render as
|
|
51
|
+
* Python ints (no decimal). Mirrors `{value!r}` formatting.
|
|
52
|
+
*/
|
|
53
|
+
export function _pyRepr(value: unknown): string {
|
|
54
|
+
if (value instanceof _Float) {
|
|
55
|
+
return _pyReprFloat(value.value);
|
|
56
|
+
}
|
|
57
|
+
if (value === null || value === undefined) {
|
|
58
|
+
return 'None';
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === 'boolean') {
|
|
61
|
+
return value ? 'True' : 'False';
|
|
62
|
+
}
|
|
63
|
+
if (typeof value === 'string') {
|
|
64
|
+
return _pyReprStr(value);
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === 'number') {
|
|
67
|
+
return String(value);
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
return `[${value.map((v) => _pyRepr(v)).join(', ')}]`;
|
|
71
|
+
}
|
|
72
|
+
if (typeof value === 'object') {
|
|
73
|
+
const parts: string[] = [];
|
|
74
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
75
|
+
parts.push(`${_pyReprStr(k)}: ${_pyRepr(v)}`);
|
|
76
|
+
}
|
|
77
|
+
return `{${parts.join(', ')}}`;
|
|
78
|
+
}
|
|
79
|
+
return String(value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Wrapper marking a number that should `repr()` as a Python float. */
|
|
83
|
+
export class _Float {
|
|
84
|
+
constructor(readonly value: number) {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Mark `n` so `_pyRepr` renders it with a Python float repr (`N.0`). */
|
|
88
|
+
export function _f(n: number): _Float {
|
|
89
|
+
return new _Float(n);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Python `type(value).__name__`. */
|
|
93
|
+
export function _pyTypeName(value: unknown): string {
|
|
94
|
+
if (value === null || value === undefined) {
|
|
95
|
+
return 'NoneType';
|
|
96
|
+
}
|
|
97
|
+
if (typeof value === 'boolean') {
|
|
98
|
+
return 'bool';
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === 'number') {
|
|
101
|
+
return Number.isInteger(value) ? 'int' : 'float';
|
|
102
|
+
}
|
|
103
|
+
if (typeof value === 'string') {
|
|
104
|
+
return 'str';
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
return 'list';
|
|
108
|
+
}
|
|
109
|
+
if (typeof value === 'object') {
|
|
110
|
+
return 'dict';
|
|
111
|
+
}
|
|
112
|
+
return typeof value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Python `sorted(set_of_strings)` rendered as a list repr `['a', 'b']`. */
|
|
116
|
+
export function _sortedListRepr(items: Iterable<string>): string {
|
|
117
|
+
const sorted = [...items].sort();
|
|
118
|
+
return `[${sorted.map((s) => _pyReprStr(s)).join(', ')}]`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Python `oct(mode)` → `0o600`-shaped string. */
|
|
122
|
+
export function _pyOct(mode: number): string {
|
|
123
|
+
return `0o${mode.toString(8)}`;
|
|
124
|
+
}
|
|
@@ -403,13 +403,76 @@ const FALLBACK_ELIGIBLE: ReadonlySet<CliFailureClass> = new Set([
|
|
|
403
403
|
* `auto` may infer — the USD spend itself remains guarded by the ordinary
|
|
404
404
|
* `cost_budget` gates the orchestrator runs on the retry.
|
|
405
405
|
*/
|
|
406
|
+
/**
|
|
407
|
+
* The three postures of `ai_council.fallback.api_on_quota`.
|
|
408
|
+
*
|
|
409
|
+
* `false` and `true` mean what they always meant. `'ask'` is the third: the
|
|
410
|
+
* billing-class decision is deferred out of configuration time and into the
|
|
411
|
+
* round that would actually cross the cliff, where the remaining scope is
|
|
412
|
+
* known. Under `'ask'` the seat is eligible only when a run-scoped billing
|
|
413
|
+
* grant is present — a human said yes for THIS run — and is parked otherwise.
|
|
414
|
+
*/
|
|
415
|
+
export type ApiOnQuota = boolean | 'ask';
|
|
416
|
+
|
|
406
417
|
export interface FallbackPolicy {
|
|
407
|
-
readonly apiOnQuota:
|
|
418
|
+
readonly apiOnQuota: ApiOnQuota;
|
|
419
|
+
/**
|
|
420
|
+
* True when a run-scoped billing grant is in force. Only consulted under
|
|
421
|
+
* `apiOnQuota: 'ask'`; absent is never read as granted, which is why this
|
|
422
|
+
* is optional rather than defaulted anywhere.
|
|
423
|
+
*/
|
|
424
|
+
readonly billingGrant?: boolean;
|
|
408
425
|
}
|
|
409
426
|
|
|
410
427
|
/** The base policy: exactly the three-plus-one no-double-charge classes. */
|
|
411
428
|
export const DEFAULT_FALLBACK_POLICY: FallbackPolicy = { apiOnQuota: false };
|
|
412
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Collapse the tri-state plus the grant into the boolean the eligibility rule
|
|
432
|
+
* needs. `'ask'` without a grant is `false` — never `true` by omission, which
|
|
433
|
+
* is the one direction that would turn a configured question into silent
|
|
434
|
+
* metered spend.
|
|
435
|
+
*/
|
|
436
|
+
export function effectiveApiOnQuota(policy: FallbackPolicy): boolean {
|
|
437
|
+
if (policy.apiOnQuota === 'ask') return policy.billingGrant === true;
|
|
438
|
+
return policy.apiOnQuota === true;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Parse a config value into the tri-state, or throw.
|
|
443
|
+
*
|
|
444
|
+
* Rejecting is the point. A parser that mapped an unknown value onto a truthy
|
|
445
|
+
* default would turn a configured `off` into metered spend — the exact failure
|
|
446
|
+
* the `'ask'` posture exists to prevent, introduced by the code that adds it.
|
|
447
|
+
* Accepts the Python-ish spellings the council config has always allowed on
|
|
448
|
+
* the boolean side, plus `ask` on the new one.
|
|
449
|
+
*/
|
|
450
|
+
export function apiOnQuotaView(raw: unknown): ApiOnQuota {
|
|
451
|
+
// Lenient by design, unlike `parseApiOnQuota`: this feeds a DISPLAY
|
|
452
|
+
// surface, where throwing on a malformed file would blank the wizard
|
|
453
|
+
// instead of showing the operator the setting they need to fix. Reading
|
|
454
|
+
// by value rather than by truthiness is the half that still matters —
|
|
455
|
+
// `=== true` alone renders `'ask'` as `false`, and the next save
|
|
456
|
+
// downgrades a configured question to a configured no.
|
|
457
|
+
return raw === 'ask' ? 'ask' : raw === true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function parseApiOnQuota(raw: unknown): ApiOnQuota {
|
|
461
|
+
if (typeof raw === 'boolean') return raw;
|
|
462
|
+
if (raw === undefined || raw === null) return false;
|
|
463
|
+
if (typeof raw === 'string') {
|
|
464
|
+
const s = raw.trim().toLowerCase();
|
|
465
|
+
if (s === 'ask') return 'ask';
|
|
466
|
+
if (s === 'true' || s === 'yes' || s === 'on' || s === '1') return true;
|
|
467
|
+
if (s === '' || s === 'false' || s === 'no' || s === 'off' || s === '0') {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
throw new Error(
|
|
472
|
+
`ai_council.fallback.api_on_quota: expected false | true | 'ask', got ${JSON.stringify(raw)}`,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
413
476
|
/** Normalise a raw client error / skip reason into a `CliFailureClass`. */
|
|
414
477
|
export function classifyCliFailure(raw: string): CliFailureClass {
|
|
415
478
|
const s = raw.trim().toLowerCase();
|
|
@@ -464,7 +527,7 @@ export function isFallbackEligibleUnder(
|
|
|
464
527
|
policy: FallbackPolicy,
|
|
465
528
|
): boolean {
|
|
466
529
|
if (FALLBACK_ELIGIBLE.has(failure)) return true;
|
|
467
|
-
return policy
|
|
530
|
+
return effectiveApiOnQuota(policy) && failure === 'quota_exhausted';
|
|
468
531
|
}
|
|
469
532
|
|
|
470
533
|
/**
|
|
@@ -28,7 +28,7 @@ import * as path from 'node:path';
|
|
|
28
28
|
import process from 'node:process';
|
|
29
29
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
30
30
|
|
|
31
|
-
import { readAdrFrontmatterScalars } from './_lib/adr_frontmatter.js';
|
|
31
|
+
import { readAdrAxisCells, readAdrFrontmatterScalars } from './_lib/adr_frontmatter.js';
|
|
32
32
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
33
33
|
|
|
34
34
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -279,8 +279,13 @@ export function scan_area(area: string): [AdrEntry[], string[]] {
|
|
|
279
279
|
errs.push(`${area}/${base}: filename does not match NNNN-<slug>.md`);
|
|
280
280
|
continue;
|
|
281
281
|
}
|
|
282
|
-
const
|
|
283
|
-
|
|
282
|
+
const text = fs.readFileSync(p, 'utf-8');
|
|
283
|
+
const fm = read_adr_meta(text);
|
|
284
|
+
// Read apart from `fm`, for the same reason the index does: the two
|
|
285
|
+
// axes are nested, so the scalar reader keeps them out of its map and
|
|
286
|
+
// spreading it could never produce these cells.
|
|
287
|
+
const axes = readAdrAxisCells(text);
|
|
288
|
+
adrs.push({ num: m[1] as string, slug: m[2] as string, path: base, ...fm, ...axes });
|
|
284
289
|
}
|
|
285
290
|
// Gap check.
|
|
286
291
|
const nums = adrs.map((a) => parseInt(a.num, 10));
|
|
@@ -322,7 +327,11 @@ export function render_area_readme(area: string, meta: AreaMeta, adrs: AdrEntry[
|
|
|
322
327
|
} else {
|
|
323
328
|
lines.push(`Contract: _not yet published_ (\`${repoRel}\`).`);
|
|
324
329
|
}
|
|
325
|
-
lines.push(
|
|
330
|
+
lines.push(
|
|
331
|
+
'',
|
|
332
|
+
'| # | Title | Status | Date | Provenance | Evidence | Supersedes |',
|
|
333
|
+
'|---|---|---|---|---|---|---|',
|
|
334
|
+
);
|
|
326
335
|
for (const a of adrs) {
|
|
327
336
|
// a.get("decision", a["slug"]).replace("-", " ").title()
|
|
328
337
|
const decision = a['decision'] !== undefined ? a['decision'] : a.slug;
|
|
@@ -330,11 +339,12 @@ export function render_area_readme(area: string, meta: AreaMeta, adrs: AdrEntry[
|
|
|
330
339
|
lines.push(
|
|
331
340
|
`| [${a.num}](${a.path}) | ${title} | ` +
|
|
332
341
|
`${a['status'] ?? '—'} | ${a['date'] ?? '—'} | ` +
|
|
342
|
+
`${a['provenance_kind'] ?? '—'} | ${a['evidence_grade'] ?? '—'} | ` +
|
|
333
343
|
`${a['supersedes'] ?? '—'} |`,
|
|
334
344
|
);
|
|
335
345
|
}
|
|
336
346
|
if (adrs.length === 0) {
|
|
337
|
-
lines.push('| _none yet_ | — | — | — | — |');
|
|
347
|
+
lines.push('| _none yet_ | — | — | — | — | — | — |');
|
|
338
348
|
}
|
|
339
349
|
return lines.join('\n') + '\n';
|
|
340
350
|
}
|
|
@@ -411,9 +421,26 @@ export function cmd_check(): number {
|
|
|
411
421
|
process.stderr.write(`❌ ${e}\n`);
|
|
412
422
|
hard += 1;
|
|
413
423
|
}
|
|
414
|
-
|
|
424
|
+
const readmePath = path.join(ADR_ROOT, area, 'README.md');
|
|
425
|
+
if (adrs.length > 0 && !_exists(readmePath)) {
|
|
415
426
|
process.stderr.write(`❌ ${area}/: README.md missing\n`);
|
|
416
427
|
hard += 1;
|
|
428
|
+
} else if (_exists(readmePath)) {
|
|
429
|
+
// Existence was the whole check until 2026-08-22, and the six
|
|
430
|
+
// committed READMEs proved what that costs: every one of them had
|
|
431
|
+
// been generated before the shared frontmatter reader landed, so
|
|
432
|
+
// they rendered `—` for `status` and `date` on ADRs that carry
|
|
433
|
+
// both — stale for months against a gate reporting zero failures.
|
|
434
|
+
// A generated file with no freshness gate is a file nobody
|
|
435
|
+
// regenerates.
|
|
436
|
+
const rendered = render_area_readme(area, meta, adrs);
|
|
437
|
+
if (fs.readFileSync(readmePath, 'utf-8') !== rendered) {
|
|
438
|
+
process.stderr.write(
|
|
439
|
+
`❌ ${area}/README.md is stale — regenerate with ` +
|
|
440
|
+
`\`audit_adr_coverage --regen-area-readme ${area}\`\n`,
|
|
441
|
+
);
|
|
442
|
+
hard += 1;
|
|
443
|
+
}
|
|
417
444
|
}
|
|
418
445
|
if (adrs.length === 0) {
|
|
419
446
|
process.stderr.write(`⚠️ ${area}/: no bootstrap ADR yet (contract: ${meta.contract})\n`);
|