@event4u/agent-config 14.21.0 → 14.22.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +198 -0
- package/CREDITS.md +37 -0
- package/dist/agent-src/commands/analyze/inbox.md +380 -5
- package/dist/agent-src/commands/analyze/repo.md +4 -2
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/token-efficiency.md +26 -3
- package/dist/agent-src/scripts/_cli_entry.ts +51 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
- package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/dist/agent-src/scripts/stubs_due.ts +4 -18
- package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/_cli_entry.js +8 -0
- package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
- package/dist/cli-delegate/archival_sweep.js +8 -0
- package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
- package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
- package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
- package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
- package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
- package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +26 -2
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/cli-delegate/dashboard_mode.js +16 -0
- package/dist/cli-delegate/gate_budget.js +24 -0
- package/dist/cli-delegate/gate_execute.js +25 -0
- package/dist/cli-delegate/guarded_baseline.js +18 -0
- package/dist/cli-delegate/resume_probe.js +22 -0
- package/dist/cli-delegate/roadmap_gates.js +638 -0
- package/dist/cli-delegate/roadmap_set_step.js +173 -0
- package/dist/cli-delegate/stub_queue.js +47 -0
- package/dist/cli-delegate/stubs_due.js +38 -0
- package/dist/cli-delegate/ticket_bundles.js +59 -0
- package/dist/cli-delegate/update_roadmap_progress.js +60 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +8 -8
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.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 +1193 -917
- package/dist/install/claudeLayerCarriage.js +237 -0
- package/dist/install/claudePathsPlan.js +79 -0
- package/dist/install/hostLayerFingerprint.js +1 -1
- package/dist/install/install.mjs +6 -0
- package/dist/install/partitionEligibility.js +231 -117
- package/dist/install/ruleLayerPartition.js +10 -7
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +39219 -38995
- package/dist/scripts/condense.js +32 -25
- package/docs/MIGRATION.md +49 -1
- package/docs/contracts/settings-classes.md +1 -1
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/src/agent-src/scripts/roadmap_gates.ts +4 -20
- package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/src/agent-src/scripts/stubs_due.ts +4 -18
- package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/src/config/continuity-surface.json +509 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/gate-coverage.yml +29 -8
- package/src/config/gate-violation-baselines.json +8 -8
- package/src/config/gitignore-block.txt +19 -0
- package/src/config/host-injection-effect.json +73 -0
- package/src/config/packed-binary-manifest.json +33 -0
- package/src/config/preamble-payload-budget.json +112 -105
- package/src/config/publish-surface.json +4 -2
- package/src/install/claudeLayerCarriage.ts +273 -0
- package/src/install/claudePathsPlan.ts +76 -0
- package/src/install/hostLayerFingerprint.ts +1 -1
- package/src/install/partitionEligibility.ts +261 -127
- package/src/install/ruleLayerPartition.ts +19 -23
- package/src/scripts/_dispatch.bash +77 -27
- package/src/scripts/_lib/auto_dispatch.ts +2 -2
- package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +1 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/ai_council/api_transport.ts +5 -0
- package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
- package/src/scripts/ai_council/prompts.ts +9 -0
- package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
- package/src/scripts/capture_skill_catalogue.ts +24 -11
- package/src/scripts/check_candidate_lines.ts +499 -0
- package/src/scripts/check_continuity_surface.ts +475 -0
- package/src/scripts/check_dependency_floors.ts +13 -6
- package/src/scripts/check_estate_count.ts +40 -79
- package/src/scripts/check_pack_size.ts +262 -23
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/code_graph/build.ts +138 -15
- package/src/scripts/code_graph/cli.ts +76 -8
- package/src/scripts/code_graph/extract.ts +11 -0
- package/src/scripts/code_graph/grammar_probe.ts +43 -0
- package/src/scripts/code_graph/loader.ts +88 -14
- package/src/scripts/code_graph/query.ts +121 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +247 -6
- package/src/scripts/code_graph/types.ts +66 -1
- package/src/scripts/code_graph/validate.ts +23 -0
- package/src/scripts/condense.ts +34 -22
- package/src/scripts/corpus_manifest.ts +5 -5
- package/src/scripts/council_cli.ts +11 -12
- package/src/scripts/count_intent_disagreement.ts +426 -0
- package/src/scripts/handoff_context_hook.ts +79 -0
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +64 -12
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
- package/src/scripts/hooks/concern_registry.ts +4 -2
- package/src/scripts/hooks/host_semantics.ts +1 -1
- package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
- package/src/scripts/hooks/rule_inject_hook.ts +1 -1
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/inbox_source_census.ts +322 -0
- package/src/scripts/install-hooks.sh +82 -0
- package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
- package/src/scripts/lint_plan_risk_register.ts +13 -17
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tools.ts +2 -56
- package/src/scripts/media/lib/adapter-contract.md +7 -2
- package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
- package/src/scripts/rdp_candidates_delta.ts +276 -0
- package/src/scripts/rdp_quality_eval.ts +217 -19
- package/src/scripts/release_mix_taxonomy.json +3 -2
- package/src/scripts/report_host_injection_effect.ts +189 -0
- package/src/scripts/report_layer_overlap.ts +41 -0
- package/src/scripts/report_skill_menu_census.ts +341 -0
- package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
- package/src/vendor/grammars/README.md +61 -0
- package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
- package/src/scripts/_lib/carrier_status.ts +0 -118
- package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
- package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
package/src/scripts/condense.ts
CHANGED
|
@@ -265,9 +265,9 @@ export {
|
|
|
265
265
|
} from '../install/claudePathsPlan.js';
|
|
266
266
|
import { rule_in_scope } from '../install/ruleInScope.js';
|
|
267
267
|
import { pruneEmptyDirs } from './_lib/prune_empty_dirs.js';
|
|
268
|
-
import {
|
|
268
|
+
import { claudeLayerHolds, commandWithheld, keepInProjectLayer, personaPartition } from '../install/partitionEligibility.js'; // ADR-236
|
|
269
269
|
import { dedupableRules, partitionRulesForDir } from '../install/ruleLayerPartition.js'; // ADR-236, per-host evidence
|
|
270
|
-
import { _claude_paths_plan, derive_trigger_globs } from '../install/claudePathsPlan.js';
|
|
270
|
+
import { _claude_paths_plan, applies_when, derive_trigger_globs } from '../install/claudePathsPlan.js';
|
|
271
271
|
|
|
272
272
|
const _HERE = path.dirname(fileURLToPath(import.meta.url)); // <repo>/src/scripts
|
|
273
273
|
const _DEFAULT_PROJECT_ROOT = path.resolve(_HERE, '..', '..');
|
|
@@ -320,7 +320,6 @@ function _deriveState(root: string): ModuleState {
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
export const MODULE_STATE: ModuleState = _deriveState(_DEFAULT_PROJECT_ROOT);
|
|
323
|
-
setPartitionAnnounce(success); // ADR-236: the mode line must be visible at the default level
|
|
324
323
|
|
|
325
324
|
/**
|
|
326
325
|
* Test seam — reassign one or more module-state fields, mirroring the pytest
|
|
@@ -1299,7 +1298,7 @@ export function _emit_cursor_mdc(source: string, target: string): void {
|
|
|
1299
1298
|
const globs = always_apply ? [] : derive_trigger_globs(meta);
|
|
1300
1299
|
const lines = [
|
|
1301
1300
|
'---',
|
|
1302
|
-
`description: ${_yaml_scalar(description)}`,
|
|
1301
|
+
`description: ${_yaml_scalar(always_apply ? description : applies_when(description, meta))}`,
|
|
1303
1302
|
`globs: ${globs.join(',')}`,
|
|
1304
1303
|
`alwaysApply: ${always_apply ? 'true' : 'false'}`,
|
|
1305
1304
|
'---',
|
|
@@ -1321,7 +1320,7 @@ export function _emit_windsurf_rule(source: string, target: string): void {
|
|
|
1321
1320
|
const lines = [
|
|
1322
1321
|
'---',
|
|
1323
1322
|
`trigger: ${trigger}`,
|
|
1324
|
-
`description: ${_yaml_scalar(description)}`,
|
|
1323
|
+
`description: ${_yaml_scalar(always_apply ? description : applies_when(description, meta))}`,
|
|
1325
1324
|
`globs: ${globs.join(',')}`,
|
|
1326
1325
|
'---',
|
|
1327
1326
|
'',
|
|
@@ -1628,23 +1627,22 @@ function _render_native_model_md(src_md: string, tier: string): string {
|
|
|
1628
1627
|
}
|
|
1629
1628
|
|
|
1630
1629
|
export function generate_claude_skills(active_skill_names: ReadonlySet<string> | null = null): number {
|
|
1631
|
-
if (partitionActive(MODULE_STATE.PROJECT_ROOT)) active_skill_names = new Set<string>();
|
|
1632
1630
|
if (!_exists(MODULE_STATE.SKILLS_SOURCE)) {
|
|
1633
1631
|
process.stderr.write(' ⚠️ dist/agent-src/skills/ not found — skipping skills\n');
|
|
1634
1632
|
return 0;
|
|
1635
1633
|
}
|
|
1636
1634
|
|
|
1637
|
-
|
|
1635
|
+
const all_skills = _iterdirSorted(MODULE_STATE.SKILLS_SOURCE)
|
|
1638
1636
|
.filter((p) => _isDir(p))
|
|
1639
1637
|
.map((p) => path.basename(p))
|
|
1640
1638
|
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
1639
|
+
// ADR-236: withhold every skill `~/.claude/skills` holds — see `keepInProjectLayer`.
|
|
1640
|
+
let skills = keepInProjectLayer('skills', all_skills);
|
|
1641
1641
|
if (active_skill_names !== null) {
|
|
1642
1642
|
skills = skills.filter((s) => active_skill_names.has(s));
|
|
1643
1643
|
}
|
|
1644
1644
|
const skill_set = new Set(skills);
|
|
1645
|
-
const command_slugs =
|
|
1646
|
-
? new Set<string>()
|
|
1647
|
-
: new Set([...iterCommands()].map(([, slug]) => slug));
|
|
1645
|
+
const command_slugs = _emitted_wrapper_slugs(new Set(all_skills));
|
|
1648
1646
|
|
|
1649
1647
|
_mkdirp(MODULE_STATE.CLAUDE_SKILLS_DIR);
|
|
1650
1648
|
const auto = _read_model_auto_switch() === 'auto';
|
|
@@ -1737,6 +1735,22 @@ function _nested_command_subpath(source_file: string): string | null {
|
|
|
1737
1735
|
return sub.includes('/') ? sub : null;
|
|
1738
1736
|
}
|
|
1739
1737
|
|
|
1738
|
+
/** The emitter's four skips, in one place — see `emittedWrapperSlugs`. */
|
|
1739
|
+
const _wrapper_is_emitted = (f: string, s: string, skills: ReadonlySet<string>): boolean =>
|
|
1740
|
+
!skills.has(s) &&
|
|
1741
|
+
_nested_command_subpath(f) === null &&
|
|
1742
|
+
!is_claude_builtin_name(s) &&
|
|
1743
|
+
!claudeLayerHolds('skills', s);
|
|
1744
|
+
|
|
1745
|
+
/** Wrapper slugs the command emitter really writes — see `emittedWrapperSlugs`. */
|
|
1746
|
+
function _emitted_wrapper_slugs(skill_names: ReadonlySet<string>): Set<string> {
|
|
1747
|
+
return new Set(
|
|
1748
|
+
[...iterCommands()]
|
|
1749
|
+
.filter(([f, s]) => _wrapper_is_emitted(f, s, skill_names))
|
|
1750
|
+
.map(([, slug]) => slug),
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1740
1754
|
/**
|
|
1741
1755
|
* Project-scope `.claude/commands/<cluster>/<sub>.md` — the colon form.
|
|
1742
1756
|
*
|
|
@@ -1749,7 +1763,7 @@ function _nested_command_subpath(source_file: string): string | null {
|
|
|
1749
1763
|
*
|
|
1750
1764
|
* Emitting the nested form here lets `generate_claude_commands` stop wrapping
|
|
1751
1765
|
* the clustered commands: one listing, same reachability, and reachability no
|
|
1752
|
-
* longer depends on a global deploy. Dedup AND precedence: `
|
|
1766
|
+
* longer depends on a global deploy. Dedup AND precedence: `commandWithheld`.
|
|
1753
1767
|
*
|
|
1754
1768
|
* ADR-003 (colon canonical for clusters) and ADR-044 (flat commands stay
|
|
1755
1769
|
* hyphenated) both hold: flat commands are untouched and keep their wrapper.
|
|
@@ -1757,8 +1771,6 @@ function _nested_command_subpath(source_file: string): string | null {
|
|
|
1757
1771
|
export function generate_claude_project_commands(
|
|
1758
1772
|
active_command_slugs: ReadonlySet<string> | null = null,
|
|
1759
1773
|
): number {
|
|
1760
|
-
if (commandsWithheld(MODULE_STATE.PROJECT_ROOT)) active_command_slugs = new Set<string>();
|
|
1761
|
-
// Returns before the stale sweep — invariant, see `commandsWithheld`.
|
|
1762
1774
|
if (!_isDir(path.join(MODULE_STATE.PROJECT_ROOT, 'src', 'domains'))) {
|
|
1763
1775
|
return 0;
|
|
1764
1776
|
}
|
|
@@ -1768,7 +1780,7 @@ export function generate_claude_project_commands(
|
|
|
1768
1780
|
continue;
|
|
1769
1781
|
}
|
|
1770
1782
|
const sub = _nested_command_subpath(source_file);
|
|
1771
|
-
if (sub !== null) {
|
|
1783
|
+
if (sub !== null && !commandWithheld(`${sub}.md`)) {
|
|
1772
1784
|
nested.push([source_file, sub]);
|
|
1773
1785
|
}
|
|
1774
1786
|
}
|
|
@@ -1805,7 +1817,6 @@ export function generate_claude_project_commands(
|
|
|
1805
1817
|
}
|
|
1806
1818
|
|
|
1807
1819
|
export function generate_claude_commands(active_command_slugs: ReadonlySet<string> | null = null): number {
|
|
1808
|
-
if (partitionActive(MODULE_STATE.PROJECT_ROOT)) active_command_slugs = new Set<string>();
|
|
1809
1820
|
if (!_isDir(path.join(MODULE_STATE.PROJECT_ROOT, 'src', 'domains'))) {
|
|
1810
1821
|
process.stderr.write(' ⚠️ src/domains/ not found — skipping commands\n');
|
|
1811
1822
|
return 0;
|
|
@@ -1826,6 +1837,7 @@ export function generate_claude_commands(active_command_slugs: ReadonlySet<strin
|
|
|
1826
1837
|
let count = 0;
|
|
1827
1838
|
let skipped = 0;
|
|
1828
1839
|
let reserved = 0;
|
|
1840
|
+
let withheld = 0;
|
|
1829
1841
|
let rendered = 0;
|
|
1830
1842
|
const auto = _read_model_auto_switch() === 'auto';
|
|
1831
1843
|
for (const [source_file, slug] of iterCommands()) {
|
|
@@ -1854,6 +1866,9 @@ export function generate_claude_commands(active_command_slugs: ReadonlySet<strin
|
|
|
1854
1866
|
reserved += 1;
|
|
1855
1867
|
continue;
|
|
1856
1868
|
}
|
|
1869
|
+
// ADR-236 per-name: the INSTALLER re-projects visible flat commands as
|
|
1870
|
+
// `~/.claude/skills/<slug>/SKILL.md` (install.ts:_apply_claude_flat_command_wrappers).
|
|
1871
|
+
if (claudeLayerHolds('skills', slug)) { withheld += 1; continue; }
|
|
1857
1872
|
current_slugs.add(slug);
|
|
1858
1873
|
|
|
1859
1874
|
const skill_dir = path.join(MODULE_STATE.CLAUDE_SKILLS_DIR, slug);
|
|
@@ -1906,6 +1921,9 @@ export function generate_claude_commands(active_command_slugs: ReadonlySet<strin
|
|
|
1906
1921
|
if (reserved) {
|
|
1907
1922
|
msg += ` (${reserved} withheld — Claude Code built-in name)`;
|
|
1908
1923
|
}
|
|
1924
|
+
if (withheld > 0) {
|
|
1925
|
+
msg += ` (${withheld} withheld — ~/.claude/skills carries the wrapper)`;
|
|
1926
|
+
}
|
|
1909
1927
|
if (removed_dirs) {
|
|
1910
1928
|
msg += ` (${removed_dirs} stale dirs removed)`;
|
|
1911
1929
|
}
|
|
@@ -2244,19 +2262,13 @@ export function generate_persona_symlinks(): number {
|
|
|
2244
2262
|
_print(' ⚠️ dist/agent-src/personas/ not found — skipping personas');
|
|
2245
2263
|
return 0;
|
|
2246
2264
|
}
|
|
2247
|
-
const personas = _rglobSorted(MODULE_STATE.PERSONAS_SOURCE, '*.md')
|
|
2248
|
-
.filter((p) => _isFile(p) && path.basename(p, '.md') !== 'README')
|
|
2249
|
-
.map((p) => path.basename(p))
|
|
2250
|
-
// glob('*.md') is non-recursive in Python; mirror that — only top level.
|
|
2251
|
-
.filter((name) => _isFile(path.join(MODULE_STATE.PERSONAS_SOURCE, name)));
|
|
2252
2265
|
// Python uses PERSONAS_SOURCE.glob("*.md") (non-recursive). Re-derive
|
|
2253
2266
|
// from a direct listing to match exactly.
|
|
2254
2267
|
const direct = _iterdirSorted(MODULE_STATE.PERSONAS_SOURCE)
|
|
2255
2268
|
.filter((p) => p.endsWith('.md') && _isFile(p) && path.basename(p, '.md') !== 'README')
|
|
2256
2269
|
.map((p) => path.basename(p))
|
|
2257
2270
|
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
2258
|
-
|
|
2259
|
-
const partition = personaPartition(MODULE_STATE.PROJECT_ROOT, direct);
|
|
2271
|
+
const partition = personaPartition(direct);
|
|
2260
2272
|
const tool_dirs = _filter_tool_dirs(PERSONA_TOOL_DIRS);
|
|
2261
2273
|
let total = 0;
|
|
2262
2274
|
for (const [tool_dir, rel_prefix] of Object.entries(tool_dirs)) {
|
|
@@ -23,7 +23,7 @@ import * as path from 'node:path';
|
|
|
23
23
|
import process from 'node:process';
|
|
24
24
|
import { fileURLToPath } from 'node:url';
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import { resolveHostLayerVerdict } from '../install/partitionEligibility.js';
|
|
27
27
|
import {
|
|
28
28
|
type CorpusManifest,
|
|
29
29
|
captureManifest,
|
|
@@ -126,9 +126,9 @@ export function parseArgs(argv: readonly string[]): Parsed | string {
|
|
|
126
126
|
/**
|
|
127
127
|
* One real capture over the real tree.
|
|
128
128
|
*
|
|
129
|
-
* This is the one caller for which the process globals `
|
|
130
|
-
* — the host layer under the running user's home, and the install lockfile
|
|
131
|
-
* ARE this run's own host, so it is the one caller that passes the predicate.
|
|
129
|
+
* This is the one caller for which the process globals `resolveHostLayerVerdict`
|
|
130
|
+
* reads — the host layer under the running user's home, and the install lockfile
|
|
131
|
+
* — ARE this run's own host, so it is the one caller that passes the predicate.
|
|
132
132
|
* Everywhere else the field stays `null` rather than recording some other
|
|
133
133
|
* machine's state under this tree's name.
|
|
134
134
|
*/
|
|
@@ -138,7 +138,7 @@ function capture(limit: number): CorpusManifest {
|
|
|
138
138
|
userHome: process.env['HOME'] ?? os.homedir(),
|
|
139
139
|
limit,
|
|
140
140
|
enumerationRule: ENUMERATION_RULE_ID,
|
|
141
|
-
|
|
141
|
+
hostLayerVerified: (root) => resolveHostLayerVerdict(root).verified,
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -93,6 +93,7 @@ import {
|
|
|
93
93
|
_quorum_min_present_from,
|
|
94
94
|
_quorum_setting_from,
|
|
95
95
|
} from './ai_council/quorum_wiring.js';
|
|
96
|
+
import { measureDeliberation, runCallsFrom } from './ai_council/deliberation_metrics.js';
|
|
96
97
|
import { formatQualificationLine, type MemberQualification } from './ai_council/qualification.js';
|
|
97
98
|
import {
|
|
98
99
|
absenceReasonFor,
|
|
@@ -2730,18 +2731,15 @@ function cmd_run(
|
|
|
2730
2731
|
}
|
|
2731
2732
|
const estimated_total = estimates.reduce((acc, e) => acc + _total_usd(e), 0.0);
|
|
2732
2733
|
let actual_total = 0.0;
|
|
2733
|
-
const
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
|
-
all_responses.push(chairman.response);
|
|
2743
|
-
}
|
|
2744
|
-
all_responses.push(...stance_repairs);
|
|
2734
|
+
const run_calls = runCallsFrom({
|
|
2735
|
+
deliberation: responses,
|
|
2736
|
+
peerReview: peer_review?.responses ?? null,
|
|
2737
|
+
consensusExtraction: consensus?.extraction_responses ?? null,
|
|
2738
|
+
consensusScoring: consensus?.scoring_responses ?? null,
|
|
2739
|
+
chairman: chairman?.response ?? null,
|
|
2740
|
+
stanceRepairs: stance_repairs,
|
|
2741
|
+
});
|
|
2742
|
+
const all_responses: CouncilResponse[] = run_calls.responses;
|
|
2745
2743
|
// Billable-aware: a subscription-CLI seat spent nothing, so pricing it at
|
|
2746
2744
|
// API rates and calling the figure "actual" is a false statement about
|
|
2747
2745
|
// money. See pricing.ts § billable-aware aggregation.
|
|
@@ -2791,6 +2789,7 @@ function cmd_run(
|
|
|
2791
2789
|
// one for a machine consumer), even when every field is `null`: the
|
|
2792
2790
|
// work-order envelope for whatever executes on this verdict next.
|
|
2793
2791
|
payload['handoff'] = handoff;
|
|
2792
|
+
payload['deliberation'] = measureDeliberation(run_calls.calls);
|
|
2794
2793
|
const out_path = _validate_council_output_path(args.output as string, {
|
|
2795
2794
|
kind: 'responses',
|
|
2796
2795
|
subcommand: 'run',
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* count_intent_disagreement.ts — the counter `mandated-lines.md` asks for.
|
|
4
|
+
*
|
|
5
|
+
* `src/agent-src/contexts/execution/mandated-lines.md` § Honest scope names one
|
|
6
|
+
* cheap observation and then names its own precondition:
|
|
7
|
+
*
|
|
8
|
+
* "track how often an emitted intent line has its three slots **disagree**.
|
|
9
|
+
* If disagreements are effectively never found, the line is decorating
|
|
10
|
+
* decisions already made and the set should shrink. [...] That observation
|
|
11
|
+
* costs one counter and is the first thing to look at before adding a sixth
|
|
12
|
+
* line."
|
|
13
|
+
*
|
|
14
|
+
* This is that counter. It answers a question no eval in this tree measures:
|
|
15
|
+
* whether the five EXISTING mandated lines earn their place — as opposed to
|
|
16
|
+
* whether a sixth would.
|
|
17
|
+
*
|
|
18
|
+
* WHAT IT CAN AND CANNOT DECIDE, stated here rather than discovered later.
|
|
19
|
+
* "Disagree" is a semantic relation between three claims, and no lexical pass
|
|
20
|
+
* decides it. So the counter reports two layers and never conflates them:
|
|
21
|
+
*
|
|
22
|
+
* Layer 1, deterministic. Population size, malformed lines (fewer than three
|
|
23
|
+
* slots), and RESTATEMENT lines — slots whose content-word sets overlap so
|
|
24
|
+
* heavily that the line reads the same three ways. That shape is the
|
|
25
|
+
* contract's own named failure ("A line that reads the same three ways is a
|
|
26
|
+
* line that was written after deciding"), so it is the one disagreement-
|
|
27
|
+
* adjacent signal a machine can own. `distinct` is NOT `disagree`: three
|
|
28
|
+
* slots can be phrased differently and still say the same thing.
|
|
29
|
+
*
|
|
30
|
+
* Layer 2, rated and opt-in. A model rater classifies each well-formed line
|
|
31
|
+
* as agree / disagree / undecidable. Only meaningful over a non-empty
|
|
32
|
+
* population, and deliberately not wired here — the rater path already
|
|
33
|
+
* exists in `rdp_quality_eval.ts --score-only` and duplicating its billing
|
|
34
|
+
* guard would be a second unguarded way to bill the same account.
|
|
35
|
+
*
|
|
36
|
+
* NO THRESHOLD IS SET IN ADVANCE. The split AI council of 2026-09-07 flagged a
|
|
37
|
+
* proposed "≥ 80 % zero disagreement = ceremony" figure as invented; what
|
|
38
|
+
* counts as ceremony is read off the distribution once one exists. This tool
|
|
39
|
+
* therefore reports a distribution and never a verdict.
|
|
40
|
+
*
|
|
41
|
+
* NOT WIRED INTO THE `ci` CORPUS SWEEP, on the same grounds
|
|
42
|
+
* `lint_mandated_lines.ts` states for itself: there is no standing tree of run
|
|
43
|
+
* reports to scan, and a gate pointed at a root that does not accumulate is
|
|
44
|
+
* the dead-scan-root shape this repository has already paid for. It is a
|
|
45
|
+
* callable counter, invoked when someone wants the reading.
|
|
46
|
+
*
|
|
47
|
+
* An EMPTY population exits 2, not 0. A rate over nothing is not a reading —
|
|
48
|
+
* the same call `rdp_quality_eval --score-only` makes when a results file
|
|
49
|
+
* carries no transcript text.
|
|
50
|
+
*
|
|
51
|
+
* Sources are supplied explicitly so the reading is reproducible and the tool
|
|
52
|
+
* stays hermetic under test:
|
|
53
|
+
*
|
|
54
|
+
* ./scripts-run src/scripts/count_intent_disagreement --git-log
|
|
55
|
+
* ./scripts-run src/scripts/count_intent_disagreement --pr-bodies prs.json
|
|
56
|
+
* ./scripts-run src/scripts/count_intent_disagreement --dir path/to/reports
|
|
57
|
+
* ./scripts-run src/scripts/count_intent_disagreement --stdin < report.md
|
|
58
|
+
*
|
|
59
|
+
* Gather the durable population for this repository with:
|
|
60
|
+
*
|
|
61
|
+
* gh pr list --state merged --limit 400 --json number,body > prs.json
|
|
62
|
+
*
|
|
63
|
+
* Exit: 0 population read and classified · 1 usage/IO error · 2 empty population.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
import { execFileSync } from 'node:child_process';
|
|
67
|
+
import * as fs from 'node:fs';
|
|
68
|
+
import * as path from 'node:path';
|
|
69
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
70
|
+
|
|
71
|
+
import { stripFences, unwrapLines } from './lint_mandated_lines.js';
|
|
72
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
73
|
+
|
|
74
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
75
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
76
|
+
|
|
77
|
+
/** The slot separator the contract specifies. */
|
|
78
|
+
const SLOT_SEPARATOR = '·';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `Intent: …` — EMPHASIS-TOLERANT, and deliberately NOT the shipped `INTENT_RE`.
|
|
82
|
+
*
|
|
83
|
+
* The first version of this counter imported `INTENT_RE` from
|
|
84
|
+
* `lint_mandated_lines.ts` on the reasoning that reusing the gate's own pattern
|
|
85
|
+
* makes the reading authoritative. That was backwards. The shipped pattern
|
|
86
|
+
* anchors the bare label at line start and matches no emphasis, so
|
|
87
|
+
* `**Intent:**` — the form an assistant actually writes for a labelled line —
|
|
88
|
+
* is invisible to it. Measured 2026-09-08: the paid treatment run emitted its
|
|
89
|
+
* one compliant candidates line as `**Candidates:**` and a pattern of the
|
|
90
|
+
* shipped shape scored it absent.
|
|
91
|
+
*
|
|
92
|
+
* A COUNTER MUST BE WIDER THAN THE GATE IT REPORTS ON. Sharing the gate's blind
|
|
93
|
+
* spot makes the null self-confirming: the gate cannot see the line, so the
|
|
94
|
+
* counter cannot see the line, so the counter reports that the line is not
|
|
95
|
+
* emitted — which is a fact about the regex, not about the tree.
|
|
96
|
+
*
|
|
97
|
+
* The shipped gap is REAL and is recorded as a finding rather than fixed here:
|
|
98
|
+
* `lint_mandated_lines.ts` is a shipped gate with its own 19 tests, sitting
|
|
99
|
+
* against the contract the 2026-09-07 council blocked from changing, so
|
|
100
|
+
* widening its discrimination is its own change with its own review.
|
|
101
|
+
*
|
|
102
|
+
* CASE-SENSITIVE, and that is the other half of the correction. Widening the
|
|
103
|
+
* pattern with an `i` flag took the count from 0 to 172, and essentially every
|
|
104
|
+
* new match was a lowercase `intent:` YAML key in a prompt-pattern or command
|
|
105
|
+
* config — a line anchor turned into a prose detector. The contract specifies
|
|
106
|
+
* the label `Intent:` capitalised, an emitted line carries it that way, and a
|
|
107
|
+
* lowercase `intent:` at line start is data. Widening in one direction and not
|
|
108
|
+
* checking the other would have replaced a false null with a false population,
|
|
109
|
+
* which is the worse error: it looks like evidence.
|
|
110
|
+
*/
|
|
111
|
+
export const INTENT_LABEL_RE =
|
|
112
|
+
/^[ \t]*(?:>[ \t]*)?(?:[-*+][ \t]+)?(?:\*\*|__|\*|_)?Intent(?:\*\*|__|\*|_)?(?:[ \t]*\([^)]*\))?(?:\*\*|__|\*|_)?:(?:\*\*|__|\*|_)?[ \t]*(.+)$/gm;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Words carrying no discriminating content.
|
|
116
|
+
*
|
|
117
|
+
* Kept small on purpose. A large stop list makes two slots look identical
|
|
118
|
+
* because everything distinguishing them was filtered out, which would inflate
|
|
119
|
+
* the restatement count — the number this tool exists to report honestly.
|
|
120
|
+
*/
|
|
121
|
+
const STOPWORDS = new Set([
|
|
122
|
+
'a', 'an', 'and', 'as', 'at', 'be', 'but', 'by', 'for', 'from', 'in', 'is',
|
|
123
|
+
'it', 'its', 'of', 'on', 'or', 'that', 'the', 'to', 'was', 'were', 'with',
|
|
124
|
+
'says', 'say', 'expects', 'expect', 'does', 'do', 'should',
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
export type LineClass = 'malformed' | 'restatement' | 'distinct';
|
|
128
|
+
|
|
129
|
+
export interface ClassifiedLine {
|
|
130
|
+
/** Where it came from, for the report. */
|
|
131
|
+
source: string;
|
|
132
|
+
/** The raw text after the `Intent:` label. */
|
|
133
|
+
body: string;
|
|
134
|
+
slots: string[];
|
|
135
|
+
klass: LineClass;
|
|
136
|
+
/** Max pairwise Jaccard overlap across the three slots, for the distribution. */
|
|
137
|
+
maxOverlap: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface Population {
|
|
141
|
+
lines: ClassifiedLine[];
|
|
142
|
+
/** Per-source count, so a source contributing nothing is visible. */
|
|
143
|
+
bySource: Record<string, number>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Content words of a slot, lower-cased, stop-words and punctuation dropped. */
|
|
147
|
+
export function contentWords(slot: string): Set<string> {
|
|
148
|
+
const words = slot
|
|
149
|
+
.toLowerCase()
|
|
150
|
+
.replace(/`[^`]*`/g, (m) => m.replace(/`/g, ''))
|
|
151
|
+
.split(/[^a-z0-9_]+/)
|
|
152
|
+
.filter((w) => w.length > 2 && !STOPWORDS.has(w));
|
|
153
|
+
return new Set(words);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Jaccard overlap of two content-word sets. Empty-vs-anything is 0. */
|
|
157
|
+
export function overlap(a: Set<string>, b: Set<string>): number {
|
|
158
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
159
|
+
let shared = 0;
|
|
160
|
+
for (const w of a) if (b.has(w)) shared += 1;
|
|
161
|
+
const union = a.size + b.size - shared;
|
|
162
|
+
return union === 0 ? 0 : shared / union;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The overlap above which two slots are read as restating each other.
|
|
167
|
+
*
|
|
168
|
+
* A STATED DEFAULT, not a measured optimum — said plainly rather than implying
|
|
169
|
+
* a derivation it does not have. It is reported alongside every reading so a
|
|
170
|
+
* later reader can recompute at a different value from the published
|
|
171
|
+
* `maxOverlap` per line, which is why the per-line number is in the output.
|
|
172
|
+
*/
|
|
173
|
+
export const RESTATEMENT_OVERLAP = 0.6;
|
|
174
|
+
|
|
175
|
+
export function classify(body: string, source: string): ClassifiedLine {
|
|
176
|
+
const slots = body.split(SLOT_SEPARATOR).map((s) => s.trim()).filter((s) => s !== '');
|
|
177
|
+
if (slots.length < 3) {
|
|
178
|
+
return { source, body, slots, klass: 'malformed', maxOverlap: 0 };
|
|
179
|
+
}
|
|
180
|
+
const sets = slots.map(contentWords);
|
|
181
|
+
let maxOverlap = 0;
|
|
182
|
+
for (let i = 0; i < sets.length; i++) {
|
|
183
|
+
for (let j = i + 1; j < sets.length; j++) {
|
|
184
|
+
maxOverlap = Math.max(maxOverlap, overlap(sets[i] as Set<string>, sets[j] as Set<string>));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
maxOverlap = Math.round(maxOverlap * 1000) / 1000;
|
|
188
|
+
return {
|
|
189
|
+
source,
|
|
190
|
+
body,
|
|
191
|
+
slots,
|
|
192
|
+
klass: maxOverlap >= RESTATEMENT_OVERLAP ? 'restatement' : 'distinct',
|
|
193
|
+
maxOverlap,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Extract emitted intent lines from one text.
|
|
199
|
+
*
|
|
200
|
+
* Reuses `lint_mandated_lines`' own regex and its two preparation passes
|
|
201
|
+
* verbatim. That matters more than it looks: `stripFences` is what stops the
|
|
202
|
+
* contract's own illustrative example from counting as an emitted line, and
|
|
203
|
+
* this counter's whole finding turns on that distinction.
|
|
204
|
+
*/
|
|
205
|
+
export function extract(text: string, source: string): ClassifiedLine[] {
|
|
206
|
+
const prepared = unwrapLines(stripFences(text));
|
|
207
|
+
INTENT_LABEL_RE.lastIndex = 0;
|
|
208
|
+
return [...prepared.matchAll(INTENT_LABEL_RE)].map((m) =>
|
|
209
|
+
classify((m[1] ?? '').replace(/(?:\*\*|__|\*|_)+[ \t]*$/, '').trim(), source),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Decode a JSONL transcript into the prose it actually carries.
|
|
215
|
+
*
|
|
216
|
+
* WHY THIS IS NOT OPTIONAL. A session transcript is newline-delimited JSON, so
|
|
217
|
+
* the assistant prose inside it carries `\n` as a two-character ESCAPE, not as
|
|
218
|
+
* a newline. A line-anchored regex therefore matches nothing in a raw `.jsonl`
|
|
219
|
+
* and the source reads zero — a false null that looks exactly like a real one.
|
|
220
|
+
* The first version of this tool had that bug and reported `dir: 0` over 4,909
|
|
221
|
+
* transcript files while a plain grep found five candidate labels in them.
|
|
222
|
+
*
|
|
223
|
+
* Every string leaf is concatenated with a real newline between values, so a
|
|
224
|
+
* label that began a line in the original prose begins a line here too.
|
|
225
|
+
*/
|
|
226
|
+
export function decodeJsonl(raw: string): string {
|
|
227
|
+
const chunks: string[] = [];
|
|
228
|
+
const visit = (v: unknown): void => {
|
|
229
|
+
if (typeof v === 'string') chunks.push(v);
|
|
230
|
+
else if (Array.isArray(v)) for (const x of v) visit(x);
|
|
231
|
+
else if (v !== null && typeof v === 'object') for (const x of Object.values(v)) visit(x);
|
|
232
|
+
};
|
|
233
|
+
for (const line of raw.split('\n')) {
|
|
234
|
+
if (line.trim() === '') continue;
|
|
235
|
+
try {
|
|
236
|
+
visit(JSON.parse(line));
|
|
237
|
+
} catch {
|
|
238
|
+
// Not a JSON record — treat the line as prose rather than dropping
|
|
239
|
+
// it, so a mixed or truncated file degrades to the plain-text read.
|
|
240
|
+
chunks.push(line);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return chunks.join('\n');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function walkFiles(root: string): string[] {
|
|
247
|
+
const found: string[] = [];
|
|
248
|
+
const stack = [root];
|
|
249
|
+
while (stack.length > 0) {
|
|
250
|
+
const dir = stack.pop() as string;
|
|
251
|
+
let entries: fs.Dirent[];
|
|
252
|
+
try {
|
|
253
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
254
|
+
} catch {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
for (const e of entries) {
|
|
258
|
+
const p = path.join(dir, e.name);
|
|
259
|
+
if (e.isDirectory()) stack.push(p);
|
|
260
|
+
else if (e.isFile()) found.push(p);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return found.sort();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function gather(argv: readonly string[]): Population | { error: string } {
|
|
267
|
+
const lines: ClassifiedLine[] = [];
|
|
268
|
+
const bySource: Record<string, number> = {};
|
|
269
|
+
const add = (source: string, text: string): void => {
|
|
270
|
+
const got = extract(text, source);
|
|
271
|
+
bySource[source] = (bySource[source] ?? 0) + got.length;
|
|
272
|
+
lines.push(...got);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
if (argv.includes('--git-log')) {
|
|
276
|
+
try {
|
|
277
|
+
const body = execFileSync('git', ['log', '--all', '--format=%B'], {
|
|
278
|
+
cwd: REPO_ROOT,
|
|
279
|
+
encoding: 'utf-8',
|
|
280
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
281
|
+
});
|
|
282
|
+
add('git-log', body);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
return { error: `--git-log failed: ${String(e)}` };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const prIdx = argv.indexOf('--pr-bodies');
|
|
289
|
+
if (prIdx !== -1) {
|
|
290
|
+
const p = argv[prIdx + 1];
|
|
291
|
+
if (p === undefined) return { error: '--pr-bodies needs a path to a `gh pr list --json body` dump' };
|
|
292
|
+
try {
|
|
293
|
+
const raw = JSON.parse(fs.readFileSync(path.resolve(p), 'utf-8')) as Array<{ body?: string | null }>;
|
|
294
|
+
bySource['pr-bodies'] = 0;
|
|
295
|
+
for (const pr of raw) {
|
|
296
|
+
if (typeof pr.body === 'string' && pr.body !== '') add('pr-bodies', pr.body);
|
|
297
|
+
}
|
|
298
|
+
// A dump of N bodies that yields zero lines must still record that N
|
|
299
|
+
// bodies were read — otherwise an empty result is indistinguishable
|
|
300
|
+
// from a source that was never consulted.
|
|
301
|
+
process.stdout.write(` pr-bodies: read ${String(raw.length)} PR body/bodies\n`);
|
|
302
|
+
} catch (e) {
|
|
303
|
+
return { error: `cannot read ${p}: ${String(e)}` };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const dirIdx = argv.indexOf('--dir');
|
|
308
|
+
if (dirIdx !== -1) {
|
|
309
|
+
const d = argv[dirIdx + 1];
|
|
310
|
+
if (d === undefined) return { error: '--dir needs a path' };
|
|
311
|
+
const files = walkFiles(path.resolve(d));
|
|
312
|
+
bySource['dir'] = 0;
|
|
313
|
+
let jsonl = 0;
|
|
314
|
+
for (const f of files) {
|
|
315
|
+
try {
|
|
316
|
+
const raw = fs.readFileSync(f, 'utf-8');
|
|
317
|
+
if (f.endsWith('.jsonl') || f.endsWith('.json')) {
|
|
318
|
+
jsonl += 1;
|
|
319
|
+
add('dir', decodeJsonl(raw));
|
|
320
|
+
} else {
|
|
321
|
+
add('dir', raw);
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
process.stdout.write(
|
|
328
|
+
` dir: read ${String(files.length)} file(s) under ${d} ` +
|
|
329
|
+
`(${String(jsonl)} JSON/JSONL, string leaves decoded)\n`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (argv.includes('--stdin')) {
|
|
334
|
+
try {
|
|
335
|
+
add('stdin', fs.readFileSync(0, 'utf-8'));
|
|
336
|
+
} catch (e) {
|
|
337
|
+
return { error: `cannot read stdin: ${String(e)}` };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (Object.keys(bySource).length === 0) {
|
|
342
|
+
return {
|
|
343
|
+
error:
|
|
344
|
+
'no source selected. Pass at least one of --git-log, --pr-bodies <file>, --dir <path>, --stdin.\n' +
|
|
345
|
+
'Gather this repository\'s durable population with:\n' +
|
|
346
|
+
' gh pr list --state merged --limit 400 --json number,body > prs.json',
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return { lines, bySource };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function report(pop: Population, write: (s: string) => void): void {
|
|
353
|
+
const total = pop.lines.length;
|
|
354
|
+
const counts: Record<LineClass, number> = { malformed: 0, restatement: 0, distinct: 0 };
|
|
355
|
+
for (const l of pop.lines) counts[l.klass] += 1;
|
|
356
|
+
|
|
357
|
+
write('\nintent-line population\n');
|
|
358
|
+
for (const [src, n] of Object.entries(pop.bySource)) {
|
|
359
|
+
write(` ${src}: ${String(n)} emitted intent line(s)\n`);
|
|
360
|
+
}
|
|
361
|
+
write(` total: ${String(total)}\n`);
|
|
362
|
+
if (total === 0) return;
|
|
363
|
+
|
|
364
|
+
write('\nlayer 1 — deterministic classification\n');
|
|
365
|
+
write(` malformed (<3 slots): ${String(counts.malformed)}\n`);
|
|
366
|
+
write(` restatement (overlap >= ${RESTATEMENT_OVERLAP}): ${String(counts.restatement)}\n`);
|
|
367
|
+
write(` distinct: ${String(counts.distinct)}\n`);
|
|
368
|
+
const wellFormed = total - counts.malformed;
|
|
369
|
+
if (wellFormed > 0) {
|
|
370
|
+
const rate = Math.round((counts.restatement / wellFormed) * 1000) / 10;
|
|
371
|
+
write(` restatement rate over well-formed lines: ${String(rate)} %\n`);
|
|
372
|
+
}
|
|
373
|
+
write('\n per-line max pairwise slot overlap (recompute at another threshold from these)\n');
|
|
374
|
+
for (const [i, l] of pop.lines.entries()) {
|
|
375
|
+
write(` ${String(i + 1).padStart(3, ' ')}. [${l.klass}] overlap=${String(l.maxOverlap)} ${l.source} :: ${l.body.slice(0, 90)}\n`);
|
|
376
|
+
}
|
|
377
|
+
write(
|
|
378
|
+
'\nlayer 2 — semantic agree/disagree is NOT reported here. `distinct` is not ' +
|
|
379
|
+
'`disagree`: three slots can be worded differently and still say the same thing.\n',
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
384
|
+
if (argv.includes('--help') || argv.length === 0) {
|
|
385
|
+
process.stdout.write(
|
|
386
|
+
'usage: count_intent_disagreement [--git-log] [--pr-bodies <file.json>] [--dir <path>] [--stdin]\n',
|
|
387
|
+
);
|
|
388
|
+
return argv.length === 0 ? 1 : 0;
|
|
389
|
+
}
|
|
390
|
+
const got = gather(argv);
|
|
391
|
+
if ('error' in got) {
|
|
392
|
+
process.stderr.write(`${got.error}\n`);
|
|
393
|
+
return 1;
|
|
394
|
+
}
|
|
395
|
+
report(got, (s) => process.stdout.write(s));
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
assertScanned({
|
|
399
|
+
gate: 'count_intent_disagreement',
|
|
400
|
+
scanned: got.lines.length,
|
|
401
|
+
units: 'emitted intent line(s)',
|
|
402
|
+
roots: Object.keys(got.bySource),
|
|
403
|
+
});
|
|
404
|
+
} catch (e) {
|
|
405
|
+
if (e instanceof DeadScopeError) {
|
|
406
|
+
process.stderr.write(
|
|
407
|
+
`\n❌ ${e.message}\n\n` +
|
|
408
|
+
'This is a FINDING, not a tool failure. Zero emitted intent lines means the\n' +
|
|
409
|
+
'disagreement rate the `mandated-lines.md` lock asks for does not exist yet,\n' +
|
|
410
|
+
'because the line whose slots would disagree is not being emitted at all.\n' +
|
|
411
|
+
'Record the null. Do not read it as "no disagreements found".\n',
|
|
412
|
+
);
|
|
413
|
+
return 2;
|
|
414
|
+
}
|
|
415
|
+
throw e;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
process.stdout.write(`\nscanned: ${String(got.lines.length)}\n`);
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
423
|
+
process.exit(main());
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export { REPO_ROOT };
|