@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,282 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse chain nudge — deterministic, warn-only.
|
|
4
|
+
*
|
|
5
|
+
* WHY A CARRIER EXISTS AT ALL. `token-efficiency` § One command per Bash call
|
|
6
|
+
* is `type: auto`, so its triggers match the PROMPT, and no prompt announces
|
|
7
|
+
* that the next tool call will be a chained Bash command. The obligation
|
|
8
|
+
* applies at tool-call time and was reachable by nothing there. This concern
|
|
9
|
+
* is that reach, and it is the only thing in the tree that reads
|
|
10
|
+
* `tool_input.command` for this purpose.
|
|
11
|
+
*
|
|
12
|
+
* WHAT IT IS FOR, MEASURED. The host splits a compound command on `&&`, `||`,
|
|
13
|
+
* `;`, `|`, `|&`, `&` and newlines and requires each segment to match the
|
|
14
|
+
* allowlist independently, so one unmatched segment sends the whole call down
|
|
15
|
+
* the permission path even when every other segment was already allowed. Over
|
|
16
|
+
* 40,268 real Bash calls on the maintainer's machine, 17.9 % had a head token
|
|
17
|
+
* matching no pattern and 5,037 of those were a leading `VAR=…` assignment —
|
|
18
|
+
* a shape that cannot be written as an allowlist pattern at all. That class
|
|
19
|
+
* shrinks only by not writing it.
|
|
20
|
+
*
|
|
21
|
+
* WHAT IT DOES NOT FLAG, DELIBERATELY. A pipe of ordinary filters
|
|
22
|
+
* (`grep foo file | head`) is one command with a filter, not two work steps.
|
|
23
|
+
* A redirect has its target checked against the file rules on its own. A
|
|
24
|
+
* heredoc is a command with input. Flagging any of those would make the
|
|
25
|
+
* nudge noise, and a nudge that fires on correct usage is worse than none.
|
|
26
|
+
*
|
|
27
|
+
* NEVER BLOCKS. The dispatcher contract is 0 allow · 2 warn, and a warn on
|
|
28
|
+
* `pre_tool_use` with `severity: advisory` is an injection, not a deny — the
|
|
29
|
+
* same channel `code-graph-nudge` and `ui-route-nudge` already use. Every
|
|
30
|
+
* failure path returns allow: unreadable stdin, malformed JSON, unwritable
|
|
31
|
+
* latch. A token optimisation must never break a tool call.
|
|
32
|
+
*
|
|
33
|
+
* ONCE PER SESSION. A latch keyed by session id, in the state directory
|
|
34
|
+
* `code-graph-nudge.json` already occupies. The rule body reaches the model
|
|
35
|
+
* once, early, on the first chained call — repeating it every call would be
|
|
36
|
+
* the nagging this codebase refuses elsewhere.
|
|
37
|
+
*
|
|
38
|
+
* ON BY DEFAULT, WITH AN OPT-OUT. Unlike `code-graph-nudge`, which gates on a
|
|
39
|
+
* project capability that may not exist, the fact this nudge carries is a
|
|
40
|
+
* property of the HOST's permission matcher and is therefore true in every
|
|
41
|
+
* consumer. `hooks.chain_nudge.enabled: false` turns it off.
|
|
42
|
+
*/
|
|
43
|
+
import fs from 'node:fs';
|
|
44
|
+
import path from 'node:path';
|
|
45
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
46
|
+
|
|
47
|
+
import { readHookStdin } from './hook_stdin.js';
|
|
48
|
+
|
|
49
|
+
const SETTINGS_FILE = '.agent-settings.yml';
|
|
50
|
+
const EXIT_ALLOW = 0;
|
|
51
|
+
const EXIT_WARN = 2;
|
|
52
|
+
|
|
53
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
54
|
+
type JsonObject = { [k: string]: JsonValue };
|
|
55
|
+
|
|
56
|
+
function isObject(v: unknown): v is JsonObject {
|
|
57
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `hooks.chain_nudge.enabled: false` mini-parser. Absent means ON, which is
|
|
62
|
+
* the inverse of `code_graph_nudge.enabled` and stated in the header: the
|
|
63
|
+
* fact carried here holds on every host, so absent-means-off would ship a
|
|
64
|
+
* carrier nobody receives.
|
|
65
|
+
*/
|
|
66
|
+
export function enabled(root: string): boolean {
|
|
67
|
+
let text: string;
|
|
68
|
+
try {
|
|
69
|
+
const f = path.join(root, SETTINGS_FILE);
|
|
70
|
+
if (!fs.statSync(f).isFile()) return true;
|
|
71
|
+
text = fs.readFileSync(f, 'utf-8');
|
|
72
|
+
} catch {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
let inHooks = false;
|
|
76
|
+
let inCn = false;
|
|
77
|
+
for (const raw of text.split(/\r\n|\r|\n/)) {
|
|
78
|
+
const line = raw.replace(/\s+$/, '');
|
|
79
|
+
if (!line || line.replace(/^\s+/, '').startsWith('#')) continue;
|
|
80
|
+
if (!(line.startsWith(' ') || line.startsWith('\t'))) {
|
|
81
|
+
inHooks = /^hooks\s*:\s*$/.test(line);
|
|
82
|
+
inCn = false;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (inHooks) {
|
|
86
|
+
if (/^\s+chain_nudge\s*:\s*$/.test(line)) {
|
|
87
|
+
inCn = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (inCn && /^\s{0,3}\S/.test(line)) inCn = false;
|
|
91
|
+
}
|
|
92
|
+
if (inCn && /^\s+enabled\s*:\s*false\b/.test(line)) return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Strip quoted spans and heredoc bodies so an operator inside a string
|
|
99
|
+
* literal cannot be read as chaining. `grep "a && b" f` is one command, and a
|
|
100
|
+
* scanner that misses that fires on correct usage — the failure this concern
|
|
101
|
+
* must not have.
|
|
102
|
+
*/
|
|
103
|
+
export function stripLiterals(cmd: string): string {
|
|
104
|
+
let out = '';
|
|
105
|
+
let i = 0;
|
|
106
|
+
let quote: string | null = null;
|
|
107
|
+
while (i < cmd.length) {
|
|
108
|
+
const c = cmd[i] as string;
|
|
109
|
+
if (quote) {
|
|
110
|
+
if (c === '\\' && quote === '"') {
|
|
111
|
+
i += 2;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (c === quote) quote = null;
|
|
115
|
+
i += 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (c === '\\') {
|
|
119
|
+
i += 2;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (c === '"' || c === "'") {
|
|
123
|
+
quote = c;
|
|
124
|
+
i += 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const heredoc = /^<<-?\s*'?"?([A-Za-z_][A-Za-z0-9_]*)'?"?/.exec(cmd.slice(i));
|
|
128
|
+
if (heredoc) {
|
|
129
|
+
const tag = heredoc[1] as string;
|
|
130
|
+
const end = cmd.indexOf(`\n${tag}`, i);
|
|
131
|
+
i = end === -1 ? cmd.length : end + tag.length + 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
out += c;
|
|
135
|
+
i += 1;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** What the nudge saw, or null when the command is not chained work. */
|
|
141
|
+
export function detectChaining(command: string): string | null {
|
|
142
|
+
const bare = stripLiterals(command);
|
|
143
|
+
if (/^\s*[A-Za-z_][A-Za-z0-9_]*=/.test(bare) && /[;\n]/.test(bare)) {
|
|
144
|
+
return 'a leading `VAR=…` assignment carrying state into a later segment';
|
|
145
|
+
}
|
|
146
|
+
if (/&&|\|\|/.test(bare)) return 'work steps chained with `&&` / `||`';
|
|
147
|
+
if (/;\s*\S/.test(bare)) return 'work steps chained with `;`';
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Tools that carry a shell command. Mirrors `ship_diff_volume_hook` and
|
|
153
|
+
* `git_command_classifier` rather than hardcoding `Bash`: the tool name is
|
|
154
|
+
* host-specific, and the manifest's own `code-graph-nudge` block warns about
|
|
155
|
+
* exactly this spread. A named tool outside the set is declined; a payload
|
|
156
|
+
* naming no tool still reads, which is the bare-host shape.
|
|
157
|
+
*/
|
|
158
|
+
const COMMAND_TOOLS: ReadonlySet<string> = new Set([
|
|
159
|
+
'launch-process',
|
|
160
|
+
'launch_process',
|
|
161
|
+
'Bash',
|
|
162
|
+
'BashTool',
|
|
163
|
+
'run-process',
|
|
164
|
+
'runProcess',
|
|
165
|
+
'shell',
|
|
166
|
+
'execute_shell',
|
|
167
|
+
'RunShellCommand',
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
/** The shell command this call carries, or '' when it carries none. */
|
|
171
|
+
export function bashCommand(envelope: JsonObject): string {
|
|
172
|
+
const payload = isObject(envelope['payload']) ? envelope['payload'] : envelope;
|
|
173
|
+
const nameVal = payload['tool_name'] ?? payload['toolName'] ?? payload['tool'];
|
|
174
|
+
if (typeof nameVal === 'string' && !COMMAND_TOOLS.has(nameVal)) return '';
|
|
175
|
+
const ti = payload['tool_input'] ?? payload['toolInput'];
|
|
176
|
+
if (isObject(ti)) {
|
|
177
|
+
const c = ti['command'];
|
|
178
|
+
if (typeof c === 'string') return c;
|
|
179
|
+
}
|
|
180
|
+
const c = payload['command'];
|
|
181
|
+
return typeof c === 'string' ? c : '';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function sessionId(envelope: JsonObject): string {
|
|
185
|
+
const s = envelope['session_id'] ?? envelope['sessionId'];
|
|
186
|
+
return typeof s === 'string' && s ? s : 'default';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function latchFile(root: string): string {
|
|
190
|
+
return path.join(root, 'agents', 'runtime', 'state', 'chain-nudge.json');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function alreadyNudged(root: string, session: string): boolean {
|
|
194
|
+
try {
|
|
195
|
+
const state = JSON.parse(fs.readFileSync(latchFile(root), 'utf-8')) as Record<string, boolean>;
|
|
196
|
+
return state[session] === true;
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function latch(root: string, session: string): void {
|
|
203
|
+
try {
|
|
204
|
+
const p = latchFile(root);
|
|
205
|
+
let state: Record<string, boolean> = {};
|
|
206
|
+
try {
|
|
207
|
+
state = JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, boolean>;
|
|
208
|
+
} catch {
|
|
209
|
+
/* fresh */
|
|
210
|
+
}
|
|
211
|
+
state[session] = true;
|
|
212
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
213
|
+
fs.writeFileSync(p, JSON.stringify(state));
|
|
214
|
+
} catch {
|
|
215
|
+
/* fail-open: a persistence failure must not break the tool call */
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The nudge line — one line, names what was seen and what to do instead. */
|
|
220
|
+
export function nudgeReason(seen: string): string {
|
|
221
|
+
return (
|
|
222
|
+
`This Bash call chains work: ${seen}. The host matches each segment against ` +
|
|
223
|
+
'the allowlist independently, so one unmatched segment sends the whole call ' +
|
|
224
|
+
'down the permission path. Send N commands as N Bash calls in ONE block — ' +
|
|
225
|
+
'that is the batch. `D=/repo; cd $D && git status` is `git -C /repo status`. ' +
|
|
226
|
+
'Pipes of filters, redirects and heredocs are fine and are not what this flags. ' +
|
|
227
|
+
'See `token-efficiency` § One command per Bash call.'
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function main(): number {
|
|
232
|
+
let envelope: JsonValue;
|
|
233
|
+
try {
|
|
234
|
+
const raw = readHookStdin();
|
|
235
|
+
envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
|
|
236
|
+
} catch {
|
|
237
|
+
return EXIT_ALLOW;
|
|
238
|
+
}
|
|
239
|
+
if (!isObject(envelope)) return EXIT_ALLOW;
|
|
240
|
+
|
|
241
|
+
// ORDER IS LOAD-BEARING, and getting it wrong cost 26 ms at p95. Every
|
|
242
|
+
// predicate below that touches the filesystem runs AFTER the in-memory
|
|
243
|
+
// ones, so the overwhelming majority of tool calls — every non-shell
|
|
244
|
+
// call, and every unchained shell call — exit having read no file at all.
|
|
245
|
+
// The first draft called `enabled()` first, which put a settings read in
|
|
246
|
+
// front of every single pre_tool_use event on the host.
|
|
247
|
+
const command = bashCommand(envelope);
|
|
248
|
+
if (!command) return EXIT_ALLOW;
|
|
249
|
+
|
|
250
|
+
const seen = detectChaining(command);
|
|
251
|
+
if (!seen) return EXIT_ALLOW;
|
|
252
|
+
|
|
253
|
+
const cwd = envelope['cwd'];
|
|
254
|
+
const pr = envelope['workspace_root'] ?? envelope['project_root'];
|
|
255
|
+
const root = typeof cwd === 'string' && cwd ? cwd : typeof pr === 'string' && pr ? pr : '.';
|
|
256
|
+
if (!enabled(root)) return EXIT_ALLOW;
|
|
257
|
+
|
|
258
|
+
const session = sessionId(envelope);
|
|
259
|
+
if (alreadyNudged(root, session)) return EXIT_ALLOW;
|
|
260
|
+
|
|
261
|
+
latch(root, session);
|
|
262
|
+
process.stdout.write(`${JSON.stringify({ decision: 'warn', reason: nudgeReason(seen) })}\n`);
|
|
263
|
+
return EXIT_WARN;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
267
|
+
// every module shares the bundle's `import.meta.url` (see cmd_migrate.ts).
|
|
268
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
269
|
+
function isCliEntry(): boolean {
|
|
270
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
if (process.argv[1] === undefined) return false;
|
|
274
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
275
|
+
if (import.meta.url === argvUrl) return true;
|
|
276
|
+
try {
|
|
277
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
278
|
+
} catch {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (isCliEntry()) process.exit(main());
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse code-graph context line (road-to-a-graph-that-is-shipped 1.2).
|
|
4
|
+
*
|
|
5
|
+
* Replaces `code_graph_nudge_hook.ts`, which shipped default-OFF behind
|
|
6
|
+
* `hooks.code_graph.enabled` — a flag the settings table itself described as
|
|
7
|
+
* "deprecated — honest null 2026-07-28", requiring a manual install of the
|
|
8
|
+
* ABI-locked parser pair. Phase 0.1 made that manual install unnecessary, so
|
|
9
|
+
* the flag's premise is gone and the flag with it.
|
|
10
|
+
*
|
|
11
|
+
* Two behavioural changes, and both are the reason the replacement exists
|
|
12
|
+
* rather than a rename:
|
|
13
|
+
*
|
|
14
|
+
* 1. It speaks ONLY when a graph actually exists — `fresh` or `behind:N`.
|
|
15
|
+
* The nudge also fired on ABSENT, i.e. it advertised a capability the
|
|
16
|
+
* consumer had not built and, before 0.1, could not build. A hook whose
|
|
17
|
+
* common case is "you do not have this" is an advertisement, not context.
|
|
18
|
+
* Absent → silent, which is what makes default-on defensible: a consumer
|
|
19
|
+
* who never builds a graph never hears from this hook at all.
|
|
20
|
+
* 2. It carries STATE, not an instruction. `fresh` and `behind:N` are facts
|
|
21
|
+
* the model cannot otherwise see, and `behind:N` is the one that changes a
|
|
22
|
+
* decision — an answer from a graph N commits stale is worth less than the
|
|
23
|
+
* same answer from a fresh one, and only the hook knows N.
|
|
24
|
+
*
|
|
25
|
+
* Delivery is the dispatcher's, not this hook's: returning `warn` + a reason
|
|
26
|
+
* makes `emitFor` lower it to the host's structured envelope — on Claude Code
|
|
27
|
+
* `hookSpecificOutput.additionalContext` at exit 0 (`claudeAdditionalContext`
|
|
28
|
+
* in host_semantics.ts). It is never a plain echo. On a host with no verified
|
|
29
|
+
* PreToolUse contract the dispatcher swallows concern stdout, so the same line
|
|
30
|
+
* is carried as a rule instead (`external-code-graph-interop`), which is what
|
|
31
|
+
* "per-host `enforced_by` resolved from the table, not from a host name" means
|
|
32
|
+
* in practice.
|
|
33
|
+
*
|
|
34
|
+
* Once per session (a latch keyed by session id), so it cannot nag.
|
|
35
|
+
* fail_closed: false — every error path returns allow; a context line must
|
|
36
|
+
* never break a tool call.
|
|
37
|
+
*/
|
|
38
|
+
import fs from 'node:fs';
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
41
|
+
|
|
42
|
+
import { detectSources, pickSource } from '../code_graph/detect.js';
|
|
43
|
+
import { readHookStdin } from './hook_stdin.js';
|
|
44
|
+
|
|
45
|
+
const EXIT_ALLOW = 0;
|
|
46
|
+
const EXIT_WARN = 2;
|
|
47
|
+
const CODE_EXT = /\.(php|ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i;
|
|
48
|
+
|
|
49
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
50
|
+
type JsonObject = { [k: string]: JsonValue };
|
|
51
|
+
|
|
52
|
+
function isObject(v: unknown): v is JsonObject {
|
|
53
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ToolIntent {
|
|
57
|
+
isSearch: boolean;
|
|
58
|
+
isCodeRead: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Best-effort read of the intercepted tool + its target from the envelope.
|
|
63
|
+
*
|
|
64
|
+
* Carried over from the nudge unchanged, and the manifest's `tools:` filter is
|
|
65
|
+
* pinned against exactly this branch surface — Grep, Glob, Read.
|
|
66
|
+
*/
|
|
67
|
+
export function classifyTool(envelope: JsonObject): ToolIntent {
|
|
68
|
+
const payload = isObject(envelope['payload']) ? envelope['payload'] : envelope;
|
|
69
|
+
const nameVal =
|
|
70
|
+
payload['tool_name'] ?? payload['toolName'] ?? payload['tool'] ?? envelope['tool_name'] ?? envelope['tool'];
|
|
71
|
+
const name = typeof nameVal === 'string' ? nameVal : '';
|
|
72
|
+
const ti = (isObject(payload['tool_input']) ? payload['tool_input'] : envelope['tool_input']) as
|
|
73
|
+
| JsonObject
|
|
74
|
+
| undefined;
|
|
75
|
+
const isSearch = name === 'Grep' || name === 'Glob';
|
|
76
|
+
let isCodeRead = false;
|
|
77
|
+
if (name === 'Read' && isObject(ti)) {
|
|
78
|
+
const fp = ti['file_path'] ?? ti['path'];
|
|
79
|
+
if (typeof fp === 'string') isCodeRead = CODE_EXT.test(fp);
|
|
80
|
+
}
|
|
81
|
+
return { isSearch, isCodeRead };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sessionId(envelope: JsonObject): string {
|
|
85
|
+
const s = envelope['session_id'] ?? envelope['sessionId'];
|
|
86
|
+
return typeof s === 'string' && s ? s : 'default';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function latchFile(root: string): string {
|
|
90
|
+
return path.join(root, 'agents', 'runtime', 'state', 'code-graph-context.json');
|
|
91
|
+
}
|
|
92
|
+
function alreadySpoke(root: string, session: string): boolean {
|
|
93
|
+
try {
|
|
94
|
+
const state = JSON.parse(fs.readFileSync(latchFile(root), 'utf-8')) as Record<string, boolean>;
|
|
95
|
+
return state[session] === true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function latch(root: string, session: string): void {
|
|
101
|
+
try {
|
|
102
|
+
const p = latchFile(root);
|
|
103
|
+
let state: Record<string, boolean> = {};
|
|
104
|
+
try {
|
|
105
|
+
state = JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, boolean>;
|
|
106
|
+
} catch {
|
|
107
|
+
/* fresh */
|
|
108
|
+
}
|
|
109
|
+
state[session] = true;
|
|
110
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
111
|
+
fs.writeFileSync(p, JSON.stringify(state));
|
|
112
|
+
} catch {
|
|
113
|
+
/* fail-open: a persistence failure must not break the tool call */
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const NATIVE_CACHE = path.join('agents', 'runtime', 'state', 'code-graph-v1.json');
|
|
118
|
+
|
|
119
|
+
/** The three-state staleness token this hook reports. */
|
|
120
|
+
export type GraphState = 'absent' | 'fresh' | `behind:${number}`;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the graph's state for `root`.
|
|
124
|
+
*
|
|
125
|
+
* `absent` means no source at all — the silent case. A picked source whose
|
|
126
|
+
* staleness is UNKNOWN reads as `fresh`, mirroring `computeVerdict`'s own
|
|
127
|
+
* `picked.stale ? STALE : FRESH`: unknown is not treated as stale, because
|
|
128
|
+
* inventing a commit count would be worse than reporting none.
|
|
129
|
+
*/
|
|
130
|
+
export function graphState(root: string): GraphState {
|
|
131
|
+
const picked = pickSource(detectSources(root, path.join(root, NATIVE_CACHE)));
|
|
132
|
+
if (!picked) return 'absent';
|
|
133
|
+
if (picked.stale !== true) return 'fresh';
|
|
134
|
+
const behind = picked.commits_behind;
|
|
135
|
+
return `behind:${typeof behind === 'number' ? behind : 0}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The one line, ≤ ~45 tokens.
|
|
140
|
+
*
|
|
141
|
+
* It names the state and the verbs, and — on the stale branch — what the state
|
|
142
|
+
* costs. It does NOT claim the graph beats grep: that ordering question is
|
|
143
|
+
* `later/road-to-a-graph-that-wins.md`'s, and Kill register K5 forbids
|
|
144
|
+
* "graph-first" wording here.
|
|
145
|
+
*/
|
|
146
|
+
export function contextLine(state: GraphState): string {
|
|
147
|
+
if (state === 'fresh') {
|
|
148
|
+
return (
|
|
149
|
+
'code-graph: fresh. For structure questions (who calls / where used / impact) ' +
|
|
150
|
+
'`agent-config code-graph query|affected <symbol>` answers from the index; ' +
|
|
151
|
+
'grep is the other path — say which one answered.'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const n = state.slice('behind:'.length);
|
|
155
|
+
return (
|
|
156
|
+
`code-graph: ${n} commit(s) behind. Relationship answers may miss anything newer — ` +
|
|
157
|
+
'`agent-config code-graph refresh` rebuilds it, or use grep and say so.'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function main(): number {
|
|
162
|
+
let envelope: JsonValue;
|
|
163
|
+
try {
|
|
164
|
+
const raw = readHookStdin();
|
|
165
|
+
envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
|
|
166
|
+
} catch {
|
|
167
|
+
return EXIT_ALLOW;
|
|
168
|
+
}
|
|
169
|
+
if (!isObject(envelope)) return EXIT_ALLOW;
|
|
170
|
+
|
|
171
|
+
const cwd = envelope['cwd'];
|
|
172
|
+
const pr = envelope['workspace_root'] ?? envelope['project_root'];
|
|
173
|
+
const root = typeof cwd === 'string' && cwd ? cwd : typeof pr === 'string' && pr ? pr : '.';
|
|
174
|
+
|
|
175
|
+
const { isSearch, isCodeRead } = classifyTool(envelope);
|
|
176
|
+
if (!isSearch && !isCodeRead) return EXIT_ALLOW;
|
|
177
|
+
|
|
178
|
+
const session = sessionId(envelope);
|
|
179
|
+
if (alreadySpoke(root, session)) return EXIT_ALLOW; // once per session
|
|
180
|
+
|
|
181
|
+
let state: GraphState;
|
|
182
|
+
try {
|
|
183
|
+
state = graphState(root);
|
|
184
|
+
} catch {
|
|
185
|
+
return EXIT_ALLOW;
|
|
186
|
+
}
|
|
187
|
+
// No graph is the silent case — see the header. Nothing is latched either,
|
|
188
|
+
// so a session that builds a graph mid-flight still gets the line once.
|
|
189
|
+
if (state === 'absent') return EXIT_ALLOW;
|
|
190
|
+
|
|
191
|
+
latch(root, session);
|
|
192
|
+
process.stdout.write(`${JSON.stringify({ decision: 'warn', reason: contextLine(state) })}\n`);
|
|
193
|
+
return EXIT_WARN;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
197
|
+
// every module shares the bundle's `import.meta.url` (see cmd_migrate.ts).
|
|
198
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
199
|
+
function isCliEntry(): boolean {
|
|
200
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
if (process.argv[1] === undefined) return false;
|
|
204
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
205
|
+
if (import.meta.url === argvUrl) return true;
|
|
206
|
+
try {
|
|
207
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (isCliEntry()) process.exit(main());
|
|
@@ -54,10 +54,12 @@ import { main as blockNoVerifyMain } from './block_no_verify.js';
|
|
|
54
54
|
import { main as blockKernelRuleWritesMain } from './block_kernel_rule_writes.js';
|
|
55
55
|
import { main as blockConfigWeakeningMain } from './block_config_weakening.js';
|
|
56
56
|
import { main as blockSpeakingInboxDirMain } from './block_speaking_inbox_dir.js';
|
|
57
|
+
import { main as oneQuestionPerAskMain } from './one_question_per_ask_hook.js';
|
|
57
58
|
import { main as rtkWrapMain } from './rtk_wrap_hook.js';
|
|
58
59
|
import { _main as designPassMain } from './design_pass_hook.js';
|
|
59
60
|
import { main as designSlopMain } from './design_slop_hook.js';
|
|
60
|
-
import { main as
|
|
61
|
+
import { main as codeGraphContextMain } from './code_graph_context_hook.js';
|
|
62
|
+
import { main as chainNudgeMain } from './chain_nudge_hook.js';
|
|
61
63
|
import { main as suggestionCaptureMain } from './suggestion_capture_hook.js';
|
|
62
64
|
import { main as uiRouteNudgeMain } from './ui_route_nudge_hook.js';
|
|
63
65
|
import { main as orchestrationRecordMain } from './orchestration_record_hook.js';
|
|
@@ -115,10 +117,12 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
|
|
|
115
117
|
'src/scripts/hooks/block_kernel_rule_writes.ts': blockKernelRuleWritesMain as ConcernMain,
|
|
116
118
|
'src/scripts/hooks/block_config_weakening.ts': blockConfigWeakeningMain as ConcernMain,
|
|
117
119
|
'src/scripts/hooks/block_speaking_inbox_dir.ts': blockSpeakingInboxDirMain as ConcernMain,
|
|
120
|
+
'src/scripts/hooks/one_question_per_ask_hook.ts': oneQuestionPerAskMain as ConcernMain,
|
|
118
121
|
'src/scripts/hooks/rtk_wrap_hook.ts': rtkWrapMain as ConcernMain,
|
|
119
122
|
'src/scripts/hooks/design_pass_hook.ts': designPassMain as ConcernMain,
|
|
120
123
|
'src/scripts/hooks/design_slop_hook.ts': designSlopMain as ConcernMain,
|
|
121
|
-
'src/scripts/hooks/
|
|
124
|
+
'src/scripts/hooks/code_graph_context_hook.ts': codeGraphContextMain as ConcernMain,
|
|
125
|
+
'src/scripts/hooks/chain_nudge_hook.ts': chainNudgeMain as ConcernMain,
|
|
122
126
|
'src/scripts/hooks/suggestion_capture_hook.ts': suggestionCaptureMain as ConcernMain,
|
|
123
127
|
'src/scripts/hooks/ui_route_nudge_hook.ts': uiRouteNudgeMain as ConcernMain,
|
|
124
128
|
'src/scripts/hooks/orchestration_record_hook.ts': orchestrationRecordMain as ConcernMain,
|
|
@@ -375,6 +375,33 @@ export function buildNudgeLine(
|
|
|
375
375
|
`${classification.reason}.</delegation-nudge>`
|
|
376
376
|
);
|
|
377
377
|
}
|
|
378
|
+
// Rung 4 (1.3): a contested-judgment verdict names the council's own entry
|
|
379
|
+
// point. The carrier NEVER runs it — `council_cli run` spends a metered or
|
|
380
|
+
// subscription seat, which is Hard-Floor reserved and is never inferred from
|
|
381
|
+
// a hook line.
|
|
382
|
+
if (rung === 4) {
|
|
383
|
+
return (
|
|
384
|
+
`<delegation-nudge>rung-4: council. This is contested judgment, not a ` +
|
|
385
|
+
`slice to dispatch — route it to the council yourself via ` +
|
|
386
|
+
"`./scripts-run src/scripts/council_cli run --input <file>` " +
|
|
387
|
+
"(check reachability first with `agent-config council:status`; the file " +
|
|
388
|
+
`path is the input, never an inline question). Not a spawn, and not run ` +
|
|
389
|
+
`for you — ${classification.reason}.</delegation-nudge>`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
// Rung 3 (1.3): slices that must communicate DURING the run. The pointer
|
|
393
|
+
// names the host teams primitive; the fallback when the host has none is the
|
|
394
|
+
// same one-completion path rung 0.5 cites.
|
|
395
|
+
if (rung === 3) {
|
|
396
|
+
return (
|
|
397
|
+
`<delegation-nudge>rung-3: team. These slices must see each other's state ` +
|
|
398
|
+
`during the run, so a set of independent subagent spawns cannot express ` +
|
|
399
|
+
`it — use the host teams primitive, or fall back to ` +
|
|
400
|
+
"`./scripts-run src/scripts/ask_transport \"<question>\"` for a bounded " +
|
|
401
|
+
`sub-question. Not a spawn, and not run for you — ` +
|
|
402
|
+
`${classification.reason}.</delegation-nudge>`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
378
405
|
// Rung 1 (single bounded slice) carries no do-in-steps/do-in-parallel
|
|
379
406
|
// mode of its own — `classification.mode` is `null` for that shape.
|
|
380
407
|
const modeLabel = classification.mode ?? "single-slice";
|
|
@@ -437,8 +464,25 @@ export function classifyPrompt(
|
|
|
437
464
|
// surfaces. Silence here also guarantees a question-shaped prompt can
|
|
438
465
|
// never produce a spawn nudge ("the carrier stops nudging full spawns
|
|
439
466
|
// for question-shaped slices").
|
|
467
|
+
// Rung 3/4: these used to be discarded by the same `verdict !== "subagent"`
|
|
468
|
+
// return as rung 0/0.5, so a RESOLVED council verdict produced no output on
|
|
469
|
+
// the only runtime carrier.
|
|
470
|
+
//
|
|
471
|
+
// What they emit is a POINTER, and every field here is chosen so it cannot
|
|
472
|
+
// read as a spawn authorisation: `delegable` stays false, the action stays
|
|
473
|
+
// `ask` (the existing token for "a verdict, never a speculative spawn"),
|
|
474
|
+
// the slice count is 0, and buildNudgeLine's rung-3/4 branch names an
|
|
475
|
+
// entry point rather than a dispatch.
|
|
476
|
+
if (ladder.verdict === "team" || ladder.verdict === "council") {
|
|
477
|
+
return {
|
|
478
|
+
classification: { delegable: false, action: "ask", mode: null, reason: ladder.reason },
|
|
479
|
+
sliceCount: 0,
|
|
480
|
+
tier: recommendSliceTier(downshift, separate_quota_pool),
|
|
481
|
+
rung: ladder.rung,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
440
484
|
if (ladder.verdict !== "subagent") {
|
|
441
|
-
return null; // rung-0 script, rung-0.5 ask-transport,
|
|
485
|
+
return null; // rung-0 script, rung-0.5 ask-transport, or ∅ user-ask/in-session
|
|
442
486
|
}
|
|
443
487
|
const tier = recommendSliceTier(downshift, separate_quota_pool);
|
|
444
488
|
// Rung 1 (single bounded slice) has no explicit slice count of its own —
|
|
@@ -95,7 +95,7 @@ function _joinReasons(reasons: readonly string[]): string {
|
|
|
95
95
|
*
|
|
96
96
|
* Exit 0 + this JSON is the documented way to surface advisory feedback
|
|
97
97
|
* WITHOUT blocking — the mapping the four advisory PreToolUse concerns
|
|
98
|
-
* (design-slop, code-graph-
|
|
98
|
+
* (design-slop, code-graph-context, rtk-wrap, block-config-weakening's
|
|
99
99
|
* advisory path) always intended.
|
|
100
100
|
*/
|
|
101
101
|
export function claudeAdditionalContext(event: string, text: string): string {
|