@event4u/agent-config 14.7.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 +175 -390
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +34 -4
- package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
- package/dist/agent-src/commands/evals/roadmap.json +17 -1
- package/dist/agent-src/commands/pr/create.md +14 -2
- package/dist/agent-src/commands/pr/merge.md +383 -0
- package/dist/agent-src/commands/review/changes.md +95 -14
- package/dist/agent-src/commands/roadmap/process-full.md +157 -3
- package/dist/agent-src/commands/roadmap.md +1 -1
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +77 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +100 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
- package/dist/agent-src/rules/recurring-criticism.md +1 -1
- package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
- package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
- package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
- 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/merge-conflicts/SKILL.md +26 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
- package/dist/agent-src/skills/security/SKILL.md +8 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +28 -2
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-77GR3VD4.js} +68 -43
- package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-CYH5SP6M.js} +4 -23
- package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
- package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
- package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-PLSG6UL5.js} +14 -14
- package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +101 -25
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +7 -7
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_handoff.js +7 -6
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_route_audit.js +6 -5
- package/dist/cli-delegate/cmd_route_explain.js +3 -3
- package/dist/cli-delegate/cmd_session_recycle.js +3 -3
- package/dist/cli-delegate/cmd_settings_get.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +83 -30
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +11 -9
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +9 -7
- package/dist/hooks/dispatch.js +1295 -812
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.js.map +1 -0
- package/dist/install/partitionEligibility.js +156 -0
- package/dist/install/partitionEligibility.js.map +1 -1
- package/dist/install/ruleLayerPartition.js +157 -0
- package/dist/install/ruleLayerPartition.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1358 -805
- 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/prune_empty_dirs.js +85 -0
- package/dist/scripts/_lib/prune_empty_dirs.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/scripts/condense.js +52 -89
- package/dist/scripts/condense.js.map +1 -1
- 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/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/false-green.md +47 -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/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/src/config/ci-local-parity.yml +21 -0
- package/src/config/estate-count-budget.json +13 -471
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +458 -12
- package/src/config/gate-violation-baselines.json +18 -8
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/publish-surface.json +60 -0
- package/src/config/requirements-trace-budget.json +32 -0
- package/src/config/rule-stub-ceilings.json +25 -4
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/partitionEligibility.ts +177 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_dispatch.bash +18 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -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/envelope_grounding.ts +11 -1
- package/src/scripts/_lib/host_capability.ts +59 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- 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/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +25 -38
- package/src/scripts/adr_cite_check.ts +540 -39
- 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 +134 -21
- package/src/scripts/bench_hook_latency.ts +204 -0
- package/src/scripts/build_archive_index.ts +34 -0
- package/src/scripts/check_adr_frontmatter.ts +574 -39
- 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_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -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_references.ts +18 -0
- package/src/scripts/check_requirements_trace.ts +378 -0
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_secret_leak.ts +166 -12
- package/src/scripts/check_single_delivery.ts +137 -4
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/code_graph/cli.ts +44 -5
- package/src/scripts/code_graph/extract.ts +87 -0
- package/src/scripts/condense.ts +61 -91
- 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/external_sources_denylist.json +4 -1
- 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 +41 -3
- package/src/scripts/hooks/dispatch_hook.ts +154 -187
- package/src/scripts/hooks/dispatch_issues.ts +38 -19
- package/src/scripts/hooks/fallback_yaml.ts +66 -0
- package/src/scripts/hooks/host_semantics.ts +21 -0
- package/src/scripts/hooks/injection_budget.ts +13 -3
- package/src/scripts/hooks/py_json_dumps.ts +103 -0
- package/src/scripts/hooks/run_continuation_hook.ts +3 -1
- package/src/scripts/hooks/state_io.ts +88 -1
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
- package/src/scripts/injection_scan_hook.ts +257 -15
- package/src/scripts/lint_adr_sweep_routing.ts +365 -0
- package/src/scripts/lint_hook_manifest.ts +49 -0
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- 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/routing_doctor.ts +5 -1
- package/src/scripts/run_supervise.ts +46 -1
- package/src/scripts/schemas/evals.schema.json +5 -0
- package/src/scripts/sync_pr_branch.ts +170 -20
- 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-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
- package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quota_parked — the marker that says a run stopped because plan quota ran out,
|
|
3
|
+
* rather than because it crashed.
|
|
4
|
+
*
|
|
5
|
+
* `run_supervise` can already tell a dead session from a live one, and a
|
|
6
|
+
* finished run from an unfinished one. What it cannot tell is WHY a run
|
|
7
|
+
* stopped, and for one cause that distinction decides the whole response: a run
|
|
8
|
+
* held back by an exhausted plan quota is not broken, it is early. Reported as
|
|
9
|
+
* `relaunchable` with "the session is gone" it reads as a crash, and the one
|
|
10
|
+
* fact an operator needs — that waiting is a strategy here and is not a
|
|
11
|
+
* strategy for a crash — is the fact that gets lost.
|
|
12
|
+
*
|
|
13
|
+
* **The trigger is ours, and that is the point.** Detecting the billing cliff
|
|
14
|
+
* inside Claude Code needs a host signal nobody has shown exists
|
|
15
|
+
* (`agents/roadmaps/later/road-to-billing-cliff-detection.md`). But the council
|
|
16
|
+
* establishes the same fact under its own roof: when `api_on_quota: 'ask'`
|
|
17
|
+
* parks a seat, plan quota for that provider is exhausted, measured by us, with
|
|
18
|
+
* no host surface involved. This marker is written from there.
|
|
19
|
+
*
|
|
20
|
+
* PII-exclusion-by-construction, the same shape as `billing_grant`: a record
|
|
21
|
+
* holds a run id, provider names, and an ISO stamp. There is no field capable
|
|
22
|
+
* of carrying a prompt, a path, or an error body, so there is no scrubbing pass
|
|
23
|
+
* here that could fail.
|
|
24
|
+
*
|
|
25
|
+
* **It records, it does not act.** Nothing here sleeps, probes, or relaunches.
|
|
26
|
+
* Unattended relaunch is a published refusal
|
|
27
|
+
* (road-to-long-horizon-execution 4.0, AI council 2026-08-19) whose reopen
|
|
28
|
+
* condition has since fired — see
|
|
29
|
+
* `agents/evidence/billing-cliff/spawn-reopen-condition.md` — and a fired
|
|
30
|
+
* condition reopens the question in the venue that closed it, never the
|
|
31
|
+
* capability by itself.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import * as fs from 'node:fs';
|
|
35
|
+
import * as path from 'node:path';
|
|
36
|
+
|
|
37
|
+
/** Directory, relative to the repo root, holding one marker per parked run. */
|
|
38
|
+
export const QUOTA_PARKED_DIR_REL = path.join('agents', 'runtime', 'state', 'quota-parked');
|
|
39
|
+
|
|
40
|
+
export interface QuotaParkedMarker {
|
|
41
|
+
readonly run_id: string;
|
|
42
|
+
/** Providers whose plan quota was exhausted, sorted and deduplicated. */
|
|
43
|
+
readonly providers: string[];
|
|
44
|
+
readonly parked_at: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Filesystem-safe path for a run's marker. Mirrors `billing_grant.grantFile`. */
|
|
48
|
+
export function markerFile(repoRoot: string, runId: string): string {
|
|
49
|
+
return path.join(
|
|
50
|
+
repoRoot,
|
|
51
|
+
QUOTA_PARKED_DIR_REL,
|
|
52
|
+
`${runId.replace(/[^A-Za-z0-9_-]/g, '_')}.json`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Record that `runId` is held back by exhausted plan quota on `providers`.
|
|
58
|
+
*
|
|
59
|
+
* Idempotent on the timestamp: a second park inside one run keeps the original
|
|
60
|
+
* `parked_at` and merges the provider set, so "when did this run first hit the
|
|
61
|
+
* cliff" survives a round that parks twice.
|
|
62
|
+
*/
|
|
63
|
+
export function writeQuotaParked(
|
|
64
|
+
repoRoot: string,
|
|
65
|
+
runId: string,
|
|
66
|
+
providers: readonly string[],
|
|
67
|
+
now: () => Date = () => new Date(),
|
|
68
|
+
): QuotaParkedMarker {
|
|
69
|
+
const file = markerFile(repoRoot, runId);
|
|
70
|
+
const existing = readMarker(file);
|
|
71
|
+
const merged = [...new Set([...(existing?.providers ?? []), ...providers])].sort();
|
|
72
|
+
const marker: QuotaParkedMarker = {
|
|
73
|
+
run_id: runId,
|
|
74
|
+
providers: merged,
|
|
75
|
+
parked_at: existing?.parked_at ?? now().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
78
|
+
fs.writeFileSync(file, `${JSON.stringify(marker, null, 2)}\n`, 'utf8');
|
|
79
|
+
return marker;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Remove a run's marker. Safe to call when none exists. */
|
|
83
|
+
export function clearQuotaParked(repoRoot: string, runId: string): void {
|
|
84
|
+
try {
|
|
85
|
+
fs.unlinkSync(markerFile(repoRoot, runId));
|
|
86
|
+
} catch {
|
|
87
|
+
// Already gone is the state the caller asked for.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The marker for a session, or `null`.
|
|
93
|
+
*
|
|
94
|
+
* Looks up by run id first, then scans. The scan is not redundant: the run id
|
|
95
|
+
* the council writes comes from `currentRunId`, whose chain is
|
|
96
|
+
* `AC_BILLING_GRANT` → `AC_RUN_ID` → `CLAUDE_CODE_SESSION_ID`, while the
|
|
97
|
+
* register's `session_id` comes from `AGENT_CONFIG_SESSION_ID` →
|
|
98
|
+
* `CLAUDE_CODE_SESSION_ID`. The two coincide in an ordinary Claude Code session
|
|
99
|
+
* and diverge the moment an orchestrator sets `AC_RUN_ID` — so a direct hit is
|
|
100
|
+
* the common case and the scan is what stops the uncommon one from silently
|
|
101
|
+
* reporting a parked run as a crashed one. The directory holds one small file
|
|
102
|
+
* per parked run, so the scan is cheap by construction.
|
|
103
|
+
*/
|
|
104
|
+
export function findQuotaParked(repoRoot: string, sessionId: string): QuotaParkedMarker | null {
|
|
105
|
+
const direct = readMarker(markerFile(repoRoot, sessionId));
|
|
106
|
+
if (direct !== null) return direct;
|
|
107
|
+
let names: string[];
|
|
108
|
+
try {
|
|
109
|
+
names = fs.readdirSync(path.join(repoRoot, QUOTA_PARKED_DIR_REL));
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
for (const name of names) {
|
|
114
|
+
if (!name.endsWith('.json')) continue;
|
|
115
|
+
const m = readMarker(path.join(repoRoot, QUOTA_PARKED_DIR_REL, name));
|
|
116
|
+
if (m !== null && m.run_id === sessionId) return m;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readMarker(file: string): QuotaParkedMarker | null {
|
|
122
|
+
try {
|
|
123
|
+
const raw: unknown = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
124
|
+
if (raw === null || typeof raw !== 'object') return null;
|
|
125
|
+
const o = raw as Record<string, unknown>;
|
|
126
|
+
if (typeof o['run_id'] !== 'string' || typeof o['parked_at'] !== 'string') return null;
|
|
127
|
+
if (!Array.isArray(o['providers'])) return null;
|
|
128
|
+
const providers = o['providers'].filter((p): p is string => typeof p === 'string');
|
|
129
|
+
return { run_id: o['run_id'], providers, parked_at: o['parked_at'] };
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -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[];
|