@event4u/agent-config 9.29.0 → 9.30.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 +108 -7
- package/README.md +1 -0
- package/dist/agent-src/commands/analyze/inbox.md +22 -1
- package/dist/agent-src/commands/explain-run.md +31 -1
- package/dist/agent-src/commands/team/delegate.md +1 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +4 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +17 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +11 -1
- package/dist/agent-src/rules/preservation-guard.md +9 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +312 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +72 -4
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +10 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-OJM3MBJM.js → chunk-BTJNRUX3.js} +21 -34
- package/dist/cli-delegate/{chunk-MMXA5GY7.js → chunk-DDUNNFYX.js} +14 -0
- package/dist/cli-delegate/{chunk-NZ5N2XQL.js → chunk-EQY4FLJB.js} +1 -1
- package/dist/cli-delegate/{chunk-RCIA67XN.js → chunk-IYTGIG2Y.js} +44 -1
- package/dist/cli-delegate/chunk-MMJL4TIX.js +18 -0
- package/dist/cli-delegate/chunk-O3CO5BFB.js +161 -0
- package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
- package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
- package/dist/cli-delegate/cmd_conformance.js +2798 -95
- package/dist/cli-delegate/cmd_doctor.js +2 -1
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +86 -6
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +2 -1
- package/dist/cli-delegate/cmd_session_recycle.js +177 -0
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +7 -7
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1904 -575
- package/dist/install/install.mjs +14 -0
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3760 -726
- package/dist/server/schemas/settings.js +6 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/index-B5qGYOJo.js +26 -0
- package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +21 -5
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +312 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
- package/src/config/agent-settings.template.yml +36 -0
- package/src/config/dispatch-economy-metrics.json +61 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/hook-token-budget.json +91 -0
- package/src/config/recycle-threshold-budget.json +12 -0
- package/src/config/rule-enforcement-baseline.json +86 -0
- package/src/scripts/_cli/cmd_conformance.ts +17 -2
- package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
- package/src/scripts/_dispatch.bash +23 -0
- package/src/scripts/_lib/judgment_ladder.ts +254 -2
- package/src/scripts/_lib/orchestration_record.ts +41 -10
- package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
- package/src/scripts/_lib/self_repair.ts +194 -0
- package/src/scripts/_lib/session_eol.ts +230 -0
- package/src/scripts/_lib/session_role.ts +42 -0
- package/src/scripts/_lib/subagent_capsule.ts +159 -0
- package/src/scripts/_lib/subagent_response.ts +75 -0
- package/src/scripts/ai_council/clients.ts +50 -1
- package/src/scripts/ask_transport.ts +221 -0
- package/src/scripts/bench_hook_injection.ts +234 -0
- package/src/scripts/check_adr_frontmatter.ts +84 -40
- package/src/scripts/check_branch_freshness.ts +1 -0
- package/src/scripts/check_corpus_staleness.ts +18 -4
- package/src/scripts/check_detector_corpus.ts +17 -0
- package/src/scripts/check_gitignore_freshness.ts +42 -8
- package/src/scripts/check_hook_bundle_freshness.ts +20 -0
- package/src/scripts/check_kernel_prefix_stability.ts +1 -0
- package/src/scripts/check_standing_rule_delivery.ts +73 -32
- package/src/scripts/check_static_layer_stability.ts +154 -0
- package/src/scripts/check_suppression_hygiene.ts +48 -0
- package/src/scripts/dispatch_economy_report.ts +448 -0
- package/src/scripts/dispatch_r2_reviewer.ts +15 -2
- package/src/scripts/explain_run.ts +206 -4
- package/src/scripts/generate_settings_reference.ts +1 -0
- package/src/scripts/handoff_context_hook.ts +112 -4
- package/src/scripts/hook_manifest.yaml +117 -10
- package/src/scripts/hooks/concern_registry.ts +8 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +40 -3
- package/src/scripts/hooks/dispatch_hook.ts +39 -2
- package/src/scripts/hooks/edit_shape_hook.ts +303 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
- package/src/scripts/hooks/reread_guard_hook.ts +314 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
- package/src/scripts/hooks/session_eol_hook.ts +268 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +507 -0
- package/src/scripts/install-hooks.sh +1 -1
- package/src/scripts/language_mirror_hook.ts +89 -0
- package/src/scripts/lint_decision_impact.ts +14 -1
- package/src/scripts/lint_hook_manifest.ts +82 -0
- package/src/scripts/lint_readme_serial_comma.ts +1 -0
- package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
- package/src/scripts/measure_turn_end_gate.ts +194 -0
- package/src/scripts/orchestration_record.ts +4 -1
- package/src/scripts/rebuild_hook_bundle.ts +1 -0
- package/src/scripts/report_conformance_funnel.ts +284 -0
- package/src/scripts/report_imperative_density.ts +1 -0
- package/src/scripts/routing_doctor.ts +97 -0
- package/src/scripts/self_repair_cli.ts +51 -3
- package/src/scripts/session_eol_report.ts +261 -0
- package/src/server/schemas/settings.ts +14 -0
- package/src/templates/compact-instructions.md +40 -0
- package/dist/ui/assets/index-JDAF00bC.js +0 -26
- package/dist/ui/assets/index-JDAF00bC.js.map +0 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* report_conformance_funnel.ts — delivery → activation → compliance, one view.
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ REPORT ONLY — THIS SCRIPT MUST NEVER BE WIRED INTO A CI WORKFLOW WITHOUT
|
|
6
|
+
* A MEASURED FALSE-POSITIVE RATE FIRST (locked policy; council requirement,
|
|
7
|
+
* road-to-feedback-9-29 Phase 4.2). Every source it joins is itself advisory
|
|
8
|
+
* and says so; a join of advisories does not become a gate by aggregation.
|
|
9
|
+
*
|
|
10
|
+
* Executes road-to-feedback-9-29 Phase 4.2: one report join over the existing
|
|
11
|
+
* conformance sources, so the three questions that were answered in three
|
|
12
|
+
* places read as the funnel they are:
|
|
13
|
+
*
|
|
14
|
+
* DELIVERY what rule text the carriers put in front of the model, and
|
|
15
|
+
* whether the two carriers agree —
|
|
16
|
+
* `conformance_scan.measureDelivered` (carrier census) +
|
|
17
|
+
* `report_carrier_divergence.compareCarriers` (divergence).
|
|
18
|
+
* ACTIVATION whether the shipped skills are invoked at all —
|
|
19
|
+
* `report_skill_activation.censusSkills` + `measureUsage`.
|
|
20
|
+
* COMPLIANCE whether a loaded skill's obligations were violated (SK-2) —
|
|
21
|
+
* `report_skill_obligation_violations.scanStore`.
|
|
22
|
+
*
|
|
23
|
+
* JOINS, NEVER RE-DERIVES. Every number below comes from an EXPORTED function
|
|
24
|
+
* of the source that owns it. A second copy of a classifier is the "second
|
|
25
|
+
* artefact to keep in sync" this repo forbids — it is how the 8 → 30 spread
|
|
26
|
+
* and the 303-vs-626 detector defect happened, and both are cited in the
|
|
27
|
+
* sources this file imports.
|
|
28
|
+
*
|
|
29
|
+
* Each axis also carries the honest-scope statement its source already
|
|
30
|
+
* carries (SK-2's mechanisable-subset ratio, activation's catalogue-not-
|
|
31
|
+
* persisted caveat, delivery's not-per-session caveat). Where a source has no
|
|
32
|
+
* local data, the axis prints its honest "no data" line rather than
|
|
33
|
+
* disappearing — an absent store is an answer, not a zero.
|
|
34
|
+
*
|
|
35
|
+
* Reached via `agent-config conformance --funnel` (no new top-level verb —
|
|
36
|
+
* the flag hangs off the existing `conformance` verb per the dispatcher-verb
|
|
37
|
+
* registry trap named in the roadmap's risk table).
|
|
38
|
+
*
|
|
39
|
+
* Exit: 0 always, except a usage error (1). Deliberate — see the header.
|
|
40
|
+
*/
|
|
41
|
+
import * as fs from 'node:fs';
|
|
42
|
+
import * as os from 'node:os';
|
|
43
|
+
import * as path from 'node:path';
|
|
44
|
+
import process from 'node:process';
|
|
45
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
46
|
+
|
|
47
|
+
import {
|
|
48
|
+
measureDelivered,
|
|
49
|
+
defaultStore,
|
|
50
|
+
type DeliveredPayload,
|
|
51
|
+
} from './conformance_scan.js';
|
|
52
|
+
import {
|
|
53
|
+
compareCarriers,
|
|
54
|
+
GLOBAL_RULES,
|
|
55
|
+
PROJECT_RULES,
|
|
56
|
+
PROJECTION_SOURCE,
|
|
57
|
+
type CarrierDivergence,
|
|
58
|
+
} from './report_carrier_divergence.js';
|
|
59
|
+
import {
|
|
60
|
+
censusSkills,
|
|
61
|
+
measureUsage,
|
|
62
|
+
SKILLS_ROOT,
|
|
63
|
+
type SkillCensus,
|
|
64
|
+
type UsageReport,
|
|
65
|
+
} from './report_skill_activation.js';
|
|
66
|
+
import { scanStore as scanObligationStore, type Sk2Report } from './report_skill_obligation_violations.js';
|
|
67
|
+
|
|
68
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
69
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
70
|
+
|
|
71
|
+
export interface FunnelOptions {
|
|
72
|
+
/** The checkout whose skills and projection source are measured. */
|
|
73
|
+
repoRoot?: string;
|
|
74
|
+
/** The project rule carrier — what the host reads at project scope. */
|
|
75
|
+
projectRulesDir?: string;
|
|
76
|
+
/** The machine-global rule carrier. */
|
|
77
|
+
globalRulesDir?: string;
|
|
78
|
+
/** The projection source, read only to attribute manual-only absences. */
|
|
79
|
+
sourceDir?: string;
|
|
80
|
+
/** Transcript store for the activation and compliance axes. */
|
|
81
|
+
store?: string;
|
|
82
|
+
/** Most-recent sessions to scan. */
|
|
83
|
+
limit?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface FunnelReport {
|
|
87
|
+
/** DELIVERY — carrier census (conformance_scan) + divergence (report_carrier_divergence). */
|
|
88
|
+
delivered: DeliveredPayload;
|
|
89
|
+
divergence: CarrierDivergence;
|
|
90
|
+
/** ACTIVATION — skill census + usage (report_skill_activation). */
|
|
91
|
+
skills: SkillCensus;
|
|
92
|
+
usage: UsageReport;
|
|
93
|
+
/** `false` is the no-data branch for both transcript-fed axes. */
|
|
94
|
+
storePresent: boolean;
|
|
95
|
+
/** COMPLIANCE — SK-2 loaded-but-violated (report_skill_obligation_violations). */
|
|
96
|
+
sk2: Sk2Report;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildFunnel(opts: FunnelOptions = {}): FunnelReport {
|
|
100
|
+
const repoRoot = opts.repoRoot ?? REPO_ROOT;
|
|
101
|
+
const projectRulesDir = opts.projectRulesDir ?? path.join(repoRoot, PROJECT_RULES);
|
|
102
|
+
const globalRulesDir = opts.globalRulesDir ?? path.join(os.homedir(), GLOBAL_RULES);
|
|
103
|
+
const sourceDir = opts.sourceDir ?? path.join(repoRoot, PROJECTION_SOURCE);
|
|
104
|
+
const store = opts.store ?? defaultStore(repoRoot);
|
|
105
|
+
const limit = opts.limit ?? 30;
|
|
106
|
+
return {
|
|
107
|
+
delivered: measureDelivered(projectRulesDir, globalRulesDir),
|
|
108
|
+
divergence: compareCarriers(projectRulesDir, globalRulesDir, sourceDir),
|
|
109
|
+
skills: censusSkills(path.join(repoRoot, SKILLS_ROOT)),
|
|
110
|
+
usage: measureUsage(store, limit),
|
|
111
|
+
storePresent: fs.existsSync(store),
|
|
112
|
+
sk2: scanObligationStore(repoRoot, store, limit),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _pct(n: number, d: number): string {
|
|
117
|
+
return d === 0 ? 'n/a' : `${((100 * n) / d).toFixed(1)}%`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function _carrierLine(label: string, c: { present: boolean; files: number; tokens: number }): string {
|
|
121
|
+
return c.present
|
|
122
|
+
? ` ${label} ${String(c.files).padStart(4)} rules ${String(c.tokens).padStart(7)} tok`
|
|
123
|
+
: ` ${label} ABSENT — not a measured zero, this carrier does not exist here`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function render(r: FunnelReport): string {
|
|
127
|
+
const lines: string[] = [];
|
|
128
|
+
lines.push('conformance funnel — delivery → activation → compliance (report, not a gate)');
|
|
129
|
+
lines.push('');
|
|
130
|
+
|
|
131
|
+
// ── DELIVERY ───────────────────────────────────────────────────────────
|
|
132
|
+
lines.push('DELIVERY — rule text the carriers put in front of the model');
|
|
133
|
+
lines.push(_carrierLine('project', r.delivered.project));
|
|
134
|
+
lines.push(_carrierLine('global ', r.delivered.global));
|
|
135
|
+
lines.push(` union ${String(r.delivered.union_tokens).padStart(7)} tok`);
|
|
136
|
+
lines.push(' Carriers as they stand NOW — a session\'s own payload is NOT recoverable:');
|
|
137
|
+
lines.push(' the transcript records no system or tools field (conformance_scan).');
|
|
138
|
+
const d = r.divergence;
|
|
139
|
+
if (!d.globalPresent) {
|
|
140
|
+
lines.push(' Divergence: the global carrier is absent — this machine loads the project');
|
|
141
|
+
lines.push(' projection only, so there is no second copy to diverge from. A real');
|
|
142
|
+
lines.push(' answer, not a skipped check.');
|
|
143
|
+
} else if (!d.projectPresent) {
|
|
144
|
+
lines.push(' Divergence: the project rule tree does not exist in this checkout — it is');
|
|
145
|
+
lines.push(' generated and gitignored. Run `task generate-tools`, then re-run. Not');
|
|
146
|
+
lines.push(' substituted with dist/, which would answer a different question.');
|
|
147
|
+
} else {
|
|
148
|
+
lines.push(
|
|
149
|
+
` Divergence: ${d.shared} shared · ${d.bodyDiff.length} body-diff · ` +
|
|
150
|
+
`${d.provenanceOnly.length} provenance-only · ${d.projectOnly.length} project-only · ` +
|
|
151
|
+
`${d.globalOnly.length} global-only` +
|
|
152
|
+
(d.manualOnlyGlobal.length > 0 ? ` (${d.manualOnlyGlobal.length} by design, ADR-004 manual)` : ''),
|
|
153
|
+
);
|
|
154
|
+
if (d.bodyDiff.length > 0) {
|
|
155
|
+
lines.push(` BODY DIVERGENCE — both copies reach the model: ${d.bodyDiff.join(', ')}`);
|
|
156
|
+
lines.push(' Precedence: the project projection is generated from src/ at this commit');
|
|
157
|
+
lines.push(' and wins; the global install is a release snapshot.');
|
|
158
|
+
}
|
|
159
|
+
if (d.unreadable.length > 0) {
|
|
160
|
+
lines.push(` Unreadable on one side (${d.unreadable.length}) — a broken install, not a disagreement.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
lines.push('');
|
|
164
|
+
|
|
165
|
+
// ── ACTIVATION ─────────────────────────────────────────────────────────
|
|
166
|
+
lines.push('ACTIVATION — are the delivered skills invoked?');
|
|
167
|
+
lines.push(` skills shipped ${r.skills.total}`);
|
|
168
|
+
lines.push(
|
|
169
|
+
` with a machine-matchable trigger key ${r.skills.withTriggerKey.length} ` +
|
|
170
|
+
`(${_pct(r.skills.withTriggerKey.length, r.skills.total)})`,
|
|
171
|
+
);
|
|
172
|
+
if (!r.storePresent) {
|
|
173
|
+
lines.push(` NO DATA: no transcript store at ${r.usage.store} — usage is not`);
|
|
174
|
+
lines.push(' measurable here. An absent store is an answer, not a zero.');
|
|
175
|
+
} else {
|
|
176
|
+
const distinct = Object.keys(r.usage.bySkill).length;
|
|
177
|
+
lines.push(
|
|
178
|
+
` sessions=${r.usage.sessions} assistant turns=${r.usage.assistantTurns} ` +
|
|
179
|
+
`Skill invocations=${r.usage.invocations} distinct skills=${distinct} of ${r.skills.total} ` +
|
|
180
|
+
`(${_pct(distinct, r.skills.total)})`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (r.skills.withTriggerKey.length === 0) {
|
|
184
|
+
lines.push(' Honest scope: activation is NOT measurable as a rate — no skill declares');
|
|
185
|
+
lines.push(' a machine-matchable trigger, and matching description prose is the class');
|
|
186
|
+
lines.push(' this suite excludes (report_skill_activation).');
|
|
187
|
+
}
|
|
188
|
+
lines.push(' NOT MEASURED: whether each skill reached the model WITH its description —');
|
|
189
|
+
lines.push(' the host\'s injected catalogue is not persisted in the transcript, so the');
|
|
190
|
+
lines.push(' bare-name (truncation) hypothesis stays a single-session observation.');
|
|
191
|
+
lines.push('');
|
|
192
|
+
|
|
193
|
+
// ── COMPLIANCE ─────────────────────────────────────────────────────────
|
|
194
|
+
const c = r.sk2.census;
|
|
195
|
+
lines.push('COMPLIANCE — SK-2 loaded-but-violated (coverage is the headline)');
|
|
196
|
+
lines.push(` skills with a deterministic obligation ${c.skills.length}`);
|
|
197
|
+
lines.push(
|
|
198
|
+
` testable without judgement ${c.forbidden.length} artefact(s) over ` +
|
|
199
|
+
`${c.totalLines} obligation line(s)`,
|
|
200
|
+
);
|
|
201
|
+
lines.push(' The rest are deterministic in WORDING and need a reading to test — the');
|
|
202
|
+
lines.push(' FC-8 class this suite excludes, reported as uncovered, not approximated');
|
|
203
|
+
lines.push(' (report_skill_obligation_violations).');
|
|
204
|
+
if (!r.storePresent) {
|
|
205
|
+
lines.push(' NO DATA: no transcript store — loaded-but-violated cannot be observed');
|
|
206
|
+
lines.push(' here. The coverage census above is static and still holds.');
|
|
207
|
+
} else {
|
|
208
|
+
lines.push(
|
|
209
|
+
` sessions scanned=${r.sk2.sessions} with a skill in context=${r.sk2.sessionsWithASkill} ` +
|
|
210
|
+
`flags=${r.sk2.flags.length}`,
|
|
211
|
+
);
|
|
212
|
+
for (const f of r.sk2.flags) {
|
|
213
|
+
lines.push(` ${f.session} [${f.skill}] ${f.artefact} via ${f.tool}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
lines.push(' PRECISION: every flag is hand-read before any number here is cited — the');
|
|
217
|
+
lines.push(' detector cannot state its false-positive rate, so it ships detection-only.');
|
|
218
|
+
lines.push('');
|
|
219
|
+
|
|
220
|
+
lines.push('This is a REPORT. It gates on nothing and must never be wired into a CI');
|
|
221
|
+
lines.push('workflow without a measured false-positive rate first (locked policy).');
|
|
222
|
+
return lines.join('\n');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function main(argv?: readonly string[]): number {
|
|
226
|
+
const args = argv ?? process.argv.slice(2);
|
|
227
|
+
const opts: FunnelOptions = {};
|
|
228
|
+
// A value must not itself be a flag — the same hole the sibling reports
|
|
229
|
+
// close on both the flag-name and flag-value halves.
|
|
230
|
+
const value = (i: number): string | null => {
|
|
231
|
+
const v = args[i + 1];
|
|
232
|
+
return v === undefined || v.startsWith('-') ? null : v;
|
|
233
|
+
};
|
|
234
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
235
|
+
const a = args[i];
|
|
236
|
+
if (a === '--store' || a === '--project' || a === '--global' || a === '--source') {
|
|
237
|
+
const v = value(i);
|
|
238
|
+
if (v === null) {
|
|
239
|
+
process.stderr.write(`report_conformance_funnel: ${a} needs a value\n`);
|
|
240
|
+
return 1;
|
|
241
|
+
}
|
|
242
|
+
const resolved = path.resolve(v);
|
|
243
|
+
if (a === '--store') opts.store = resolved;
|
|
244
|
+
else if (a === '--project') opts.projectRulesDir = resolved;
|
|
245
|
+
else if (a === '--global') opts.globalRulesDir = resolved;
|
|
246
|
+
else opts.sourceDir = resolved;
|
|
247
|
+
i += 1;
|
|
248
|
+
} else if (a === '--limit') {
|
|
249
|
+
const v = value(i);
|
|
250
|
+
const n = v === null ? Number.NaN : Number.parseInt(v, 10);
|
|
251
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
252
|
+
process.stderr.write('report_conformance_funnel: --limit must be a positive integer\n');
|
|
253
|
+
return 1;
|
|
254
|
+
}
|
|
255
|
+
opts.limit = n;
|
|
256
|
+
i += 1;
|
|
257
|
+
} else if (a === '--help' || a === '-h') {
|
|
258
|
+
process.stdout.write(
|
|
259
|
+
'usage: report_conformance_funnel [--store PATH] [--limit N]\n' +
|
|
260
|
+
' [--project DIR] [--global DIR] [--source DIR]\n',
|
|
261
|
+
);
|
|
262
|
+
return 0;
|
|
263
|
+
} else if (a !== undefined && a.startsWith('--') && a !== '--quiet') {
|
|
264
|
+
process.stderr.write(`report_conformance_funnel: unknown flag ${a}\n`);
|
|
265
|
+
return 1;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
process.stdout.write(`${render(buildFunnel(opts))}\n`);
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function _isCliEntry(): boolean {
|
|
273
|
+
if (process.argv[1] === undefined) return false;
|
|
274
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
275
|
+
try {
|
|
276
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
277
|
+
} catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (_isCliEntry()) {
|
|
283
|
+
process.exit(main());
|
|
284
|
+
}
|
|
@@ -32,6 +32,7 @@ import * as fs from 'node:fs';
|
|
|
32
32
|
import * as path from 'node:path';
|
|
33
33
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
34
34
|
|
|
35
|
+
// ledger-exempt: advisory report that gates on nothing BY DESIGN (exit 0 always, no threshold, no baseline entry, no gate-coverage row — see the header); there is no pass/fail outcome per target to account, and acquiring one would be the promotion this report must not undergo.
|
|
35
36
|
const _HERE = fileURLToPath(import.meta.url);
|
|
36
37
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
37
38
|
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* processes are the two freshness checks, both themselves read-only.
|
|
36
36
|
*/
|
|
37
37
|
import fs from "node:fs";
|
|
38
|
+
import os from "node:os";
|
|
38
39
|
import path from "node:path";
|
|
39
40
|
import { spawnSync } from "node:child_process";
|
|
40
41
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -62,6 +63,11 @@ import {
|
|
|
62
63
|
type ActivationInputs,
|
|
63
64
|
} from "./_lib/auto_dispatch.js";
|
|
64
65
|
import { TIER_ORDER, readCooldowns } from "./_lib/tier_budget_routing.js";
|
|
66
|
+
import {
|
|
67
|
+
instructionsLoadedRecord,
|
|
68
|
+
measureStandingDelivery,
|
|
69
|
+
type StandingDeliveryMeasure,
|
|
70
|
+
} from "./check_standing_rule_delivery.js";
|
|
65
71
|
|
|
66
72
|
const REPO_ROOT = path.resolve(
|
|
67
73
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -197,9 +203,93 @@ export interface DoctorReport {
|
|
|
197
203
|
gates: GateReport[];
|
|
198
204
|
freshness: FreshnessReport;
|
|
199
205
|
orchestration: OrchestrationReport;
|
|
206
|
+
standing_delivery: StandingDeliveryMeasure | null;
|
|
200
207
|
bridge_table: string;
|
|
201
208
|
}
|
|
202
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Standing-rule-delivery savings for the doctor surface — the measurement is
|
|
212
|
+
* `check_standing_rule_delivery`'s, reused rather than re-derived, so the gate
|
|
213
|
+
* and the doctor can never report different numbers for the same layers.
|
|
214
|
+
* Read-only, like everything else here. `null` = no rule layer installed.
|
|
215
|
+
*/
|
|
216
|
+
export function collect_standing_delivery(
|
|
217
|
+
workspace_root: string,
|
|
218
|
+
global_rules_dir: string = path.join(os.homedir(), ".claude", "rules"),
|
|
219
|
+
): StandingDeliveryMeasure | null {
|
|
220
|
+
return measureStandingDelivery(
|
|
221
|
+
global_rules_dir,
|
|
222
|
+
path.join(workspace_root, ".claude", "rules"),
|
|
223
|
+
instructionsLoadedRecord(workspace_root),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Render the delivery line in one of its states — scoped-clean (one layer,
|
|
229
|
+
* nothing duplicated) or both-layers-active (red, pointing at
|
|
230
|
+
* `install --layer`). Every number printed is one the measurement produced:
|
|
231
|
+
* the received total, the measured overlap, and the received-minus-overlap
|
|
232
|
+
* total suppression would leave. No baseline is invented for the layer that
|
|
233
|
+
* is not installed, so no percentage is derived from one either. The
|
|
234
|
+
* filesystem-vs-host-confirmed caveat is the gate's own honest-scope
|
|
235
|
+
* sentence, carried here so the doctor never claims the host confirmed what
|
|
236
|
+
* only the filesystem projects.
|
|
237
|
+
*/
|
|
238
|
+
export function render_standing_delivery(m: StandingDeliveryMeasure | null): string[] {
|
|
239
|
+
if (m === null) {
|
|
240
|
+
return [
|
|
241
|
+
"Standing delivery: no rule layer installed (neither ~/.claude/rules nor .claude/rules) — nothing delivered, nothing to save.",
|
|
242
|
+
];
|
|
243
|
+
}
|
|
244
|
+
const lines: string[] = [];
|
|
245
|
+
if (m.layers.length === 2 && m.overlap_rules > 0) {
|
|
246
|
+
const scoped = m.received_tokens - m.overlap_tokens;
|
|
247
|
+
lines.push(
|
|
248
|
+
`⚠️ Standing delivery: both rule layers active — ${m.overlap_rules} rule(s) delivered twice ` +
|
|
249
|
+
`(~${m.overlap_tokens} tok). This session's host receives ${m.received_tokens} rule tokens; ` +
|
|
250
|
+
`scoped delivery would be ~${scoped} — run \`agent-config install --layer=<global|project>\`.`,
|
|
251
|
+
);
|
|
252
|
+
if (m.divergent_rules > 0) {
|
|
253
|
+
// Suppressing a layer whose copies differ drops whichever obligations only
|
|
254
|
+
// the suppressed copy carried — the same guard `decideLayerAction` applies.
|
|
255
|
+
lines.push(
|
|
256
|
+
` ${m.divergent_rules} of the shared rule(s) differ in body — refresh before suppressing, ` +
|
|
257
|
+
"or obligations only the suppressed copy carries are lost.",
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
} else if (m.layers.length === 2) {
|
|
261
|
+
lines.push(
|
|
262
|
+
`Standing delivery: this session's host receives ${m.received_tokens} rule tokens across two ` +
|
|
263
|
+
"non-overlapping layers — nothing delivered twice, Δ0 to save.",
|
|
264
|
+
);
|
|
265
|
+
} else {
|
|
266
|
+
// One layer = the scoped state, and the honest report of it is the received
|
|
267
|
+
// total plus the measured fact that nothing is delivered twice.
|
|
268
|
+
//
|
|
269
|
+
// This line used to invent an "unscoped both-layers baseline" of
|
|
270
|
+
// `received × 2` and report the arithmetic consequence — which is always
|
|
271
|
+
// exactly 50%, for every non-zero input, i.e. a tautology dressed as a
|
|
272
|
+
// measurement. It also assumed the absent second layer would carry an
|
|
273
|
+
// identical corpus; the branch directly above exists because two installed
|
|
274
|
+
// layers can be wholly disjoint, so even with both present the doubling
|
|
275
|
+
// does not follow. A saving needs a second layer to measure against, and
|
|
276
|
+
// here there is none.
|
|
277
|
+
const layer = m.layers[0]?.label ?? "one";
|
|
278
|
+
lines.push(
|
|
279
|
+
`Standing delivery: this session's host receives ${m.received_tokens} rule tokens (scoped, ` +
|
|
280
|
+
`${layer} layer only); 0 tokens delivered twice — one layer is installed, so there is no ` +
|
|
281
|
+
"second layer to measure a saving against.",
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
if (m.input === "filesystem") {
|
|
285
|
+
lines.push(
|
|
286
|
+
" note: filesystem projection, not host-confirmed — no InstructionsLoaded record; a rule " +
|
|
287
|
+
"projected but never loaded, or loaded from an unprojected layer, is invisible to this input.",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return lines;
|
|
291
|
+
}
|
|
292
|
+
|
|
203
293
|
/**
|
|
204
294
|
* Orchestration-routing state: settings gates, host capability, budget
|
|
205
295
|
* inputs, and (optionally) a dry-run classification for a sample prompt.
|
|
@@ -444,6 +534,8 @@ export function collect_report(options: {
|
|
|
444
534
|
workspace_root: string;
|
|
445
535
|
no_freshness: boolean;
|
|
446
536
|
classify?: string | null;
|
|
537
|
+
/** Test seam: fixture global rules dir. Defaults to the real `~/.claude/rules`. */
|
|
538
|
+
global_rules_dir?: string;
|
|
447
539
|
}): DoctorReport {
|
|
448
540
|
const manifest = _load_yaml(MANIFEST_PATH);
|
|
449
541
|
const chain = resolve_chain(manifest, options.platform);
|
|
@@ -477,6 +569,9 @@ export function collect_report(options: {
|
|
|
477
569
|
options.platform,
|
|
478
570
|
options.platform_assumed ?? true,
|
|
479
571
|
),
|
|
572
|
+
standing_delivery: options.global_rules_dir === undefined
|
|
573
|
+
? collect_standing_delivery(options.workspace_root)
|
|
574
|
+
: collect_standing_delivery(options.workspace_root, options.global_rules_dir),
|
|
480
575
|
bridge_table,
|
|
481
576
|
};
|
|
482
577
|
}
|
|
@@ -499,6 +594,8 @@ function _render(report: DoctorReport): string {
|
|
|
499
594
|
` · dispatch bundle=${report.freshness.dispatch_bundle}`,
|
|
500
595
|
);
|
|
501
596
|
lines.push("");
|
|
597
|
+
lines.push(...render_standing_delivery(report.standing_delivery));
|
|
598
|
+
lines.push("");
|
|
502
599
|
const o = report.orchestration;
|
|
503
600
|
lines.push(
|
|
504
601
|
`Orchestration: halted=${o.halted} · downshift=${o.downshift} · ` +
|
|
@@ -34,12 +34,15 @@ import {
|
|
|
34
34
|
chooseEgress,
|
|
35
35
|
type DefectRecord,
|
|
36
36
|
type EgressCapability,
|
|
37
|
+
type EgressChoice,
|
|
37
38
|
type EgressRoute,
|
|
38
39
|
egressBlockedReason,
|
|
40
|
+
patchDeniedReason,
|
|
39
41
|
type PushVia,
|
|
40
42
|
renderReport,
|
|
41
43
|
sanitizeEvidence,
|
|
42
44
|
} from './_lib/self_repair.js';
|
|
45
|
+
import { buildSettingsClassIndex, parseSettingsClassRows } from '../shared/settingsClasses.js';
|
|
43
46
|
import {
|
|
44
47
|
attachReleaseErrors,
|
|
45
48
|
listRecords,
|
|
@@ -177,21 +180,61 @@ export interface ReleasePlan {
|
|
|
177
180
|
route: EgressRoute;
|
|
178
181
|
pushVia: PushVia | null;
|
|
179
182
|
blocked: string | null;
|
|
183
|
+
/**
|
|
184
|
+
* Non-null when the record targets a may-not-modify surface (kernel rules,
|
|
185
|
+
* safety floors, settings class C, CI enforcement, self-repair's own
|
|
186
|
+
* policy/code): the patch/PR path is refused with this reason and the
|
|
187
|
+
* route degrades to report-only. Detection and the issue path stay open.
|
|
188
|
+
*/
|
|
189
|
+
patch_denied: string | null;
|
|
180
190
|
title: string;
|
|
181
191
|
body: string;
|
|
182
192
|
}
|
|
183
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Class-C settings keys from the contract in the agent-config checkout, for
|
|
196
|
+
* the deny-list's dynamic key match. `null` when there is no checkout or the
|
|
197
|
+
* contract is unreadable — the deny-list's static path matches still apply.
|
|
198
|
+
*/
|
|
199
|
+
export function readClassCKeys(checkout: string | null): ReadonlySet<string> | null {
|
|
200
|
+
if (checkout === null) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const md = fs.readFileSync(
|
|
205
|
+
path.join(checkout, 'docs', 'contracts', 'settings-classes.md'),
|
|
206
|
+
'utf-8',
|
|
207
|
+
);
|
|
208
|
+
const index = buildSettingsClassIndex(parseSettingsClassRows(md));
|
|
209
|
+
return new Set([...index].filter(([, cls]) => cls === 'C').map(([key]) => key));
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
184
215
|
/** Pure: everything `release` decides before it touches the network. */
|
|
185
|
-
export function planRelease(
|
|
216
|
+
export function planRelease(
|
|
217
|
+
record: DefectRecord,
|
|
218
|
+
probe: Probe,
|
|
219
|
+
repoRoot: string,
|
|
220
|
+
opts?: { classCKeys?: ReadonlySet<string> | null },
|
|
221
|
+
): ReleasePlan {
|
|
186
222
|
const blocked = egressBlockedReason(record, repoRoot);
|
|
187
|
-
const
|
|
223
|
+
const patch_denied = blocked === null ? patchDeniedReason(record, opts) : null;
|
|
224
|
+
let choice: EgressChoice =
|
|
188
225
|
blocked !== null
|
|
189
226
|
? { route: 'local-only' as const, pushVia: null }
|
|
190
227
|
: chooseEgress(capabilityOf(probe));
|
|
228
|
+
if (patch_denied !== null && choice.route === 'pull-request') {
|
|
229
|
+
// chooseEgress only returns pull-request when gh is authenticated, so
|
|
230
|
+
// report-only degrades to an issue, never to a dropped record.
|
|
231
|
+
choice = { route: 'issue', pushVia: null };
|
|
232
|
+
}
|
|
191
233
|
return {
|
|
192
234
|
route: choice.route,
|
|
193
235
|
pushVia: choice.pushVia,
|
|
194
236
|
blocked,
|
|
237
|
+
patch_denied,
|
|
195
238
|
title: titleFor(record),
|
|
196
239
|
body: renderReport(record, choice.route),
|
|
197
240
|
};
|
|
@@ -391,13 +434,18 @@ function releaseCmd(root: string, argv: string[]): number {
|
|
|
391
434
|
const repoArg = argv.indexOf('--repo');
|
|
392
435
|
const repo = repoArg >= 0 ? (argv[repoArg + 1] ?? UPSTREAM_REPO) : UPSTREAM_REPO;
|
|
393
436
|
const probe = probeMachine(process.env, process.cwd());
|
|
394
|
-
const plan = planRelease(record, probe, root
|
|
437
|
+
const plan = planRelease(record, probe, root, {
|
|
438
|
+
classCKeys: readClassCKeys(probe.agentConfigCheckout),
|
|
439
|
+
});
|
|
395
440
|
|
|
396
441
|
const via = plan.pushVia !== null ? ` via=${plan.pushVia}` : '';
|
|
397
442
|
process.stdout.write(`self-repair:release ${fp} -> route=${plan.route}${via}\n`);
|
|
398
443
|
if (plan.blocked !== null) {
|
|
399
444
|
process.stdout.write(` WARN ${plan.blocked}\n`);
|
|
400
445
|
}
|
|
446
|
+
if (plan.patch_denied !== null) {
|
|
447
|
+
process.stdout.write(` DENY ${plan.patch_denied}\n`);
|
|
448
|
+
}
|
|
401
449
|
if (dryRun) {
|
|
402
450
|
process.stdout.write(`\n--- ${plan.title} ---\n${plan.body}\n`);
|
|
403
451
|
return EXIT_OK;
|