@event4u/agent-config 14.21.0 → 14.23.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 +164 -187
- package/CREDITS.md +37 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +380 -5
- package/dist/agent-src/commands/analyze/repo.md +4 -2
- package/dist/agent-src/commands/pr/merge.md +47 -38
- package/dist/agent-src/commands/roadmap/next.md +8 -0
- 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/autonomy-mechanics.md +12 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
- 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 +120 -27
- 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 +82 -1
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
- 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/registry.js +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 +542 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- 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 +2 -2
- package/dist/cli-delegate/cmd_handoff.js +45 -13
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +4 -14
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- 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 +13 -13
- 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 +1813 -946
- 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 +21 -3
- 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 +10870 -7641
- package/dist/scripts/_lib/lean_projection_mode.js +108 -0
- package/dist/scripts/_lib/router_match.js +243 -0
- package/dist/scripts/_lib/rule_injection.js +216 -0
- package/dist/scripts/condense.js +52 -45
- package/dist/scripts/project_thin_rules.js +139 -19
- package/dist/server/schemas/settings.js +16 -3
- package/docs/MIGRATION.md +105 -1
- package/docs/contracts/settings-classes.md +40 -8
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
- package/llms.txt +1 -1
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
- 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/agent-settings.template.yml +47 -1
- package/src/config/continuity-surface.json +516 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- 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/hook-token-budget.json +7 -6
- 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/routing-coverage-seed.json +9 -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/_cli/cmd_session_recycle.ts +2 -25
- package/src/scripts/_cli/handoff_sessions.ts +14 -44
- 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_slot.ts +295 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/continuity_writer.ts +253 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +86 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/lean_projection_mode.ts +127 -0
- package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
- package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/session_eol.ts +44 -0
- package/src/scripts/_lib/session_index_trust.ts +346 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
- 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/audit_mcp_tools.ts +69 -25
- package/src/scripts/build_mcp_catalog.ts +24 -1
- 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_host_tree_parity.ts +291 -0
- package/src/scripts/check_pack_size.ts +262 -23
- package/src/scripts/check_preamble_payload_budget.ts +136 -7
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_rule_projection_integrity.ts +102 -2
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/ci_settle.ts +205 -5
- package/src/scripts/code_graph/build.ts +217 -15
- package/src/scripts/code_graph/cli.ts +334 -15
- package/src/scripts/code_graph/detect.ts +38 -0
- 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 +144 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +262 -6
- package/src/scripts/code_graph/types.ts +117 -3
- package/src/scripts/code_graph/validate.ts +26 -1
- package/src/scripts/code_graph/verbs.ts +590 -0
- package/src/scripts/condense.ts +54 -42
- 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 +118 -1
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +87 -12
- package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +201 -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/push_settle_hook.ts +3 -2
- package/src/scripts/hooks/rule_inject_hook.ts +108 -21
- package/src/scripts/hooks/session_eol_hook.ts +120 -2
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/hot_context_hook.ts +62 -7
- 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/lint_roadmap_blockers.ts +164 -1
- package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
- package/src/scripts/mcp_server/graph_tools.ts +294 -0
- package/src/scripts/mcp_server/path_util.ts +31 -0
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
- package/src/scripts/mcp_server/tools.ts +5 -69
- 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/model_rule_injection.ts +61 -4
- package/src/scripts/project_thin_rules.ts +149 -16
- 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/report_standing_payload_by_host.ts +650 -0
- package/src/scripts/schemas/agent-settings.schema.json +7 -1
- package/src/scripts/session_memory_index.ts +81 -8
- 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/server/schemas/settings.ts +23 -4
- package/src/vendor/grammars/README.md +86 -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,201 @@
|
|
|
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 { type GraphState, graphState } 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
|
+
/**
|
|
118
|
+
* The three-state staleness token this hook reports.
|
|
119
|
+
*
|
|
120
|
+
* Re-exported, not defined here: Phase 3's verbs must print the same token, and
|
|
121
|
+
* an engine module importing this hook to learn it would invert the dependency
|
|
122
|
+
* direction D9 measures. The definition lives in `code_graph/detect.ts`; this
|
|
123
|
+
* re-export keeps every existing importer of the hook working unchanged.
|
|
124
|
+
*/
|
|
125
|
+
export { type GraphState, graphState };
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The one line, ≤ ~45 tokens.
|
|
129
|
+
*
|
|
130
|
+
* It names the state and the verbs, and — on the stale branch — what the state
|
|
131
|
+
* costs. It does NOT claim the graph beats grep: that ordering question is
|
|
132
|
+
* `later/road-to-a-graph-that-wins.md`'s, and Kill register K5 forbids
|
|
133
|
+
* "graph-first" wording here.
|
|
134
|
+
*/
|
|
135
|
+
export function contextLine(state: GraphState): string {
|
|
136
|
+
if (state === 'fresh') {
|
|
137
|
+
return (
|
|
138
|
+
'code-graph: fresh. For structure questions (who calls / where used / impact) ' +
|
|
139
|
+
'`agent-config code-graph query|affected <symbol>` answers from the index; ' +
|
|
140
|
+
'grep is the other path — say which one answered.'
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const n = state.slice('behind:'.length);
|
|
144
|
+
return (
|
|
145
|
+
`code-graph: ${n} commit(s) behind. Relationship answers may miss anything newer — ` +
|
|
146
|
+
'`agent-config code-graph refresh` rebuilds it, or use grep and say so.'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function main(): number {
|
|
151
|
+
let envelope: JsonValue;
|
|
152
|
+
try {
|
|
153
|
+
const raw = readHookStdin();
|
|
154
|
+
envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
|
|
155
|
+
} catch {
|
|
156
|
+
return EXIT_ALLOW;
|
|
157
|
+
}
|
|
158
|
+
if (!isObject(envelope)) return EXIT_ALLOW;
|
|
159
|
+
|
|
160
|
+
const cwd = envelope['cwd'];
|
|
161
|
+
const pr = envelope['workspace_root'] ?? envelope['project_root'];
|
|
162
|
+
const root = typeof cwd === 'string' && cwd ? cwd : typeof pr === 'string' && pr ? pr : '.';
|
|
163
|
+
|
|
164
|
+
const { isSearch, isCodeRead } = classifyTool(envelope);
|
|
165
|
+
if (!isSearch && !isCodeRead) return EXIT_ALLOW;
|
|
166
|
+
|
|
167
|
+
const session = sessionId(envelope);
|
|
168
|
+
if (alreadySpoke(root, session)) return EXIT_ALLOW; // once per session
|
|
169
|
+
|
|
170
|
+
let state: GraphState;
|
|
171
|
+
try {
|
|
172
|
+
state = graphState(root);
|
|
173
|
+
} catch {
|
|
174
|
+
return EXIT_ALLOW;
|
|
175
|
+
}
|
|
176
|
+
// No graph is the silent case — see the header. Nothing is latched either,
|
|
177
|
+
// so a session that builds a graph mid-flight still gets the line once.
|
|
178
|
+
if (state === 'absent') return EXIT_ALLOW;
|
|
179
|
+
|
|
180
|
+
latch(root, session);
|
|
181
|
+
process.stdout.write(`${JSON.stringify({ decision: 'warn', reason: contextLine(state) })}\n`);
|
|
182
|
+
return EXIT_WARN;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
186
|
+
// every module shares the bundle's `import.meta.url` (see cmd_migrate.ts).
|
|
187
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
188
|
+
function isCliEntry(): boolean {
|
|
189
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
if (process.argv[1] === undefined) return false;
|
|
193
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
194
|
+
if (import.meta.url === argvUrl) return true;
|
|
195
|
+
try {
|
|
196
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (isCliEntry()) process.exit(main());
|
|
@@ -58,7 +58,8 @@ import { main as oneQuestionPerAskMain } from './one_question_per_ask_hook.js';
|
|
|
58
58
|
import { main as rtkWrapMain } from './rtk_wrap_hook.js';
|
|
59
59
|
import { _main as designPassMain } from './design_pass_hook.js';
|
|
60
60
|
import { main as designSlopMain } from './design_slop_hook.js';
|
|
61
|
-
import { main as
|
|
61
|
+
import { main as codeGraphContextMain } from './code_graph_context_hook.js';
|
|
62
|
+
import { main as chainNudgeMain } from './chain_nudge_hook.js';
|
|
62
63
|
import { main as suggestionCaptureMain } from './suggestion_capture_hook.js';
|
|
63
64
|
import { main as uiRouteNudgeMain } from './ui_route_nudge_hook.js';
|
|
64
65
|
import { main as orchestrationRecordMain } from './orchestration_record_hook.js';
|
|
@@ -120,7 +121,8 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
|
|
|
120
121
|
'src/scripts/hooks/rtk_wrap_hook.ts': rtkWrapMain as ConcernMain,
|
|
121
122
|
'src/scripts/hooks/design_pass_hook.ts': designPassMain as ConcernMain,
|
|
122
123
|
'src/scripts/hooks/design_slop_hook.ts': designSlopMain as ConcernMain,
|
|
123
|
-
'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,
|
|
124
126
|
'src/scripts/hooks/suggestion_capture_hook.ts': suggestionCaptureMain as ConcernMain,
|
|
125
127
|
'src/scripts/hooks/ui_route_nudge_hook.ts': uiRouteNudgeMain as ConcernMain,
|
|
126
128
|
'src/scripts/hooks/orchestration_record_hook.ts': orchestrationRecordMain as ConcernMain,
|
|
@@ -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 {
|
|
@@ -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'];
|
|
@@ -191,8 +191,9 @@ export function buildReminder(branch: string, pr: number | null): { reason: stri
|
|
|
191
191
|
`own exit code.\n\n` +
|
|
192
192
|
`Before this turn ends:\n` +
|
|
193
193
|
` 1. Settle it:\n ${settle}\n` +
|
|
194
|
-
` Exit 0 = settled green
|
|
195
|
-
`(timed out or
|
|
194
|
+
` Exit 0 = settled green, and the line names the head it is about · 1 = settled, ` +
|
|
195
|
+
`something failed · 2 = NOT a verdict (timed out, unreadable, or the PR is merged / ` +
|
|
196
|
+
`closed / head-diverged — never report 2 as green).\n` +
|
|
196
197
|
` 2. Red? Read only the failing part:\n` +
|
|
197
198
|
` gh run view --job <id> --log-failed | grep -E '×|FAIL|Error'\n` +
|
|
198
199
|
` then fix it and push again (fix-what-you-see: the author is irrelevant).\n` +
|
|
@@ -11,12 +11,23 @@
|
|
|
11
11
|
* the 36.2 %-against-48 % run at `docs/CLAIMS.md:188-189`. Whatever that
|
|
12
12
|
* instrument's flaws — and ADR-202 closed it — it points one way.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* SHIPPED ON FOR CLAUDE CODE SINCE ADR-265 — corrected 2026-09-08 (R2 finding
|
|
15
|
+
* 4), because this paragraph read "DEFAULT OFF, AND OFF MEANS ZERO BYTES …
|
|
16
|
+
* under every shipped default it emits nothing … there is no measured emission
|
|
17
|
+
* to register yet" and all four clauses became false in the same change that
|
|
18
|
+
* edited the paragraph below it. The shipped template carries
|
|
19
|
+
* `lean_projection.mode: delivery` with `hosts: [claude-code]`
|
|
20
|
+
* (`src/config/agent-settings.template.yml`), `gateOpen` returns true on that
|
|
21
|
+
* pair, and the emission IS measured: p50 6,674 B, p90 16,188 B, max 20,406 B
|
|
22
|
+
* over 318 gate-open fires on the frozen corpus, which is the distribution the
|
|
23
|
+
* `user_prompt_submit` slot raise in `src/config/hook-token-budget.json` is
|
|
24
|
+
* derived from.
|
|
25
|
+
*
|
|
26
|
+
* OFF STILL MEANS ZERO BYTES, and that half is unchanged: the concern returns
|
|
27
|
+
* before reading the router unless the resolved (mode, hosts) pair actually
|
|
28
|
+
* thins THIS host — see `gateOpen`, which checks both, because the projector
|
|
29
|
+
* does. On any other host, and on a `lean_projection.mode: eager-all` rollback,
|
|
30
|
+
* it emits nothing.
|
|
20
31
|
*
|
|
21
32
|
* ONE MATCHER, SHARED WITH THE OFFLINE MODEL. Everything about selection,
|
|
22
33
|
* ordering, capping and body loading comes from `_lib/rule_injection.ts`, which
|
|
@@ -31,10 +42,33 @@
|
|
|
31
42
|
* uses. State lives under `agents/runtime/state/`, the class
|
|
32
43
|
* `context-hygiene.json` already occupies; no new state convention is created.
|
|
33
44
|
*
|
|
45
|
+
* WHAT IS RE-DELIVERED AFTER A COMPACTION, EXACTLY
|
|
46
|
+
* (road-to-delivery-for-every-host 2.4 — stated because a rule lost at a
|
|
47
|
+
* compaction boundary is lost for the rest of the session, and "re-armed" alone
|
|
48
|
+
* does not say what a reader may rely on):
|
|
49
|
+
*
|
|
50
|
+
* · `pre_compact` clears the WHOLE seen-set for that session, not the rules
|
|
51
|
+
* matched on the compacted turn. There is no per-rule bookkeeping to be
|
|
52
|
+
* partially wrong about.
|
|
53
|
+
* · Nothing is delivered BY the compaction itself. The slot emits zero bytes
|
|
54
|
+
* and exits allow; re-arming is silent.
|
|
55
|
+
* · A rule's body returns on the NEXT turn whose trigger matches it — which
|
|
56
|
+
* means a rule whose trigger does not fire again is NOT restored. Delivery
|
|
57
|
+
* is trigger-driven on both sides of the boundary; compaction resets the
|
|
58
|
+
* de-duplication, it does not replay a transcript.
|
|
59
|
+
* · The seen-set is per session, so a compaction in one session re-arms only
|
|
60
|
+
* that session.
|
|
61
|
+
*
|
|
62
|
+
* Held by three fixtures in `tests/scripts/rule_inject_hook.test.ts` under
|
|
63
|
+
* "once per session per rule, re-armed on compaction": the dedup case, the
|
|
64
|
+
* matched-rule → compaction → matching-turn → body-present case, and the
|
|
65
|
+
* per-session case. All three predate this roadmap; 2.4 adds the contract
|
|
66
|
+
* above, not the coverage, and says so rather than claiming new tests.
|
|
67
|
+
*
|
|
34
68
|
* NEVER BLOCKS. Every failure path returns 0: unreadable stdin, malformed JSON,
|
|
35
69
|
* missing router, unreadable body, unwritable state. The one non-zero exit is
|
|
36
70
|
* the host's advisory context channel (exit 2 + `decision: "warn"`), the same
|
|
37
|
-
* channel `ui-route-nudge` and `code-graph-
|
|
71
|
+
* channel `ui-route-nudge` and `code-graph-context` already use on `pre_tool_use`
|
|
38
72
|
* with `severity: advisory` — a warn there is an injection, not a deny.
|
|
39
73
|
*
|
|
40
74
|
* HOST BOUNDARY, STATED RATHER THAN IMPLIED. `user_prompt_submit` and
|
|
@@ -54,8 +88,12 @@ import * as path from 'node:path';
|
|
|
54
88
|
import process from 'node:process';
|
|
55
89
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
56
90
|
|
|
57
|
-
import { hookSectionEnabled, leanProjectionModeRaw } from '../_lib/hook_settings.js';
|
|
58
|
-
import {
|
|
91
|
+
import { hookSectionEnabled, leanProjectionHostsRaw, leanProjectionModeRaw } from '../_lib/hook_settings.js';
|
|
92
|
+
import {
|
|
93
|
+
deliversBodies,
|
|
94
|
+
normalizeLeanProjectionMode,
|
|
95
|
+
resolveLeanProjectionHosts,
|
|
96
|
+
} from '../_lib/lean_projection_mode.js';
|
|
59
97
|
import {
|
|
60
98
|
loadRuleBody,
|
|
61
99
|
loadRouter,
|
|
@@ -81,15 +119,38 @@ const EXIT_WARN = 2;
|
|
|
81
119
|
* slot, for a concern that is default-OFF and emits nothing. Measured: 202 ms
|
|
82
120
|
* -> 196 ms on the `pre_tool_use` p95 when the concern is unbound entirely, and
|
|
83
121
|
* the CI latency gate went red on the branch that introduced it while passing
|
|
84
|
-
* on main. So the runtime cap is
|
|
122
|
+
* on main. So the runtime cap is stated in the unit
|
|
85
123
|
* `hook-token-budget.json` already enforces — 5,000 tok at the ~4 bytes/token
|
|
86
|
-
* this corpus measures
|
|
87
|
-
* row.
|
|
124
|
+
* this corpus measures was 20,480 B, and that WAS this concern's registered
|
|
125
|
+
* row until 2026-09-08. It matched the concern row and not the slot row; see
|
|
126
|
+
* the correction below.
|
|
127
|
+
*
|
|
128
|
+
* LOWERED 20480 -> 16384 on 2026-09-08 (R2 finding 3), and the paragraphs above
|
|
129
|
+
* are kept because they record how the retired number was derived. The defect
|
|
130
|
+
* was that the derivation above and the one behind the
|
|
131
|
+
* `user_prompt_submit` slot row were TWO STATISTICS IN TWO UNITS: this cap was
|
|
132
|
+
* the p90 of the matched-body TOKEN distribution, converted at ~4 B/tok; the
|
|
133
|
+
* slot row is the p90 gate-open FIRE SIZE in bytes, which is the activation
|
|
134
|
+
* charge owner ruling E2 specifies. They disagreed by 25 %, in the direction
|
|
135
|
+
* that licensed ONE concern to emit more than the whole slot — carrying 12
|
|
136
|
+
* other concerns — is registered for. Reconciled downward onto the slot row,
|
|
137
|
+
* because that is the owner-specified charge; the cost is measured and recorded
|
|
138
|
+
* in `hook-token-budget.json`'s own `rule-inject_reason` (33 -> 45 fires
|
|
139
|
+
* truncated, 63 -> 88 bodies withheld over 330 corpus fires).
|
|
140
|
+
*
|
|
141
|
+
* WHAT THIS CAP DOES, precisely: `selectForInjection` drops whole bodies to
|
|
142
|
+
* stay under it, so a fire is TRUNCATED and never over-emitted. It is the only
|
|
143
|
+
* number that acts on a single fire — the per-slot sums are an authoring-time
|
|
144
|
+
* control read by `bench_hook_injection`, and the runtime dispatcher enforces
|
|
145
|
+
* only `per_turn_aggregate_bytes.ceiling_bytes`.
|
|
88
146
|
*
|
|
89
147
|
* Re-run that command if the corpus or the bodies move; a cap copied from a
|
|
90
|
-
* stale measurement is worse than no cap, because it looks derived.
|
|
148
|
+
* stale measurement is worse than no cap, because it looks derived. The
|
|
149
|
+
* tripwire in `tests/scripts/rule_inject_hook.test.ts` holds this equal to the
|
|
150
|
+
* registered concern row and at or below the slot sum, so the two units cannot
|
|
151
|
+
* drift apart again unnoticed.
|
|
91
152
|
*/
|
|
92
|
-
export const CAP_BYTES =
|
|
153
|
+
export const CAP_BYTES = 16384;
|
|
93
154
|
|
|
94
155
|
/** Tools whose input names a file this concern can match path triggers against. */
|
|
95
156
|
export const FILE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit', 'Read', 'MultiEdit']);
|
|
@@ -216,18 +277,44 @@ export function buildInjection(
|
|
|
216
277
|
|
|
217
278
|
// ── main ─────────────────────────────────────────────────────────────────
|
|
218
279
|
|
|
280
|
+
/**
|
|
281
|
+
* The one host this concern is bound on.
|
|
282
|
+
*
|
|
283
|
+
* The manifest binds `rule-inject` under `claude` alone, so the host axis
|
|
284
|
+
* `gateOpen` has to consult is a constant here rather than something read off
|
|
285
|
+
* the envelope — the envelope carries no host id, and inventing one from the
|
|
286
|
+
* process environment would be a guess where the manifest is a fact.
|
|
287
|
+
*/
|
|
288
|
+
export const DELIVERY_HOST = 'claude-code';
|
|
289
|
+
|
|
219
290
|
/**
|
|
220
291
|
* Whether the settings gate applies.
|
|
221
292
|
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
293
|
+
* BOTH AXES, because the projector reads both. Until 2026-09-08 this keyed on
|
|
294
|
+
* `lean_projection.mode` alone while `condense` gates stub-writing on
|
|
295
|
+
* `thinsHost(mode, hosts, host)` (R2 finding 5). The two disagreed in exactly
|
|
296
|
+
* the states the host axis exists for: `mode: delivery` with a `hosts:` list
|
|
297
|
+
* that does not resolve to `claude-code` — the documented "thin no host"
|
|
298
|
+
* state, which a fully typo'd list also produces — left this host with a
|
|
299
|
+
* FULL-BODIED tree while the hook kept injecting on a match, so every matched
|
|
300
|
+
* rule was delivered twice. `hosts: [cursor]` was the same defect with a
|
|
301
|
+
* second half: cursor received stubs with no bound slot to deliver them back.
|
|
302
|
+
*
|
|
303
|
+
* `deliversBodies` is checked separately from the host list rather than via
|
|
304
|
+
* `thinsHost`, which is true for `thin` as well: `thin` writes the same stubs
|
|
305
|
+
* and binds NO delivery concern, so it must never open this gate.
|
|
306
|
+
*
|
|
307
|
+
* An explicit `hooks.rule_inject` opt-in still opens it — that is an operator
|
|
308
|
+
* asking for the concern by name, independent of the projection mode. A DIRECT
|
|
309
|
+
* CLI invocation is a probe by definition — an operator piping an envelope into
|
|
310
|
+
* this file is asking to see what it would deliver — so there the gate defaults
|
|
311
|
+
* to open. `AGENT_CONFIG_REPLAY` re-imposes it, which is what keeps
|
|
312
|
+
* `bench_hook_injection` measuring the configured tree rather than the probe.
|
|
228
313
|
*/
|
|
229
314
|
export function gateOpen(root: string, cliEntry: boolean): boolean {
|
|
230
|
-
|
|
315
|
+
const mode = normalizeLeanProjectionMode(leanProjectionModeRaw(root));
|
|
316
|
+
const hosts = resolveLeanProjectionHosts(leanProjectionHostsRaw(root)).hosts;
|
|
317
|
+
if (deliversBodies(mode) && hosts.includes(DELIVERY_HOST)) return true;
|
|
231
318
|
if (hookSectionEnabled(root, 'rule_inject')) return true;
|
|
232
319
|
return cliEntry && process.env['AGENT_CONFIG_REPLAY'] !== '1';
|
|
233
320
|
}
|
|
@@ -45,7 +45,11 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
45
45
|
|
|
46
46
|
import recycleThresholdConfig from '../../config/recycle-threshold-budget.json';
|
|
47
47
|
|
|
48
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
listContinuityRecords,
|
|
50
|
+
RECYCLE_MAX_AGE_HOURS,
|
|
51
|
+
recycle_envelope_rel,
|
|
52
|
+
} from '../_lib/recycle_envelope_paths.js';
|
|
49
53
|
import {
|
|
50
54
|
emptyCounters,
|
|
51
55
|
eolSessionKey,
|
|
@@ -56,6 +60,12 @@ import {
|
|
|
56
60
|
} from '../_lib/session_eol.js';
|
|
57
61
|
import { readContextObservation } from '../_lib/context_observation.js';
|
|
58
62
|
import { buildCheckpoint, writeCheckpoint } from '../_lib/run_checkpoint.js';
|
|
63
|
+
import {
|
|
64
|
+
auto_record_enabled,
|
|
65
|
+
buildContinuityRecord,
|
|
66
|
+
run_checkpoints_enabled,
|
|
67
|
+
} from '../_lib/continuity_writer.js';
|
|
68
|
+
import { publishContinuityRecord } from '../_lib/continuity_slot.js';
|
|
59
69
|
import { read_claimed_slug } from '../session_register_hook.js';
|
|
60
70
|
import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
|
|
61
71
|
import { readHookStdin } from './hook_stdin.js';
|
|
@@ -210,6 +220,62 @@ export function envelopeExists(
|
|
|
210
220
|
}
|
|
211
221
|
}
|
|
212
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Records that were written and never read — the detection layer this defect
|
|
225
|
+
* did not have.
|
|
226
|
+
*
|
|
227
|
+
* `road-to-a-recycle-envelope-that-is-consumed` step 1.2. Twelve unconsumed
|
|
228
|
+
* envelopes accumulated across the estate over roughly a month, and the only
|
|
229
|
+
* reason anyone found them was a stray-copy check during an unrelated task. The
|
|
230
|
+
* producer reported success, the consumer reported `absent`, and nothing
|
|
231
|
+
* compared the two — so the mechanism was inert for weeks with no signal.
|
|
232
|
+
*
|
|
233
|
+
* Advisory ONLY. A hook that blocks a session end over a stale runtime file is
|
|
234
|
+
* worse than the file, and that is Kill-register K3 of the roadmap rather than
|
|
235
|
+
* a preference here.
|
|
236
|
+
*
|
|
237
|
+
* Silent in the normal case, which is what keeps it worth reading: a record
|
|
238
|
+
* younger than {@link RECYCLE_MAX_AGE_HOURS} is a pending resume, not a defect,
|
|
239
|
+
* and past that age the consumer discards it loudly on its own. What this
|
|
240
|
+
* reports is the third state — old enough to be dead, still sitting there,
|
|
241
|
+
* which means no successor ever came for it.
|
|
242
|
+
*/
|
|
243
|
+
export function unconsumedRecordLines(
|
|
244
|
+
workspaceRoot: string,
|
|
245
|
+
now: Date = new Date(),
|
|
246
|
+
maxAgeHours: number = RECYCLE_MAX_AGE_HOURS,
|
|
247
|
+
): string[] {
|
|
248
|
+
const out: string[] = [];
|
|
249
|
+
for (const file of listContinuityRecords(workspaceRoot)) {
|
|
250
|
+
let raw: string;
|
|
251
|
+
try {
|
|
252
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
253
|
+
} catch {
|
|
254
|
+
continue; // an unreadable record proves nothing in either direction
|
|
255
|
+
}
|
|
256
|
+
let written: unknown;
|
|
257
|
+
let nextTask: unknown;
|
|
258
|
+
try {
|
|
259
|
+
const parsed = JSON.parse(raw) as { written_at?: unknown; next_task?: unknown };
|
|
260
|
+
written = parsed.written_at;
|
|
261
|
+
nextTask = parsed.next_task;
|
|
262
|
+
} catch {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (typeof written !== 'string') continue;
|
|
266
|
+
const stamp = Date.parse(written);
|
|
267
|
+
if (Number.isNaN(stamp)) continue;
|
|
268
|
+
const ageHours = (now.getTime() - stamp) / 3_600_000;
|
|
269
|
+
if (ageHours <= maxAgeHours) continue;
|
|
270
|
+
const task = typeof nextTask === 'string' && nextTask.trim() !== '' ? nextTask.trim() : '(no next_task)';
|
|
271
|
+
out.push(
|
|
272
|
+
`unconsumed continuity record: ${path.relative(workspaceRoot, file)} is ` +
|
|
273
|
+
`${ageHours.toFixed(0)}h old and was never read — next_task was "${task.slice(0, 120)}"`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
|
|
213
279
|
/**
|
|
214
280
|
* The follow-up line: the advisory already fired, and no envelope arrived.
|
|
215
281
|
*
|
|
@@ -389,7 +455,7 @@ export function main(): number {
|
|
|
389
455
|
// Best-effort throughout. A checkpoint is a recovery aid, and a
|
|
390
456
|
// recovery aid that can fail a Stop is a liability.
|
|
391
457
|
const checkpointRunId = payloadSessionId(payload, envelope);
|
|
392
|
-
if (shouldAdvise && checkpointRunId !== '') {
|
|
458
|
+
if (shouldAdvise && checkpointRunId !== '' && run_checkpoints_enabled(workspaceRoot)) {
|
|
393
459
|
try {
|
|
394
460
|
const slug = read_claimed_slug(workspaceRoot, checkpointRunId);
|
|
395
461
|
if (slug !== null) {
|
|
@@ -411,6 +477,8 @@ export function main(): number {
|
|
|
411
477
|
}
|
|
412
478
|
}
|
|
413
479
|
|
|
480
|
+
writeContinuityRecord(workspaceRoot, rawSessionId, counters, threshold, tokens);
|
|
481
|
+
|
|
414
482
|
if (shouldAdvise && threshold !== null && tokens !== null) {
|
|
415
483
|
process.stdout.write(
|
|
416
484
|
`${JSON.stringify({
|
|
@@ -434,6 +502,56 @@ export function main(): number {
|
|
|
434
502
|
return 0;
|
|
435
503
|
}
|
|
436
504
|
|
|
505
|
+
/**
|
|
506
|
+
* The continuity-record handler — one of the three lifecycle handlers this
|
|
507
|
+
* concern carries, and independently switchable from the other two.
|
|
508
|
+
*
|
|
509
|
+
* `road-to-continuity-writer-activation` step 1.2, and it lands INSIDE this
|
|
510
|
+
* concern rather than as a fourth concern id on purpose: `check_estate_count`
|
|
511
|
+
* ratchets `concern_count` with allowance 0, and the 2026-09-08 AI council
|
|
512
|
+
* refused a temporary allowance 2/2. The substance the 2026-09-07 council's D2
|
|
513
|
+
* asked for is independent switching and failure isolation, and a handler
|
|
514
|
+
* behind its own settings key inside a `try` gives both without a manifest
|
|
515
|
+
* split. The split itself is step 2.1 and stays blocked until a concern is
|
|
516
|
+
* retired to pay for it.
|
|
517
|
+
*
|
|
518
|
+
* Fires from the recycle THRESHOLD, not from the once-per-session advisory
|
|
519
|
+
* stamp: the stamp fires once, so a record written from it would be frozen at
|
|
520
|
+
* the moment the session crossed the threshold and would go stale as the
|
|
521
|
+
* session continued. Driving it from the raw threshold condition instead means
|
|
522
|
+
* every later Stop supersedes the record under the slot's own policy, so what a
|
|
523
|
+
* successor reads is always the newest state, and the write rate is still
|
|
524
|
+
* bounded to the end phase of a session rather than every turn of it.
|
|
525
|
+
*
|
|
526
|
+
* Best-effort throughout, like the checkpoint above it: a continuity record is
|
|
527
|
+
* a recovery aid, and a recovery aid that can fail a Stop is a liability.
|
|
528
|
+
*/
|
|
529
|
+
function writeContinuityRecord(
|
|
530
|
+
workspaceRoot: string,
|
|
531
|
+
rawSessionId: string | null,
|
|
532
|
+
counters: EolCounters,
|
|
533
|
+
threshold: number | null,
|
|
534
|
+
tokens: number | null,
|
|
535
|
+
): void {
|
|
536
|
+
if (threshold === null || tokens === null || tokens < threshold) return;
|
|
537
|
+
const sessionId = String(rawSessionId ?? '').trim();
|
|
538
|
+
if (sessionId === '') return;
|
|
539
|
+
try {
|
|
540
|
+
if (!auto_record_enabled(workspaceRoot)) return;
|
|
541
|
+
const slug = read_claimed_slug(workspaceRoot, sessionId);
|
|
542
|
+
const decision = buildContinuityRecord({
|
|
543
|
+
root: workspaceRoot,
|
|
544
|
+
sessionId,
|
|
545
|
+
slug,
|
|
546
|
+
counters,
|
|
547
|
+
});
|
|
548
|
+
if (decision.record === null) return;
|
|
549
|
+
publishContinuityRecord(workspaceRoot, sessionId, decision.record);
|
|
550
|
+
} catch {
|
|
551
|
+
// never block the Stop path, and never suppress a sibling handler
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
437
555
|
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
438
556
|
// every module shares the bundle's `import.meta.url` (see cmd_migrate.ts).
|
|
439
557
|
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
@@ -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
|