@event4u/agent-config 14.7.0 → 14.9.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 +175 -390
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +34 -4
- 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 +383 -0
- package/dist/agent-src/commands/review/changes.md +95 -14
- package/dist/agent-src/commands/roadmap/process-full.md +157 -3
- 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/host-capability-manifest.md +77 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +100 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
- package/dist/agent-src/rules/recurring-criticism.md +1 -1
- package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
- 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/humanizer/SKILL.md +39 -0
- package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +57 -0
- package/dist/agent-src/skills/humanizer/references/fixtures.md +54 -0
- package/dist/agent-src/skills/judge-spec-compliance/SKILL.md +154 -0
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +45 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
- 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/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +28 -2
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-77GR3VD4.js} +68 -43
- package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-CYH5SP6M.js} +4 -23
- package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
- package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
- package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-PLSG6UL5.js} +14 -14
- package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +101 -25
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +7 -7
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_handoff.js +7 -6
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +6 -6
- 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 +3 -3
- package/dist/cli-delegate/cmd_settings_get.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +83 -30
- 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 +11 -9
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +9 -7
- package/dist/hooks/dispatch.js +1295 -812
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.js.map +1 -0
- package/dist/install/partitionEligibility.js +156 -0
- package/dist/install/partitionEligibility.js.map +1 -1
- 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 +1358 -805
- package/dist/scripts/_lib/claude_plugin.js +162 -0
- package/dist/scripts/_lib/claude_plugin.js.map +1 -0
- package/dist/scripts/_lib/environment_detector.js +397 -0
- package/dist/scripts/_lib/environment_detector.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/_lib/spawn_env.js +129 -0
- package/dist/scripts/_lib/spawn_env.js.map +1 -0
- package/dist/scripts/ai_council/modes.js +199 -0
- package/dist/scripts/ai_council/modes.js.map +1 -0
- package/dist/scripts/ai_council/transport_resolver.js +418 -0
- package/dist/scripts/ai_council/transport_resolver.js.map +1 -0
- package/dist/scripts/condense.js +52 -89
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/routes/wizard.js +3 -2
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/ui/assets/index-Cxgy63kz.js +26 -0
- package/dist/ui/assets/index-Cxgy63kz.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/false-green.md +47 -1
- package/docs/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +17 -0
- package/package.json +7 -3
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/src/config/ci-local-parity.yml +21 -0
- package/src/config/estate-count-budget.json +13 -471
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +458 -12
- package/src/config/gate-violation-baselines.json +18 -8
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/publish-surface.json +60 -0
- package/src/config/requirements-trace-budget.json +32 -0
- package/src/config/rule-stub-ceilings.json +25 -4
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/partitionEligibility.ts +177 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_dispatch.bash +18 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -0
- package/src/scripts/_lib/base_tree.ts +175 -0
- package/src/scripts/_lib/billing_grant.ts +165 -0
- package/src/scripts/_lib/billing_grant_cli.ts +123 -0
- package/src/scripts/_lib/council_fallback_posture.ts +53 -6
- package/src/scripts/_lib/council_fallback_wiring.ts +61 -1
- package/src/scripts/_lib/envelope_grounding.ts +11 -1
- package/src/scripts/_lib/host_capability.ts +59 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- package/src/scripts/_lib/quota_parked.ts +133 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +62 -0
- package/src/scripts/_lib/review_telemetry.ts +196 -0
- package/src/scripts/_lib/subagent_response.ts +92 -0
- package/src/scripts/adr/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +25 -38
- package/src/scripts/adr_cite_check.ts +540 -39
- package/src/scripts/ai_council/clients.ts +12 -1
- package/src/scripts/ai_council/config.ts +72 -115
- package/src/scripts/ai_council/fallback_config.ts +19 -7
- package/src/scripts/ai_council/mid_flight_fallback.ts +88 -7
- package/src/scripts/ai_council/orchestrator.ts +2 -2
- package/src/scripts/ai_council/py_format.ts +124 -0
- package/src/scripts/ai_council/transport_resolver.ts +65 -2
- package/src/scripts/audit_adr_coverage.ts +134 -21
- 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_council_pin_staleness.ts +300 -0
- package/src/scripts/check_estate_count.ts +420 -293
- package/src/scripts/check_gate_coverage.ts +91 -0
- package/src/scripts/check_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -0
- package/src/scripts/check_no_stub_inventory_table.ts +292 -0
- package/src/scripts/check_pack_size.ts +234 -1
- package/src/scripts/check_publish_surface.ts +297 -0
- package/src/scripts/check_references.ts +18 -0
- package/src/scripts/check_requirements_trace.ts +378 -0
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_secret_leak.ts +166 -12
- package/src/scripts/check_single_delivery.ts +137 -4
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/code_graph/cli.ts +44 -5
- package/src/scripts/code_graph/extract.ts +87 -0
- package/src/scripts/condense.ts +61 -91
- package/src/scripts/council_cli.ts +12 -16
- package/src/scripts/detect_ai_tells.ts +87 -0
- package/src/scripts/dispatch_r2_reviewer.ts +42 -0
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/generate_capabilities_index.ts +41 -0
- package/src/scripts/git_authorization_hook.ts +54 -4
- package/src/scripts/hooks/block_unauthorized_git.ts +41 -3
- 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/run_continuation_hook.ts +3 -1
- package/src/scripts/hooks/state_io.ts +88 -1
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
- package/src/scripts/injection_scan_hook.ts +257 -15
- package/src/scripts/lint_adr_sweep_routing.ts +365 -0
- package/src/scripts/lint_hook_manifest.ts +49 -0
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- package/src/scripts/lint_workflow_security.ts +53 -10
- package/src/scripts/lint_workflow_security_allowlist.json +2 -18
- package/src/scripts/report_envelope_rate.ts +166 -0
- package/src/scripts/review_axis_report.ts +218 -0
- package/src/scripts/routing_doctor.ts +5 -1
- package/src/scripts/run_supervise.ts +46 -1
- package/src/scripts/schemas/evals.schema.json +5 -0
- package/src/scripts/sync_pr_branch.ts +170 -20
- package/dist/cli/commands/commands.candidates.test.js +0 -257
- package/dist/cli/commands/commands.candidates.test.js.map +0 -1
- package/dist/cli/commands/profiles.test.js +0 -88
- package/dist/cli/commands/profiles.test.js.map +0 -1
- package/dist/cli/commands/recordTriggerEval.test.js +0 -113
- package/dist/cli/commands/recordTriggerEval.test.js.map +0 -1
- package/dist/cli/configRoot.test.js +0 -97
- package/dist/cli/configRoot.test.js.map +0 -1
- package/dist/cli/mcp/content.test.js +0 -54
- package/dist/cli/mcp/content.test.js.map +0 -1
- package/dist/cli/mcp/dispatch.test.js +0 -108
- package/dist/cli/mcp/dispatch.test.js.map +0 -1
- package/dist/server/writeRoot.test.js +0 -91
- package/dist/server/writeRoot.test.js.map +0 -1
- package/dist/shared/capabilities.test.js +0 -39
- package/dist/shared/capabilities.test.js.map +0 -1
- package/dist/ui/assets/index-C1pBRm1f.js +0 -26
- package/dist/ui/assets/index-C1pBRm1f.js.map +0 -1
- package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
- package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
|
@@ -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[],
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `json.dumps(obj, indent=n)` byte-for-byte, extracted from `dispatch_hook.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Pure and self-contained: it recurses into itself and calls nothing else, which
|
|
5
|
+
* is what made it safe to move out of a dispatcher that runs on every tool call.
|
|
6
|
+
* Extracted because dispatch_hook.ts sits at the 1500-line source ceiling.
|
|
7
|
+
*
|
|
8
|
+
* For anyone tempted to share it: mcp_telemetry_store, mcp_telemetry_query and
|
|
9
|
+
* mcp_telemetry_health each define their OWN `_py_json_dumps`. They were left
|
|
10
|
+
* alone — unifying four copies is a change with its own risk, not a merge-time
|
|
11
|
+
* side effect.
|
|
12
|
+
*/
|
|
13
|
+
// Python json.dumps(record, indent=2) byte-for-byte (ensure_ascii=True).
|
|
14
|
+
export function _py_json_dumps(value: unknown, indent: number): string {
|
|
15
|
+
const pad = " ".repeat(indent);
|
|
16
|
+
const escapeString = (s: string): string => {
|
|
17
|
+
let out = '"';
|
|
18
|
+
for (const ch of s) {
|
|
19
|
+
const code = ch.codePointAt(0) as number;
|
|
20
|
+
switch (ch) {
|
|
21
|
+
case '"':
|
|
22
|
+
out += '\\"';
|
|
23
|
+
break;
|
|
24
|
+
case "\\":
|
|
25
|
+
out += "\\\\";
|
|
26
|
+
break;
|
|
27
|
+
case "\n":
|
|
28
|
+
out += "\\n";
|
|
29
|
+
break;
|
|
30
|
+
case "\r":
|
|
31
|
+
out += "\\r";
|
|
32
|
+
break;
|
|
33
|
+
case "\t":
|
|
34
|
+
out += "\\t";
|
|
35
|
+
break;
|
|
36
|
+
case "\b":
|
|
37
|
+
out += "\\b";
|
|
38
|
+
break;
|
|
39
|
+
case "\f":
|
|
40
|
+
out += "\\f";
|
|
41
|
+
break;
|
|
42
|
+
default:
|
|
43
|
+
if (code < 0x20 || code > 0x7e) {
|
|
44
|
+
if (code > 0xffff) {
|
|
45
|
+
const c = code - 0x10000;
|
|
46
|
+
const hi = 0xd800 + (c >> 10);
|
|
47
|
+
const lo = 0xdc00 + (c & 0x3ff);
|
|
48
|
+
out +=
|
|
49
|
+
"\\u" +
|
|
50
|
+
hi.toString(16).padStart(4, "0") +
|
|
51
|
+
"\\u" +
|
|
52
|
+
lo.toString(16).padStart(4, "0");
|
|
53
|
+
} else {
|
|
54
|
+
out += "\\u" + code.toString(16).padStart(4, "0");
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
out += ch;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out + '"';
|
|
62
|
+
};
|
|
63
|
+
const render = (v: unknown, depth: number): string => {
|
|
64
|
+
if (v === null || v === undefined) return "null";
|
|
65
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
66
|
+
if (typeof v === "number") {
|
|
67
|
+
if (!Number.isFinite(v)) {
|
|
68
|
+
if (Number.isNaN(v)) return "NaN";
|
|
69
|
+
return v > 0 ? "Infinity" : "-Infinity";
|
|
70
|
+
}
|
|
71
|
+
return String(v);
|
|
72
|
+
}
|
|
73
|
+
if (typeof v === "string") return escapeString(v);
|
|
74
|
+
const curPad = pad.repeat(depth + 1);
|
|
75
|
+
const closePad = pad.repeat(depth);
|
|
76
|
+
if (Array.isArray(v)) {
|
|
77
|
+
if (v.length === 0) return "[]";
|
|
78
|
+
return (
|
|
79
|
+
"[\n" +
|
|
80
|
+
v.map((item) => curPad + render(item, depth + 1)).join(",\n") +
|
|
81
|
+
"\n" +
|
|
82
|
+
closePad +
|
|
83
|
+
"]"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (typeof v === "object") {
|
|
87
|
+
const obj = v as Record<string, unknown>;
|
|
88
|
+
const keys = Object.keys(obj);
|
|
89
|
+
if (keys.length === 0) return "{}";
|
|
90
|
+
return (
|
|
91
|
+
"{\n" +
|
|
92
|
+
keys
|
|
93
|
+
.map((k) => curPad + escapeString(k) + ": " + render(obj[k], depth + 1))
|
|
94
|
+
.join(",\n") +
|
|
95
|
+
"\n" +
|
|
96
|
+
closePad +
|
|
97
|
+
"}"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
throw new TypeError(`Object of type ${typeof v} is not JSON serializable`);
|
|
101
|
+
};
|
|
102
|
+
return render(value, 0);
|
|
103
|
+
}
|
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
*/
|
|
130
130
|
|
|
131
131
|
import * as fs from 'node:fs';
|
|
132
|
+
import { revokeBillingGrant } from '../_lib/billing_grant.js';
|
|
132
133
|
import * as path from 'node:path';
|
|
133
134
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
134
135
|
|
|
@@ -599,12 +600,13 @@ function writeState(file: string, state: RunState): boolean {
|
|
|
599
600
|
* stamp with it. Adoption is a fact this read already established; it is passed in
|
|
600
601
|
* rather than re-derived from a condition that cannot express it.
|
|
601
602
|
*/
|
|
602
|
-
function clearRunState(
|
|
603
|
+
export function clearRunState(
|
|
603
604
|
workspaceRoot: string,
|
|
604
605
|
runId: string,
|
|
605
606
|
keyedFile: string,
|
|
606
607
|
adoptedLegacy: boolean,
|
|
607
608
|
): void {
|
|
609
|
+
revokeBillingGrant(workspaceRoot, runId); // the run IS the grant's TTL
|
|
608
610
|
try {
|
|
609
611
|
fs.rmSync(keyedFile, { force: true });
|
|
610
612
|
} catch {
|
|
@@ -540,7 +540,9 @@ function _atomic_write_text(target: string, text: string): void {
|
|
|
540
540
|
* Layout:
|
|
541
541
|
* <state_root>/.dispatcher/<session_id>/
|
|
542
542
|
* <concern>.json — one per concern that ran
|
|
543
|
-
* summary.json —
|
|
543
|
+
* summary.json — capped list of per-invocation rollups
|
|
544
|
+
* (schema 2; see hook-architecture-v1 for why
|
|
545
|
+
* a single rollup object lost one per overlap)
|
|
544
546
|
*
|
|
545
547
|
* Per Council Round 2 (2026-05-04): exit-code reduction collapses
|
|
546
548
|
* multiple concern signals into a single platform-native code; the
|
|
@@ -1003,6 +1005,91 @@ export function owns_session_state(state: unknown, session_id: string): boolean
|
|
|
1003
1005
|
* sites at the time of the change and both were converted; a new caller must
|
|
1004
1006
|
* compare against a member of the union, never coerce to a boolean.
|
|
1005
1007
|
*/
|
|
1008
|
+
/**
|
|
1009
|
+
* Read a TEXT file, transform it, and write it back — the WHOLE sequence under
|
|
1010
|
+
* one lock.
|
|
1011
|
+
*
|
|
1012
|
+
* The text sibling of {@link update_json_under_lock}, added for P3 of
|
|
1013
|
+
* `b-stop-async-split-prerequisites` (council 2026-08-20, option (a),
|
|
1014
|
+
* "P3 before anything else"). The concrete case is
|
|
1015
|
+
* `agents/runtime/state/dispatch-issues.jsonl`, which had NO lock and NO
|
|
1016
|
+
* tmp+rename: `log_dispatch_issue` read the whole capped log, appended a line,
|
|
1017
|
+
* and `writeFileSync`'d the target directly.
|
|
1018
|
+
*
|
|
1019
|
+
* That is corruption-capable rather than merely lossy, and the distinction is
|
|
1020
|
+
* why this needed its own primitive instead of a comment. `writeFileSync`
|
|
1021
|
+
* truncates and then writes, so a concurrent reader can observe a half-written
|
|
1022
|
+
* log and a concurrent writer can interleave into one — and the file is written
|
|
1023
|
+
* precisely when something has ALREADY gone wrong, so the evidence destroyed is
|
|
1024
|
+
* the evidence of the failure being reported. Two concurrent dispatchers in one
|
|
1025
|
+
* workspace is a supported configuration (two platforms installed side by side),
|
|
1026
|
+
* and a host that runs tool calls in parallel produces it without any second
|
|
1027
|
+
* platform at all.
|
|
1028
|
+
*
|
|
1029
|
+
* `transform` receives the file's content as read INSIDE the lock — `null` when
|
|
1030
|
+
* the file does not exist, so a caller can tell "absent" from "empty" — and
|
|
1031
|
+
* returns what to write. Returning `null` writes nothing and still counts as
|
|
1032
|
+
* success.
|
|
1033
|
+
*
|
|
1034
|
+
* Returns `written` / `skipped` / `failed`, the same three-state contract
|
|
1035
|
+
* `update_json_under_lock` carries since `bcbb0380b` — a boolean could not tell
|
|
1036
|
+
* a deliberate decline from a failure, and every member of the union is a truthy
|
|
1037
|
+
* string, so `if (!result)` is a silent no-op rather than a compile error. Under
|
|
1038
|
+
* `AGENT_CONFIG_REPLAY=1` this reports `failed` without touching the tree.
|
|
1039
|
+
*/
|
|
1040
|
+
export function update_text_under_lock(
|
|
1041
|
+
target: string,
|
|
1042
|
+
transform: (loaded: string | null) => string | null,
|
|
1043
|
+
options: { blocking?: boolean } = {},
|
|
1044
|
+
): LockedUpdateResult {
|
|
1045
|
+
if (is_replay_mode()) return "failed";
|
|
1046
|
+
const targetPath = path.resolve(target);
|
|
1047
|
+
const state_dir = path.dirname(targetPath);
|
|
1048
|
+
try {
|
|
1049
|
+
fs.mkdirSync(state_dir, { recursive: true });
|
|
1050
|
+
} catch {
|
|
1051
|
+
return "failed";
|
|
1052
|
+
}
|
|
1053
|
+
// FILE-keyed, matching `update_json_under_lock` rather than
|
|
1054
|
+
// `_atomic_write_text`. Converged onto the sibling deliberately after
|
|
1055
|
+
// `bcbb0380b` landed the measured argument: a read-modify-write needs mutual
|
|
1056
|
+
// exclusion against writers of THIS file and nothing else, and the shared
|
|
1057
|
+
// directory lock was measured paying the full cost of exclusion for writes
|
|
1058
|
+
// that require none (8 workers: slowest 138-267 ms shared vs 83-95 ms
|
|
1059
|
+
// unshared). This function was written against the directory-keyed primitive
|
|
1060
|
+
// one commit earlier; leaving it there would have made
|
|
1061
|
+
// `dispatch-issues.jsonl` serialise against every unrelated
|
|
1062
|
+
// `atomic_write_text` in the state dir.
|
|
1063
|
+
const lock_path = _target_lock_path(targetPath);
|
|
1064
|
+
let fd: number | null;
|
|
1065
|
+
try {
|
|
1066
|
+
fd = _acquire_lock(lock_path, { blocking: options.blocking ?? true });
|
|
1067
|
+
} catch {
|
|
1068
|
+
return "failed";
|
|
1069
|
+
}
|
|
1070
|
+
// FAIL CLOSED, for the same reason the JSON sibling does: an append without
|
|
1071
|
+
// exclusion is the truncating write this function exists to prevent.
|
|
1072
|
+
if (fd === null) return "failed";
|
|
1073
|
+
try {
|
|
1074
|
+
let loaded: string | null = null;
|
|
1075
|
+
try {
|
|
1076
|
+
loaded = fs.readFileSync(targetPath, "utf-8");
|
|
1077
|
+
} catch {
|
|
1078
|
+
// Absent or unreadable. `null` lets the transform distinguish the two
|
|
1079
|
+
// from an empty file, which matters for an append-only log: an
|
|
1080
|
+
// unreadable existing log must not be silently replaced by one line.
|
|
1081
|
+
}
|
|
1082
|
+
const next = transform(loaded);
|
|
1083
|
+
if (next === null) return "skipped"; // deliberate no-write, not a failure
|
|
1084
|
+
_publish_text_locked(targetPath, next);
|
|
1085
|
+
return "written";
|
|
1086
|
+
} catch {
|
|
1087
|
+
return "failed";
|
|
1088
|
+
} finally {
|
|
1089
|
+
_release_lock(fd, lock_path);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1006
1093
|
export function update_json_under_lock<T>(
|
|
1007
1094
|
target: string,
|
|
1008
1095
|
mutate: (loaded: Partial<T>) => T | null,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stdin-read-failure policy, extracted from `dispatch_hook.ts`.
|
|
3
|
+
*
|
|
4
|
+
* These functions are pure with respect to dispatch_hook's own state: they take
|
|
5
|
+
* what they need as parameters and return a verdict. They do call two imported
|
|
6
|
+
* helpers (`readFd0ToEnd`, `isBlockCapable`), which move with them as imports. The module-level flag they feed, `_stdin_read_failed`, stays in
|
|
7
|
+
* dispatch_hook.ts, written at exactly one call site and read at three. That
|
|
8
|
+
* asymmetry is what makes moving the functions safe and the state untouched.
|
|
9
|
+
*
|
|
10
|
+
* Extracted because dispatch_hook.ts sits at the 1500-line source ceiling.
|
|
11
|
+
*/
|
|
12
|
+
import type { JsonObject } from './dispatch_hook.js';
|
|
13
|
+
import { readFd0ToEnd } from './hook_stdin.js';
|
|
14
|
+
import { isBlockCapable } from './host_semantics.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The seam where a FAILED read becomes an empty string — isolated so the three
|
|
18
|
+
* failure classes the policy names can be driven with real errno values.
|
|
19
|
+
*
|
|
20
|
+
* Returns the failure message, or `null` when the read succeeded. The read
|
|
21
|
+
* itself is injectable for exactly one reason: `EAGAIN` exhaustion, `EIO` and
|
|
22
|
+
* `EBADF` cannot be staged against a live fd 0 portably, and a policy whose
|
|
23
|
+
* trigger is untestable is a policy nobody can show works. What the injection
|
|
24
|
+
* does NOT buy is proof that the production path reaches here — that is the
|
|
25
|
+
* wiring, pinned separately by driving `main()`.
|
|
26
|
+
*/
|
|
27
|
+
export function stdinReadFailure(read: () => string = readFd0ToEnd): {
|
|
28
|
+
text: string;
|
|
29
|
+
failure: string | null;
|
|
30
|
+
} {
|
|
31
|
+
try {
|
|
32
|
+
// Reads to EOF and retries on EAGAIN rather than treating it as empty
|
|
33
|
+
// input; shared with the concern-side reader so both paths cannot drift.
|
|
34
|
+
return { text: read(), failure: null };
|
|
35
|
+
} catch (exc) {
|
|
36
|
+
return { text: "", failure: exc instanceof Error ? exc.message : String(exc) };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Is this concern one whose verdict a failed read silently converted into an
|
|
42
|
+
* allow?
|
|
43
|
+
*
|
|
44
|
+
* Both halves are required and neither is redundant. `fail_closed: true` is the
|
|
45
|
+
* manifest's declaration that this concern's own crash must not become a pass;
|
|
46
|
+
* `severity: blocking` is the declaration that it can refuse at all. An
|
|
47
|
+
* advisory concern that happens to be `fail_closed` has no verdict to lose, and
|
|
48
|
+
* a blocking concern that is not `fail_closed` has already declared the opposite
|
|
49
|
+
* intent.
|
|
50
|
+
*/
|
|
51
|
+
export function _is_fail_closed_blocking(concern: JsonObject): boolean {
|
|
52
|
+
const severity = String(concern["severity"] ?? "").trim().toLowerCase();
|
|
53
|
+
return concern["fail_closed"] === true && severity === "blocking";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `b-stdin-read-failure-policy`, option (c) — deny a failed read ONLY where a
|
|
58
|
+
* deny is honoured AND something was actually silenced.
|
|
59
|
+
*
|
|
60
|
+
* Decided by council 2026-08-20, 2/2 quorum, both seats on option (c):
|
|
61
|
+
* "on a read failure, deny only when the slot is block-capable and at least one
|
|
62
|
+
* selected concern is both blocking and `fail_closed: true`".
|
|
63
|
+
*
|
|
64
|
+
* The two rejected options are worth keeping visible, because each is the
|
|
65
|
+
* obvious move. Denying on EVERY slot (option (a)) refuses nothing on
|
|
66
|
+
* `post_tool_use` — the tool already ran — and on `stop` it breaks a turn end
|
|
67
|
+
* to protect a guard that was never there; it buys availability risk for no
|
|
68
|
+
* enforcement. Keeping the allow (option (b), the status quo) leaves a
|
|
69
|
+
* documented allow-on-failure on a security path: F-1 measured a
|
|
70
|
+
* `git commit --no-verify` DENIED at small payload size and ALLOWED at 300 KB,
|
|
71
|
+
* and the loud stderr line that now ships makes that visible without making it
|
|
72
|
+
* stop.
|
|
73
|
+
*
|
|
74
|
+
* The cost is stated rather than hidden: a transient I/O error on
|
|
75
|
+
* `pre_tool_use` now refuses a tool call the user must retry. The retry budget
|
|
76
|
+
* already survived ~10 s of `EAGAIN` before this point, so the class of error
|
|
77
|
+
* that reaches here is not "the writer was slow".
|
|
78
|
+
*
|
|
79
|
+
* Returns `null` when the dispatch proceeds normally.
|
|
80
|
+
*/
|
|
81
|
+
export function denyOnStdinFailure(
|
|
82
|
+
platform: string,
|
|
83
|
+
event: string,
|
|
84
|
+
concerns: readonly JsonObject[],
|
|
85
|
+
failure: string,
|
|
86
|
+
): { reason: string } | null {
|
|
87
|
+
if (!isBlockCapable(platform, event)) return null;
|
|
88
|
+
const silenced = concerns.filter(_is_fail_closed_blocking).map((c) => String(c["name"]));
|
|
89
|
+
if (silenced.length === 0) return null;
|
|
90
|
+
return {
|
|
91
|
+
reason:
|
|
92
|
+
`agent-config: refusing this action — the hook payload could not be read ` +
|
|
93
|
+
`(${failure}), so ${silenced.join(", ")} evaluated nothing. ` +
|
|
94
|
+
`A fail-closed guard on a block-capable slot must not pass by default. Retry.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -112,7 +112,7 @@ import * as fs from 'node:fs';
|
|
|
112
112
|
import * as path from 'node:path';
|
|
113
113
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
114
114
|
|
|
115
|
-
import { validateResponse } from '../_lib/subagent_response.js';
|
|
115
|
+
import { countContractFields, validateResponse } from '../_lib/subagent_response.js';
|
|
116
116
|
import { atomic_write_json, is_replay_mode } from './state_io.js';
|
|
117
117
|
import { readHookStdin } from './hook_stdin.js';
|
|
118
118
|
|
|
@@ -197,7 +197,7 @@ function extractParentId(payload: JsonObject, ownId: string | null): string | nu
|
|
|
197
197
|
return null;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
export type EnvelopeParse = 'ok' | 'fail' | 'no_message' | 'no_envelope';
|
|
200
|
+
export type EnvelopeParse = 'ok' | 'fail' | 'foreign_object' | 'no_message' | 'no_envelope';
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
203
|
* The retired fifth value. Records written before 2026-08-20 carry `absent`,
|
|
@@ -212,16 +212,49 @@ export type EnvelopeParse = 'ok' | 'fail' | 'no_message' | 'no_envelope';
|
|
|
212
212
|
*/
|
|
213
213
|
export const RETIRED_ENVELOPE_PARSE = 'absent';
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* READING THE HISTORICAL WINDOW — the caveat, not a version field.
|
|
217
|
+
*
|
|
218
|
+
* A verdict records conformance to the shape that was canonical **when the row
|
|
219
|
+
* was written**, and that shape was reconciled on 2026-08-22
|
|
220
|
+
* (`subagent-response-contract.md` § The canonical shape). Before that date one
|
|
221
|
+
* contract existed in three mutually inconsistent states: the spawn contract's
|
|
222
|
+
* rule (f), a narrower JSON injected by `team_dispatch.ts:297`, and the
|
|
223
|
+
* validator's five required fields. Nothing recorded which one a given row was
|
|
224
|
+
* judged against, because until the reconciliation there was no single answer.
|
|
225
|
+
*
|
|
226
|
+
* So a row from the 2026-08 window must NOT be read as conforming — or as
|
|
227
|
+
* failing to conform — to the reconciled shape. What it says is narrower: the
|
|
228
|
+
* classifier at the time either found a `validateResponse`-passing object or did
|
|
229
|
+
* not. As it happens the two agree for every row in that window, because the
|
|
230
|
+
* count is **0 of 1,845** and a rate of zero is the same number under either
|
|
231
|
+
* reading — but that is a property of this particular window, not of the
|
|
232
|
+
* comparison, and the next window will not have it.
|
|
233
|
+
*
|
|
234
|
+
* Deliberately a comment and not a `contract_version` field. Adding one would
|
|
235
|
+
* be honest going forward and would still say nothing about the rows already on
|
|
236
|
+
* disk, which are the rows this caveat exists for. A council seat asked for the
|
|
237
|
+
* field; it is the right next step and it is not this one, because a field
|
|
238
|
+
* stamped from today cannot retro-tag yesterday.
|
|
239
|
+
*/
|
|
240
|
+
|
|
215
241
|
export interface ParseVerdict {
|
|
216
242
|
verdict: EnvelopeParse;
|
|
217
243
|
/** Count of validator errors. A COUNT — the messages never leave this function. */
|
|
218
244
|
error_count: number;
|
|
245
|
+
/**
|
|
246
|
+
* How many of the five REQUIRED contract fields the judged candidate
|
|
247
|
+
* carried. Recorded beside the error count so the `fail` / `foreign_object`
|
|
248
|
+
* boundary is auditable from the row itself rather than re-derived from a
|
|
249
|
+
* probe — which is how the split's own premise had to be established.
|
|
250
|
+
*/
|
|
251
|
+
field_hits: number;
|
|
219
252
|
}
|
|
220
253
|
|
|
221
254
|
/**
|
|
222
255
|
* Classify a subagent's final message against the response-envelope contract.
|
|
223
256
|
*
|
|
224
|
-
*
|
|
257
|
+
* Five outcomes, and every boundary between them separates two defects with
|
|
225
258
|
* two different fixes:
|
|
226
259
|
*
|
|
227
260
|
* - `no_message` — the host delivered no `last_assistant_message` at all
|
|
@@ -232,9 +265,22 @@ export interface ParseVerdict {
|
|
|
232
265
|
* the overwhelmingly common case: subagents answer in prose. It is not a
|
|
233
266
|
* channel failure, and a fallback keyed on it would fire on nearly every
|
|
234
267
|
* dispatch.
|
|
235
|
-
* - `fail` — an object arrived
|
|
268
|
+
* - `fail` — an object arrived, carried at least one required contract
|
|
269
|
+
* field, and did not satisfy `validateResponse`. A near-miss: someone
|
|
270
|
+
* aimed at the envelope and got it wrong.
|
|
271
|
+
* - `foreign_object` — an object arrived and carried NONE of the required
|
|
272
|
+
* fields. A fenced tool call, or a prose-embedded JSON blob: it never
|
|
273
|
+
* aimed at the envelope at all.
|
|
236
274
|
* - `ok` — an object arrived and validated.
|
|
237
275
|
*
|
|
276
|
+
* The last two used to be one value, `fail`, and the collapse had the same
|
|
277
|
+
* shape as the `absent` one below it. Re-derived over the live month ledger
|
|
278
|
+
* (7,282 rows, 6,315 stops) the all-time `fail` histogram was `{5: 27}` —
|
|
279
|
+
* every recorded `fail` carried `error_count: 5`, which is exactly "zero
|
|
280
|
+
* required fields present". So the bucket a threshold would be read off held
|
|
281
|
+
* no contract attempts at all, and could neither rise nor fall for the reason
|
|
282
|
+
* its name suggests.
|
|
283
|
+
*
|
|
238
284
|
* The first two used to be one value, `absent`. Over the first measured window
|
|
239
285
|
* that collapse read 25 of 25 `absent` — including the #58109 control arm that
|
|
240
286
|
* returned a complete report — so a rate computed off it measured the answer
|
|
@@ -244,21 +290,37 @@ export interface ParseVerdict {
|
|
|
244
290
|
* column, and Phase 2's fallback condition, expressible at all.
|
|
245
291
|
*/
|
|
246
292
|
export function classifyEnvelope(message: string | null): ParseVerdict {
|
|
247
|
-
if (message === null || !message.trim())
|
|
293
|
+
if (message === null || !message.trim()) {
|
|
294
|
+
return { verdict: 'no_message', error_count: 0, field_hits: 0 };
|
|
295
|
+
}
|
|
248
296
|
|
|
249
297
|
const candidates = _jsonObjectCandidates(message);
|
|
250
|
-
if (candidates.length === 0) return { verdict: 'no_envelope', error_count: 0 };
|
|
298
|
+
if (candidates.length === 0) return { verdict: 'no_envelope', error_count: 0, field_hits: 0 };
|
|
251
299
|
|
|
252
300
|
// A message may carry several object-shaped spans. The envelope is the one
|
|
253
|
-
// that validates, so a
|
|
254
|
-
// been tried — otherwise a stray leading `{}` would mask a
|
|
301
|
+
// that validates, so a failure verdict is only reported once every
|
|
302
|
+
// candidate has been tried — otherwise a stray leading `{}` would mask a
|
|
303
|
+
// valid envelope.
|
|
304
|
+
//
|
|
305
|
+
// Among failures the MOST contract-shaped candidate wins, not the first
|
|
306
|
+
// one met. First-match was safe while every failure was one bucket; with
|
|
307
|
+
// the near-miss split it is not, because a leading `{"note":1}` would
|
|
308
|
+
// report `foreign_object` for a message that also carried a real attempt —
|
|
309
|
+
// the same masking defect one register down.
|
|
255
310
|
let best: ParseVerdict | null = null;
|
|
256
311
|
for (const candidate of candidates) {
|
|
257
312
|
const result = validateResponse(candidate);
|
|
258
|
-
|
|
259
|
-
if (
|
|
313
|
+
const fieldHits = countContractFields(candidate);
|
|
314
|
+
if (result.valid) return { verdict: 'ok', error_count: 0, field_hits: fieldHits };
|
|
315
|
+
if (best === null || fieldHits > best.field_hits) {
|
|
316
|
+
best = {
|
|
317
|
+
verdict: fieldHits === 0 ? 'foreign_object' : 'fail',
|
|
318
|
+
error_count: result.errors.length,
|
|
319
|
+
field_hits: fieldHits,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
260
322
|
}
|
|
261
|
-
return best ?? { verdict: 'no_envelope', error_count: 0 };
|
|
323
|
+
return best ?? { verdict: 'no_envelope', error_count: 0, field_hits: 0 };
|
|
262
324
|
}
|
|
263
325
|
|
|
264
326
|
/**
|
|
@@ -685,6 +747,9 @@ function handleStop(root: string, payload: JsonObject, nowIso: string, sessionId
|
|
|
685
747
|
start_seen: rec !== null,
|
|
686
748
|
envelope_parse: parse.verdict,
|
|
687
749
|
envelope_error_count: parse.error_count,
|
|
750
|
+
// Beside the count, never instead of it: the count says how wrong the
|
|
751
|
+
// candidate was, this says whether it was aimed at the envelope.
|
|
752
|
+
envelope_field_hits: parse.field_hits,
|
|
688
753
|
concurrent_open: countOpen(root),
|
|
689
754
|
});
|
|
690
755
|
}
|