@event4u/agent-config 9.27.0 → 9.29.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/AGENTS.md +13 -12
- package/CHANGELOG.md +165 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/analyze/conformance.md +24 -0
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +13 -12
- package/dist/agent-src/commands/team/review.md +9 -8
- package/dist/agent-src/commands/team/status.md +9 -8
- package/dist/agent-src/commands/team.md +29 -19
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +28 -3
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
- package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
- package/dist/agent-src/rules/context-hygiene.md +3 -1
- package/dist/agent-src/rules/council-availability.md +93 -0
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/delegation-policy.md +38 -10
- package/dist/agent-src/rules/design-fidelity.md +3 -0
- package/dist/agent-src/rules/evaluator-independence.md +2 -1
- package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
- package/dist/agent-src/rules/finance-safety-floor.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +1 -1
- package/dist/agent-src/rules/history-discipline.md +3 -1
- package/dist/agent-src/rules/legal-safety-floor.md +1 -1
- package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
- package/dist/agent-src/rules/onboarding-gate.md +3 -1
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
- package/dist/agent-src/rules/role-mode-adherence.md +1 -1
- package/dist/agent-src/rules/scale-discipline.md +3 -1
- package/dist/agent-src/rules/self-repair-loop.md +103 -0
- package/dist/agent-src/rules/session-canary.md +2 -1
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
- package/dist/agent-src/rules/token-budget-discipline.md +4 -2
- package/dist/agent-src/rules/tool-safety.md +5 -1
- package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
- package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
- package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
- package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
- package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
- package/dist/agent-src/skills/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
- package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +8 -12
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +23 -0
- package/dist/cli/registry.js +7 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-66OVWGB5.js +270 -0
- package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-LCFJIQ4J.js} +89 -7
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
- package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-MP3JT3IU.js} +3 -3
- package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-NZ5N2XQL.js} +467 -305
- package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-OJM3MBJM.js} +1 -1
- package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-RCIA67XN.js} +117 -96
- package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +134 -0
- package/dist/cli-delegate/cmd_conformance.js +10 -47
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +10 -6
- package/dist/cli-delegate/cmd_explain.js +80 -330
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_mcp_available.js +433 -0
- package/dist/cli-delegate/cmd_migrate.js +3 -3
- package/dist/cli-delegate/cmd_packs_active.js +153 -0
- package/dist/cli-delegate/cmd_preflight.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_route_audit.js +8 -8
- package/dist/cli-delegate/cmd_route_explain.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +266 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +8 -2968
- 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 +4 -4
- package/dist/cli-delegate/cmd_upgrade.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +164 -55
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +44 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +16 -6
- package/dist/hooks/dispatch.js +9341 -6131
- package/dist/install/install.mjs +739 -524
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +777 -281
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +134 -15
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +147 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +6 -7
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsCarveOut.js +0 -9
- package/dist/shared/settingsCarveOut.js.map +1 -1
- package/dist/shared/settingsClasses.js +11 -9
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
- package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +68 -17
- package/docs/guidelines/agent-infra/memory-access.md +14 -3
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +8 -12
- package/src/config/agent-settings.template.yml +52 -66
- package/src/config/budgets.yml +29 -0
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +26 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_cli/cmd_brand_status.ts +174 -0
- package/src/scripts/_cli/cmd_conformance.ts +14 -42
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
- package/src/scripts/_cli/cmd_packs_active.ts +225 -0
- package/src/scripts/_cli/cmd_settings_get.ts +324 -0
- package/src/scripts/_dispatch.bash +57 -0
- package/src/scripts/_lib/agent_settings.ts +140 -16
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
- package/src/scripts/_lib/auto_dispatch.ts +23 -18
- package/src/scripts/_lib/capsule_trigger.ts +124 -0
- package/src/scripts/_lib/carrier_divergence.ts +135 -0
- package/src/scripts/_lib/host_capability.ts +144 -4
- package/src/scripts/_lib/judgment_ladder.ts +382 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +37 -0
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/prompt_shape.ts +88 -0
- package/src/scripts/_lib/review_skipped_record.ts +102 -0
- package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
- package/src/scripts/_lib/self_repair.ts +489 -0
- package/src/scripts/_lib/self_repair_store.ts +128 -0
- package/src/scripts/_lib/subagent_capsule.ts +182 -0
- package/src/scripts/_lib/subagent_response.ts +19 -0
- package/src/scripts/_lib/subagent_spawn.ts +13 -0
- package/src/scripts/_lib/subagent_steering.ts +9 -4
- package/src/scripts/_lib/tier_budget_routing.ts +6 -1
- package/src/scripts/_lib/transcript_entry.ts +80 -0
- package/src/scripts/_lib/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +76 -54
- package/src/scripts/ai_council/config.ts +94 -6
- package/src/scripts/ai_council/handoff.ts +113 -0
- package/src/scripts/ai_council/modes.ts +19 -4
- package/src/scripts/ai_council/orchestrator.ts +105 -0
- package/src/scripts/ai_council/quorum.ts +84 -0
- package/src/scripts/ai_council/session.ts +44 -0
- package/src/scripts/ai_council/transport_resolver.ts +127 -1
- package/src/scripts/ai_team/availability.ts +110 -0
- package/src/scripts/ai_team/config.ts +34 -14
- package/src/scripts/ai_team/team_dispatch.ts +71 -29
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/check_branch_freshness.ts +255 -0
- package/src/scripts/check_council_config_location.ts +125 -1
- package/src/scripts/check_detector_corpus.ts +202 -0
- package/src/scripts/check_hook_bundle_freshness.ts +4 -1
- package/src/scripts/check_standing_rule_delivery.ts +314 -0
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/config/profiles.ts +13 -3
- package/src/scripts/conformance_scan.ts +272 -24
- package/src/scripts/council_availability_hook.ts +188 -0
- package/src/scripts/council_cli.ts +527 -32
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/hook_manifest.yaml +107 -26
- package/src/scripts/hooks/block_no_verify.ts +39 -2
- package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
- package/src/scripts/hooks/concern_registry.ts +10 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +484 -0
- package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
- package/src/scripts/install.ts +151 -0
- package/src/scripts/language_mirror_hook.ts +30 -37
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/measure_scope_dedup.ts +19 -13
- package/src/scripts/onboarding_gate_hook.ts +40 -1
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/preamble_byte_census.ts +9 -20
- package/src/scripts/rebuild_hook_bundle.ts +177 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/report_carrier_divergence.ts +364 -0
- package/src/scripts/report_skill_activation.ts +257 -0
- package/src/scripts/report_skill_obligation_violations.ts +579 -0
- package/src/scripts/routing_doctor.ts +95 -39
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +470 -0
- package/src/scripts/self_repair_hook.ts +241 -0
- package/src/scripts/team_events_spike.ts +271 -0
- package/src/scripts/team_review_gate_hook.ts +31 -9
- package/src/server/schemas/settings.ts +10 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- package/src/shared/settingsClasses.ts +11 -9
package/src/scripts/condense.ts
CHANGED
|
@@ -1174,11 +1174,27 @@ export function generate_rule_symlinks(): number {
|
|
|
1174
1174
|
const skip = new Set(rules.filter((r) => !emit_here.has(r)));
|
|
1175
1175
|
skipped_per_tool[tool_dir] = skip;
|
|
1176
1176
|
_mkdirp(target_dir);
|
|
1177
|
-
// Clean stale
|
|
1177
|
+
// Clean stale entries — a rule that became de-duplicable this run is
|
|
1178
1178
|
// stale at project scope too, so it has to go with the rest.
|
|
1179
|
+
//
|
|
1180
|
+
// Symlink-only was correct while every tool dir was a symlink tree. Two
|
|
1181
|
+
// paths now write REAL files (`thin` mode, and the Claude emitter
|
|
1182
|
+
// below), and a stale real file is invisible to an `_isSymlink` test —
|
|
1183
|
+
// so a renamed or newly de-duplicable rule would leave its body behind,
|
|
1184
|
+
// loaded unconditionally, forever.
|
|
1185
|
+
//
|
|
1186
|
+
// Ownership test for a real file: its basename is an agent-config rule
|
|
1187
|
+
// in the projection source. A consumer's own hand-written rule is only
|
|
1188
|
+
// touched if it collides with one of ours by name, in which case it was
|
|
1189
|
+
// already being overwritten.
|
|
1179
1190
|
for (const item of _iterdirSorted(target_dir)) {
|
|
1180
1191
|
const name = path.basename(item);
|
|
1181
|
-
if (
|
|
1192
|
+
if (name === 'README.md') {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const is_ours =
|
|
1196
|
+
_isSymlink(item) || _isFile(path.join(MODULE_STATE.RULES_SOURCE, name));
|
|
1197
|
+
if (is_ours && (!rules.includes(name) || skip.has(name))) {
|
|
1182
1198
|
fs.unlinkSync(item);
|
|
1183
1199
|
}
|
|
1184
1200
|
}
|
|
@@ -1192,6 +1208,11 @@ export function generate_rule_symlinks(): number {
|
|
|
1192
1208
|
}
|
|
1193
1209
|
if (thin_files !== null) {
|
|
1194
1210
|
_writeText(link, thin_files[rule] as string);
|
|
1211
|
+
} else if (tool_dir === '.claude/rules') {
|
|
1212
|
+
// Host-native activation (P3.1): emit the rule with the host's
|
|
1213
|
+
// own `paths:` key instead of symlinking agent-config
|
|
1214
|
+
// frontmatter this host does not read.
|
|
1215
|
+
_emit_claude_rule(path.join(MODULE_STATE.RULES_SOURCE, rule), link);
|
|
1195
1216
|
} else {
|
|
1196
1217
|
fs.symlinkSync(path.join(rel_prefix, rule), link);
|
|
1197
1218
|
}
|
|
@@ -1355,6 +1376,145 @@ export function _emit_windsurf_rule(source: string, target: string): void {
|
|
|
1355
1376
|
_writeText(target, lines.join('\n'));
|
|
1356
1377
|
}
|
|
1357
1378
|
|
|
1379
|
+
/**
|
|
1380
|
+
* A brace group the host expands, versus an agent-config placeholder that only
|
|
1381
|
+
* LOOKS like one.
|
|
1382
|
+
*
|
|
1383
|
+
* `path_prefix: "{module_root}/"` in `roadmap-ci-steps-policy` is resolved by
|
|
1384
|
+
* this suite from `modules.root_paths`. Emitted verbatim into a host `paths:`
|
|
1385
|
+
* list it is a brace group with no comma, which the host expands to the literal
|
|
1386
|
+
* string `{module_root}` — a pattern that matches no file. The rule would then
|
|
1387
|
+
* reach the model **never** instead of on-demand, and nothing would say so: the
|
|
1388
|
+
* probed contract records this exact degradation as silent
|
|
1389
|
+
* (`claude-code-rules-dir-contract.md` § constraints).
|
|
1390
|
+
*
|
|
1391
|
+
* Dropping the pattern is fail-safe in the one direction that matters. A rule
|
|
1392
|
+
* left with no pattern at all carries no `paths:` key and loads
|
|
1393
|
+
* unconditionally — the status quo, not a regression. A rule carrying a
|
|
1394
|
+
* no-op pattern is invisible.
|
|
1395
|
+
*
|
|
1396
|
+
* The discriminator is the comma, not a placeholder allowlist: `{ts,tsx}` is a
|
|
1397
|
+
* real alternation, `{module_root}` cannot be one.
|
|
1398
|
+
*/
|
|
1399
|
+
export function _is_unresolved_placeholder(pattern: string): boolean {
|
|
1400
|
+
for (const m of pattern.matchAll(/\{([^}]*)\}/g)) {
|
|
1401
|
+
if (!(m[1] as string).includes(',')) {
|
|
1402
|
+
return true;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* `[` opens a bracket expression for the host. An unparseable one makes that
|
|
1410
|
+
* pattern match nothing while the rule's other patterns keep working — so a
|
|
1411
|
+
* literal `[` in a trigger is escaped rather than trusted.
|
|
1412
|
+
*/
|
|
1413
|
+
export function _escape_claude_bracket(pattern: string): string {
|
|
1414
|
+
return pattern.replace(/\[/g, '\\[');
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Expanded-pattern cost of one glob: the product of its brace alternations.
|
|
1419
|
+
* The host budgets a rule's whole `paths:` list at 1,000 expanded patterns and
|
|
1420
|
+
* 4 MiB, and a list over budget is used UNEXPANDED — braces then match
|
|
1421
|
+
* literally, i.e. nothing. Counting keeps the emitter under the cliff instead
|
|
1422
|
+
* of discovering it as a silent no-op.
|
|
1423
|
+
*/
|
|
1424
|
+
export function _expanded_pattern_count(pattern: string): number {
|
|
1425
|
+
let n = 1;
|
|
1426
|
+
for (const m of pattern.matchAll(/\{([^}]*)\}/g)) {
|
|
1427
|
+
n *= (m[1] as string).split(',').length;
|
|
1428
|
+
}
|
|
1429
|
+
return n;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/** Host-documented budget for one rule's whole `paths:` list. */
|
|
1433
|
+
export const CLAUDE_PATHS_PATTERN_BUDGET = 1000;
|
|
1434
|
+
|
|
1435
|
+
export interface ClaudePathsPlan {
|
|
1436
|
+
/** Patterns to emit under `paths:`, in declaration order. */
|
|
1437
|
+
globs: string[];
|
|
1438
|
+
/** Patterns deliberately not emitted, with the reason a reader can act on. */
|
|
1439
|
+
dropped: { pattern: string; reason: 'unresolved-placeholder' | 'over-budget' }[];
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Derive the host's `paths:` list for one rule, with every drop recorded.
|
|
1444
|
+
*
|
|
1445
|
+
* Kernel / always-apply rules get an EMPTY list on purpose: absent `paths:` is
|
|
1446
|
+
* how this host says "load unconditionally", which is exactly what an Iron-Law
|
|
1447
|
+
* rule needs. Scoping one would be a correctness regression dressed as a byte
|
|
1448
|
+
* saving — and the same contract notes path-scoped rules are not re-injected
|
|
1449
|
+
* after `/compact`, so an obligation that must survive compaction cannot be
|
|
1450
|
+
* path-scoped at all.
|
|
1451
|
+
*/
|
|
1452
|
+
export function _claude_paths_plan(meta: Record<string, unknown>): ClaudePathsPlan {
|
|
1453
|
+
const always_apply = Boolean(meta['alwaysApply'] || meta['type'] === 'always');
|
|
1454
|
+
if (always_apply) {
|
|
1455
|
+
return { globs: [], dropped: [] };
|
|
1456
|
+
}
|
|
1457
|
+
const globs: string[] = [];
|
|
1458
|
+
const dropped: ClaudePathsPlan['dropped'] = [];
|
|
1459
|
+
let spent = 0;
|
|
1460
|
+
for (const raw of derive_trigger_globs(meta)) {
|
|
1461
|
+
if (_is_unresolved_placeholder(raw)) {
|
|
1462
|
+
dropped.push({ pattern: raw, reason: 'unresolved-placeholder' });
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
const cost = _expanded_pattern_count(raw);
|
|
1466
|
+
if (spent + cost > CLAUDE_PATHS_PATTERN_BUDGET) {
|
|
1467
|
+
dropped.push({ pattern: raw, reason: 'over-budget' });
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
spent += cost;
|
|
1471
|
+
globs.push(_escape_claude_bracket(raw));
|
|
1472
|
+
}
|
|
1473
|
+
return { globs, dropped };
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* Emit a Claude Code rule carrying the host's OWN activation key.
|
|
1478
|
+
*
|
|
1479
|
+
* Sibling of {@link _emit_cursor_mdc} and {@link _emit_windsurf_rule}, and the
|
|
1480
|
+
* third member of a set that had two. `.claude/rules` was a symlink projection
|
|
1481
|
+
* of `dist/agent-src/rules`, which carried agent-config's own frontmatter
|
|
1482
|
+
* vocabulary (`type`, `tier`, `triggers`, `alwaysApply`, …) into a host that
|
|
1483
|
+
* reads **none** of it — so every rule loaded unconditionally and the corpus
|
|
1484
|
+
* arrived in full every session.
|
|
1485
|
+
*
|
|
1486
|
+
* Emitted frontmatter is therefore `paths:` and nothing else: the one key the
|
|
1487
|
+
* probed contract records as read by this host. A rule with no path-shaped
|
|
1488
|
+
* trigger gets no frontmatter at all rather than a block the host ignores —
|
|
1489
|
+
* keys that do nothing are bytes in every session's standing context.
|
|
1490
|
+
*
|
|
1491
|
+
* The body is copied byte-for-byte from the projection source (ADR-201's
|
|
1492
|
+
* copy-plus-rewrite discipline); only the frontmatter is host-shaped.
|
|
1493
|
+
*/
|
|
1494
|
+
export function _emit_claude_rule(source: string, target: string): void {
|
|
1495
|
+
const [meta, body] = _parse_frontmatter(_readText(source));
|
|
1496
|
+
const plan = _claude_paths_plan(meta);
|
|
1497
|
+
for (const d of plan.dropped) {
|
|
1498
|
+
_print(
|
|
1499
|
+
` ⚠️ ${path.basename(target)}: dropped \`paths:\` pattern ${JSON.stringify(d.pattern)} (${d.reason}) — ` +
|
|
1500
|
+
(plan.globs.length > 0
|
|
1501
|
+
? 'the rule keeps its other patterns'
|
|
1502
|
+
: 'the rule now loads unconditionally'),
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
const lines: string[] = [];
|
|
1506
|
+
if (plan.globs.length > 0) {
|
|
1507
|
+
lines.push('---', 'paths:');
|
|
1508
|
+
for (const g of plan.globs) {
|
|
1509
|
+
lines.push(` - ${_yaml_scalar(g)}`);
|
|
1510
|
+
}
|
|
1511
|
+
lines.push('---', '');
|
|
1512
|
+
}
|
|
1513
|
+
lines.push(_pyRstrip(body) + '\n');
|
|
1514
|
+
_mkdirp(path.dirname(target));
|
|
1515
|
+
_writeText(target, lines.join('\n'));
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1358
1518
|
/** Python str.rstrip() with no args. */
|
|
1359
1519
|
function _pyRstrip(s: string): string {
|
|
1360
1520
|
return s.replace(/\s+$/u, '');
|
|
@@ -130,7 +130,17 @@ function _load_yaml(p: string): Dict {
|
|
|
130
130
|
return _isPlainDict(data) ? (data as Dict) : {};
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
/**
|
|
134
|
+
* The file `resolve_profile` will read for `profile_id`, or the legacy path
|
|
135
|
+
* when no candidate exists.
|
|
136
|
+
*
|
|
137
|
+
* Exported because a probe that reports "which profile is active" must name
|
|
138
|
+
* the file the resolver actually used. Re-deriving that path in the caller is
|
|
139
|
+
* how the two answers drift apart, which is the defect class
|
|
140
|
+
* `road-to-capability-answerability` exists to close — so there is exactly one
|
|
141
|
+
* implementation and both the resolver and `packs:active` call it.
|
|
142
|
+
*/
|
|
143
|
+
export function profile_file(project_root: string, profile_id: string): string {
|
|
134
144
|
const legacy = path.join(project_root, PROFILES_DIRNAME, `${profile_id}.yml`);
|
|
135
145
|
if (_exists(legacy)) {
|
|
136
146
|
return legacy;
|
|
@@ -230,11 +240,11 @@ export function resolve_profile(params: {
|
|
|
230
240
|
}
|
|
231
241
|
return _build_resolved(
|
|
232
242
|
DEFAULT_PROFILE_ID,
|
|
233
|
-
_load_yaml(
|
|
243
|
+
_load_yaml(profile_file(params.project_root, DEFAULT_PROFILE_ID)),
|
|
234
244
|
{ source: SOURCE_DEFAULT },
|
|
235
245
|
);
|
|
236
246
|
}
|
|
237
|
-
const yaml_path =
|
|
247
|
+
const yaml_path = profile_file(params.project_root, profile_id);
|
|
238
248
|
if (!_exists(yaml_path)) {
|
|
239
249
|
throw new ProfileError(
|
|
240
250
|
`profile.id=${_repr(profile_id)} (${source}) but ${yaml_path} not found`,
|
|
@@ -27,16 +27,35 @@
|
|
|
27
27
|
* store. It answers "did the shipped gates have anything to bite on?" —
|
|
28
28
|
* which is also the only honest way to find out whether they help.
|
|
29
29
|
*/
|
|
30
|
+
import { createHash } from "node:crypto";
|
|
30
31
|
import fs from "node:fs";
|
|
32
|
+
import os from "node:os";
|
|
31
33
|
import path from "node:path";
|
|
32
34
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
33
35
|
|
|
36
|
+
import { censusRuleDir } from "./_lib/carrier_divergence.js";
|
|
37
|
+
import { entryText, isSidechain } from "./_lib/transcript_entry.js";
|
|
38
|
+
|
|
34
39
|
import { classify } from "./language_mirror_hook.js";
|
|
40
|
+
import { isSyntheticPrompt } from "./_lib/prompt_shape.js";
|
|
35
41
|
import { classifyAuthorization, type GitOp } from "./git_authorization_hook.js";
|
|
36
42
|
import { BLOCK_OPS, commandOp } from "./hooks/block_unauthorized_git.js";
|
|
37
43
|
import { isVacuousOutput, isCiPoll, pendingCount } from "./before_complete_hook.js";
|
|
38
44
|
import { isEvaluationPrompt, isSelfScoped, preloadedVerdict } from "./hooks/evidence_independence.js";
|
|
39
45
|
|
|
46
|
+
/**
|
|
47
|
+
* This module's own repo root, NOT `process.cwd()`.
|
|
48
|
+
*
|
|
49
|
+
* Both the project carrier and the rate series are anchored here. Resolving them
|
|
50
|
+
* against the cwd made a run from any subdirectory record
|
|
51
|
+
* `delivered_project_tokens: 0` and append the series to a fresh
|
|
52
|
+
* `agents/runtime/state/` tree at that location — outside the ignore rule whose
|
|
53
|
+
* coverage the docstring claims, and silently splitting a series whose whole
|
|
54
|
+
* point is comparability. Under the bundled CLI `import.meta.url` still resolves
|
|
55
|
+
* inside the package, which is the tree whose rules are being measured.
|
|
56
|
+
*/
|
|
57
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
58
|
+
|
|
40
59
|
export interface Violation {
|
|
41
60
|
check: "language-pin" | "git-authorization" | "vacuous-evidence" | "evidence-steering";
|
|
42
61
|
session: string;
|
|
@@ -61,12 +80,98 @@ export interface SessionReport {
|
|
|
61
80
|
violations: Violation[];
|
|
62
81
|
}
|
|
63
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Rule text reaching context, per carrier — round-6 Phase 4.3, and the
|
|
85
|
+
* instrument the `essential` default-flip decision has been waiting on. Token
|
|
86
|
+
* basis: chars/4, the same estimate `preamble_byte_census` and
|
|
87
|
+
* `measure_scope_dedup` use, so the three numbers are comparable.
|
|
88
|
+
*
|
|
89
|
+
* MEASURED AT SCAN TIME, NOT PER SESSION — and the step asked for per-session.
|
|
90
|
+
* The delivered payload is a property of the CARRIERS ON DISK, and the
|
|
91
|
+
* transcript records none of it (verified in `preamble_byte_census`: a
|
|
92
|
+
* transcript carries `message.usage` counts and response content, no system or
|
|
93
|
+
* tools field). The carriers also change under the sessions — this round alone
|
|
94
|
+
* added three rules and refreshed the global install. So attaching today's
|
|
95
|
+
* figure to a session from three weeks ago would be a fabrication dressed as a
|
|
96
|
+
* per-session measurement, and the honest shape is one reading per scan run
|
|
97
|
+
* plus the forward series 4.5 records. That correction is why 4.3 and 4.5 are
|
|
98
|
+
* one mechanism rather than two.
|
|
99
|
+
*/
|
|
100
|
+
export interface DeliveredPayload {
|
|
101
|
+
/**
|
|
102
|
+
* `present` distinguishes a measured zero from a carrier that is not there.
|
|
103
|
+
* Without it a run from the wrong directory records `tokens: 0`, halves
|
|
104
|
+
* `union_tokens`, and persists a record indistinguishable from a real reading
|
|
105
|
+
* into a series whose entire purpose is comparability over time. The sibling
|
|
106
|
+
* `report_carrier_divergence` carries the same flag and refuses to substitute a
|
|
107
|
+
* different tree; this is the same refusal.
|
|
108
|
+
*/
|
|
109
|
+
project: { dir: string; present: boolean; files: number; tokens: number };
|
|
110
|
+
global: { dir: string; present: boolean; files: number; tokens: number };
|
|
111
|
+
/** What a machine carrying BOTH carriers pays — the figure M5 compared across projects. */
|
|
112
|
+
union_tokens: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The band M5 observed across the only three stores with a corpus worth
|
|
117
|
+
* measuring (`private/capisco` 39.2 %, this package 25.4 %,
|
|
118
|
+
* `private/agent-switch` 9.1 %). Both council members independently named the
|
|
119
|
+
* same falsifier for cancelling the volume test: **a fourth project falling
|
|
120
|
+
* outside this band**. It is a constant here, with its source, because a
|
|
121
|
+
* falsifier nobody can evaluate is not one.
|
|
122
|
+
*/
|
|
123
|
+
export const OBSERVED_BAND = { low: 9.1, high: 39.2 } as const;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The smallest corpus the band was derived from — `private/capisco`, 1 978
|
|
127
|
+
* assistant turns. NOT a chosen threshold: a rate computed over materially fewer
|
|
128
|
+
* turns is not comparable to the three that defined the band, and declaring it
|
|
129
|
+
* out-of-band would fire the falsifier on corpus size rather than on behaviour.
|
|
130
|
+
*
|
|
131
|
+
* The first run of this instrument proved the point: the worktree's own store
|
|
132
|
+
* read 4.1 % over 606 turns and would have announced the falsifier on its second
|
|
133
|
+
* day of existence.
|
|
134
|
+
*/
|
|
135
|
+
export const BAND_MIN_TURNS = 1978;
|
|
136
|
+
|
|
137
|
+
export type BandVerdict = "inside" | "outside" | "corpus-too-small";
|
|
138
|
+
|
|
139
|
+
/** Round-6 Phase 4.5 — the forward-capture record for one scan run. */
|
|
140
|
+
export interface RateRecord {
|
|
141
|
+
/** Not the store path: a stable digest of it. See `storeKey`. */
|
|
142
|
+
store_key: string;
|
|
143
|
+
sessions: number;
|
|
144
|
+
assistant_turns: number;
|
|
145
|
+
language_pin: number;
|
|
146
|
+
rate_pct: number;
|
|
147
|
+
/** `outside` is the falsifier firing; `corpus-too-small` is not a reading. */
|
|
148
|
+
band: BandVerdict;
|
|
149
|
+
/**
|
|
150
|
+
* `null` when the scanned store is NOT this repo's own — `measureDelivered` is
|
|
151
|
+
* always anchored on this checkout, so pairing another project's rate with
|
|
152
|
+
* these tokens would persist a figure that belongs to neither. Cross-project
|
|
153
|
+
* `--record` is exactly the use 4.5 exists for, so the mismatch would have been
|
|
154
|
+
* the common case rather than an edge one.
|
|
155
|
+
*/
|
|
156
|
+
delivered_project_tokens: number | null;
|
|
157
|
+
delivered_global_tokens: number | null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function bandVerdict(rate_pct: number, assistant_turns: number): BandVerdict {
|
|
161
|
+
if (assistant_turns < BAND_MIN_TURNS) {
|
|
162
|
+
return "corpus-too-small";
|
|
163
|
+
}
|
|
164
|
+
return rate_pct >= OBSERVED_BAND.low && rate_pct <= OBSERVED_BAND.high ? "inside" : "outside";
|
|
165
|
+
}
|
|
166
|
+
|
|
64
167
|
export interface ScanReport {
|
|
65
168
|
scanned_at: string;
|
|
66
169
|
store: string;
|
|
67
170
|
sessions: number;
|
|
68
171
|
totals: Record<string, number>;
|
|
69
172
|
per_session: SessionReport[];
|
|
173
|
+
delivered: DeliveredPayload;
|
|
174
|
+
rate: RateRecord;
|
|
70
175
|
}
|
|
71
176
|
|
|
72
177
|
type Entry = Record<string, unknown>;
|
|
@@ -77,20 +182,14 @@ function _isObject(v: unknown): v is Record<string, unknown> {
|
|
|
77
182
|
|
|
78
183
|
/** Text of a user-role entry, or null when it is not a real chat message. */
|
|
79
184
|
export function userText(entry: Entry): string | null {
|
|
80
|
-
if (entry["type"] !== "user" || entry
|
|
185
|
+
if (entry["type"] !== "user" || isSidechain(entry)) {
|
|
81
186
|
return null;
|
|
82
187
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
} else if (Array.isArray(content)) {
|
|
89
|
-
text = content
|
|
90
|
-
.filter((b) => _isObject(b) && b["type"] === "text")
|
|
91
|
-
.map((b) => String((b as Record<string, unknown>)["text"] ?? ""))
|
|
92
|
-
.join("\n");
|
|
93
|
-
}
|
|
188
|
+
// Shape handling moved to `_lib/transcript_entry.ts` so a second reader of the
|
|
189
|
+
// same field cannot get it wrong. It already did once, in this branch: a
|
|
190
|
+
// string-only reader saw 0 of the 41 injected skill bodies in this store,
|
|
191
|
+
// because they all arrive as content blocks.
|
|
192
|
+
const text = entryText(entry);
|
|
94
193
|
if (!text.trim()) {
|
|
95
194
|
return null;
|
|
96
195
|
}
|
|
@@ -102,20 +201,10 @@ export function userText(entry: Entry): string | null {
|
|
|
102
201
|
|
|
103
202
|
/** Assistant prose, or null. */
|
|
104
203
|
export function assistantText(entry: Entry): string | null {
|
|
105
|
-
if (entry["type"] !== "assistant" || entry
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
const msg = entry["message"];
|
|
109
|
-
const content = _isObject(msg) ? msg["content"] : undefined;
|
|
110
|
-
if (!Array.isArray(content)) {
|
|
204
|
+
if (entry["type"] !== "assistant" || isSidechain(entry)) {
|
|
111
205
|
return null;
|
|
112
206
|
}
|
|
113
|
-
|
|
114
|
-
.filter((b) => _isObject(b) && b["type"] === "text")
|
|
115
|
-
.map((b) => String((b as Record<string, unknown>)["text"] ?? ""))
|
|
116
|
-
.join("\n")
|
|
117
|
-
.trim();
|
|
118
|
-
return text || null;
|
|
207
|
+
return entryText(entry).trim() || null;
|
|
119
208
|
}
|
|
120
209
|
|
|
121
210
|
/** Tool-use blocks from an assistant entry. */
|
|
@@ -228,6 +317,13 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
228
317
|
if (entry["isCompactSummary"] === true) {
|
|
229
318
|
continue;
|
|
230
319
|
}
|
|
320
|
+
// A harness-generated turn is not a chat message. The HOOK has skipped
|
|
321
|
+
// these since round 5 and this scanner did not, so the two were measuring
|
|
322
|
+
// different populations while reporting one number. Same predicate, one
|
|
323
|
+
// module, so they cannot drift again.
|
|
324
|
+
if (isSyntheticPrompt(ut)) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
231
327
|
if (isInjectedBody(ut)) {
|
|
232
328
|
// Not a chat message — it changes neither the pin nor the ledger.
|
|
233
329
|
continue;
|
|
@@ -328,6 +424,19 @@ export function scanSession(sessionId: string, lines: string[]): SessionReport {
|
|
|
328
424
|
return report;
|
|
329
425
|
}
|
|
330
426
|
|
|
427
|
+
/**
|
|
428
|
+
* Where `--record` appends by default. Under the gitignored `agents/runtime/`
|
|
429
|
+
* because the series is per-machine observation, and committing it would publish
|
|
430
|
+
* the project set `storeKey` exists to hide.
|
|
431
|
+
*/
|
|
432
|
+
export const DEFAULT_RATE_SERIES = path.join(
|
|
433
|
+
REPO_ROOT,
|
|
434
|
+
"agents",
|
|
435
|
+
"runtime",
|
|
436
|
+
"state",
|
|
437
|
+
"conformance-rates.jsonl",
|
|
438
|
+
);
|
|
439
|
+
|
|
331
440
|
/** Default Claude Code transcript store for a project directory. */
|
|
332
441
|
export function defaultStore(projectDir: string): string {
|
|
333
442
|
// Claude Code slugs BOTH separators and dots: /Users/x/.claude → -Users-x--claude.
|
|
@@ -335,6 +444,51 @@ export function defaultStore(projectDir: string): string {
|
|
|
335
444
|
return path.join(process.env["HOME"] ?? "", ".claude", "projects", slug);
|
|
336
445
|
}
|
|
337
446
|
|
|
447
|
+
/** chars/4, the estimate every other payload measurement in this repo uses. */
|
|
448
|
+
function _tokens(chars: number): number {
|
|
449
|
+
return Math.round(chars / 4);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Measure what each carrier delivers. Defaults are the two real carriers; both
|
|
454
|
+
* are injectable so a test never reads the developer's home.
|
|
455
|
+
*/
|
|
456
|
+
export function measureDelivered(projectRulesDir: string, globalRulesDir: string): DeliveredPayload {
|
|
457
|
+
const p = censusRuleDir(projectRulesDir);
|
|
458
|
+
const g = censusRuleDir(globalRulesDir);
|
|
459
|
+
return {
|
|
460
|
+
project: {
|
|
461
|
+
dir: projectRulesDir,
|
|
462
|
+
present: fs.existsSync(projectRulesDir),
|
|
463
|
+
files: p.files,
|
|
464
|
+
tokens: _tokens(p.chars),
|
|
465
|
+
},
|
|
466
|
+
global: {
|
|
467
|
+
dir: globalRulesDir,
|
|
468
|
+
present: fs.existsSync(globalRulesDir),
|
|
469
|
+
files: g.files,
|
|
470
|
+
tokens: _tokens(g.chars),
|
|
471
|
+
},
|
|
472
|
+
union_tokens: _tokens(p.chars + g.chars),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* A stable, non-reversible key for a store — NEVER the path.
|
|
478
|
+
*
|
|
479
|
+
* The store path is `-Users-<realname>-projects-<client>-…`: a real name, a
|
|
480
|
+
* directory layout, and often a customer identifier. `domain-safety-pii` §
|
|
481
|
+
* Surface 3 forbids exporting a direct identifier and
|
|
482
|
+
* `low-impact-corpus-privacy-floor` names project-rooted paths outright, so the
|
|
483
|
+
* forward series is keyed on a digest. It still answers the only question 4.5
|
|
484
|
+
* asks of it — "is this the same project as last time, and is a FOURTH one
|
|
485
|
+
* outside the band" — because equality and novelty survive hashing while the
|
|
486
|
+
* identity does not.
|
|
487
|
+
*/
|
|
488
|
+
export function storeKey(store: string): string {
|
|
489
|
+
return createHash("sha256").update(path.resolve(store)).digest("hex").slice(0, 12);
|
|
490
|
+
}
|
|
491
|
+
|
|
338
492
|
export function scanStore(store: string, limit: number): ScanReport {
|
|
339
493
|
const files = fs
|
|
340
494
|
.readdirSync(store)
|
|
@@ -365,15 +519,59 @@ export function scanStore(store: string, limit: number): ScanReport {
|
|
|
365
519
|
}
|
|
366
520
|
}
|
|
367
521
|
|
|
522
|
+
const assistant_turns = per_session.reduce((n, s) => n + s.assistant_turns, 0);
|
|
523
|
+
const language_pin = totals["language-pin"] ?? 0;
|
|
524
|
+
// Round ONCE, then use the rounded value for both the record and the verdict.
|
|
525
|
+
// Rounding after the comparison lets a raw 9.06 persist `rate_pct: 9.1` beside
|
|
526
|
+
// `band: "outside"`, and print "9.1%" directly above "OUTSIDE the 9.1-39.2%
|
|
527
|
+
// band" — a rounding artefact announcing the declared falsifier.
|
|
528
|
+
const rate_pct =
|
|
529
|
+
assistant_turns === 0 ? 0 : Number(((100 * language_pin) / assistant_turns).toFixed(1));
|
|
530
|
+
const delivered = measureDelivered(
|
|
531
|
+
path.join(REPO_ROOT, ".claude", "rules"),
|
|
532
|
+
path.join(process.env["HOME"] ?? os.homedir(), ".claude", "rules"),
|
|
533
|
+
);
|
|
534
|
+
const ownStore = path.resolve(store) === path.resolve(defaultStore(REPO_ROOT));
|
|
535
|
+
|
|
368
536
|
return {
|
|
369
537
|
scanned_at: new Date().toISOString(),
|
|
370
538
|
store,
|
|
371
539
|
sessions: per_session.length,
|
|
372
540
|
totals,
|
|
373
541
|
per_session,
|
|
542
|
+
delivered,
|
|
543
|
+
rate: {
|
|
544
|
+
store_key: storeKey(store),
|
|
545
|
+
sessions: per_session.length,
|
|
546
|
+
assistant_turns,
|
|
547
|
+
language_pin,
|
|
548
|
+
rate_pct,
|
|
549
|
+
band: bandVerdict(rate_pct, assistant_turns),
|
|
550
|
+
// Only attach the carrier figures when the rate and the carriers describe
|
|
551
|
+
// the same project.
|
|
552
|
+
delivered_project_tokens: ownStore ? delivered.project.tokens : null,
|
|
553
|
+
delivered_global_tokens: ownStore ? delivered.global.tokens : null,
|
|
554
|
+
},
|
|
374
555
|
};
|
|
375
556
|
}
|
|
376
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Append one scan's rate record — round-6 Phase 4.5's forward capture.
|
|
560
|
+
*
|
|
561
|
+
* PII-exclusion-by-construction, the shape `artifact-engagement-recording`
|
|
562
|
+
* applies to telemetry: `RateRecord` has NO field able to hold a path, a prompt,
|
|
563
|
+
* a session id, or any free text, so there is no scrubber here that could fail.
|
|
564
|
+
* Keep it that way — never widen it with a `note` or `extra`.
|
|
565
|
+
*
|
|
566
|
+
* Default destination is under the gitignored `agents/runtime/`, because the
|
|
567
|
+
* series is per-machine observation and committing it would publish the very
|
|
568
|
+
* project set the key exists to hide.
|
|
569
|
+
*/
|
|
570
|
+
export function recordRate(file: string, scanned_at: string, rate: RateRecord): void {
|
|
571
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
572
|
+
fs.appendFileSync(file, `${JSON.stringify({ measured_at: scanned_at, ...rate })}\n`, "utf8");
|
|
573
|
+
}
|
|
574
|
+
|
|
377
575
|
export function render(report: ScanReport): string {
|
|
378
576
|
const lines: string[] = [];
|
|
379
577
|
lines.push(`conformance:behavior · ${report.sessions} session(s) · ${report.store}`);
|
|
@@ -391,6 +589,45 @@ export function render(report: ScanReport): string {
|
|
|
391
589
|
lines.push(` ${s.session} ${s.violations.length} across ${s.assistant_turns} turns`);
|
|
392
590
|
}
|
|
393
591
|
}
|
|
592
|
+
const d = report.delivered;
|
|
593
|
+
lines.push("");
|
|
594
|
+
lines.push(" Delivered rule text (chars/4, carriers as they stand NOW — not per-session):");
|
|
595
|
+
const carrier = (label: string, c: { present: boolean; files: number; tokens: number }): string =>
|
|
596
|
+
c.present
|
|
597
|
+
? ` ${label} ${String(c.files).padStart(4)} rules ${String(c.tokens).padStart(7)} tok`
|
|
598
|
+
: ` ${label} ABSENT — not a measured zero, this carrier does not exist here`;
|
|
599
|
+
lines.push(carrier("project", d.project));
|
|
600
|
+
lines.push(carrier("global ", d.global));
|
|
601
|
+
lines.push(` union ${String(d.union_tokens).padStart(7)} tok`);
|
|
602
|
+
lines.push(" A session's own payload is NOT recoverable: the transcript records no system");
|
|
603
|
+
lines.push(" or tools field, and the carriers change under the sessions. This is one");
|
|
604
|
+
lines.push(" reading; the series below is what makes it interpretable over time.");
|
|
605
|
+
lines.push("");
|
|
606
|
+
lines.push(
|
|
607
|
+
` language-pin rate ${report.rate.rate_pct.toFixed(1)}% ` +
|
|
608
|
+
`(${report.rate.language_pin} of ${report.rate.assistant_turns} assistant turns)`,
|
|
609
|
+
);
|
|
610
|
+
if (report.rate.band === "corpus-too-small") {
|
|
611
|
+
lines.push(
|
|
612
|
+
` Not compared: ${report.rate.assistant_turns} turns is below the ${BAND_MIN_TURNS} of the`,
|
|
613
|
+
);
|
|
614
|
+
lines.push(" smallest corpus the band was derived from, so a verdict here would be about");
|
|
615
|
+
lines.push(" corpus size, not behaviour.");
|
|
616
|
+
} else if (report.rate.band === "inside") {
|
|
617
|
+
lines.push(
|
|
618
|
+
` Inside the observed ${OBSERVED_BAND.low}-${OBSERVED_BAND.high}% band (M5, n=3).`,
|
|
619
|
+
);
|
|
620
|
+
} else {
|
|
621
|
+
lines.push(
|
|
622
|
+
` ⚠️ OUTSIDE the observed ${OBSERVED_BAND.low}-${OBSERVED_BAND.high}% band (M5, n=3).`,
|
|
623
|
+
);
|
|
624
|
+
lines.push(" This is the falsifier both council members named when the volume test was");
|
|
625
|
+
lines.push(" cancelled: a project outside the band is the signal that the cancellation");
|
|
626
|
+
lines.push(" was wrong. It is a reason to look, not a defect in this run.");
|
|
627
|
+
lines.push(" CHECK FIRST that this is a different PROJECT: a git worktree gets its own");
|
|
628
|
+
lines.push(" transcript store under the same project, so a store-keyed series counts it");
|
|
629
|
+
lines.push(" as new. Store novelty is not project novelty.");
|
|
630
|
+
}
|
|
394
631
|
lines.push("");
|
|
395
632
|
lines.push(" Scanned checks are exactly the mechanised ones. Ask-shape, session-canary,");
|
|
396
633
|
lines.push(" promissory closings and checkbox batching are left as prose and NOT measured.");
|
|
@@ -403,6 +640,7 @@ export function main(argv?: string[]): number {
|
|
|
403
640
|
let limit = 30;
|
|
404
641
|
let json = false;
|
|
405
642
|
let out: string | null = null;
|
|
643
|
+
let record: string | null = null;
|
|
406
644
|
for (let i = 0; i < args.length; i += 1) {
|
|
407
645
|
const a = args[i];
|
|
408
646
|
if (a === "--store" && args[i + 1] !== undefined) {
|
|
@@ -413,6 +651,13 @@ export function main(argv?: string[]): number {
|
|
|
413
651
|
json = true;
|
|
414
652
|
} else if (a === "--output" && args[i + 1] !== undefined) {
|
|
415
653
|
out = args[++i] as string;
|
|
654
|
+
} else if (a === "--record") {
|
|
655
|
+
// Opt-in, and a bare `--record` takes the default path rather than
|
|
656
|
+
// swallowing the next flag as a filename.
|
|
657
|
+
record =
|
|
658
|
+
args[i + 1] !== undefined && !(args[i + 1] as string).startsWith("-")
|
|
659
|
+
? (args[++i] as string)
|
|
660
|
+
: DEFAULT_RATE_SERIES;
|
|
416
661
|
}
|
|
417
662
|
}
|
|
418
663
|
const resolved = store ?? defaultStore(process.cwd());
|
|
@@ -424,6 +669,9 @@ export function main(argv?: string[]): number {
|
|
|
424
669
|
if (out) {
|
|
425
670
|
fs.writeFileSync(out, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
426
671
|
}
|
|
672
|
+
if (record) {
|
|
673
|
+
recordRate(record, report.scanned_at, report.rate);
|
|
674
|
+
}
|
|
427
675
|
// fs.writeSync, not process.stdout.write: `process.exit()` below does not
|
|
428
676
|
// flush an async pipe write, so `--json | jq` reproducibly received a
|
|
429
677
|
// truncated 64 KB document with no error (measured: 85,169 vs 65,536 bytes).
|