@event4u/agent-config 14.5.0 → 14.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +384 -3
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/decision.md +7 -0
- package/dist/agent-src/commands/analyze/inbox.md +264 -10
- package/dist/agent-src/commands/analyze.md +1 -1
- package/dist/agent-src/commands/optimize/project.md +10 -0
- package/dist/agent-src/commands/roadmap/process-full.md +132 -4
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +31 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +161 -1
- package/dist/agent-src/rules/context-hygiene.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +111 -7
- package/dist/agent-src/rules/fix-what-you-see.md +108 -0
- package/dist/agent-src/rules/no-roadmap-references.md +0 -5
- package/dist/agent-src/rules/recurring-criticism.md +110 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
- package/dist/agent-src/rules/rule-type-governance.md +0 -2
- package/dist/agent-src/rules/self-repair-loop.md +17 -0
- package/dist/agent-src/rules/skill-quality.md +0 -2
- package/dist/agent-src/rules/source-confidentiality.md +37 -7
- package/dist/agent-src/scripts/archival_sweep.ts +117 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
- package/dist/agent-src/scripts/gate_budget.ts +282 -0
- package/dist/agent-src/scripts/gate_execute.ts +72 -31
- package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
- package/dist/agent-src/scripts/update_roadmap_progress.ts +251 -7
- package/dist/agent-src/skills/adr-create/SKILL.md +25 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
- package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
- package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
- package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
- package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
- package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
- package/dist/agent-src/skills/decision-review/SKILL.md +18 -0
- package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
- package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
- package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
- package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
- package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
- package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
- package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
- package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
- package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
- package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +45 -2
- package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/dist/cli/registry.js +1 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
- package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-4VR4ORQ6.js} +10 -7
- package/dist/cli-delegate/chunk-6EOCNQAM.js +402 -0
- package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-74E3TKWV.js} +381 -280
- package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-7F3VYSM2.js} +4 -4
- package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
- package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
- package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-GJ5BVHXM.js} +72 -165
- package/dist/cli-delegate/{chunk-ZPSXJTLI.js → chunk-JZ4NR2TW.js} +353 -1
- package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
- package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
- package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
- package/dist/cli-delegate/cmd_conformance.js +459 -709
- package/dist/cli-delegate/cmd_doctor.js +6 -6
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +7 -6
- package/dist/cli-delegate/cmd_handoff.js +6 -6
- package/dist/cli-delegate/cmd_packs_active.js +1 -1
- package/dist/cli-delegate/cmd_preflight.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +7 -6
- package/dist/cli-delegate/cmd_session_recycle.js +840 -30
- package/dist/cli-delegate/cmd_settings_migrate.js +7 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +7 -6
- package/dist/cli-delegate/cmd_uninstall.js +9 -7
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +7 -6
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +72 -29
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +3127 -2022
- package/dist/install/claudePathsPlan.js +187 -0
- package/dist/install/claudePathsPlan.js.map +1 -0
- package/dist/install/hostLayerFingerprint.js +136 -0
- package/dist/install/hostLayerFingerprint.js.map +1 -0
- package/dist/install/install.mjs +553 -425
- package/dist/install/partitionEligibility.js +276 -0
- package/dist/install/partitionEligibility.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1744 -1191
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_src.js +41 -0
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/scripts/_lib/install_layout.js +83 -0
- package/dist/scripts/_lib/install_layout.js.map +1 -0
- package/dist/scripts/_lib/installed_lock.js +444 -0
- package/dist/scripts/_lib/installed_lock.js.map +1 -0
- package/dist/scripts/condense.js +13 -164
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +4 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +12 -6
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
- package/docs/guidelines/agent-infra/engineering-memory-data-format.md +49 -0
- package/docs/guidelines/agent-infra/memory-access.md +17 -0
- package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archival_sweep.ts +117 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
- package/src/agent-src/scripts/gate_budget.ts +282 -0
- package/src/agent-src/scripts/gate_execute.ts +72 -31
- package/src/agent-src/scripts/roadmap_gates.ts +74 -3
- package/src/agent-src/scripts/update_roadmap_progress.ts +251 -7
- package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/src/config/agent-settings.template.yml +27 -2
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/estate-count-budget.json +367 -3
- package/src/config/gate-violation-baselines.json +41 -41
- package/src/config/pack-size-budget.json +3 -2
- package/src/config/rule-activation-census.json +14 -6
- package/src/config/rule-stub-ceilings.json +19 -6
- package/src/install/claudePathsPlan.ts +196 -0
- package/src/install/hostLayerFingerprint.ts +145 -0
- package/src/install/partitionEligibility.ts +325 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
- package/src/scripts/_dispatch.bash +6 -1
- package/src/scripts/_lib/ac_heading.ts +30 -0
- package/src/scripts/_lib/agent_src.ts +42 -0
- package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
- package/src/scripts/_lib/envelope_grounding.ts +38 -8
- package/src/scripts/_lib/installed_lock.ts +50 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +85 -0
- package/src/scripts/_lib/md_section.ts +129 -0
- package/src/scripts/_lib/scope_guard.sh +86 -4
- package/src/scripts/_lib/self_repair.ts +19 -0
- package/src/scripts/_lib/self_repair_class_b.ts +171 -0
- package/src/scripts/_lib/subagent_capsule.ts +81 -1
- package/src/scripts/adr/regenerate_index.ts +13 -2
- package/src/scripts/adr_cite_check.ts +424 -0
- package/src/scripts/ai_council/cli_help.ts +55 -0
- package/src/scripts/before_complete_hook.ts +153 -28
- package/src/scripts/bench_ab_v2_complexity.ts +27 -7
- package/src/scripts/check_adr_frontmatter.ts +187 -1
- package/src/scripts/check_references.ts +60 -0
- package/src/scripts/check_release_published.ts +32 -11
- package/src/scripts/check_single_delivery.ts +517 -0
- package/src/scripts/ci_settle.ts +170 -0
- package/src/scripts/condense.ts +20 -170
- package/src/scripts/council_cli.ts +3 -3
- package/src/scripts/dispatch_r2_reviewer.ts +6 -14
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +77 -14
- package/src/scripts/hooks/block_no_verify.ts +61 -10
- package/src/scripts/hooks/block_unauthorized_git.ts +1 -1
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
- package/src/scripts/hooks/run_continuation_hook.ts +887 -68
- package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
- package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
- package/src/scripts/hooks/state_io.ts +782 -20
- package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
- package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
- package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
- package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
- package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
- package/src/scripts/install.sh +12 -2
- package/src/scripts/install.ts +16 -11
- package/src/scripts/language_mirror_hook.ts +561 -25
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_plan_risk_register.ts +4 -3
- package/src/scripts/lint_skill_router_head.ts +4 -4
- package/src/scripts/mcp_server/tools.ts +2 -2
- package/src/scripts/memory_consolidation.ts +189 -0
- package/src/scripts/memory_eviction.ts +384 -0
- package/src/scripts/new_roadmap.ts +136 -0
- package/src/scripts/pack_mcp_content.ts +20 -5
- package/src/scripts/release.ts +37 -38
- package/src/scripts/report_layer_overlap.ts +78 -0
- package/src/scripts/report_memory_pointers.ts +622 -0
- package/src/scripts/self_repair_hook.ts +37 -5
- package/src/scripts/session_register_hook.ts +115 -15
- package/src/scripts/skill_usage_collect.ts +11 -1
- package/src/scripts/skill_usage_report.ts +278 -8
- package/src/scripts/sync_pr_branch.ts +7 -0
- package/src/server/schemas/settings.ts +8 -0
- package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
- package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
- /package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
|
@@ -33,6 +33,7 @@ import path from 'node:path';
|
|
|
33
33
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
34
34
|
|
|
35
35
|
import { readHookStdin } from './hook_stdin.js';
|
|
36
|
+
import { isPayloadStub } from './payload_stub.js';
|
|
36
37
|
import { atomic_write_json } from './state_io.js';
|
|
37
38
|
|
|
38
39
|
export const STATE_FILE = path.join('agents', 'state', 'ship-diff-volume.json');
|
|
@@ -89,31 +90,96 @@ export function correctedVolume(numstat: string): { volume: number; excluded: nu
|
|
|
89
90
|
return { volume, excluded, files };
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Tools that carry a shell command. Mirrors `block_unauthorized_git`'s set.
|
|
95
|
+
*
|
|
96
|
+
* `SHIP_PATTERNS` are unanchored, so without this gate a `grep` for the string
|
|
97
|
+
* `git push` spawns two `git` subprocesses on a blocking slot. ALLOW-list: a
|
|
98
|
+
* named tool outside it is declined, so an unlisted host shell goes dark until
|
|
99
|
+
* added — the trade that sibling already makes while BLOCKING. A payload naming
|
|
100
|
+
* no tool still reads; that is the bare-host shape, with nothing to gate on.
|
|
101
|
+
*/
|
|
102
|
+
const COMMAND_TOOLS: ReadonlySet<string> = new Set([
|
|
103
|
+
'launch-process',
|
|
104
|
+
'launch_process',
|
|
105
|
+
'Bash',
|
|
106
|
+
'BashTool',
|
|
107
|
+
'run-process',
|
|
108
|
+
'runProcess',
|
|
109
|
+
'shell',
|
|
110
|
+
'execute_shell',
|
|
111
|
+
'RunShellCommand',
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
function isObj(v: unknown): v is Record<string, unknown> {
|
|
115
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Descend to the platform payload.
|
|
120
|
+
*
|
|
121
|
+
* The dispatcher nests the host-shaped tool fields under `payload`
|
|
122
|
+
* (`_build_envelope`); a bare host invocation puts them at the top level. This
|
|
123
|
+
* concern shipped reading the top level ONLY, so under the real dispatcher it
|
|
124
|
+
* found nothing and returned `''` every time — it ran, cost a dispatch, and
|
|
125
|
+
* could never fire.
|
|
126
|
+
*
|
|
127
|
+
* Deliberately NOT `envelope.ts`'s shared `unwrap`, which descends only when all
|
|
128
|
+
* four `ENVELOPE_KEYS` are present: a partial envelope would return this concern
|
|
129
|
+
* to its pre-fix dead state with no test noticing. `design-slop` and
|
|
130
|
+
* `ui-route-nudge` each carry their own local descent for the same reason.
|
|
131
|
+
*/
|
|
132
|
+
function payloadOf(root: Record<string, unknown>): Record<string, unknown> {
|
|
133
|
+
const inner = root['payload'];
|
|
134
|
+
return isObj(inner) ? inner : root;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The ship command, or `''` when this payload does not carry one. */
|
|
138
|
+
function shipCommandFrom(payload: Record<string, unknown>): string {
|
|
139
|
+
const toolRaw = payload['tool_name'] ?? payload['toolName'] ?? payload['tool'];
|
|
140
|
+
if (typeof toolRaw === 'string' && !COMMAND_TOOLS.has(toolRaw)) return '';
|
|
141
|
+
|
|
142
|
+
const ti = payload['tool_input'] ?? payload['toolInput'];
|
|
143
|
+
if (isPayloadStub(ti)) {
|
|
144
|
+
// Body omitted despite `needs_payload_bodies: [input]`. A bare '' here is
|
|
145
|
+
// indistinguishable from "no ship verb" — the silent death this file was
|
|
146
|
+
// fixed to remove — so say so instead of dying quietly again.
|
|
147
|
+
process.stderr.write(
|
|
148
|
+
'ship-diff-volume: tool_input arrived stubbed; the concern declares ' +
|
|
149
|
+
'needs_payload_bodies: [input] and cannot read a command without it.\n',
|
|
150
|
+
);
|
|
151
|
+
return '';
|
|
152
|
+
}
|
|
153
|
+
if (isObj(ti)) {
|
|
154
|
+
const c = ti['command'];
|
|
96
155
|
if (typeof c === 'string') return c;
|
|
97
156
|
}
|
|
98
|
-
const c =
|
|
157
|
+
const c = payload['command'];
|
|
99
158
|
return typeof c === 'string' ? c : '';
|
|
100
159
|
}
|
|
101
160
|
|
|
161
|
+
/**
|
|
162
|
+
* The stdin boundary: raw concern stdin → the ship command. Exported so a
|
|
163
|
+
* regression drives the boundary rather than the extractor. Never throws.
|
|
164
|
+
*/
|
|
165
|
+
export function commandFromStdin(raw: string): string {
|
|
166
|
+
if (!raw.trim()) return '';
|
|
167
|
+
let root: unknown;
|
|
168
|
+
try {
|
|
169
|
+
root = JSON.parse(raw);
|
|
170
|
+
} catch {
|
|
171
|
+
return '';
|
|
172
|
+
}
|
|
173
|
+
if (!isObj(root)) return '';
|
|
174
|
+
return shipCommandFrom(payloadOf(root));
|
|
175
|
+
}
|
|
176
|
+
|
|
102
177
|
export function main(argv?: string[]): number {
|
|
103
178
|
const args = argv ?? process.argv.slice(2);
|
|
104
179
|
const idx = args.indexOf('--command');
|
|
105
180
|
let cmd = idx >= 0 && args[idx + 1] !== undefined ? args[idx + 1]! : '';
|
|
106
181
|
if (!cmd) {
|
|
107
|
-
|
|
108
|
-
let envelope: Record<string, unknown> = {};
|
|
109
|
-
if (raw.trim()) {
|
|
110
|
-
try {
|
|
111
|
-
envelope = JSON.parse(raw) as Record<string, unknown>;
|
|
112
|
-
} catch {
|
|
113
|
-
envelope = {};
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
cmd = extractCommand(envelope);
|
|
182
|
+
cmd = commandFromStdin(readHookStdin());
|
|
117
183
|
}
|
|
118
184
|
if (!isShipCommand(cmd)) return 0;
|
|
119
185
|
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* source-first-gate — SHADOW ONLY (road-to-source-first-frontend Phase 3
|
|
4
|
+
* Steps 1–2, activated per `docs/contracts/concern-activation-policy.md`).
|
|
5
|
+
*
|
|
6
|
+
* What it does: on every capture-shaped tool call, record what a source-first
|
|
7
|
+
* gate WOULD have warned under several candidate gating rules. It emits
|
|
8
|
+
* nothing to the model and it never blocks. Exit is always 0.
|
|
9
|
+
*
|
|
10
|
+
* ── Why shadow and not the step's literal "exit 2 warn" ───────────────────
|
|
11
|
+
*
|
|
12
|
+
* Phase 3 Step 1 was drafted as `exit 2 warn`; Step 2 left the posture as an
|
|
13
|
+
* explicit maintainer call and the roadmap's own 2026-08-17 correction records
|
|
14
|
+
* that the activation policy **contradicts** the drafted default-ON warn and
|
|
15
|
+
* that the step "needs re-arguing against it, not a citation". Re-argued, and
|
|
16
|
+
* the policy wins on both of the step's own dependencies:
|
|
17
|
+
*
|
|
18
|
+
* 1. **The verifier exemption stops being a shipping dependency.** Risk 3 of
|
|
19
|
+
* that roadmap is "the gate warns the one actor doing it right" — a
|
|
20
|
+
* verifier subagent screenshotting for QA. The harm in that risk is
|
|
21
|
+
* entirely in the *emission*: a concern that emits nothing warns nobody,
|
|
22
|
+
* correctly or otherwise. The clean exemption key is the payload
|
|
23
|
+
* `agent_id` / `agent_type` field that `road-to-subagent-lifecycle-
|
|
24
|
+
* integrity` Phase 0 Step 4 spikes and its Phase 4 Step 1 binds; both are
|
|
25
|
+
* still `[ ]`, and both are *fields on the record below* rather than
|
|
26
|
+
* branches, so the shadow window measures how large the exemption would
|
|
27
|
+
* have to be instead of guessing it.
|
|
28
|
+
* 2. **The matcher census stops being a shipping dependency.** Phase 1
|
|
29
|
+
* Step 2 is `[~]`: one host was censused and its only capture tool
|
|
30
|
+
* (`screencapture`) photographs the display rather than a page, so a
|
|
31
|
+
* matcher built from it alone "would watch the wrong surface". Under a
|
|
32
|
+
* warn posture that is a false-positive generator. Under shadow it is the
|
|
33
|
+
* measurement — `capture_kind` below separates page-reaching capture from
|
|
34
|
+
* display capture on every record, which is exactly the datum the census
|
|
35
|
+
* is missing.
|
|
36
|
+
*
|
|
37
|
+
* The policy's ladder is `shadow → blocking`, with advisory as a *destination*
|
|
38
|
+
* rather than a waiting room. This concern's intended terminal posture IS
|
|
39
|
+
* advisory (a screenshot is a judgement call, the `ui-route-nudge` argument
|
|
40
|
+
* verbatim, and Phase 3 Step 1 says "never a block"). That does not license
|
|
41
|
+
* skipping shadow: the policy skips the advisory *rung* on the way to blocking
|
|
42
|
+
* because a reminder buys little, and the same measurement debt applies to a
|
|
43
|
+
* terminal advisory — without the shadow window there is no false-positive
|
|
44
|
+
* rate for a warn either.
|
|
45
|
+
*
|
|
46
|
+
* ── Flip condition, pre-registered as Step 2 requires ─────────────────────
|
|
47
|
+
*
|
|
48
|
+
* Shadow → advisory (warn, exit 2) when ALL of:
|
|
49
|
+
* a. >= 100 capture-shaped records, or >= 2 weeks of window, whichever
|
|
50
|
+
* comes first, including at least one record with
|
|
51
|
+
* `handover_seen_in_session: true` (the policy's window floor);
|
|
52
|
+
* b. one candidate rule's false-positive share — records where the
|
|
53
|
+
* candidate would warn AND `capture_kind` is not `page` — is <= 1 %;
|
|
54
|
+
* c. `verifier_exemption_decidable` is true on the records, i.e. SLI
|
|
55
|
+
* Phase 4 Step 1 has landed and the payload axis can carry the
|
|
56
|
+
* exemption;
|
|
57
|
+
* d. the shipped matcher is narrowed to the entries that actually appear,
|
|
58
|
+
* which closes Phase 1 Step 2's multi-host half by observation.
|
|
59
|
+
* Reverse trigger: no records in 8 weeks → evaluate removal; the concern's
|
|
60
|
+
* population would then be as empty as the read-before-write rate's.
|
|
61
|
+
*
|
|
62
|
+
* ── What this does NOT measure, stated rather than implied ────────────────
|
|
63
|
+
*
|
|
64
|
+
* "The session has a routed design handover" is the step's first condition and
|
|
65
|
+
* **nothing records it**. `design-fidelity` is a rule; it has no runtime
|
|
66
|
+
* carrier, so "the same trigger event `design-fidelity` records" describes an
|
|
67
|
+
* event that does not exist. Two observable proxies are recorded side by side
|
|
68
|
+
* instead of one being promoted by preference — a handover path named by any
|
|
69
|
+
* tool this session, and a handover artifact present on disk — and the
|
|
70
|
+
* candidate spread is what tells them apart. Picking one now would be the
|
|
71
|
+
* threshold-first mistake the activation policy names.
|
|
72
|
+
*
|
|
73
|
+
* PRIVACY BY CONSTRUCTION: booleans, counts, a matcher label, and the
|
|
74
|
+
* id-shaped `agent_type` enum (recorded verbatim, the same stance
|
|
75
|
+
* `subagent_ledger_hook.ts` states for that field). No prompt, no command
|
|
76
|
+
* text, no file path, no `agent_id` — the record type has no field able to
|
|
77
|
+
* hold any.
|
|
78
|
+
*/
|
|
79
|
+
import * as fs from 'node:fs';
|
|
80
|
+
import * as path from 'node:path';
|
|
81
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
82
|
+
|
|
83
|
+
import { is_replay_mode } from './state_io.js';
|
|
84
|
+
import { readHookStdin } from './hook_stdin.js';
|
|
85
|
+
import { resolveConsumerRoot } from './subagent_ledger_hook.js';
|
|
86
|
+
|
|
87
|
+
const EXIT_ALLOW = 0;
|
|
88
|
+
|
|
89
|
+
/** Would-warns per session before the valve goes silent — `MAX_NUDGES` shape. */
|
|
90
|
+
export const MAX_SHADOW_WARNS = 2;
|
|
91
|
+
|
|
92
|
+
/** Shadow log; `/agents/runtime/` is gitignored, so this stays local-only. */
|
|
93
|
+
export const SHADOW_LOG = path.join('agents', 'runtime', 'state', 'source-first-gate.jsonl');
|
|
94
|
+
/** Per-session latches. */
|
|
95
|
+
export const STATE_FILE = path.join('agents', 'runtime', 'state', 'source-first-gate.json');
|
|
96
|
+
|
|
97
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
98
|
+
type JsonObject = { [k: string]: JsonValue };
|
|
99
|
+
|
|
100
|
+
function isObject(v: unknown): v is JsonObject {
|
|
101
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function unwrapPayload(envelope: JsonObject): JsonObject {
|
|
105
|
+
const inner = envelope['payload'];
|
|
106
|
+
return isObject(inner) ? inner : envelope;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* What a capture-shaped tool call reaches.
|
|
111
|
+
*
|
|
112
|
+
* `page` is the surface this concern cares about; `display` is the Phase-1
|
|
113
|
+
* census finding that the one tool actually present on the censused host
|
|
114
|
+
* photographs the physical screen, so it cannot be the "screenshot instead of
|
|
115
|
+
* source" path at all. Keeping them apart on every record is what lets the
|
|
116
|
+
* flip condition compute a false-positive share instead of asserting one.
|
|
117
|
+
*/
|
|
118
|
+
export type CaptureKind = 'page' | 'display' | 'unknown';
|
|
119
|
+
|
|
120
|
+
export interface MatcherHit {
|
|
121
|
+
readonly label: string;
|
|
122
|
+
readonly capture_kind: CaptureKind;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Tool-name matchers. TREE-SOURCED, not recalled — each entry is a name this
|
|
127
|
+
* repository already writes down, which is the closest available substitute
|
|
128
|
+
* for the multi-host census Phase 1 Step 2 could not finish:
|
|
129
|
+
*
|
|
130
|
+
* - `browser_take_screenshot` — `src/skills/mcp/SKILL.md` § tool table.
|
|
131
|
+
* - `claude-in-chrome` — `src/skills/screenshot-hygiene/SKILL.md`,
|
|
132
|
+
* `docs/decisions/ADR-125-doc-screenshot-anonymization.md`.
|
|
133
|
+
* - `screencapture` — the one entry the census measured present,
|
|
134
|
+
* `tests/design-artifacts/eval-fixtures.md` § the census note.
|
|
135
|
+
*
|
|
136
|
+
* The trailing generic patterns exist because host tool naming varies (the
|
|
137
|
+
* `Bash`/`BashTool`/`launch-process` spread the manifest already warns about
|
|
138
|
+
* for the blocking guards). Under shadow an over-broad matcher costs a log
|
|
139
|
+
* line, and its breadth is the thing being measured; a narrowed shipped
|
|
140
|
+
* matcher is flip condition (d).
|
|
141
|
+
*/
|
|
142
|
+
export const TOOL_MATCHERS: readonly { pattern: RegExp; hit: MatcherHit }[] = [
|
|
143
|
+
{ pattern: /^browser_take_screenshot$/i, hit: { label: 'playwright-mcp', capture_kind: 'page' } },
|
|
144
|
+
{ pattern: /^mcp__claude-in-chrome__/i, hit: { label: 'claude-in-chrome', capture_kind: 'page' } },
|
|
145
|
+
{ pattern: /^(browser_)?take_screenshot$/i, hit: { label: 'chrome-devtools-mcp', capture_kind: 'page' } },
|
|
146
|
+
{ pattern: /screen_?shot/i, hit: { label: 'tool-name-generic', capture_kind: 'unknown' } },
|
|
147
|
+
{ pattern: /screen_?capture/i, hit: { label: 'tool-name-generic', capture_kind: 'unknown' } },
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
/** Shell-shaped tool names whose command string is worth reading. */
|
|
151
|
+
const SHELL_TOOLS = /^(bash|bashtool|launch[-_]process|shell|run[-_]command)$/i;
|
|
152
|
+
|
|
153
|
+
/** Command-text matchers for a capture reached through a shell. */
|
|
154
|
+
export const COMMAND_MATCHERS: readonly { pattern: RegExp; hit: MatcherHit }[] = [
|
|
155
|
+
{ pattern: /\bscreencapture\b/i, hit: { label: 'macos-screencapture', capture_kind: 'display' } },
|
|
156
|
+
{ pattern: /\bimport\s+-window\b/i, hit: { label: 'x11-import', capture_kind: 'display' } },
|
|
157
|
+
{ pattern: /\bgnome-screenshot\b|\bspectacle\b|\bscrot\b/i, hit: { label: 'linux-screenshot', capture_kind: 'display' } },
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The handover path shapes `design-fidelity` routes on a FILE rather than on a
|
|
162
|
+
* word: `file_pattern: *design.html` and `path_prefix: .claude/design-system/`.
|
|
163
|
+
*
|
|
164
|
+
* DIRECTION-AGNOSTIC ON PURPOSE, and that is the one difference from
|
|
165
|
+
* `ui_route_nudge_hook.isArtifactRead`, which requires a non-write. Handover
|
|
166
|
+
* *presence* and handover *having been read* are the two separate facts this
|
|
167
|
+
* concern needs, and a single predicate cannot carry both. The read latch
|
|
168
|
+
* below delegates to that exported predicate rather than re-deriving it;
|
|
169
|
+
* `source_first_gate.test.ts` pins the two path shapes against each other so
|
|
170
|
+
* the copy cannot drift.
|
|
171
|
+
*/
|
|
172
|
+
export function isHandoverPath(file: string): boolean {
|
|
173
|
+
if (!file) return false;
|
|
174
|
+
const normalized = file.replace(/\\/g, '/').toLowerCase();
|
|
175
|
+
return normalized.endsWith('design.html') || normalized.includes('.claude/design-system/');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface CaptureEvent {
|
|
179
|
+
/** Matcher hit, or null when this is not a capture-shaped call. */
|
|
180
|
+
readonly capture: MatcherHit | null;
|
|
181
|
+
/** Path the tool names; "" when it names none. */
|
|
182
|
+
readonly file: string;
|
|
183
|
+
/** True when the tool proposes new content (Write / Edit shape). */
|
|
184
|
+
readonly isWrite: boolean;
|
|
185
|
+
readonly agentIdPresent: boolean;
|
|
186
|
+
readonly agentType: string | null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function str(o: JsonObject, ...keys: string[]): string | null {
|
|
190
|
+
for (const k of keys) {
|
|
191
|
+
const v = o[k];
|
|
192
|
+
if (typeof v === 'string' && v) return v;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Classify one envelope. Pure apart from the regex tables above. */
|
|
198
|
+
export function extractEvent(outer: JsonValue): CaptureEvent | null {
|
|
199
|
+
if (!isObject(outer)) return null;
|
|
200
|
+
const payload = unwrapPayload(outer);
|
|
201
|
+
const toolName = str(payload, 'tool_name', 'toolName', 'tool');
|
|
202
|
+
|
|
203
|
+
const ti = payload['tool_input'] ?? payload['toolInput'] ?? payload['input'];
|
|
204
|
+
const input = isObject(ti) ? ti : {};
|
|
205
|
+
const file = str(input, 'file_path', 'path', 'filePath') ?? '';
|
|
206
|
+
let isWrite = false;
|
|
207
|
+
for (const key of ['content', 'new_string', 'new_str', 'text', 'newText']) {
|
|
208
|
+
if (typeof input[key] === 'string' && (input[key] as string).length > 0) isWrite = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let capture: MatcherHit | null = null;
|
|
212
|
+
if (toolName) {
|
|
213
|
+
for (const m of TOOL_MATCHERS) {
|
|
214
|
+
if (m.pattern.test(toolName)) {
|
|
215
|
+
capture = m.hit;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!capture && SHELL_TOOLS.test(toolName)) {
|
|
220
|
+
const command = str(input, 'command', 'cmd', 'script') ?? '';
|
|
221
|
+
for (const m of COMMAND_MATCHERS) {
|
|
222
|
+
if (m.pattern.test(command)) {
|
|
223
|
+
capture = m.hit;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
capture,
|
|
232
|
+
file,
|
|
233
|
+
isWrite,
|
|
234
|
+
agentIdPresent: str(payload, 'agent_id', 'agentId') !== null,
|
|
235
|
+
agentType: str(payload, 'agent_type', 'agentType'),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface SessionState {
|
|
240
|
+
/** A handover path was named by any tool this session. */
|
|
241
|
+
handoverSeen: boolean;
|
|
242
|
+
/** A handover artifact was READ this session (rung 1 of the ladder). */
|
|
243
|
+
sourceRead: boolean;
|
|
244
|
+
/** Would-warns already recorded, for the valve arm. */
|
|
245
|
+
wouldWarn: number;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export const EMPTY_STATE: SessionState = { handoverSeen: false, sourceRead: false, wouldWarn: 0 };
|
|
249
|
+
|
|
250
|
+
/** Sessions retained; oldest drop past this — `ui_route_nudge` shape. */
|
|
251
|
+
export const MAX_SESSIONS = 50;
|
|
252
|
+
|
|
253
|
+
export function readState(root: string, session: string): SessionState {
|
|
254
|
+
try {
|
|
255
|
+
const all = JSON.parse(fs.readFileSync(path.join(root, STATE_FILE), 'utf-8')) as Record<
|
|
256
|
+
string,
|
|
257
|
+
SessionState
|
|
258
|
+
>;
|
|
259
|
+
const entry = all[session];
|
|
260
|
+
if (
|
|
261
|
+
entry &&
|
|
262
|
+
typeof entry.handoverSeen === 'boolean' &&
|
|
263
|
+
typeof entry.sourceRead === 'boolean' &&
|
|
264
|
+
typeof entry.wouldWarn === 'number'
|
|
265
|
+
) {
|
|
266
|
+
return entry;
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
/* fresh session */
|
|
270
|
+
}
|
|
271
|
+
return { ...EMPTY_STATE };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function writeState(root: string, session: string, state: SessionState): void {
|
|
275
|
+
if (is_replay_mode()) return;
|
|
276
|
+
try {
|
|
277
|
+
const file = path.join(root, STATE_FILE);
|
|
278
|
+
let all: Record<string, SessionState> = {};
|
|
279
|
+
try {
|
|
280
|
+
all = JSON.parse(fs.readFileSync(file, 'utf-8')) as Record<string, SessionState>;
|
|
281
|
+
} catch {
|
|
282
|
+
// Fresh or unparseable. A corrupt file has already lost whatever
|
|
283
|
+
// it held, so starting empty is recovery rather than discard; the
|
|
284
|
+
// cost is bounded to a re-latch in the sessions in flight.
|
|
285
|
+
}
|
|
286
|
+
if (!isObject(all as unknown)) all = {};
|
|
287
|
+
all[session] = state;
|
|
288
|
+
const keys = Object.keys(all);
|
|
289
|
+
if (keys.length > MAX_SESSIONS) {
|
|
290
|
+
const pruned: Record<string, SessionState> = {};
|
|
291
|
+
for (const key of keys.slice(keys.length - MAX_SESSIONS)) pruned[key] = all[key]!;
|
|
292
|
+
all = pruned;
|
|
293
|
+
}
|
|
294
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
295
|
+
fs.writeFileSync(file, JSON.stringify(all));
|
|
296
|
+
} catch {
|
|
297
|
+
/* fail-open: a persistence failure must never disturb a tool call */
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** One candidate gating rule under shadow evaluation. */
|
|
302
|
+
export interface Candidate {
|
|
303
|
+
readonly label: string;
|
|
304
|
+
readonly requiresHandoverSeen: boolean;
|
|
305
|
+
readonly requiresHandoverOnDisk: boolean;
|
|
306
|
+
/** Either proxy satisfies the handover condition. */
|
|
307
|
+
readonly handoverEither: boolean;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The candidate spread. `handover-session` is Phase 3 Step 1's literal gating
|
|
312
|
+
* rule, carried as ONE candidate rather than as the answer; `unread-only` is
|
|
313
|
+
* the loosest rule and therefore the fire-volume ceiling the others are read
|
|
314
|
+
* against. A spread yields the curve the activation policy derives from — one
|
|
315
|
+
* candidate would only yield a yes/no.
|
|
316
|
+
*/
|
|
317
|
+
export const CANDIDATES: readonly Candidate[] = [
|
|
318
|
+
{ label: 'handover-session', requiresHandoverSeen: true, requiresHandoverOnDisk: false, handoverEither: false },
|
|
319
|
+
{ label: 'handover-disk', requiresHandoverSeen: false, requiresHandoverOnDisk: true, handoverEither: false },
|
|
320
|
+
{ label: 'handover-either', requiresHandoverSeen: false, requiresHandoverOnDisk: false, handoverEither: true },
|
|
321
|
+
{ label: 'unread-only', requiresHandoverSeen: false, requiresHandoverOnDisk: false, handoverEither: false },
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
export interface CandidateVerdict {
|
|
325
|
+
readonly label: string;
|
|
326
|
+
readonly would_warn: boolean;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function evaluateCandidates(
|
|
330
|
+
handoverSeen: boolean,
|
|
331
|
+
handoverOnDisk: boolean,
|
|
332
|
+
sourceRead: boolean,
|
|
333
|
+
): CandidateVerdict[] {
|
|
334
|
+
return CANDIDATES.map((c) => {
|
|
335
|
+
if (sourceRead) return { label: c.label, would_warn: false };
|
|
336
|
+
if (c.requiresHandoverSeen) return { label: c.label, would_warn: handoverSeen };
|
|
337
|
+
if (c.requiresHandoverOnDisk) return { label: c.label, would_warn: handoverOnDisk };
|
|
338
|
+
if (c.handoverEither) return { label: c.label, would_warn: handoverSeen || handoverOnDisk };
|
|
339
|
+
return { label: c.label, would_warn: true };
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** True when a handover artifact sits in the workspace, independent of reads. */
|
|
344
|
+
export function handoverPresentOnDisk(root: string): boolean {
|
|
345
|
+
try {
|
|
346
|
+
return fs.statSync(path.join(root, '.claude', 'design-system')).isDirectory();
|
|
347
|
+
} catch {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function appendShadowLine(root: string, line: Record<string, unknown>): void {
|
|
353
|
+
if (is_replay_mode()) return;
|
|
354
|
+
try {
|
|
355
|
+
const file = path.join(root, SHADOW_LOG);
|
|
356
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
357
|
+
fs.appendFileSync(file, `${JSON.stringify(line)}\n`, 'utf8');
|
|
358
|
+
} catch {
|
|
359
|
+
/* never disturb a tool call for a measurement */
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* The state transition, pure — so the test exercises the logic rather than the
|
|
365
|
+
* filesystem. A handover path seen latches presence; a handover READ latches
|
|
366
|
+
* rung 1. The read latch must be evaluated independently of the presence latch:
|
|
367
|
+
* an early return on the first hit would drop the read from the numerator, the
|
|
368
|
+
* same not-disjoint hazard `isArtifactRead`'s own header records.
|
|
369
|
+
*/
|
|
370
|
+
export function nextState(event: CaptureEvent, state: SessionState): SessionState {
|
|
371
|
+
let next = state;
|
|
372
|
+
if (isHandoverPath(event.file)) next = { ...next, handoverSeen: true };
|
|
373
|
+
if (!event.isWrite && isHandoverPath(event.file)) next = { ...next, sourceRead: true };
|
|
374
|
+
return next;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function processEnvelope(envelope: JsonValue, consumerRoot: string, session: string): number {
|
|
378
|
+
try {
|
|
379
|
+
const event = extractEvent(envelope);
|
|
380
|
+
if (!event) return EXIT_ALLOW;
|
|
381
|
+
|
|
382
|
+
const before = readState(consumerRoot, session);
|
|
383
|
+
const after = nextState(event, before);
|
|
384
|
+
|
|
385
|
+
if (event.capture === null) {
|
|
386
|
+
if (
|
|
387
|
+
after.handoverSeen !== before.handoverSeen ||
|
|
388
|
+
after.sourceRead !== before.sourceRead
|
|
389
|
+
) {
|
|
390
|
+
writeState(consumerRoot, session, after);
|
|
391
|
+
}
|
|
392
|
+
return EXIT_ALLOW;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const onDisk = handoverPresentOnDisk(consumerRoot);
|
|
396
|
+
const verdicts = evaluateCandidates(after.handoverSeen, onDisk, after.sourceRead);
|
|
397
|
+
const anyWarn = verdicts.some((v) => v.would_warn);
|
|
398
|
+
const valveWouldSilence = before.wouldWarn >= MAX_SHADOW_WARNS;
|
|
399
|
+
const nowIso = new Date().toISOString();
|
|
400
|
+
|
|
401
|
+
appendShadowLine(consumerRoot, {
|
|
402
|
+
event: 'source_first_gate_shadow',
|
|
403
|
+
ts: nowIso,
|
|
404
|
+
tool_matcher: event.capture.label,
|
|
405
|
+
// The census finding, on every record: `display` capture cannot be
|
|
406
|
+
// the "screenshot instead of source" path, so a candidate warning
|
|
407
|
+
// on one is a false positive by construction. Flip condition (b)
|
|
408
|
+
// computes its share from this field.
|
|
409
|
+
capture_kind: event.capture.capture_kind,
|
|
410
|
+
handover_seen_in_session: after.handoverSeen,
|
|
411
|
+
handover_present_on_disk: onDisk,
|
|
412
|
+
source_read_in_session: after.sourceRead,
|
|
413
|
+
prior_would_warn_this_session: before.wouldWarn,
|
|
414
|
+
valve_would_silence: valveWouldSilence,
|
|
415
|
+
candidates: verdicts.map((v) => ({ label: v.label, would_warn: v.would_warn })),
|
|
416
|
+
// The verifier exemption's key, measured rather than assumed. Both
|
|
417
|
+
// SLI Phase 0 Step 4 and Phase 4 Step 1 are open, so this is
|
|
418
|
+
// expected false today; a run of trues is what makes flip
|
|
419
|
+
// condition (c) satisfiable.
|
|
420
|
+
agent_id_present: event.agentIdPresent,
|
|
421
|
+
agent_type: event.agentType,
|
|
422
|
+
verifier_exemption_decidable: event.agentIdPresent || event.agentType !== null,
|
|
423
|
+
// Shadow is a property of this file, not of a setting — there is no
|
|
424
|
+
// code path here that can warn or deny.
|
|
425
|
+
posture: 'shadow',
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const persisted =
|
|
429
|
+
anyWarn && !valveWouldSilence ? { ...after, wouldWarn: after.wouldWarn + 1 } : after;
|
|
430
|
+
writeState(consumerRoot, session, persisted);
|
|
431
|
+
} catch {
|
|
432
|
+
return EXIT_ALLOW;
|
|
433
|
+
}
|
|
434
|
+
return EXIT_ALLOW;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function main(): number {
|
|
438
|
+
const raw = readHookStdin();
|
|
439
|
+
let envelope: JsonValue = {};
|
|
440
|
+
try {
|
|
441
|
+
envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
|
|
442
|
+
} catch {
|
|
443
|
+
return EXIT_ALLOW;
|
|
444
|
+
}
|
|
445
|
+
const sessionVal = isObject(envelope) ? envelope['session_id'] : null;
|
|
446
|
+
const session = typeof sessionVal === 'string' && sessionVal ? sessionVal : 'unknown';
|
|
447
|
+
return processEnvelope(envelope, resolveConsumerRoot(envelope), session);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
451
|
+
// every module shares the bundle's `import.meta.url`.
|
|
452
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
453
|
+
function _isCliEntry(): boolean {
|
|
454
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) return false;
|
|
455
|
+
if (process.argv[1] === undefined) return false;
|
|
456
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
457
|
+
if (import.meta.url === argvUrl) return true;
|
|
458
|
+
try {
|
|
459
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
460
|
+
} catch {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (_isCliEntry()) process.exit(main());
|