@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
|
@@ -92,6 +92,9 @@ export const BLOCK_OPS: ReadonlySet<GitOp> = new Set<GitOp>([
|
|
|
92
92
|
"tag",
|
|
93
93
|
"release",
|
|
94
94
|
"pr-merge",
|
|
95
|
+
// Enabling auto-merge commits the outcome to a condition the agent does not
|
|
96
|
+
// control, so it is blocked on the same terms as the merge it schedules.
|
|
97
|
+
"pr-merge-auto",
|
|
95
98
|
]);
|
|
96
99
|
|
|
97
100
|
/** Recoverable operations — warned without a this-turn authorization. */
|
|
@@ -162,10 +165,25 @@ const COMMAND_OPS: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
|
|
|
162
165
|
op: "tag",
|
|
163
166
|
re: new RegExp(`${P}git\\s+${G}push\\b[^\\n;|&]*(--tags|--follow-tags|refs\\/tags\\/)`, "i"),
|
|
164
167
|
},
|
|
168
|
+
// ORDER IS LOAD-BEARING, per this table's own rule that the most specific
|
|
169
|
+
// pattern wins: `gh pr merge 12 --auto` matches the plain merge pattern too.
|
|
170
|
+
//
|
|
171
|
+
// The GraphQL mutation needs its OWN pattern rather than a widening of
|
|
172
|
+
// `ghApiWrite()`. That helper requires BOTH a REST-shaped path and an
|
|
173
|
+
// explicit write method, and `gh api graphql -f query='mutation{...}'` has
|
|
174
|
+
// neither — widening it to reach this would loosen every other op that uses
|
|
175
|
+
// it, which is a bigger change than the defect.
|
|
176
|
+
{
|
|
177
|
+
op: "pr-merge-auto",
|
|
178
|
+
re: new RegExp(
|
|
179
|
+
`${P}gh\\s+pr\\s+merge\\b(?=[^\\n]*--auto\\b)|${P}gh\\s+api\\b(?=[^\\n]*enablePullRequestAutoMerge\\b)`,
|
|
180
|
+
"i",
|
|
181
|
+
),
|
|
182
|
+
},
|
|
165
183
|
{
|
|
166
184
|
op: "pr-merge",
|
|
167
185
|
re: new RegExp(
|
|
168
|
-
`${P}gh\\s+pr\\s+merge\\b|${ghApiWrite("\\/pulls\\/\\d+\\/merge\\b").source}`,
|
|
186
|
+
`${P}gh\\s+pr\\s+merge\\b(?![^\\n]*--(auto|disable-auto)\\b)|${ghApiWrite("\\/pulls\\/\\d+\\/merge\\b").source}`,
|
|
169
187
|
"i",
|
|
170
188
|
),
|
|
171
189
|
},
|
|
@@ -485,8 +503,28 @@ export function commandOp(command: string): GitOp | null {
|
|
|
485
503
|
return found;
|
|
486
504
|
}
|
|
487
505
|
|
|
488
|
-
/**
|
|
489
|
-
|
|
506
|
+
/**
|
|
507
|
+
* A ledger older than this is not "this turn" under any reading.
|
|
508
|
+
*
|
|
509
|
+
* **Widening this constant for a long run is forbidden practice.** On
|
|
510
|
+
* 2026-08-21 it was sed-patched to twelve times this value — six hours —
|
|
511
|
+
* behind a marker promising a revert that never came, so an autonomous PR-drain could
|
|
512
|
+
* merge past the 30-minute bound. The widening was committed to the trunk and
|
|
513
|
+
* left there. It is a twelvefold expansion of the authorization lifetime on
|
|
514
|
+
* the guard that gates `pr-merge`, which is a `BLOCK_OPS` member precisely
|
|
515
|
+
* because it is irreversible.
|
|
516
|
+
*
|
|
517
|
+
* Neither the widened expression nor the marker text is written out here: the
|
|
518
|
+
* obvious regression check for either is a grep, and a comment reciting the
|
|
519
|
+
* literal makes that grep match the guard's own prose on a clean tree.
|
|
520
|
+
*
|
|
521
|
+
* The supported answer to "my run is longer than the window" is that the run
|
|
522
|
+
* STOPS and REPORTS at expiry and the user re-authorizes — never that the
|
|
523
|
+
* window grows. The agent never edits this constant, this file, or the built
|
|
524
|
+
* bundle; `dist/hooks/dispatch.js` is what actually executes, and a source
|
|
525
|
+
* edit without a rebuild is silently inert.
|
|
526
|
+
*/
|
|
527
|
+
export const LEDGER_MAX_AGE_MS = 30 * 60 * 1000;
|
|
490
528
|
|
|
491
529
|
/**
|
|
492
530
|
* Load the ledger, and refuse one that does not belong to THIS session and
|
|
@@ -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
|
+
}
|