@event4u/agent-config 14.6.0 → 14.8.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 +255 -145
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +264 -10
- package/dist/agent-src/commands/analyze.md +1 -1
- package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
- package/dist/agent-src/commands/evals/roadmap.json +17 -1
- package/dist/agent-src/commands/pr/create.md +14 -2
- package/dist/agent-src/commands/pr/merge.md +379 -0
- package/dist/agent-src/commands/roadmap/process-full.md +272 -32
- package/dist/agent-src/commands/roadmap.md +1 -1
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +95 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +289 -106
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/context-hygiene.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +67 -32
- 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 +20 -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 +26 -7
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- 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 +296 -53
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- 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-record/SKILL.md +61 -3
- package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -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/security/SKILL.md +8 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
- 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 +65 -4
- 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 +2 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
- package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-7PRE6ZV2.js} +338 -0
- package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
- package/dist/cli-delegate/chunk-E4XIZDIB.js +402 -0
- package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-EEJUMCDZ.js} +16 -16
- package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
- package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-H4VIVD4C.js} +385 -284
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-LPPWO5CE.js} +12 -8
- package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
- package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-PMEKGDED.js} +74 -167
- 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_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +466 -716
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +10 -10
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +10 -9
- package/dist/cli-delegate/cmd_handoff.js +10 -9
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +1 -1
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +11 -10
- package/dist/cli-delegate/cmd_route_audit.js +6 -5
- package/dist/cli-delegate/cmd_route_explain.js +3 -3
- package/dist/cli-delegate/cmd_session_recycle.js +840 -30
- package/dist/cli-delegate/cmd_settings_get.js +3 -3
- package/dist/cli-delegate/cmd_settings_migrate.js +10 -9
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +11 -10
- package/dist/cli-delegate/cmd_uninstall.js +13 -11
- package/dist/cli-delegate/cmd_update.js +4 -4
- package/dist/cli-delegate/cmd_upgrade.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +11 -10
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +111 -37
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +5 -5
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +15 -12
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +11 -8
- package/dist/hooks/dispatch.js +3165 -2192
- package/dist/install/claudePathsPlan.js +187 -0
- package/dist/install/claudePathsPlan.js.map +1 -0
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.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 +432 -0
- package/dist/install/partitionEligibility.js.map +1 -0
- package/dist/install/ruleLayerPartition.js +157 -0
- package/dist/install/ruleLayerPartition.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1863 -1234
- 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/_lib/prune_empty_dirs.js +85 -0
- package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
- package/dist/scripts/condense.js +63 -251
- 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/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
- package/docs/guidelines/agent-infra/false-green.md +47 -1
- 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 +26 -7
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- 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 +296 -53
- 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 +39 -0
- package/src/config/estate-count-budget.json +612 -3
- package/src/config/evaluator-budgets.json +3 -3
- package/src/config/gate-coverage.yml +220 -12
- package/src/config/gate-violation-baselines.json +73 -63
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/rule-activation-census.json +14 -6
- package/src/config/rule-stub-ceilings.json +34 -7
- package/src/install/claudePathsPlan.ts +196 -0
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/hostLayerFingerprint.ts +145 -0
- package/src/install/partitionEligibility.ts +502 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
- package/src/scripts/_dispatch.bash +17 -1
- package/src/scripts/_lib/ac_heading.ts +30 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -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 +49 -9
- package/src/scripts/_lib/installed_lock.ts +50 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/md_section.ts +129 -0
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- package/src/scripts/_lib/scope_guard.sh +26 -3
- package/src/scripts/_lib/self_repair.ts +19 -0
- package/src/scripts/_lib/self_repair_class_b.ts +171 -0
- package/src/scripts/adr/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +12 -32
- package/src/scripts/adr_cite_check.ts +540 -39
- package/src/scripts/ai_council/cli_help.ts +55 -0
- package/src/scripts/audit_adr_coverage.ts +103 -17
- package/src/scripts/before_complete_hook.ts +153 -28
- package/src/scripts/bench_ab_v2_complexity.ts +27 -7
- package/src/scripts/bench_hook_latency.ts +204 -0
- package/src/scripts/build_archive_index.ts +34 -0
- package/src/scripts/check_adr_frontmatter.ts +574 -39
- package/src/scripts/check_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -0
- package/src/scripts/check_references.ts +78 -0
- package/src/scripts/check_release_published.ts +32 -11
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_single_delivery.ts +194 -17
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/ci_settle.ts +170 -0
- package/src/scripts/condense.ts +79 -259
- package/src/scripts/council_cli.ts +3 -3
- package/src/scripts/dispatch_r2_reviewer.ts +6 -14
- package/src/scripts/external_sources_denylist.json +4 -1
- 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 +21 -1
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +154 -187
- package/src/scripts/hooks/dispatch_issues.ts +38 -19
- package/src/scripts/hooks/fallback_yaml.ts +66 -0
- package/src/scripts/hooks/host_semantics.ts +21 -0
- package/src/scripts/hooks/injection_budget.ts +13 -3
- package/src/scripts/hooks/py_json_dumps.ts +103 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
- package/src/scripts/hooks/run_continuation_hook.ts +8 -5
- 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 +870 -21
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- 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/injection_scan_hook.ts +137 -5
- 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_hook_manifest.ts +49 -0
- package/src/scripts/lint_plan_risk_register.ts +4 -3
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- package/src/scripts/lint_skill_router_head.ts +4 -4
- package/src/scripts/mcp_server/tools.ts +2 -2
- 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 +6 -0
- package/src/scripts/routing_doctor.ts +5 -1
- package/src/scripts/self_repair_hook.ts +37 -5
- package/src/scripts/session_register_hook.ts +11 -0
- 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 +170 -20
- 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-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
atomic_write_json,
|
|
42
42
|
feedback_dir,
|
|
43
43
|
is_replay_mode,
|
|
44
|
+
update_json_under_lock,
|
|
44
45
|
} from "./state_io.js";
|
|
45
46
|
import { log_dispatch_issue, fix_hint } from "./dispatch_issues.js";
|
|
46
47
|
import { shapeAndRecord, type ConcernMessage } from "./injection_budget.js";
|
|
@@ -48,11 +49,15 @@ import { CONCERN_REGISTRY, type ConcernMain } from "./concern_registry.js";
|
|
|
48
49
|
import {
|
|
49
50
|
setHookStdinOverride,
|
|
50
51
|
clearHookStdinOverride,
|
|
51
|
-
readFd0ToEnd,
|
|
52
52
|
} from "./hook_stdin.js";
|
|
53
53
|
import { emitFor, type Severity } from "./host_semantics.js";
|
|
54
54
|
import { _concern_body_classes, planPayloadShapes } from "./payload_stub.js";
|
|
55
55
|
import { resolveSessionRole, type SessionRole } from "../_lib/session_role.js";
|
|
56
|
+
import { stdinReadFailure, denyOnStdinFailure } from './stdin_failure_policy.js';
|
|
57
|
+
export { stdinReadFailure, denyOnStdinFailure, _is_fail_closed_blocking } from './stdin_failure_policy.js';
|
|
58
|
+
import { _py_json_dumps } from './py_json_dumps.js';
|
|
59
|
+
import { _fallback_yaml } from './fallback_yaml.js';
|
|
60
|
+
export { _fallback_yaml } from './fallback_yaml.js';
|
|
56
61
|
|
|
57
62
|
// Free-form JSON values flow through every helper here; a documented
|
|
58
63
|
// alias keeps the surface honest without `any` (ADR-200 § strict TS).
|
|
@@ -145,6 +150,16 @@ interface Args {
|
|
|
145
150
|
dry_run: boolean;
|
|
146
151
|
project_dir: string;
|
|
147
152
|
min_version: number;
|
|
153
|
+
/**
|
|
154
|
+
* Measurement-only: read stdin, build the envelope, exit. See `--read-exit`.
|
|
155
|
+
*
|
|
156
|
+
* OPTIONAL, unlike `dry_run` beside it, and the asymmetry is deliberate: this
|
|
157
|
+
* flag exists for one bench cell, so every caller that constructs an `Args`
|
|
158
|
+
* by hand — eight test fixtures across three files — would otherwise have to
|
|
159
|
+
* name a field it has no opinion about. `_parse_args` still sets it
|
|
160
|
+
* explicitly, so the production path never reads an absent value.
|
|
161
|
+
*/
|
|
162
|
+
read_exit?: boolean;
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
function _resolve_session_id(envelope: JsonObject): string {
|
|
@@ -234,63 +249,6 @@ export function _load_yaml(p: string): JsonObject {
|
|
|
234
249
|
return _isObject(data) ? data : {};
|
|
235
250
|
}
|
|
236
251
|
|
|
237
|
-
/**
|
|
238
|
-
* Indent-aware mini-parser for the manifest's flat shape only.
|
|
239
|
-
* Handles: scalars, `key: null`, `key: true/false`, `key: [a, b]`.
|
|
240
|
-
* Drops comments + blank lines. Two-space indent assumed.
|
|
241
|
-
*
|
|
242
|
-
* Ported verbatim from the Python `_fallback_yaml` so the parser unit
|
|
243
|
-
* tests have an exact twin; runtime prefers `_load_yaml` above.
|
|
244
|
-
*/
|
|
245
|
-
export function _fallback_yaml(text: string): JsonObject {
|
|
246
|
-
const root: JsonObject = {};
|
|
247
|
-
const stack: Array<[number, JsonObject]> = [[-1, root]];
|
|
248
|
-
for (const raw of text.split("\n")) {
|
|
249
|
-
const line = raw.split("#", 1)[0]!.replace(/\s+$/, "");
|
|
250
|
-
if (!line.trim()) {
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
const indent = line.length - line.replace(/^ +/, "").length;
|
|
254
|
-
while (stack.length > 0 && stack[stack.length - 1]![0] >= indent) {
|
|
255
|
-
stack.pop();
|
|
256
|
-
}
|
|
257
|
-
const parent = stack.length > 0 ? stack[stack.length - 1]![1] : root;
|
|
258
|
-
const body = line.trim();
|
|
259
|
-
if (!body.includes(":")) {
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
const idx = body.indexOf(":");
|
|
263
|
-
const key = body.slice(0, idx).trim();
|
|
264
|
-
const val = body.slice(idx + 1).trim();
|
|
265
|
-
if (!val) {
|
|
266
|
-
const newObj: JsonObject = {};
|
|
267
|
-
parent[key] = newObj;
|
|
268
|
-
stack.push([indent, newObj]);
|
|
269
|
-
} else {
|
|
270
|
-
const lower = val.toLowerCase();
|
|
271
|
-
if (lower === "null" || lower === "~" || lower === "") {
|
|
272
|
-
parent[key] = null;
|
|
273
|
-
} else if (lower === "true") {
|
|
274
|
-
parent[key] = true;
|
|
275
|
-
} else if (lower === "false") {
|
|
276
|
-
parent[key] = false;
|
|
277
|
-
} else if (val.startsWith("[") && val.endsWith("]")) {
|
|
278
|
-
const inner = val.slice(1, -1).trim();
|
|
279
|
-
parent[key] = inner
|
|
280
|
-
? inner
|
|
281
|
-
.split(",")
|
|
282
|
-
.map((s) => s.trim())
|
|
283
|
-
.filter((s) => s)
|
|
284
|
-
: [];
|
|
285
|
-
} else if (/^-?\d+$/.test(val)) {
|
|
286
|
-
parent[key] = parseInt(val, 10);
|
|
287
|
-
} else {
|
|
288
|
-
parent[key] = val.replace(/^['"]+|['"]+$/g, "");
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
return root;
|
|
293
|
-
}
|
|
294
252
|
|
|
295
253
|
interface ConcernDef extends JsonObject {
|
|
296
254
|
name: string;
|
|
@@ -478,97 +436,6 @@ export function _resolve_concerns(
|
|
|
478
436
|
return out;
|
|
479
437
|
}
|
|
480
438
|
|
|
481
|
-
// Python json.dumps(record, indent=2) byte-for-byte (ensure_ascii=True).
|
|
482
|
-
function _py_json_dumps(value: unknown, indent: number): string {
|
|
483
|
-
const pad = " ".repeat(indent);
|
|
484
|
-
const escapeString = (s: string): string => {
|
|
485
|
-
let out = '"';
|
|
486
|
-
for (const ch of s) {
|
|
487
|
-
const code = ch.codePointAt(0) as number;
|
|
488
|
-
switch (ch) {
|
|
489
|
-
case '"':
|
|
490
|
-
out += '\\"';
|
|
491
|
-
break;
|
|
492
|
-
case "\\":
|
|
493
|
-
out += "\\\\";
|
|
494
|
-
break;
|
|
495
|
-
case "\n":
|
|
496
|
-
out += "\\n";
|
|
497
|
-
break;
|
|
498
|
-
case "\r":
|
|
499
|
-
out += "\\r";
|
|
500
|
-
break;
|
|
501
|
-
case "\t":
|
|
502
|
-
out += "\\t";
|
|
503
|
-
break;
|
|
504
|
-
case "\b":
|
|
505
|
-
out += "\\b";
|
|
506
|
-
break;
|
|
507
|
-
case "\f":
|
|
508
|
-
out += "\\f";
|
|
509
|
-
break;
|
|
510
|
-
default:
|
|
511
|
-
if (code < 0x20 || code > 0x7e) {
|
|
512
|
-
if (code > 0xffff) {
|
|
513
|
-
const c = code - 0x10000;
|
|
514
|
-
const hi = 0xd800 + (c >> 10);
|
|
515
|
-
const lo = 0xdc00 + (c & 0x3ff);
|
|
516
|
-
out +=
|
|
517
|
-
"\\u" +
|
|
518
|
-
hi.toString(16).padStart(4, "0") +
|
|
519
|
-
"\\u" +
|
|
520
|
-
lo.toString(16).padStart(4, "0");
|
|
521
|
-
} else {
|
|
522
|
-
out += "\\u" + code.toString(16).padStart(4, "0");
|
|
523
|
-
}
|
|
524
|
-
} else {
|
|
525
|
-
out += ch;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
return out + '"';
|
|
530
|
-
};
|
|
531
|
-
const render = (v: unknown, depth: number): string => {
|
|
532
|
-
if (v === null || v === undefined) return "null";
|
|
533
|
-
if (typeof v === "boolean") return v ? "true" : "false";
|
|
534
|
-
if (typeof v === "number") {
|
|
535
|
-
if (!Number.isFinite(v)) {
|
|
536
|
-
if (Number.isNaN(v)) return "NaN";
|
|
537
|
-
return v > 0 ? "Infinity" : "-Infinity";
|
|
538
|
-
}
|
|
539
|
-
return String(v);
|
|
540
|
-
}
|
|
541
|
-
if (typeof v === "string") return escapeString(v);
|
|
542
|
-
const curPad = pad.repeat(depth + 1);
|
|
543
|
-
const closePad = pad.repeat(depth);
|
|
544
|
-
if (Array.isArray(v)) {
|
|
545
|
-
if (v.length === 0) return "[]";
|
|
546
|
-
return (
|
|
547
|
-
"[\n" +
|
|
548
|
-
v.map((item) => curPad + render(item, depth + 1)).join(",\n") +
|
|
549
|
-
"\n" +
|
|
550
|
-
closePad +
|
|
551
|
-
"]"
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
if (typeof v === "object") {
|
|
555
|
-
const obj = v as Record<string, unknown>;
|
|
556
|
-
const keys = Object.keys(obj);
|
|
557
|
-
if (keys.length === 0) return "{}";
|
|
558
|
-
return (
|
|
559
|
-
"{\n" +
|
|
560
|
-
keys
|
|
561
|
-
.map((k) => curPad + escapeString(k) + ": " + render(obj[k], depth + 1))
|
|
562
|
-
.join(",\n") +
|
|
563
|
-
"\n" +
|
|
564
|
-
closePad +
|
|
565
|
-
"}"
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
throw new TypeError(`Object of type ${typeof v} is not JSON serializable`);
|
|
569
|
-
};
|
|
570
|
-
return render(value, 0);
|
|
571
|
-
}
|
|
572
439
|
|
|
573
440
|
/**
|
|
574
441
|
* Write the raw stdin payload to a capture directory when
|
|
@@ -1020,13 +887,54 @@ function _write_feedback(
|
|
|
1020
887
|
}),
|
|
1021
888
|
};
|
|
1022
889
|
try {
|
|
1023
|
-
|
|
890
|
+
// P3 of `b-stop-async-split-prerequisites` (council 2026-08-20, option (a)).
|
|
891
|
+
//
|
|
892
|
+
// WAS: one `summary.json` per session, published by `atomic_write_json`.
|
|
893
|
+
// The publish is atomic and that was never the problem — the PATH is one,
|
|
894
|
+
// so two dispatches in the same session (parallel tool calls on this host,
|
|
895
|
+
// or two platforms installed into one workspace) both wrote it and the later
|
|
896
|
+
// rename discarded the earlier rollup entirely. Evidence loss, silent.
|
|
897
|
+
//
|
|
898
|
+
// The fix is the per-invocation discriminator the blocker names, held INSIDE
|
|
899
|
+
// one file rather than fanned out across per-invocation filenames. Both
|
|
900
|
+
// were considered and the file-per-invocation form was refused on growth:
|
|
901
|
+
// the feedback tree is not pruned, so a 200-tool-call session would leave
|
|
902
|
+
// 200 rollups per session dir, and capping THAT needs a directory scan on
|
|
903
|
+
// the hot path. A capped list needs one locked read-modify-write, which is
|
|
904
|
+
// the same primitive `rule-trips.json` now uses for the same reason.
|
|
905
|
+
//
|
|
906
|
+
// `invocation` is pid + monotonic clock: unique per dispatch without a
|
|
907
|
+
// shared counter, and ordered within one process.
|
|
908
|
+
const invocation = `${String(process.pid)}-${process.hrtime.bigint().toString()}`;
|
|
909
|
+
update_json_under_lock<JsonObject>(path.join(fb_dir, "summary.json"), (loaded) => {
|
|
910
|
+
const priorRaw = (loaded as JsonObject)["invocations"];
|
|
911
|
+
const prior = Array.isArray(priorRaw) ? priorRaw : [];
|
|
912
|
+
const next = [...prior, { invocation, ...summary }];
|
|
913
|
+
return {
|
|
914
|
+
schema_version: 2,
|
|
915
|
+
session_id,
|
|
916
|
+
// Newest last, oldest dropped — the same rotation direction
|
|
917
|
+
// `dispatch-issues.jsonl` uses, so a reader learns one convention.
|
|
918
|
+
invocations: next.slice(Math.max(0, next.length - SUMMARY_INVOCATION_CAP)),
|
|
919
|
+
};
|
|
920
|
+
});
|
|
1024
921
|
} catch (exc) {
|
|
1025
922
|
const msg = exc instanceof Error ? exc.message : String(exc);
|
|
1026
923
|
process.stderr.write(`dispatch_hook: summary write failed: ${msg}\n`);
|
|
1027
924
|
}
|
|
1028
925
|
}
|
|
1029
926
|
|
|
927
|
+
/**
|
|
928
|
+
* How many invocation rollups one session's `summary.json` retains.
|
|
929
|
+
*
|
|
930
|
+
* 20, matching nothing in particular and stated as such: the file is a human
|
|
931
|
+
* debugging surface, twenty dispatches is more than a reader scrolls, and the
|
|
932
|
+
* value exists to bound growth rather than to preserve a measured window.
|
|
933
|
+
* `dispatch-issues.jsonl` caps at 200 because it holds FAILURES, which are rare;
|
|
934
|
+
* this holds every dispatch, which is not.
|
|
935
|
+
*/
|
|
936
|
+
export const SUMMARY_INVOCATION_CAP = 20;
|
|
937
|
+
|
|
1030
938
|
/**
|
|
1031
939
|
* Checkable-rule trip counting (road-to-credible-install Phase 6, scoped P3
|
|
1032
940
|
* cut): every concern that returns BLOCK (1) or WARN (2) — a violation the
|
|
@@ -1051,28 +959,38 @@ function _record_rule_trips(envelope: JsonObject, entries: FeedbackEntry[]): voi
|
|
|
1051
959
|
const workspace = String(envelope["workspace_root"] || process.cwd());
|
|
1052
960
|
const state_dir = path.join(workspace, "agents", "runtime", "state");
|
|
1053
961
|
const target = path.join(state_dir, "rule-trips.json");
|
|
1054
|
-
fs.mkdirSync(state_dir, { recursive: true });
|
|
1055
|
-
let doc: JsonObject = { schema_version: 1, concerns: {} };
|
|
1056
|
-
try {
|
|
1057
|
-
const parsed = JSON.parse(fs.readFileSync(target, "utf-8")) as JsonObject;
|
|
1058
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) doc = parsed;
|
|
1059
|
-
} catch {
|
|
1060
|
-
/* absent or corrupt → fresh counter file */
|
|
1061
|
-
}
|
|
1062
|
-
const concerns = (doc["concerns"] ?? {}) as Record<string, JsonObject>;
|
|
1063
962
|
const today = _now_iso().slice(0, 10);
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
963
|
+
// P3 of `b-stop-async-split-prerequisites` (council 2026-08-20, option (a)).
|
|
964
|
+
// The read used to sit OUTSIDE the lock: `readFileSync` here,
|
|
965
|
+
// `atomic_write_json` there. `atomic_write_json` makes the PUBLISH atomic
|
|
966
|
+
// and says nothing about load → increment → publish, so two concurrent
|
|
967
|
+
// dispatchers both loaded `block: 3`, both computed 4, and one trip
|
|
968
|
+
// vanished. `update_json_under_lock` holds all three steps under one lock,
|
|
969
|
+
// which is the primitive that already exists for exactly this shape.
|
|
970
|
+
//
|
|
971
|
+
// The worse form is a lost FIELD, not a lost increment: this mutator
|
|
972
|
+
// republishes the whole `concerns` map, so a counter another dispatcher
|
|
973
|
+
// wrote in between would be reverted rather than merely missed.
|
|
974
|
+
update_json_under_lock<JsonObject>(target, (loaded) => {
|
|
975
|
+
const doc: JsonObject = { schema_version: 1, concerns: {}, ...(loaded as JsonObject) };
|
|
976
|
+
const concernsRaw = doc["concerns"];
|
|
977
|
+
const concerns = (
|
|
978
|
+
_isObject(concernsRaw) ? concernsRaw : {}
|
|
979
|
+
) as Record<string, JsonObject>;
|
|
980
|
+
for (const e of tripped) {
|
|
981
|
+
const name = String(e["concern"]);
|
|
982
|
+
const prevRaw = concerns[name];
|
|
983
|
+
const prev = (_isObject(prevRaw) ? prevRaw : { block: 0, warn: 0 }) as JsonObject;
|
|
984
|
+
concerns[name] = {
|
|
985
|
+
block: (Number(prev["block"]) || 0) + (e["exit_code"] === EXIT_BLOCK ? 1 : 0),
|
|
986
|
+
warn: (Number(prev["warn"]) || 0) + (e["exit_code"] === EXIT_WARN ? 1 : 0),
|
|
987
|
+
last_trip: today,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
doc["schema_version"] = 1;
|
|
991
|
+
doc["concerns"] = concerns;
|
|
992
|
+
return doc;
|
|
993
|
+
});
|
|
1076
994
|
} catch {
|
|
1077
995
|
/* fail-open — counters are observability, never dispatch-critical */
|
|
1078
996
|
}
|
|
@@ -1087,6 +1005,7 @@ function _parse_args(argv: string[]): Args {
|
|
|
1087
1005
|
dry_run: false,
|
|
1088
1006
|
project_dir: "",
|
|
1089
1007
|
min_version: 0,
|
|
1008
|
+
read_exit: false,
|
|
1090
1009
|
};
|
|
1091
1010
|
let sawPlatform = false;
|
|
1092
1011
|
let sawEvent = false;
|
|
@@ -1110,6 +1029,9 @@ function _parse_args(argv: string[]): Args {
|
|
|
1110
1029
|
case "--dry-run":
|
|
1111
1030
|
args.dry_run = true;
|
|
1112
1031
|
break;
|
|
1032
|
+
case "--read-exit":
|
|
1033
|
+
args.read_exit = true;
|
|
1034
|
+
break;
|
|
1113
1035
|
case "--project-dir":
|
|
1114
1036
|
args.project_dir = argv[++i] ?? "";
|
|
1115
1037
|
break;
|
|
@@ -1176,6 +1098,44 @@ export function main(argv?: string[]): number {
|
|
|
1176
1098
|
}
|
|
1177
1099
|
}
|
|
1178
1100
|
|
|
1101
|
+
// ── `--read-exit`: the transport-isolation cell ─────────────────────────────
|
|
1102
|
+
//
|
|
1103
|
+
// `b-payload-read-parse-dominates`, option (a), council 2026-08-20 (2/2
|
|
1104
|
+
// quorum): "add a same-fixture dispatcher cell that reads stdin and exits
|
|
1105
|
+
// immediately, reporting its own latency and its share of the large-payload
|
|
1106
|
+
// delta".
|
|
1107
|
+
//
|
|
1108
|
+
// WHY IT LIVES IN THE DISPATCHER rather than in a standalone probe script.
|
|
1109
|
+
// The term being isolated is `readFd0ToEnd` + one `JSON.parse` of the same
|
|
1110
|
+
// payload, and it must be measured through the SAME process shape as the
|
|
1111
|
+
// slot it is a share of — same interpreter, same bundle, same spawn. A
|
|
1112
|
+
// separate probe would have to re-implement the audited retrying reader, and
|
|
1113
|
+
// a copy of that reader is precisely the drift `hook_stdin` was consolidated
|
|
1114
|
+
// to remove. Bundle load does not vary with payload size, so it cancels in
|
|
1115
|
+
// the large-minus-small delta, which is the number the option asks for.
|
|
1116
|
+
//
|
|
1117
|
+
// Measurement-only, and it exits BEFORE the manifest load, the concern
|
|
1118
|
+
// resolution and every concern — so it can neither run a guard nor suppress
|
|
1119
|
+
// one. Same exposure `--dry-run` already carries: reaching it requires
|
|
1120
|
+
// editing the installed hook command, and anyone who can do that can delete
|
|
1121
|
+
// the hook instead.
|
|
1122
|
+
if (args.read_exit === true) {
|
|
1123
|
+
const probe = tty.isatty(0) ? { text: "", failure: null } : stdinReadFailure();
|
|
1124
|
+
if (probe.failure !== null) {
|
|
1125
|
+
process.stderr.write(`dispatch_hook: --read-exit read failed: ${probe.failure}\n`);
|
|
1126
|
+
return EXIT_ALLOW;
|
|
1127
|
+
}
|
|
1128
|
+
// The parse is the half of the term under test, so it must actually happen
|
|
1129
|
+
// and its result must be observable — otherwise a future optimiser could
|
|
1130
|
+
// elide it and the cell would silently measure the read alone.
|
|
1131
|
+
const envelope = _build_envelope(args, probe.text);
|
|
1132
|
+
process.stderr.write(
|
|
1133
|
+
`dispatch_hook: --read-exit ok (${probe.text.length} chars, ` +
|
|
1134
|
+
`${Object.keys(_isObject(envelope["payload"]) ? envelope["payload"] : {}).length} payload keys)\n`,
|
|
1135
|
+
);
|
|
1136
|
+
return EXIT_ALLOW;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1179
1139
|
if (!EVENT_VOCABULARY.has(args.event)) {
|
|
1180
1140
|
process.stderr.write(
|
|
1181
1141
|
`dispatch_hook: unknown event '${args.event}'; allowed: ` +
|
|
@@ -1208,7 +1168,9 @@ export function main(argv?: string[]): number {
|
|
|
1208
1168
|
// below throws EAGAIN on any payload above the pipe buffer and silently
|
|
1209
1169
|
// yields an EMPTY envelope. That is a measured guard bypass, not a theory —
|
|
1210
1170
|
// see readFd0ToEnd's header in hooks/hook_stdin.ts.
|
|
1211
|
-
const
|
|
1171
|
+
const read = tty.isatty(0) ? { text: "", failure: null } : stdinReadFailure();
|
|
1172
|
+
const payload_text = read.text;
|
|
1173
|
+
_stdin_read_failed = read.failure;
|
|
1212
1174
|
if (_stdin_read_failed !== null) {
|
|
1213
1175
|
// NEVER silent: an empty envelope reached from a failed read means every
|
|
1214
1176
|
// guard on this event evaluated nothing. The exit code is unchanged (see
|
|
@@ -1254,6 +1216,21 @@ export function main(argv?: string[]): number {
|
|
|
1254
1216
|
return EXIT_ALLOW; // platform unsupported / fallback-only / empty slot
|
|
1255
1217
|
}
|
|
1256
1218
|
|
|
1219
|
+
// `b-stdin-read-failure-policy` option (c). Placed HERE, not at the read:
|
|
1220
|
+
// the decision needs the resolved concern list, because the whole question is
|
|
1221
|
+
// whether a fail-closed blocking guard was among the ones that just ran
|
|
1222
|
+
// blind. Before the dry-run branch it would turn a plan printer into a
|
|
1223
|
+
// refusal; after the chain it would spend the chain first.
|
|
1224
|
+
if (_stdin_read_failed !== null) {
|
|
1225
|
+
const deny = denyOnStdinFailure(args.platform, args.event, concerns, _stdin_read_failed);
|
|
1226
|
+
if (deny !== null) {
|
|
1227
|
+
const emission = emitFor(args.platform, args.event, "block", [deny.reason], EXIT_BLOCK);
|
|
1228
|
+
if (emission.stdout) process.stdout.write(emission.stdout);
|
|
1229
|
+
if (emission.stderr) process.stderr.write(emission.stderr);
|
|
1230
|
+
return emission.exit;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1257
1234
|
const envelope = _build_envelope(args, payload_text);
|
|
1258
1235
|
const session_id = _resolve_session_id(envelope);
|
|
1259
1236
|
const started_at = _now_iso();
|
|
@@ -1447,24 +1424,14 @@ function _sortedRepr(s: ReadonlySet<string>): string {
|
|
|
1447
1424
|
* dispatcher exits 0. For a `fail_closed: true`, `severity: blocking` guard that
|
|
1448
1425
|
* is an allow, emitted silently. Found by the R2 review.
|
|
1449
1426
|
*
|
|
1450
|
-
* The failure is
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1453
|
-
*
|
|
1454
|
-
*
|
|
1427
|
+
* The failure is LOUD (stderr + a dispatch issue) on every slot, and since
|
|
1428
|
+
* `b-stdin-read-failure-policy` was decided (council 2026-08-20, option (c)) it
|
|
1429
|
+
* also DENIES — but only where a deny is honoured and a fail-closed blocking
|
|
1430
|
+
* concern was among the ones that ran blind. See `denyOnStdinFailure` for the
|
|
1431
|
+
* policy and for why the two broader options were refused.
|
|
1455
1432
|
*/
|
|
1456
1433
|
let _stdin_read_failed: string | null = null;
|
|
1457
1434
|
|
|
1458
|
-
function _readStdin(): string {
|
|
1459
|
-
try {
|
|
1460
|
-
// Reads to EOF and retries on EAGAIN rather than treating it as empty
|
|
1461
|
-
// input; shared with the concern-side reader so both paths cannot drift.
|
|
1462
|
-
return readFd0ToEnd();
|
|
1463
|
-
} catch (exc) {
|
|
1464
|
-
_stdin_read_failed = exc instanceof Error ? exc.message : String(exc);
|
|
1465
|
-
return "";
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
1435
|
|
|
1469
1436
|
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
1470
1437
|
// every module shares the bundle's `import.meta.url` (declare at top of file).
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
import fs from "node:fs";
|
|
36
36
|
import path from "node:path";
|
|
37
37
|
|
|
38
|
+
import { update_text_under_lock } from "./state_io.js";
|
|
39
|
+
|
|
38
40
|
export const LOG_CAP = 200;
|
|
39
41
|
|
|
40
42
|
export const VALID_ISSUE: ReadonlySet<string> = new Set([
|
|
@@ -185,27 +187,44 @@ export function log_dispatch_issue(
|
|
|
185
187
|
};
|
|
186
188
|
|
|
187
189
|
try {
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
190
|
+
// P3 of `b-stop-async-split-prerequisites` (council 2026-08-20, option (a),
|
|
191
|
+
// "P3 before anything else"): read + append + write is one critical
|
|
192
|
+
// section, held under the shared dispatcher lock and published by
|
|
193
|
+
// tmp+rename.
|
|
194
|
+
//
|
|
195
|
+
// What it was: `readFileSync` outside any lock, then `writeFileSync`
|
|
196
|
+
// straight onto the target. Two failures, and the second is the one that
|
|
197
|
+
// made this the first P3 item — a lost line is an absent record, while a
|
|
198
|
+
// TRUNCATED write is a corrupt one, and this file is written exactly when
|
|
199
|
+
// something has already gone wrong. Two concurrent dispatchers in one
|
|
200
|
+
// workspace is supported (two platforms installed side by side), and a host
|
|
201
|
+
// that runs tool calls in parallel produces it with one platform.
|
|
202
|
+
//
|
|
203
|
+
// Still best-effort: `update_text_under_lock` returns false rather than
|
|
204
|
+
// throwing when the lock or the write fails, and observability never breaks
|
|
205
|
+
// the agent loop.
|
|
206
|
+
const outcome = update_text_under_lock(log, (loaded) => {
|
|
207
|
+
const existing = loaded === null ? [] : loaded.split("\n");
|
|
208
|
+
// Python str.splitlines() drops a trailing empty element from a
|
|
209
|
+
// trailing newline; mirror that.
|
|
210
|
+
if (existing.length > 0 && existing[existing.length - 1] === "") {
|
|
211
|
+
existing.pop();
|
|
201
212
|
}
|
|
213
|
+
existing.push(_json_dumps_flat(entry));
|
|
214
|
+
// Cap rotation: drop the oldest entries.
|
|
215
|
+
const capped =
|
|
216
|
+
existing.length > LOG_CAP ? existing.slice(existing.length - LOG_CAP) : existing;
|
|
217
|
+
return capped.join("\n") + "\n";
|
|
218
|
+
});
|
|
219
|
+
// `!outcome` would be a silent no-op — every member of the union is a
|
|
220
|
+
// truthy string, which the primitive's own commit message names as the
|
|
221
|
+
// migration hazard. Compare the literal.
|
|
222
|
+
if (outcome !== "written") {
|
|
223
|
+
process.stderr.write(
|
|
224
|
+
`dispatch_issues: could not append to ${log} (${outcome}) — ` +
|
|
225
|
+
`the entry was dropped: ${entry.issue} on ${entry.hook}\n`,
|
|
226
|
+
);
|
|
202
227
|
}
|
|
203
|
-
existing.push(_json_dumps_flat(entry));
|
|
204
|
-
// Cap rotation: drop the oldest entries.
|
|
205
|
-
if (existing.length > LOG_CAP) {
|
|
206
|
-
existing = existing.slice(existing.length - LOG_CAP);
|
|
207
|
-
}
|
|
208
|
-
fs.writeFileSync(log, existing.join("\n") + "\n", { encoding: "utf-8" });
|
|
209
228
|
} catch (exc) {
|
|
210
229
|
// Observability never blocks the agent.
|
|
211
230
|
const msg = exc instanceof Error ? exc.message : String(exc);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dependency-free YAML-subset reader, extracted from `dispatch_hook.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Pure and self-recursive, like the JSON writer beside it. Moved for headroom
|
|
5
|
+
* rather than for the ceiling itself: two extractions clear the gate but land the
|
|
6
|
+
* file at exactly 1500, back on the cliff where the next added line reds it.
|
|
7
|
+
*/
|
|
8
|
+
import type { JsonObject } from './dispatch_hook.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Indent-aware mini-parser for the manifest's flat shape only.
|
|
12
|
+
* Handles: scalars, `key: null`, `key: true/false`, `key: [a, b]`.
|
|
13
|
+
* Drops comments + blank lines. Two-space indent assumed.
|
|
14
|
+
*
|
|
15
|
+
* Ported verbatim from the Python `_fallback_yaml` so the parser unit
|
|
16
|
+
* tests have an exact twin; runtime prefers `_load_yaml` above.
|
|
17
|
+
*/
|
|
18
|
+
export function _fallback_yaml(text: string): JsonObject {
|
|
19
|
+
const root: JsonObject = {};
|
|
20
|
+
const stack: Array<[number, JsonObject]> = [[-1, root]];
|
|
21
|
+
for (const raw of text.split("\n")) {
|
|
22
|
+
const line = raw.split("#", 1)[0]!.replace(/\s+$/, "");
|
|
23
|
+
if (!line.trim()) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const indent = line.length - line.replace(/^ +/, "").length;
|
|
27
|
+
while (stack.length > 0 && stack[stack.length - 1]![0] >= indent) {
|
|
28
|
+
stack.pop();
|
|
29
|
+
}
|
|
30
|
+
const parent = stack.length > 0 ? stack[stack.length - 1]![1] : root;
|
|
31
|
+
const body = line.trim();
|
|
32
|
+
if (!body.includes(":")) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const idx = body.indexOf(":");
|
|
36
|
+
const key = body.slice(0, idx).trim();
|
|
37
|
+
const val = body.slice(idx + 1).trim();
|
|
38
|
+
if (!val) {
|
|
39
|
+
const newObj: JsonObject = {};
|
|
40
|
+
parent[key] = newObj;
|
|
41
|
+
stack.push([indent, newObj]);
|
|
42
|
+
} else {
|
|
43
|
+
const lower = val.toLowerCase();
|
|
44
|
+
if (lower === "null" || lower === "~" || lower === "") {
|
|
45
|
+
parent[key] = null;
|
|
46
|
+
} else if (lower === "true") {
|
|
47
|
+
parent[key] = true;
|
|
48
|
+
} else if (lower === "false") {
|
|
49
|
+
parent[key] = false;
|
|
50
|
+
} else if (val.startsWith("[") && val.endsWith("]")) {
|
|
51
|
+
const inner = val.slice(1, -1).trim();
|
|
52
|
+
parent[key] = inner
|
|
53
|
+
? inner
|
|
54
|
+
.split(",")
|
|
55
|
+
.map((s) => s.trim())
|
|
56
|
+
.filter((s) => s)
|
|
57
|
+
: [];
|
|
58
|
+
} else if (/^-?\d+$/.test(val)) {
|
|
59
|
+
parent[key] = parseInt(val, 10);
|
|
60
|
+
} else {
|
|
61
|
+
parent[key] = val.replace(/^['"]+|['"]+$/g, "");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return root;
|
|
66
|
+
}
|
|
@@ -78,6 +78,27 @@ const CLAUDE_BLOCK_CAPABLE_EVENTS: ReadonlySet<string> = new Set([
|
|
|
78
78
|
"stop",
|
|
79
79
|
]);
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Does this (platform, event) pair let a hook actually REFUSE the action?
|
|
83
|
+
*
|
|
84
|
+
* Exported because a second consumer now needs the same fact and must not
|
|
85
|
+
* re-derive it: `dispatch_hook`'s stdin-read-failure policy denies only where a
|
|
86
|
+
* deny is honoured (`b-stdin-read-failure-policy`, option (c)). A copy of
|
|
87
|
+
* `CLAUDE_BLOCK_CAPABLE_EVENTS` one module away is a copy that drifts, and the
|
|
88
|
+
* drift direction here is the dangerous one: a stale copy would deny on a slot
|
|
89
|
+
* where the deny is discarded, refusing nothing while looking like enforcement.
|
|
90
|
+
*
|
|
91
|
+
* An unverified platform is NOT block-capable by this predicate, and that is
|
|
92
|
+
* deliberate rather than incidental: `emitFor` hands such a platform its legacy
|
|
93
|
+
* exit code verbatim, so this tree has no evidence its host honours a deny at
|
|
94
|
+
* all. Claiming capability without evidence is the over-claim the hook-coverage
|
|
95
|
+
* corrections in this estate exist to remove.
|
|
96
|
+
*/
|
|
97
|
+
export function isBlockCapable(platform: string, event: string): boolean {
|
|
98
|
+
if (!VERIFIED_PLATFORMS.has(platform)) return false;
|
|
99
|
+
return CLAUDE_BLOCK_CAPABLE_EVENTS.has(event);
|
|
100
|
+
}
|
|
101
|
+
|
|
81
102
|
function _joinReasons(reasons: readonly string[]): string {
|
|
82
103
|
return reasons.map((r) => r.trim()).filter((r) => r.length > 0).join(" · ");
|
|
83
104
|
}
|
|
@@ -196,9 +196,19 @@ export function evictionOrder(
|
|
|
196
196
|
* Nudge exclusivity: keep the lowest `nudge_rank`, drop every other nudge.
|
|
197
197
|
*
|
|
198
198
|
* Ties break on concern name so the winner is stable; a tie is a manifest
|
|
199
|
-
* defect rather than a runtime one, and `lint_hook_manifest` is where it
|
|
200
|
-
*
|
|
201
|
-
*
|
|
199
|
+
* defect rather than a runtime one, and `lint_hook_manifest` is where it is
|
|
200
|
+
* caught — `_check_concerns` rejects a repeated non-null `nudge_rank`, naming
|
|
201
|
+
* the rank and every concern declaring it, and rejects a non-positive-integer
|
|
202
|
+
* value rather than coercing it (road-to-wiring-truth-corrections Phase 3;
|
|
203
|
+
* cases in `tests/scripts/lint_hook_manifest.test.ts`). The tie-break below
|
|
204
|
+
* therefore stays as the runtime's stable fallback for a manifest that got
|
|
205
|
+
* past the lint, not as the only thing standing between a collision and a
|
|
206
|
+
* silently non-emitting concern.
|
|
207
|
+
*
|
|
208
|
+
* The lint enforces uniqueness GLOBALLY while this selector compares within
|
|
209
|
+
* one dispatch, so the lint is deliberately the stricter of the two — see its
|
|
210
|
+
* own note for why, and narrow it rather than delete it if a design ever needs
|
|
211
|
+
* one rank on two disjoint events.
|
|
202
212
|
*/
|
|
203
213
|
function _selectNudge(
|
|
204
214
|
candidates: readonly EmissionCandidate[],
|