@event4u/agent-config 14.20.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 +441 -0
- package/CREDITS.md +37 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +10 -0
- package/dist/agent-src/commands/analyze/inbox.md +381 -6
- package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +251 -34
- package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
- package/dist/agent-src/commands/analyze.md +12 -7
- package/dist/agent-src/commands/feature/plan.md +55 -16
- package/dist/agent-src/commands/feature/refactor.md +8 -3
- package/dist/agent-src/commands/humanize.md +48 -4
- 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/contexts/execution/contract-decision-sheet.md +22 -1
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +4 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
- package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
- package/dist/agent-src/rules/evaluator-independence.md +4 -4
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
- 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/competitive-positioning/SKILL.md +2 -2
- package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
- package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
- package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/skills/technical-specification/SKILL.md +4 -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/agent-src/templates/features.md +5 -0
- package/dist/agent-src/templates/roadmaps.md +15 -0
- 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-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
- package/dist/cli-delegate/{chunk-PI2GU3DP.js → chunk-2TAQ2K4V.js} +4 -1
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/{chunk-C6IPZRDA.js → chunk-BIXEDQPZ.js} +7 -1
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/{chunk-NUMQHLS5.js → chunk-J3RFNKG4.js} +21 -2
- 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 +19 -5
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +27 -3
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.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/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 +60 -29
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -6
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -5
- package/dist/hooks/dispatch.js +1522 -1008
- 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 +10 -1
- package/dist/install/partitionEligibility.js +231 -117
- package/dist/install/ruleLayerPartition.js +10 -7
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +39376 -38902
- package/dist/scripts/ai_council/content_ceiling.js +63 -0
- package/dist/scripts/ai_council/seat_policy.js +106 -0
- package/dist/scripts/ai_council/transport_resolver.js +21 -0
- 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/llms.txt +14 -14
- 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/config/release-gate-locality.yml +4 -3
- 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/blocked_by_marker.ts +189 -0
- 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/host_capability.ts +38 -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/release_highlights.ts +19 -211
- package/src/scripts/_lib/release_material.ts +27 -77
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/structured_ask.ts +117 -0
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/_lib/tty_prompt.ts +76 -0
- package/src/scripts/ai_council/api_transport.ts +168 -0
- package/src/scripts/ai_council/clients.ts +58 -63
- package/src/scripts/ai_council/config.ts +13 -0
- package/src/scripts/ai_council/content_ceiling.ts +79 -0
- package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
- package/src/scripts/ai_council/jury_aggregate.ts +178 -0
- package/src/scripts/ai_council/prompts.ts +9 -0
- package/src/scripts/ai_council/seat_policy.ts +137 -0
- package/src/scripts/ai_council/status_surface.ts +79 -0
- package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
- package/src/scripts/ai_council/transport_resolver.ts +59 -1
- package/src/scripts/ai_tells_rules.ts +446 -5
- package/src/scripts/ask_block_census.ts +499 -0
- package/src/scripts/bench_humanizer_eval.ts +264 -40
- package/src/scripts/bench_prose_tells_fp.ts +198 -0
- package/src/scripts/capture_skill_catalogue.ts +24 -11
- package/src/scripts/check_branch_work_committed.ts +234 -0
- 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 +263 -24
- package/src/scripts/check_release_highlights.ts +0 -157
- 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 +29 -39
- package/src/scripts/count_intent_disagreement.ts +426 -0
- package/src/scripts/detect_ai_tells.ts +142 -20
- package/src/scripts/handoff_context_hook.ts +79 -0
- package/src/scripts/harvest_reference_tokens.ts +698 -0
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +94 -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 +6 -2
- package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
- package/src/scripts/hooks/host_semantics.ts +1 -1
- package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
- 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 +134 -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/lint_roadmap_blockers.ts +51 -1
- 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/migrate_command_suggestions.ts +1 -1
- package/src/scripts/probe_unblocked_ask.ts +135 -2
- package/src/scripts/rdp_candidates_delta.ts +276 -0
- package/src/scripts/rdp_quality_eval.ts +217 -19
- package/src/scripts/release.ts +26 -71
- package/src/scripts/release_drill.ts +6 -10
- package/src/scripts/release_env.ts +1 -0
- package/src/scripts/release_mix_taxonomy.json +3 -2
- package/src/scripts/release_publication.ts +34 -29
- 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/self_review_gate.ts +409 -15
- 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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse guard: one question per structured-ask call.
|
|
4
|
+
*
|
|
5
|
+
* road-to-asked-not-parked 5.1. `ask-when-uncertain`'s Iron Law is one
|
|
6
|
+
* question per turn, and `user-interaction` § question pacing narrows what
|
|
7
|
+
* "one" means: one decision point, answerable with one reply. A host's native
|
|
8
|
+
* question picker makes it cheap to violate that — a payload can carry N
|
|
9
|
+
* questions in one call, and the user then answers a form instead of a
|
|
10
|
+
* question.
|
|
11
|
+
*
|
|
12
|
+
* DENY, NEVER TRUNCATE. Silently keeping the first question and dropping the
|
|
13
|
+
* rest would hide exactly the decisions the user was owed; the deny message
|
|
14
|
+
* names the count so the caller can split the ask instead of guessing what
|
|
15
|
+
* happened.
|
|
16
|
+
*
|
|
17
|
+
* WHAT IT SEES, honestly. `_lib/structured_ask.ts` carries no observed
|
|
18
|
+
* per-host tool, so this guard matches on a name SHAPE and is filtered by the
|
|
19
|
+
* manifest's per-concern `tools:` key to the same candidate names. On every
|
|
20
|
+
* host measured today it therefore fires on nothing — the concern exists so
|
|
21
|
+
* that the first host to ship a picker meets the rule already in force, not
|
|
22
|
+
* because a violation has been observed.
|
|
23
|
+
*
|
|
24
|
+
* ENFORCEMENT REACH, also honestly. `fail_closed: false` and the exit-1 deny
|
|
25
|
+
* are only a deny where the host both binds `pre_tool_use` AND honours the
|
|
26
|
+
* dispatcher's verdict. `hook-architecture-v1.md` § Which hosts carry
|
|
27
|
+
* pre_tool_use is the table; `agent-config hooks:status` answers it for the
|
|
28
|
+
* host actually running. This file claims no more than that.
|
|
29
|
+
*
|
|
30
|
+
* A payload whose shape is not recognised at all (`countStructuredAskQuestions`
|
|
31
|
+
* returns -1) is ALLOWED. A guard that blocks what it cannot parse would turn
|
|
32
|
+
* an unknown picker shape into a wedged session, and the failure it guards
|
|
33
|
+
* against — too many questions — is a shape defect, not a safety one.
|
|
34
|
+
*
|
|
35
|
+
* Exit codes (docs/contracts/hook-architecture-v1.md):
|
|
36
|
+
* 0 — allow
|
|
37
|
+
* 1 — block (the call carries more than one question)
|
|
38
|
+
*/
|
|
39
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
40
|
+
|
|
41
|
+
import { countStructuredAskQuestions, isStructuredAskTool } from '../_lib/structured_ask.js';
|
|
42
|
+
import { readHookStdin } from './hook_stdin.js';
|
|
43
|
+
|
|
44
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
45
|
+
|
|
46
|
+
type JsonObject = Record<string, unknown>;
|
|
47
|
+
|
|
48
|
+
function _isObject(v: unknown): v is JsonObject {
|
|
49
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AskCall {
|
|
53
|
+
readonly tool: string;
|
|
54
|
+
readonly questions: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read (tool name, question count) off a PreToolUse envelope. */
|
|
58
|
+
export function extractAskCall(envelope: JsonObject): AskCall {
|
|
59
|
+
const payload = _isObject(envelope['payload']) ? envelope['payload'] : envelope;
|
|
60
|
+
const nameVal =
|
|
61
|
+
payload['tool_name'] ??
|
|
62
|
+
payload['toolName'] ??
|
|
63
|
+
payload['tool'] ??
|
|
64
|
+
envelope['tool_name'] ??
|
|
65
|
+
envelope['tool'];
|
|
66
|
+
const tool = typeof nameVal === 'string' ? nameVal : '';
|
|
67
|
+
const ti = _isObject(payload['tool_input'])
|
|
68
|
+
? payload['tool_input']
|
|
69
|
+
: _isObject(envelope['tool_input'])
|
|
70
|
+
? envelope['tool_input']
|
|
71
|
+
: null;
|
|
72
|
+
return { tool, questions: ti === null ? -1 : countStructuredAskQuestions(ti) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The verdict, separated from I/O so the fixtures exercise the decision itself. */
|
|
76
|
+
export function verdict(call: AskCall): { block: boolean; reason: string } {
|
|
77
|
+
if (call.tool === '' || !isStructuredAskTool(call.tool)) {
|
|
78
|
+
return { block: false, reason: 'not a structured-ask call' };
|
|
79
|
+
}
|
|
80
|
+
if (call.questions < 0) {
|
|
81
|
+
return { block: false, reason: 'payload shape not recognised — allowed, never guessed' };
|
|
82
|
+
}
|
|
83
|
+
if (call.questions <= 1) {
|
|
84
|
+
return { block: false, reason: 'one question' };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
block: true,
|
|
88
|
+
reason:
|
|
89
|
+
`${call.tool} carries ${String(call.questions)} questions in one call. ` +
|
|
90
|
+
'One decision per ask: put the first question, wait for the answer, then ' +
|
|
91
|
+
'the next. The call is denied rather than truncated — dropping the extra ' +
|
|
92
|
+
'questions would hide the decisions the user is owed.',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function main(argv?: string[]): number {
|
|
97
|
+
const args = argv ?? process.argv.slice(2);
|
|
98
|
+
for (const a of args) {
|
|
99
|
+
if (a === '--self-test') {
|
|
100
|
+
return selfTest();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const raw = readHookStdin();
|
|
104
|
+
if (!raw.trim()) {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
let envelope: JsonObject;
|
|
108
|
+
try {
|
|
109
|
+
const parsed: unknown = JSON.parse(raw);
|
|
110
|
+
envelope = _isObject(parsed) ? parsed : {};
|
|
111
|
+
} catch {
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
const v = verdict(extractAskCall(envelope));
|
|
115
|
+
if (v.block) {
|
|
116
|
+
process.stderr.write(`one-question-per-ask: BLOCKED — ${v.reason}\n`);
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function selfTest(): number {
|
|
123
|
+
const cases: Array<[string, JsonObject, boolean]> = [
|
|
124
|
+
[
|
|
125
|
+
'two questions in one call is denied',
|
|
126
|
+
{ tool_name: 'AskUserQuestion', tool_input: { questions: [{}, {}] } },
|
|
127
|
+
true,
|
|
128
|
+
],
|
|
129
|
+
[
|
|
130
|
+
'four questions is denied',
|
|
131
|
+
{ tool_name: 'AskUserQuestion', tool_input: { questions: [{}, {}, {}, {}] } },
|
|
132
|
+
true,
|
|
133
|
+
],
|
|
134
|
+
[
|
|
135
|
+
'one question passes',
|
|
136
|
+
{ tool_name: 'AskUserQuestion', tool_input: { questions: [{}] } },
|
|
137
|
+
false,
|
|
138
|
+
],
|
|
139
|
+
[
|
|
140
|
+
'a single-question field passes',
|
|
141
|
+
{ tool_name: 'ask_user_question', tool_input: { question: 'which one?' } },
|
|
142
|
+
false,
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
'an empty ask passes — malformed, but not this guard\'s failure',
|
|
146
|
+
{ tool_name: 'AskUserQuestion', tool_input: { questions: [] } },
|
|
147
|
+
false,
|
|
148
|
+
],
|
|
149
|
+
[
|
|
150
|
+
'an unrecognised payload shape passes rather than wedging the session',
|
|
151
|
+
{ tool_name: 'AskUserQuestion', tool_input: { something: 'else' } },
|
|
152
|
+
false,
|
|
153
|
+
],
|
|
154
|
+
['an ordinary tool is never seen', { tool_name: 'Bash', tool_input: { command: 'ls' } }, false],
|
|
155
|
+
[
|
|
156
|
+
'a Write with two questions in its body is not an ask call',
|
|
157
|
+
{ tool_name: 'Write', tool_input: { questions: [{}, {}] } },
|
|
158
|
+
false,
|
|
159
|
+
],
|
|
160
|
+
['an empty envelope passes', {}, false],
|
|
161
|
+
];
|
|
162
|
+
let failed = 0;
|
|
163
|
+
for (const [name, envelope, want] of cases) {
|
|
164
|
+
const got = verdict(extractAskCall(envelope)).block;
|
|
165
|
+
const ok = got === want;
|
|
166
|
+
process.stdout.write(
|
|
167
|
+
`${ok ? '✅' : '❌'} ${name} — block=${String(got)}` +
|
|
168
|
+
(ok ? '\n' : ` (wanted ${String(want)})\n`),
|
|
169
|
+
);
|
|
170
|
+
if (!ok) failed += 1;
|
|
171
|
+
}
|
|
172
|
+
const positives = cases.filter((c) => c[2]).length;
|
|
173
|
+
if (positives < 2) {
|
|
174
|
+
process.stderr.write(
|
|
175
|
+
'❌ one_question_per_ask --self-test: fewer than 2 deny cases — a guard never ' +
|
|
176
|
+
'seen refuse has unknown sensitivity.\n',
|
|
177
|
+
);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
process.stdout.write(
|
|
181
|
+
`\none_question_per_ask --self-test: ${String(cases.length - failed)}/${String(cases.length)} ` +
|
|
182
|
+
`case(s) behaved (${String(positives)} deny, ${String(cases.length - positives)} allow)\n`,
|
|
183
|
+
);
|
|
184
|
+
return failed > 0 ? 1 : 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
188
|
+
process.exit(main());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export { _HERE };
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Fires only on an `Agent` / `Task` tool-use completion (the two names this
|
|
17
17
|
* repo has observed for the subagent-dispatch tool across hosts/versions).
|
|
18
18
|
* Every other tool call is a silent no-op — same tool-filter discipline as
|
|
19
|
-
* `code-graph-
|
|
19
|
+
* `code-graph-context` (`tools: [Agent, Task]` in the manifest entry keeps the
|
|
20
20
|
* dispatcher from paying this concern's cost on unrelated calls).
|
|
21
21
|
*
|
|
22
22
|
* Sync vs async (road-to-orchestrator-first-execution measured: 326 of 370
|
|
@@ -89,7 +89,7 @@ export const DISPATCH_TOOL_NAMES: ReadonlySet<string> = new Set(['Agent', 'Task'
|
|
|
89
89
|
* Unwrap the dispatcher envelope (`{schema_version, platform, event,
|
|
90
90
|
* payload}`) down to the platform-native payload. Falls back to the
|
|
91
91
|
* top-level object for direct/legacy invocation (same shape both
|
|
92
|
-
* `pr_url_reminder_hook.ts` and `
|
|
92
|
+
* `pr_url_reminder_hook.ts` and `code_graph_context_hook.ts` handle).
|
|
93
93
|
*/
|
|
94
94
|
export function unwrapPayload(envelope: JsonObject): JsonObject {
|
|
95
95
|
const inner = envelope['payload'];
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* NEVER BLOCKS. Every failure path returns 0: unreadable stdin, malformed JSON,
|
|
35
35
|
* missing router, unreadable body, unwritable state. The one non-zero exit is
|
|
36
36
|
* the host's advisory context channel (exit 2 + `decision: "warn"`), the same
|
|
37
|
-
* channel `ui-route-nudge` and `code-graph-
|
|
37
|
+
* channel `ui-route-nudge` and `code-graph-context` already use on `pre_tool_use`
|
|
38
38
|
* with `severity: advisory` — a warn there is an injection, not a deny.
|
|
39
39
|
*
|
|
40
40
|
* HOST BOUNDARY, STATED RATHER THAN IMPLIED. `user_prompt_submit` and
|
|
@@ -140,7 +140,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
140
140
|
|
|
141
141
|
import { readHookStdin } from "./hook_stdin.js";
|
|
142
142
|
import { isSyntheticPrompt } from "../_lib/prompt_shape.js";
|
|
143
|
-
import { OBSERVATION_LOG, readObservationLog,
|
|
143
|
+
import { OBSERVATION_LOG, readObservationLog, resolveSkillCatalogueRoots } from "../_lib/skill_catalogue.js";
|
|
144
144
|
import { knownBareNames } from "../_lib/skill_catalogue_series.js";
|
|
145
145
|
import { _tokenize, rank, type RankRow } from "../skill_tools/score_skill_relevance.js";
|
|
146
146
|
|
|
@@ -252,13 +252,20 @@ export interface RouteDecision {
|
|
|
252
252
|
*/
|
|
253
253
|
export function routeDecision(
|
|
254
254
|
prompt: string,
|
|
255
|
-
|
|
255
|
+
// One root, or every readable root. `null` and `[]` are the same answer — no
|
|
256
|
+
// catalogue was read — and both must stay silent rather than rank nothing and
|
|
257
|
+
// call that a result.
|
|
258
|
+
skillsDir: string | readonly string[] | null,
|
|
256
259
|
bareProvider?: () => Set<string> | null,
|
|
257
260
|
): RouteDecision {
|
|
258
261
|
const silent: RouteDecision = { rows: [], suppressed: 0 };
|
|
259
262
|
if (skillsDir === null) return silent;
|
|
263
|
+
if (Array.isArray(skillsDir) && skillsDir.length === 0) return silent;
|
|
260
264
|
// Denominator floor BEFORE the catalogue read: a prompt too short to score
|
|
261
|
-
// meaningfully cannot produce a pointer worth
|
|
265
|
+
// meaningfully cannot produce a pointer worth the read either. The count is
|
|
266
|
+
// deliberately not stated — it used to say "337 file reads", which was one
|
|
267
|
+
// root; the union reads every readable root and the figure depends on the
|
|
268
|
+
// machine's install.
|
|
262
269
|
try {
|
|
263
270
|
if (_tokenize(prompt).size < MIN_TASK_TERMS) return silent;
|
|
264
271
|
} catch {
|
|
@@ -346,7 +353,7 @@ export function main(): number {
|
|
|
346
353
|
const root = _workspaceRoot(env);
|
|
347
354
|
const platform = env["platform"];
|
|
348
355
|
const host = typeof platform === "string" && platform ? platform : null;
|
|
349
|
-
const { rows, suppressed } = routeDecision(prompt,
|
|
356
|
+
const { rows, suppressed } = routeDecision(prompt, resolveSkillCatalogueRoots(root), () =>
|
|
350
357
|
knownBareForHost(root, host),
|
|
351
358
|
);
|
|
352
359
|
if (rows.length === 0) return EXIT_ALLOW; // silence is the default
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* inbox_source_census — the denominator an inbox run cannot shrink by reading badly.
|
|
4
|
+
*
|
|
5
|
+
* THE GAP THIS CLOSES.
|
|
6
|
+
*
|
|
7
|
+
* `/analyze:inbox` already ends with a **point ledger**: points extracted in its
|
|
8
|
+
* Phase 3 against points dispositioned in Phases 4–5b, per bucket, as counts
|
|
9
|
+
* that must balance. That ledger proves the run answered what it read. It
|
|
10
|
+
* cannot prove the run read what arrived — a point nobody extracted appears in
|
|
11
|
+
* neither column, and the ledger balances perfectly without it. Under-extraction
|
|
12
|
+
* is invisible to the only completeness check the command has.
|
|
13
|
+
*
|
|
14
|
+
* Measured shape of the input that exposed it: round `inbox-2026-09-r` arrived
|
|
15
|
+
* as 12 topic folders, 67 files and ~55,800 lines, where one folder alone held
|
|
16
|
+
* six revisions of a plan beside a 58 KB transcript. Read per file, the
|
|
17
|
+
* revisions look like six sources; read as a set, they are one argument whose
|
|
18
|
+
* later revisions silently drop items the earlier ones carried.
|
|
19
|
+
*
|
|
20
|
+
* WHAT THIS COUNTS — AND WHAT IT DELIBERATELY DOES NOT.
|
|
21
|
+
*
|
|
22
|
+
* Only what is **mechanical and genre-independent**:
|
|
23
|
+
*
|
|
24
|
+
* 1. **Source sets** — files grouped by stem, so a revision family
|
|
25
|
+
* (`x.v1/x.v2`, `x.consolidated.v1`, `x-loop-1`, `x-2026-09-06`) reports as
|
|
26
|
+
* ONE set with its members ordered, instead of as N unrelated files.
|
|
27
|
+
* 2. **Anchors** — the four countable structures a source can carry: turn
|
|
28
|
+
* separators, headings, checkbox steps, and numbered items. Every class is
|
|
29
|
+
* counted on every file; the run picks the ones its genre uses.
|
|
30
|
+
* 3. **Decision ids** — the `D2` / `E1` / `K7` tokens a source argues in. If
|
|
31
|
+
* those do not appear in what the run emits, the emitted plan is
|
|
32
|
+
* undecidable by construction and no reader can tell, because the ids left
|
|
33
|
+
* with the source.
|
|
34
|
+
*
|
|
35
|
+
* It does NOT guess genre, judge relevance, or extract demands. Those are the
|
|
36
|
+
* reading, and the whole point of a denominator is that it is produced without
|
|
37
|
+
* one — a census that needed judgement would be as shrinkable as the reading it
|
|
38
|
+
* is supposed to bound.
|
|
39
|
+
*
|
|
40
|
+
* HONEST LIMIT.
|
|
41
|
+
*
|
|
42
|
+
* An anchor count is a floor on what must be accounted for, never a target and
|
|
43
|
+
* never a quality measure. A 400-anchor source can carry three demands and a
|
|
44
|
+
* 12-anchor transcript can carry twelve. What the number buys is that a run
|
|
45
|
+
* reporting "41 anchors → 41 accounted" can be challenged by anyone holding the
|
|
46
|
+
* folder, where a prose summary cannot.
|
|
47
|
+
*
|
|
48
|
+
* USAGE.
|
|
49
|
+
*
|
|
50
|
+
* ./scripts-run src/scripts/inbox_source_census --root agents/tmp/<round-id>
|
|
51
|
+
* ./scripts-run src/scripts/inbox_source_census --root <dir> --json
|
|
52
|
+
*
|
|
53
|
+
* Exit 2 when the root is missing, unreadable, or enumerates zero files — a
|
|
54
|
+
* census over an empty set is vacuous and must not read as coverage.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import * as fs from 'node:fs';
|
|
58
|
+
import * as path from 'node:path';
|
|
59
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
60
|
+
|
|
61
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
62
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
63
|
+
|
|
64
|
+
/** Extensions worth counting. Binaries and archives carry no anchors. */
|
|
65
|
+
const TEXT_EXT = new Set(['.md', '.txt', '.markdown', '.mdx']);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Version markers that make two files revisions of one source rather than two
|
|
69
|
+
* sources. Ordered most-specific first: `consolidated.v2` must not be stripped
|
|
70
|
+
* to `consolidated` by the bare `.vN` rule.
|
|
71
|
+
*/
|
|
72
|
+
const VERSION_MARKERS: readonly RegExp[] = [
|
|
73
|
+
/[.-]consolidated[.-]v\d+$/i,
|
|
74
|
+
/[.-]consolidated$/i,
|
|
75
|
+
/[.-]loop[.-]?\d+.*$/i,
|
|
76
|
+
/[.-]v\d+$/i,
|
|
77
|
+
/[.-]\d{4}-\d{2}-\d{2}$/,
|
|
78
|
+
/[.-](final|draft|deep|master)$/i,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
export interface AnchorCounts {
|
|
82
|
+
/** `----`-style turn separators — the transcript spine. */
|
|
83
|
+
separators: number;
|
|
84
|
+
/** ATX headings, h1 through h4. */
|
|
85
|
+
headings: number;
|
|
86
|
+
/** `- [ ]` / `- [x]` / `- [~]` / `- [-]` roadmap steps. */
|
|
87
|
+
steps: number;
|
|
88
|
+
/** `1.`-style numbered items and `**bold lead-in**` paragraph openers. */
|
|
89
|
+
enumerated: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface FileCensus {
|
|
93
|
+
file: string;
|
|
94
|
+
bytes: number;
|
|
95
|
+
lines: number;
|
|
96
|
+
anchors: AnchorCounts;
|
|
97
|
+
/** Sum of the four anchor classes — the file's floor. */
|
|
98
|
+
total: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SourceSet {
|
|
102
|
+
/** The shared stem the members were grouped under. */
|
|
103
|
+
stem: string;
|
|
104
|
+
/** Members, oldest-looking first (lexical on the version marker). */
|
|
105
|
+
members: FileCensus[];
|
|
106
|
+
/** True when more than one member shares the stem. */
|
|
107
|
+
isRevisionSet: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface TopicCensus {
|
|
111
|
+
topic: string;
|
|
112
|
+
sets: SourceSet[];
|
|
113
|
+
decisionIds: string[];
|
|
114
|
+
fileCount: number;
|
|
115
|
+
anchorTotal: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Strip a trailing version marker to the stem two revisions share.
|
|
120
|
+
*
|
|
121
|
+
* Applied repeatedly, most-specific first, so `road-to-x.consolidated.v3`
|
|
122
|
+
* reduces through `road-to-x.consolidated` to `road-to-x` — otherwise a
|
|
123
|
+
* `.consolidated` family and a plain `.vN` family of the same plan report as
|
|
124
|
+
* two sets and the diff obligation never fires on the pair.
|
|
125
|
+
*/
|
|
126
|
+
export function stemOf(filename: string): string {
|
|
127
|
+
let s = filename.replace(/\.(md|markdown|mdx|txt)$/i, '');
|
|
128
|
+
let changed = true;
|
|
129
|
+
while (changed) {
|
|
130
|
+
changed = false;
|
|
131
|
+
for (const re of VERSION_MARKERS) {
|
|
132
|
+
const next = s.replace(re, '');
|
|
133
|
+
if (next !== s && next.length > 0) {
|
|
134
|
+
s = next;
|
|
135
|
+
changed = true;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return s;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Count the four anchor classes. Fenced code is skipped — a fence is content. */
|
|
144
|
+
export function countAnchors(text: string): AnchorCounts {
|
|
145
|
+
const out: AnchorCounts = { separators: 0, headings: 0, steps: 0, enumerated: 0 };
|
|
146
|
+
let inFence = false;
|
|
147
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
148
|
+
const line = raw.trimEnd();
|
|
149
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
150
|
+
inFence = !inFence;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (inFence) continue;
|
|
154
|
+
if (/^-{4,}$|^={4,}$|^\*{4,}$/.test(line.trim())) {
|
|
155
|
+
out.separators += 1;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (/^#{1,4} \S/.test(line)) {
|
|
159
|
+
out.headings += 1;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (/^\s*[-*] \[[ xX~-]\]/.test(line)) {
|
|
163
|
+
out.steps += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (/^\s*\d+[.)] \S/.test(line) || /^\*\*[^*]+\*\*/.test(line)) {
|
|
167
|
+
out.enumerated += 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Decision / open-question ids the source argues in — `D2`, `E1`, `K7`, `AC3`.
|
|
175
|
+
*
|
|
176
|
+
* Deliberately narrow: one to three capitals followed by one or two digits, as
|
|
177
|
+
* a whole token. A wider pattern matches version strings, HTTP codes and every
|
|
178
|
+
* `P0` in ordinary prose, and a noisy id set is worse than none — the run would
|
|
179
|
+
* stop reading the row.
|
|
180
|
+
*/
|
|
181
|
+
export function decisionIds(text: string): string[] {
|
|
182
|
+
const hits = text.match(/\b[A-Z]{1,3}\d{1,2}\b/g) ?? [];
|
|
183
|
+
return [...new Set(hits)].sort();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function censusFile(abs: string, rel: string): FileCensus {
|
|
187
|
+
const text = fs.readFileSync(abs, 'utf-8');
|
|
188
|
+
const anchors = countAnchors(text);
|
|
189
|
+
return {
|
|
190
|
+
file: rel,
|
|
191
|
+
bytes: Buffer.byteLength(text),
|
|
192
|
+
lines: text.split(/\r?\n/).length,
|
|
193
|
+
anchors,
|
|
194
|
+
total: anchors.separators + anchors.headings + anchors.steps + anchors.enumerated,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Group one topic folder's files into source sets and count its anchors. */
|
|
199
|
+
export function censusTopic(dir: string, topic: string): TopicCensus {
|
|
200
|
+
const names = fs
|
|
201
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
202
|
+
.filter((d) => d.isFile() && TEXT_EXT.has(path.extname(d.name).toLowerCase()))
|
|
203
|
+
.map((d) => d.name)
|
|
204
|
+
.sort();
|
|
205
|
+
|
|
206
|
+
const byStem = new Map<string, FileCensus[]>();
|
|
207
|
+
const ids = new Set<string>();
|
|
208
|
+
for (const name of names) {
|
|
209
|
+
const abs = path.join(dir, name);
|
|
210
|
+
const fc = censusFile(abs, name);
|
|
211
|
+
for (const id of decisionIds(fs.readFileSync(abs, 'utf-8'))) ids.add(id);
|
|
212
|
+
const stem = stemOf(name);
|
|
213
|
+
const bucket = byStem.get(stem);
|
|
214
|
+
if (bucket) bucket.push(fc);
|
|
215
|
+
else byStem.set(stem, [fc]);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const sets: SourceSet[] = [...byStem.entries()]
|
|
219
|
+
.map(([stem, members]) => ({
|
|
220
|
+
stem,
|
|
221
|
+
members: members.sort((a, b) => a.file.localeCompare(b.file)),
|
|
222
|
+
isRevisionSet: members.length > 1,
|
|
223
|
+
}))
|
|
224
|
+
.sort((a, b) => a.stem.localeCompare(b.stem));
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
topic,
|
|
228
|
+
sets,
|
|
229
|
+
decisionIds: [...ids].sort(),
|
|
230
|
+
fileCount: names.length,
|
|
231
|
+
anchorTotal: sets.reduce(
|
|
232
|
+
(n, s) => n + s.members.reduce((m, f) => m + f.total, 0),
|
|
233
|
+
0,
|
|
234
|
+
),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Walk a round root: each immediate subdirectory is a topic, plus the root itself. */
|
|
239
|
+
export function censusRoot(root: string): TopicCensus[] {
|
|
240
|
+
const out: TopicCensus[] = [];
|
|
241
|
+
const rootLevel = censusTopic(root, '.');
|
|
242
|
+
if (rootLevel.fileCount > 0) out.push(rootLevel);
|
|
243
|
+
for (const d of fs.readdirSync(root, { withFileTypes: true })) {
|
|
244
|
+
if (!d.isDirectory()) continue;
|
|
245
|
+
const t = censusTopic(path.join(root, d.name), d.name);
|
|
246
|
+
if (t.fileCount > 0) out.push(t);
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function render(topics: TopicCensus[], write: (s: string) => unknown): void {
|
|
252
|
+
let files = 0;
|
|
253
|
+
let anchors = 0;
|
|
254
|
+
let revisionSets = 0;
|
|
255
|
+
for (const t of topics) {
|
|
256
|
+
write(`\n## ${t.topic}\n`);
|
|
257
|
+
for (const s of t.sets) {
|
|
258
|
+
const tag = s.isRevisionSet ? ` — REVISION SET (${String(s.members.length)}), diff consecutive members` : '';
|
|
259
|
+
write(` ${s.stem}${tag}\n`);
|
|
260
|
+
for (const f of s.members) {
|
|
261
|
+
const a = f.anchors;
|
|
262
|
+
write(
|
|
263
|
+
` ${f.file} ${String(f.bytes)}B/${String(f.lines)}L ` +
|
|
264
|
+
`sep ${String(a.separators)} · head ${String(a.headings)} · ` +
|
|
265
|
+
`step ${String(a.steps)} · enum ${String(a.enumerated)} = ${String(f.total)}\n`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (s.isRevisionSet) revisionSets += 1;
|
|
269
|
+
}
|
|
270
|
+
if (t.decisionIds.length > 0) write(` decision ids: ${t.decisionIds.join(' ')}\n`);
|
|
271
|
+
files += t.fileCount;
|
|
272
|
+
anchors += t.anchorTotal;
|
|
273
|
+
}
|
|
274
|
+
write(
|
|
275
|
+
`\ntopics ${String(topics.length)} · files ${String(files)} · ` +
|
|
276
|
+
`revision sets ${String(revisionSets)} · anchors ${String(anchors)}\n` +
|
|
277
|
+
'Every anchor leaves the run carrying a ledger row or named in one `no-demand` line.\n' +
|
|
278
|
+
'Every revision set owes a consecutive-member diff (/analyze:inbox Phase 2b).\n',
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
283
|
+
const rootIdx = argv.indexOf('--root');
|
|
284
|
+
const rootArg = rootIdx === -1 ? undefined : argv[rootIdx + 1];
|
|
285
|
+
if (rootArg === undefined) {
|
|
286
|
+
process.stderr.write('usage: inbox_source_census --root <dir> [--json]\n');
|
|
287
|
+
return 2;
|
|
288
|
+
}
|
|
289
|
+
const root = path.resolve(REPO_ROOT, rootArg);
|
|
290
|
+
let stat: fs.Stats;
|
|
291
|
+
try {
|
|
292
|
+
stat = fs.statSync(root);
|
|
293
|
+
} catch {
|
|
294
|
+
process.stderr.write(`❌ inbox_source_census: root not readable: ${root}\n`);
|
|
295
|
+
return 2;
|
|
296
|
+
}
|
|
297
|
+
if (!stat.isDirectory()) {
|
|
298
|
+
process.stderr.write(`❌ inbox_source_census: root is not a directory: ${root}\n`);
|
|
299
|
+
return 2;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const topics = censusRoot(root);
|
|
303
|
+
const files = topics.reduce((n, t) => n + t.fileCount, 0);
|
|
304
|
+
if (files === 0) {
|
|
305
|
+
process.stderr.write(
|
|
306
|
+
`❌ inbox_source_census: 0 text file(s) under ${root} — a census over an ` +
|
|
307
|
+
'empty set is vacuous and must not read as coverage.\n',
|
|
308
|
+
);
|
|
309
|
+
return 2;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (argv.includes('--json')) {
|
|
313
|
+
process.stdout.write(`${JSON.stringify({ root, topics }, null, 2)}\n`);
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
render(topics, (s) => process.stdout.write(s));
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
321
|
+
process.exit(main());
|
|
322
|
+
}
|