@event4u/agent-config 12.1.0 → 14.0.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 +164 -186
- package/README.md +1 -1
- package/dist/agent-src/commands/cost/report.md +23 -0
- package/dist/agent-src/commands/evals/roadmap.json +8 -0
- package/dist/agent-src/commands/fix/ci.md +39 -1
- package/dist/agent-src/commands/pr/create.md +65 -4
- package/dist/agent-src/commands/roadmap/next.md +28 -5
- package/dist/agent-src/commands/work.md +16 -0
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
- package/dist/agent-src/contexts/subagent-configuration.md +69 -7
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +36 -14
- package/dist/agent-src/rules/evaluator-independence.md +27 -5
- package/dist/agent-src/rules/git-history-discipline.md +5 -1
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +34 -12
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/dist/agent-src/scripts/gate_execute.ts +395 -0
- package/dist/agent-src/scripts/resume_probe.ts +422 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
- package/dist/agent-src/scripts/update_roadmap_progress.ts +70 -4
- package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
- package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
- package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
- package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
- package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
- package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
- package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +5 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +43 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-7PZ4FSZB.js} +368 -191
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-F3YUF7SA.js} +1 -1
- package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
- package/dist/cli-delegate/cmd_conformance.js +224 -104
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +73 -30
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -5
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +2653 -1365
- package/dist/install/install.mjs +12 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2679 -887
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +1 -1
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
- package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/design-fidelity-mechanics.md +21 -0
- package/docs/guidelines/design-handover-extraction.md +87 -0
- package/docs/guidelines/prompt-templates.md +24 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/src/agent-src/scripts/gate_execute.ts +395 -0
- package/src/agent-src/scripts/resume_probe.ts +422 -0
- package/src/agent-src/scripts/roadmap_gates.ts +138 -4
- package/src/agent-src/scripts/update_roadmap_progress.ts +70 -4
- package/src/config/ci-local-parity.yml +13 -0
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-coverage.yml +140 -12
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/hook-token-budget.json +8 -1
- package/src/config/rule-activation-census.json +41 -0
- package/src/config/rule-stub-ceilings.json +52 -0
- package/src/scripts/_dispatch.bash +4 -0
- package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
- package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
- package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
- package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
- package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
- package/src/scripts/_lib/evaluator_contract.ts +159 -0
- package/src/scripts/_lib/gh_transient.ts +80 -0
- package/src/scripts/_lib/release_material.ts +16 -5
- package/src/scripts/_lib/review_independence.ts +111 -0
- package/src/scripts/_lib/session_register.ts +14 -0
- package/src/scripts/_lib/skill_catalogue.ts +278 -5
- package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/_lib/turn_end_refusals.ts +482 -0
- package/src/scripts/ai_council/_py_json.ts +96 -0
- package/src/scripts/ai_council/cli_call_budget.ts +357 -0
- package/src/scripts/ai_council/clients.ts +101 -164
- package/src/scripts/ai_council/config.ts +25 -11
- package/src/scripts/ai_council/probe_store.ts +196 -0
- package/src/scripts/ai_council/qualification.ts +425 -0
- package/src/scripts/ai_council/qualification_wiring.ts +243 -0
- package/src/scripts/ai_council/quorum_wiring.ts +211 -0
- package/src/scripts/ai_team/team_dispatch.ts +40 -6
- package/src/scripts/bench_ab_v2_complexity.ts +252 -0
- package/src/scripts/bench_ab_v2_run.ts +3 -42
- package/src/scripts/bench_ab_v2_safety.ts +296 -0
- package/src/scripts/bench_ab_v2_search.ts +343 -0
- package/src/scripts/bench_ab_v2_stats.ts +191 -0
- package/src/scripts/build_archive_index.ts +383 -0
- package/src/scripts/capture_skill_catalogue.ts +369 -4
- package/src/scripts/check_evaluator_schema.ts +249 -0
- package/src/scripts/check_pr_ci_current.ts +409 -0
- package/src/scripts/check_review_schema.ts +195 -0
- package/src/scripts/check_rule_activation_census.ts +451 -0
- package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/condense.ts +1 -1
- package/src/scripts/cost/backfill_rates.mjs +298 -0
- package/src/scripts/cost/budget.mjs +12 -32
- package/src/scripts/cost_summary.ts +25 -0
- package/src/scripts/council_cli.ts +141 -191
- package/src/scripts/dispatch_r2_reviewer.ts +8 -1
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +99 -6
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/dispatch_hook.ts +43 -1
- package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/hooks_doctor.ts +112 -0
- package/src/scripts/interruption_report.ts +443 -0
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_evidence_artifacts.ts +359 -0
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_roadmap_blockers.ts +65 -0
- package/src/scripts/lint_roadmap_complexity.ts +7 -1
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_skill_router_head.ts +21 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
- package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
- package/src/scripts/mcp_server/tools.ts +87 -0
- package/src/scripts/release.ts +43 -58
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/rule_activation_census.ts +292 -0
- package/src/scripts/schemas/command.schema.json +1 -1
- package/src/scripts/schemas/evaluator-output.schema.json +60 -0
- package/src/scripts/schemas/review-findings.schema.json +31 -0
- package/src/scripts/schemas/skill.schema.json +18 -1
- package/src/scripts/self_review_gate.ts +6 -0
- package/src/scripts/session_register_hook.ts +30 -1
- package/src/scripts/sync_pr_branch.ts +271 -0
- package/src/scripts/test_council_qualification.ts +213 -0
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/server/schemas/settings.ts +2 -2
- package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
- package/src/config/budget-routing.json +0 -7
|
@@ -74,6 +74,21 @@ concerns:
|
|
|
74
74
|
# Phase 5 — Tier-1 hook for the minimal-safe-diff rule. Counts unique
|
|
75
75
|
# files touched per turn into agents/runtime/state/minimal-safe-diff.json and
|
|
76
76
|
# flips warning=true past hooks.minimal_safe_diff.threshold.
|
|
77
|
+
# road-to-metric-loop-and-review-integrity Phase 4 — shipping-intent gate on
|
|
78
|
+
# diff VOLUME, complementary to `minimal-safe-diff` rather than a duplicate:
|
|
79
|
+
# that concern counts unique files touched PER TURN, this one counts changed
|
|
80
|
+
# lines on the branch AT A SHIP VERB. Threshold 1695 is the corrected p90 of
|
|
81
|
+
# this repository's own last 40 merged PRs, and the exclusion set (review-input
|
|
82
|
+
# snapshots, generated indexes, projection trees) is part of the metric's
|
|
83
|
+
# definition, not a tuning knob — the replay measured the naive metric
|
|
84
|
+
# double-counting the diff it was measuring. Warn-level first; escalation waits
|
|
85
|
+
# for one release of recorded firings. Evidence:
|
|
86
|
+
# agents/evidence/eval-findings/metric-loop-s04.md
|
|
87
|
+
ship-diff-volume:
|
|
88
|
+
script: src/scripts/hooks/ship_diff_volume_hook.ts
|
|
89
|
+
args: []
|
|
90
|
+
fail_closed: false
|
|
91
|
+
severity: advisory
|
|
77
92
|
minimal-safe-diff:
|
|
78
93
|
script: src/scripts/minimal_safe_diff_hook.ts
|
|
79
94
|
args: []
|
|
@@ -434,6 +449,28 @@ concerns:
|
|
|
434
449
|
args: []
|
|
435
450
|
fail_closed: false
|
|
436
451
|
severity: advisory
|
|
452
|
+
# road-to-inbox-harvest-2026-08-d-runtime-skill-routing Phase 2 — binds the
|
|
453
|
+
# deterministic task→skill ranker, which had shipped with ZERO production
|
|
454
|
+
# callers: `score_skill_relevance.rank` and its `suggest_skill_for_task`
|
|
455
|
+
# wrapper were reachable only from their CLIs and from `routing:doctor`.
|
|
456
|
+
# Ranks the SUBMITTED prompt over the installed skill catalogue and injects
|
|
457
|
+
# at most one line naming the top-3 skill ids and their scores — POINTERS,
|
|
458
|
+
# never bodies, never a load instruction. Same delivery as `delegation-nudge`
|
|
459
|
+
# above: `{decision:"warn", reason, additional_context}` at exit 2, reduced
|
|
460
|
+
# to a real exit of 0 by `host_semantics.emitFor`'s warn branch, so it never
|
|
461
|
+
# blocks a turn.
|
|
462
|
+
#
|
|
463
|
+
# Silence is the default and its floor is MEASURED, not chosen: `rank`
|
|
464
|
+
# returns something on nearly every real prompt, so the trigger is a top-1
|
|
465
|
+
# score at or above 30/100 — the p90 of the routing-matrix corpus (496 prompt
|
|
466
|
+
# lines; median 18, p75 28, max 70), firing on 13.9% of them. A median floor
|
|
467
|
+
# would speak on half of all turns, which is the per-turn-reminder shape this
|
|
468
|
+
# estate has already measured failing. fail_closed: false.
|
|
469
|
+
skill-route:
|
|
470
|
+
script: src/scripts/hooks/skill_route_hook.ts
|
|
471
|
+
args: []
|
|
472
|
+
fail_closed: false
|
|
473
|
+
severity: advisory
|
|
437
474
|
# road-to-orchestrator-discipline-carriers Phase 5 (F4-lite) — end-review
|
|
438
475
|
# nudge. Fires ONLY when the session mutated tracked+untracked, non-doc
|
|
439
476
|
# files beyond MUTATION_LINE_THRESHOLD (50 lines; untracked files added
|
|
@@ -452,6 +489,16 @@ concerns:
|
|
|
452
489
|
args: []
|
|
453
490
|
fail_closed: false
|
|
454
491
|
severity: advisory
|
|
492
|
+
# road-to-stop-gate-honesty step 3.1 — skipped on a refusal RETRY.
|
|
493
|
+
# ARGUMENT, per concern as that step requires: the F2 fire gate is a
|
|
494
|
+
# once-per-session marker, and this concern's own header records that a
|
|
495
|
+
# second run "reads the marker and returns silently, without re-running the
|
|
496
|
+
# transcript scan". The retry therefore cannot change its artefact — the
|
|
497
|
+
# telemetry line was already written, or already suppressed, on the first
|
|
498
|
+
# Stop. Idempotence is the concern's own construction, not our assumption
|
|
499
|
+
# about it. The saving is the module load rather than a large read, which is
|
|
500
|
+
# the smaller of the two flags below it and is stated as such.
|
|
501
|
+
skip_on_refusal_retry: true
|
|
455
502
|
# road-to-conformance-round5 Phase 3 — the FIRST concern that can refuse a
|
|
456
503
|
# turn-END. Round 5 measured the reason: both blocking carriers reached zero
|
|
457
504
|
# violations, neither advisory carrier did, and 19 language violations
|
|
@@ -535,6 +582,43 @@ concerns:
|
|
|
535
582
|
args: []
|
|
536
583
|
fail_closed: false
|
|
537
584
|
severity: advisory
|
|
585
|
+
# road-to-user-out-of-the-loop Phase 0 Step 1 — the interruption ledger.
|
|
586
|
+
# Every Stop, classifies whether the turn that just ended put a synchronous
|
|
587
|
+
# contact to the user and appends one `{run_id, turn, kind, class, roadmap}`
|
|
588
|
+
# line to gitignored runtime state (agents/runtime/state/interruptions.jsonl,
|
|
589
|
+
# file shape after gate-metrics.jsonl). Three classes, not two: a hand-back
|
|
590
|
+
# ("das entscheidest Du", "your call") ends the turn and waits for the user
|
|
591
|
+
# exactly as a question does, so counting only `?` would report this
|
|
592
|
+
# package's own preferred shape as zero contacts.
|
|
593
|
+
#
|
|
594
|
+
# CAPTURE ONLY, and that is the point rather than caution: Phase 0's two
|
|
595
|
+
# pre-registered claims are baselines, and a baseline can only be read off a
|
|
596
|
+
# window no mechanism reacted to. It emits nothing, never warns, and always
|
|
597
|
+
# returns 0 — so it carries no src/config/hook-token-budget.json row, the
|
|
598
|
+
# same as subagent-ledger and orchestration-record.
|
|
599
|
+
#
|
|
600
|
+
# Reply-shape detection is IMPORTED from turn-end-gate (readTranscriptTail,
|
|
601
|
+
# finalParagraph, HANDBACK) rather than copied — a second copy would drift
|
|
602
|
+
# from the measured corpus's detector and the drift would land silently in
|
|
603
|
+
# the baseline. Dedupe on the turn ordinal, the same identity that hook
|
|
604
|
+
# derives, because a host may fire Stop twice for one turn. fail_closed: false.
|
|
605
|
+
interruption-ledger:
|
|
606
|
+
script: src/scripts/hooks/interruption_ledger_hook.ts
|
|
607
|
+
args: []
|
|
608
|
+
fail_closed: false
|
|
609
|
+
severity: advisory
|
|
610
|
+
# road-to-stop-gate-honesty step 3.1 — skipped on a refusal RETRY, and this
|
|
611
|
+
# is the flag with the real saving.
|
|
612
|
+
# ARGUMENT, per concern: `alreadyRecorded(ledgerPath, run_id, turn)` dedupes
|
|
613
|
+
# on the TURN ORDINAL, and a refusal retry is the same turn by construction
|
|
614
|
+
# — the gate's layer-2 marker is keyed on that same ordinal. So the ledger
|
|
615
|
+
# already writes nothing on the retry and the artefact is provably identical.
|
|
616
|
+
# What it does NOT already avoid is the cost: `main()` calls
|
|
617
|
+
# `readTranscriptTail` (up to 8 MB, walking the whole file) BEFORE reaching
|
|
618
|
+
# `alreadyRecorded`, so today every refused Stop pays a full transcript walk
|
|
619
|
+
# and then discards the result. That is the "pure duplicate cost" § D-3
|
|
620
|
+
# names, measured in the source rather than assumed.
|
|
621
|
+
skip_on_refusal_retry: true
|
|
538
622
|
# road-to-subagent-lifecycle-integrity Phase 1 (Steps 2+3) — the subagent
|
|
539
623
|
# lifecycle ledger. Binds ONLY the two new events; on every other slot it is
|
|
540
624
|
# not listed at all. Appends `subagent_start` / `subagent_stop` lines to
|
|
@@ -639,22 +723,31 @@ roles:
|
|
|
639
723
|
# Orchestrator-only concerns (Phase 2.3): a worker never talks to the
|
|
640
724
|
# user, never dispatches sub-workers, never runs the council, and its
|
|
641
725
|
# complaints route via the orchestrator.
|
|
642
|
-
|
|
726
|
+
# `skill-route` joins the drop list for the same reason `delegation-nudge`
|
|
727
|
+
# is on it: a worker was handed a bounded slice by an orchestrator that
|
|
728
|
+
# already chose the approach, so a routing pointer there advises nobody who
|
|
729
|
+
# can act on it and costs the dispatch anyway.
|
|
730
|
+
# `interruption-ledger` joins it on the first clause verbatim — "a worker
|
|
731
|
+
# never talks to the user". Its whole measurement is synchronous user
|
|
732
|
+
# contacts, so a worker turn that ends in a question to its orchestrator
|
|
733
|
+
# would be recorded as a contact that never reached a human, inflating the
|
|
734
|
+
# exact baseline Phase 0 pre-registers.
|
|
735
|
+
drop: [delegation-nudge, skill-route, end-review-nudge, council-availability, team-review-gate, self-repair, session-eol, interruption-ledger]
|
|
643
736
|
|
|
644
737
|
platforms:
|
|
645
738
|
augment:
|
|
646
739
|
session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
647
740
|
session_end: [chat-history, memory-learn, session-register]
|
|
648
741
|
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
649
|
-
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, reread-guard]
|
|
742
|
+
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, ship-diff-volume, reread-guard]
|
|
650
743
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, tool-result-bytes, edit-shape]
|
|
651
744
|
|
|
652
745
|
claude:
|
|
653
746
|
session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
654
747
|
session_end: [chat-history, memory-learn, session-register]
|
|
655
|
-
stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, turn-end-gate, self-repair, session-register, session-eol]
|
|
656
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, delegation-nudge, git-authorization, session-canary, self-repair, session-register]
|
|
657
|
-
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, reread-guard, spawn-guard-shadow]
|
|
748
|
+
stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, turn-end-gate, self-repair, session-register, session-eol, interruption-ledger]
|
|
749
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, delegation-nudge, skill-route, git-authorization, session-canary, self-repair, session-register]
|
|
750
|
+
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, ship-diff-volume, reread-guard, spawn-guard-shadow]
|
|
658
751
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, tool-result-bytes, edit-shape, language-mirror]
|
|
659
752
|
# `language-mirror` — round-5 § 6.1. `pre_compact` sets a pin-lost marker;
|
|
660
753
|
# the `post_tool_use` entry above re-emits the pin exactly once while that
|
|
@@ -708,7 +801,7 @@ platforms:
|
|
|
708
801
|
# still costs the dispatch, and its entire output is thrown away.
|
|
709
802
|
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
710
803
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
711
|
-
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, reread-guard, spawn-guard-shadow]
|
|
804
|
+
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, ship-diff-volume, reread-guard, spawn-guard-shadow]
|
|
712
805
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, tool-result-bytes, edit-shape]
|
|
713
806
|
# subagent-ledger IS bound here, unlike the two nudges above, and the
|
|
714
807
|
# difference is the delivery channel rather than the platform: the nudges
|
|
@@ -32,6 +32,7 @@ import { main as onboardingGateMain } from '../onboarding_gate_hook.js';
|
|
|
32
32
|
import { main as contextHygieneMain } from '../context_hygiene_hook.js';
|
|
33
33
|
import { main as verifyBeforeCompleteMain } from '../before_complete_hook.js';
|
|
34
34
|
import { main as minimalSafeDiffMain } from '../minimal_safe_diff_hook.js';
|
|
35
|
+
import { main as shipDiffVolumeMain } from './ship_diff_volume_hook.js';
|
|
35
36
|
import { main as injectionScanMain } from '../injection_scan_hook.js';
|
|
36
37
|
import { main as memoryLearnMain } from '../memory_learn_hook.js';
|
|
37
38
|
import { main as firstRunGateMain } from '../first_run_gate_hook.js';
|
|
@@ -57,6 +58,7 @@ import { main as codeGraphNudgeMain } from './code_graph_nudge_hook.js';
|
|
|
57
58
|
import { main as uiRouteNudgeMain } from './ui_route_nudge_hook.js';
|
|
58
59
|
import { main as orchestrationRecordMain } from './orchestration_record_hook.js';
|
|
59
60
|
import { main as delegationNudgeMain } from './delegation_nudge_hook.js';
|
|
61
|
+
import { main as skillRouteMain } from './skill_route_hook.js';
|
|
60
62
|
import { main as endReviewNudgeMain } from './end_review_nudge_hook.js';
|
|
61
63
|
import { main as turnEndGateMain } from './turn_end_gate_hook.js';
|
|
62
64
|
import { main as editShapeMain } from './edit_shape_hook.js';
|
|
@@ -65,6 +67,7 @@ import { main as sessionEolMain } from './session_eol_hook.js';
|
|
|
65
67
|
import { main as subagentLedgerMain } from './subagent_ledger_hook.js';
|
|
66
68
|
import { main as toolResultBytesMain } from './tool_result_bytes_hook.js';
|
|
67
69
|
import { main as spawnGuardShadowMain } from './spawn_guard_shadow_hook.js';
|
|
70
|
+
import { main as interruptionLedgerMain } from './interruption_ledger_hook.js';
|
|
68
71
|
|
|
69
72
|
/** A concern `main` — argv-taking or not; both shapes exist. */
|
|
70
73
|
export type ConcernMain = (argv?: string[]) => number | undefined | void;
|
|
@@ -79,6 +82,7 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
|
|
|
79
82
|
'src/scripts/context_hygiene_hook.ts': contextHygieneMain as ConcernMain,
|
|
80
83
|
'src/scripts/before_complete_hook.ts': verifyBeforeCompleteMain as ConcernMain,
|
|
81
84
|
'src/scripts/minimal_safe_diff_hook.ts': minimalSafeDiffMain as ConcernMain,
|
|
85
|
+
'src/scripts/hooks/ship_diff_volume_hook.ts': shipDiffVolumeMain as ConcernMain,
|
|
82
86
|
'src/scripts/injection_scan_hook.ts': injectionScanMain as ConcernMain,
|
|
83
87
|
'src/scripts/memory_learn_hook.ts': memoryLearnMain as ConcernMain,
|
|
84
88
|
'src/scripts/first_run_gate_hook.ts': firstRunGateMain as ConcernMain,
|
|
@@ -104,6 +108,7 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
|
|
|
104
108
|
'src/scripts/hooks/ui_route_nudge_hook.ts': uiRouteNudgeMain as ConcernMain,
|
|
105
109
|
'src/scripts/hooks/orchestration_record_hook.ts': orchestrationRecordMain as ConcernMain,
|
|
106
110
|
'src/scripts/hooks/delegation_nudge_hook.ts': delegationNudgeMain as ConcernMain,
|
|
111
|
+
'src/scripts/hooks/skill_route_hook.ts': skillRouteMain as ConcernMain,
|
|
107
112
|
'src/scripts/hooks/end_review_nudge_hook.ts': endReviewNudgeMain as ConcernMain,
|
|
108
113
|
'src/scripts/hooks/turn_end_gate_hook.ts': turnEndGateMain as ConcernMain,
|
|
109
114
|
'src/scripts/hooks/edit_shape_hook.ts': editShapeMain as ConcernMain,
|
|
@@ -112,4 +117,5 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
|
|
|
112
117
|
'src/scripts/hooks/subagent_ledger_hook.ts': subagentLedgerMain as ConcernMain,
|
|
113
118
|
'src/scripts/hooks/tool_result_bytes_hook.ts': toolResultBytesMain as ConcernMain,
|
|
114
119
|
'src/scripts/hooks/spawn_guard_shadow_hook.ts': spawnGuardShadowMain as ConcernMain,
|
|
120
|
+
'src/scripts/hooks/interruption_ledger_hook.ts': interruptionLedgerMain as ConcernMain,
|
|
115
121
|
};
|
|
@@ -342,16 +342,51 @@ export function _role_drop_set(
|
|
|
342
342
|
return new Set(drop.filter((d): d is string => typeof d === "string"));
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
/**
|
|
346
|
+
* True when this `stop` is the RETRY of a turn a stop-hook already refused.
|
|
347
|
+
*
|
|
348
|
+
* The host sets `stop_hook_active` on exactly that Stop; `turn_end_gate_hook`
|
|
349
|
+
* reads the same field as its layer-1 re-entrancy guard, so this is the host's
|
|
350
|
+
* own answer rather than an inference of ours. Anything unparseable is `false`,
|
|
351
|
+
* which is the full chain — the safe direction, since the cost of running a
|
|
352
|
+
* concern twice is duplicate work and the cost of skipping one wrongly is a lost
|
|
353
|
+
* write.
|
|
354
|
+
*/
|
|
355
|
+
export function _is_refusal_retry(event: string, payload_text: string): boolean {
|
|
356
|
+
if (event !== "stop") return false;
|
|
357
|
+
try {
|
|
358
|
+
const parsed: unknown = JSON.parse(payload_text);
|
|
359
|
+
if (!_isObject(parsed)) return false;
|
|
360
|
+
const payload = parsed["payload"];
|
|
361
|
+
const source = _isObject(payload) ? payload : parsed;
|
|
362
|
+
return source["stop_hook_active"] === true;
|
|
363
|
+
} catch {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
345
368
|
/**
|
|
346
369
|
* Return the ordered concern definitions for (platform, event) under the
|
|
347
370
|
* given session role (default `orchestrator` — byte-identical to the
|
|
348
371
|
* pre-role-axis behaviour).
|
|
372
|
+
*
|
|
373
|
+
* `opts.refusal_retry` drops concerns that opted in with
|
|
374
|
+
* `skip_on_refusal_retry: true` — `road-to-stop-gate-honesty` step 3.1. Stop is
|
|
375
|
+
* the heaviest slot on claude and **every refused Stop runs the whole slot again
|
|
376
|
+
* on the retry** (§ D-3), so the concerns whose second run provably produces the
|
|
377
|
+
* identical artefact are pure duplicate cost.
|
|
378
|
+
*
|
|
379
|
+
* Opt-in per concern, default off, never a blanket skip: Risk 3 of that roadmap
|
|
380
|
+
* is that a concern skipped on the retry might have been the one that needed the
|
|
381
|
+
* second pass, and the loss would be silent. The manifest carries the per-concern
|
|
382
|
+
* argument beside each flag.
|
|
349
383
|
*/
|
|
350
384
|
export function _resolve_concerns(
|
|
351
385
|
manifest: JsonObject,
|
|
352
386
|
platform: string,
|
|
353
387
|
event: string,
|
|
354
388
|
role: SessionRole = "orchestrator",
|
|
389
|
+
opts: { refusal_retry?: boolean } = {},
|
|
355
390
|
): ConcernDef[] {
|
|
356
391
|
const platformsRaw = manifest["platforms"];
|
|
357
392
|
const platforms = _isObject(platformsRaw) ? platformsRaw : {};
|
|
@@ -381,6 +416,9 @@ export function _resolve_concerns(
|
|
|
381
416
|
);
|
|
382
417
|
continue;
|
|
383
418
|
}
|
|
419
|
+
if (opts.refusal_retry && spec["skip_on_refusal_retry"] === true) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
384
422
|
out.push({ name: String(name), ...spec });
|
|
385
423
|
}
|
|
386
424
|
return out;
|
|
@@ -1081,13 +1119,17 @@ export function main(argv?: string[]): number {
|
|
|
1081
1119
|
const payload_text = process.stdin.isTTY ? "" : _readStdin();
|
|
1082
1120
|
_maybe_capture_payload(args, payload_text);
|
|
1083
1121
|
const session_role = resolveSessionRole(process.env);
|
|
1084
|
-
const
|
|
1122
|
+
const refusal_retry = _is_refusal_retry(args.event, payload_text);
|
|
1123
|
+
const concerns = _resolve_concerns(manifest, args.platform, args.event, session_role, {
|
|
1124
|
+
refusal_retry,
|
|
1125
|
+
});
|
|
1085
1126
|
|
|
1086
1127
|
if (args.dry_run) {
|
|
1087
1128
|
const plan = {
|
|
1088
1129
|
platform: args.platform,
|
|
1089
1130
|
event: args.event,
|
|
1090
1131
|
role: session_role,
|
|
1132
|
+
refusal_retry,
|
|
1091
1133
|
concerns: concerns.map((c) => c["name"]),
|
|
1092
1134
|
};
|
|
1093
1135
|
process.stdout.write(_py_json_dumps(plan, 2) + "\n");
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* interruption_ledger — road-to-user-out-of-the-loop Phase 0 Step 1.
|
|
3
|
+
*
|
|
4
|
+
* CAPTURE ONLY. Every `stop`, classifies whether the turn that just ended put a
|
|
5
|
+
* synchronous contact to the user, and appends one line to
|
|
6
|
+
* `agents/runtime/state/interruptions.jsonl`. It never emits context, never
|
|
7
|
+
* warns, and always returns 0 — the whole point of Phase 0 is a window that no
|
|
8
|
+
* mechanism reacted to, so a ledger that could change a run would corrupt the
|
|
9
|
+
* baseline it exists to measure.
|
|
10
|
+
*
|
|
11
|
+
* WHY THIS EXISTS. The roadmap's Goal names two axes — synchronous contacts per
|
|
12
|
+
* delivered roadmap, and wall-clock to open PR — and pre-registers both as
|
|
13
|
+
* claims. Neither is derivable today: nothing in the tree records that a turn
|
|
14
|
+
* ended by asking the user something. `check_enforcement_coverage` counts
|
|
15
|
+
* carriers, `gate-metrics.jsonl` counts gate verdicts, and the conformance suite
|
|
16
|
+
* scores reply SHAPE without ever asking whether the reply stopped the run.
|
|
17
|
+
*
|
|
18
|
+
* REUSE, NOT REIMPLEMENTATION. Reply-shape detection is `turn_end_gate_hook`'s,
|
|
19
|
+
* imported rather than copied: `readTranscriptTail` for the last assistant text,
|
|
20
|
+
* `finalParagraph` for the closing paragraph, and its `HANDBACK` list for the
|
|
21
|
+
* yields-without-asking shape. That file is the measured corpus's detector and
|
|
22
|
+
* has an R2 history behind each regex; a second copy here would drift from it
|
|
23
|
+
* silently and the drift would land in the baseline, not in a test.
|
|
24
|
+
*
|
|
25
|
+
* WHAT COUNTS AS A CONTACT, and why it is three classes rather than two. A
|
|
26
|
+
* question mark is the obvious one. It is not the only one: `scope-control`
|
|
27
|
+
* requires a hand-back — "das entscheidest Du", "your call" — which ends the
|
|
28
|
+
* turn and waits for the user just as hard as a question does, with no `?`
|
|
29
|
+
* anywhere. Counting only questions would report the package's own preferred
|
|
30
|
+
* hand-back shape as zero contacts, which is precisely the metric-that-flatters-
|
|
31
|
+
* the-design failure the roadmap's Risk 6 names.
|
|
32
|
+
*
|
|
33
|
+
* ask — the closing paragraph ends in a question mark.
|
|
34
|
+
* handback — no question, but the closing paragraph yields the decision.
|
|
35
|
+
* none — the turn ended without putting anything to the user.
|
|
36
|
+
*
|
|
37
|
+
* `class` refines an `ask` by the shape the user has to answer in:
|
|
38
|
+
* `numbered-options` when the reply carries a numbered block (the
|
|
39
|
+
* `user-interaction` shape — one decision point, cheap to answer), or
|
|
40
|
+
* `open-question` when it does not (free text, the expensive shape). The split
|
|
41
|
+
* exists because the two are not equal contacts and the report separates them.
|
|
42
|
+
*
|
|
43
|
+
* DEDUPE is on the turn ordinal, the same identity `turn_end_gate_hook` derives
|
|
44
|
+
* and for the same reason: a host may fire `stop` more than once for one turn,
|
|
45
|
+
* and a per-turn ledger that double-counts inflates exactly the number the
|
|
46
|
+
* baseline is pre-registered on.
|
|
47
|
+
*
|
|
48
|
+
* The roadmap slug comes from this session's own claim file — the one
|
|
49
|
+
* `agent-config sessions:claim` writes — so a contact is attributable to a
|
|
50
|
+
* delivered roadmap without this hook knowing anything about roadmaps. No claim
|
|
51
|
+
* means `roadmap: null`, which is a real and common state (any session that is
|
|
52
|
+
* not a roadmap run) and never a reason to skip the line.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import * as fs from 'node:fs';
|
|
56
|
+
import * as path from 'node:path';
|
|
57
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
58
|
+
|
|
59
|
+
import { derive_session_tag } from '../chat_history.js';
|
|
60
|
+
import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
|
|
61
|
+
import { readHookStdin } from './hook_stdin.js';
|
|
62
|
+
import { HANDBACK, finalParagraph, readTranscriptTail } from './turn_end_gate_hook.js';
|
|
63
|
+
|
|
64
|
+
const EXIT_OK = 0;
|
|
65
|
+
|
|
66
|
+
/** Same cap the turn-end gate reads its tail under. */
|
|
67
|
+
const TRANSCRIPT_READ_MAX_BYTES = 2 * 1024 * 1024;
|
|
68
|
+
|
|
69
|
+
export const LEDGER_RELPATH = path.join('agents', 'runtime', 'state', 'interruptions.jsonl');
|
|
70
|
+
|
|
71
|
+
export type ContactKind = 'ask' | 'handback' | 'none';
|
|
72
|
+
export type ContactClass = 'numbered-options' | 'open-question' | 'handback' | 'none';
|
|
73
|
+
|
|
74
|
+
export interface InterruptionRecord {
|
|
75
|
+
run_id: string;
|
|
76
|
+
turn: number;
|
|
77
|
+
kind: ContactKind;
|
|
78
|
+
class: ContactClass;
|
|
79
|
+
roadmap: string | null;
|
|
80
|
+
at: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A numbered-options block: two or more numbered lines. One numbered line is an
|
|
85
|
+
* ordinary enumeration inside prose and is not a decision surface — requiring
|
|
86
|
+
* two is what keeps "1. read the file" from reading as an ask.
|
|
87
|
+
*/
|
|
88
|
+
export function hasNumberedOptions(reply: string): boolean {
|
|
89
|
+
let count = 0;
|
|
90
|
+
for (const line of reply.split('\n')) {
|
|
91
|
+
if (/^[ \t]{0,3}\d+[.)]\s+\S/.test(line)) count += 1;
|
|
92
|
+
if (count >= 2) return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Classify the reply. The question test is `turn_end_gate_hook`'s exactly — a
|
|
99
|
+
* question that ENDS the paragraph, never `includes('?')`, because a rhetorical
|
|
100
|
+
* or quoted question mid-reply is not a contact and counting it would inflate
|
|
101
|
+
* the baseline in the direction that makes the roadmap look successful later.
|
|
102
|
+
*/
|
|
103
|
+
export function classifyReply(reply: string): { kind: ContactKind; class: ContactClass } {
|
|
104
|
+
const tail = finalParagraph(reply);
|
|
105
|
+
if (!tail) return { kind: 'none', class: 'none' };
|
|
106
|
+
|
|
107
|
+
if (/\?["'’)\]]*\s*$/.test(tail)) {
|
|
108
|
+
return {
|
|
109
|
+
kind: 'ask',
|
|
110
|
+
class: hasNumberedOptions(reply) ? 'numbered-options' : 'open-question',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (HANDBACK.some((re) => re.test(tail))) return { kind: 'handback', class: 'handback' };
|
|
114
|
+
return { kind: 'none', class: 'none' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The roadmap this session claimed, or null. Never throws. */
|
|
118
|
+
export function readClaimedRoadmap(workspaceRoot: string, sessionId: string): string | null {
|
|
119
|
+
if (!sessionId) return null;
|
|
120
|
+
try {
|
|
121
|
+
const file = path.join(
|
|
122
|
+
workspaceRoot,
|
|
123
|
+
'agents',
|
|
124
|
+
'runtime',
|
|
125
|
+
'state',
|
|
126
|
+
`roadmap-claim-${sessionId}.json`,
|
|
127
|
+
);
|
|
128
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
129
|
+
if (parsed === null || typeof parsed !== 'object') return null;
|
|
130
|
+
const slug = (parsed as Record<string, unknown>)['slug'];
|
|
131
|
+
return typeof slug === 'string' && slug.length > 0 ? slug : null;
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* True when this run has already recorded this turn. Reads only the tail of the
|
|
139
|
+
* ledger: the duplicate a repeated `stop` produces is always the most recent
|
|
140
|
+
* line, so scanning the whole file would cost more on every turn to catch
|
|
141
|
+
* nothing extra.
|
|
142
|
+
*/
|
|
143
|
+
export function alreadyRecorded(ledgerPath: string, runId: string, turn: number): boolean {
|
|
144
|
+
let raw: string;
|
|
145
|
+
try {
|
|
146
|
+
raw = fs.readFileSync(ledgerPath, 'utf8');
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const lines = raw.split('\n').filter((l) => l.trim().length > 0);
|
|
151
|
+
for (const line of lines.slice(-20)) {
|
|
152
|
+
try {
|
|
153
|
+
const rec: unknown = JSON.parse(line);
|
|
154
|
+
if (rec === null || typeof rec !== 'object') continue;
|
|
155
|
+
const r = rec as Record<string, unknown>;
|
|
156
|
+
if (r['run_id'] === runId && r['turn'] === turn) return true;
|
|
157
|
+
} catch {
|
|
158
|
+
// A corrupt line is not a reason to skip the write.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function str(v: JsonValue | undefined): string {
|
|
165
|
+
return typeof v === 'string' ? v : '';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function main(): number {
|
|
169
|
+
let envelope: JsonObject;
|
|
170
|
+
let payload: JsonObject;
|
|
171
|
+
try {
|
|
172
|
+
[envelope, payload] = unwrap(readHookStdin(), 'claude');
|
|
173
|
+
} catch {
|
|
174
|
+
return EXIT_OK;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const workspaceRoot = str(envelope['workspace_root'] as JsonValue | undefined) || process.cwd();
|
|
178
|
+
const transcriptPath = str(
|
|
179
|
+
(payload['transcript_path'] ?? payload['transcriptPath']) as JsonValue | undefined,
|
|
180
|
+
);
|
|
181
|
+
if (!transcriptPath) return EXIT_OK;
|
|
182
|
+
|
|
183
|
+
let lastAssistant = '';
|
|
184
|
+
let turnOrdinal = 0;
|
|
185
|
+
try {
|
|
186
|
+
const tail = readTranscriptTail(transcriptPath, { maxBytes: TRANSCRIPT_READ_MAX_BYTES });
|
|
187
|
+
lastAssistant = tail.lastAssistant;
|
|
188
|
+
turnOrdinal = tail.turnOrdinal;
|
|
189
|
+
} catch {
|
|
190
|
+
return EXIT_OK;
|
|
191
|
+
}
|
|
192
|
+
if (!lastAssistant) return EXIT_OK;
|
|
193
|
+
|
|
194
|
+
const sessionId =
|
|
195
|
+
str(payload['session_id'] as JsonValue | undefined) ||
|
|
196
|
+
str(envelope['session_id'] as JsonValue | undefined);
|
|
197
|
+
// `derive_session_tag`, NOT the turn-end gate's `deriveSessionKey`. Both
|
|
198
|
+
// hash the session id, and only this one produces the tag `chat_history`
|
|
199
|
+
// already writes as `s`. The wall-clock axis is a join between this ledger
|
|
200
|
+
// and those timestamps, so a 32-char key from the other derivation would
|
|
201
|
+
// have made the roadmap's second pre-registered claim underivable — while
|
|
202
|
+
// looking correct in every test that never crossed the two files.
|
|
203
|
+
const runId = sessionId ? derive_session_tag(sessionId) : 'unknown';
|
|
204
|
+
|
|
205
|
+
const ledgerPath = path.join(workspaceRoot, LEDGER_RELPATH);
|
|
206
|
+
if (alreadyRecorded(ledgerPath, runId, turnOrdinal)) return EXIT_OK;
|
|
207
|
+
|
|
208
|
+
const verdict = classifyReply(lastAssistant);
|
|
209
|
+
const record: InterruptionRecord = {
|
|
210
|
+
run_id: runId,
|
|
211
|
+
turn: turnOrdinal,
|
|
212
|
+
kind: verdict.kind,
|
|
213
|
+
class: verdict.class,
|
|
214
|
+
roadmap: readClaimedRoadmap(workspaceRoot, sessionId),
|
|
215
|
+
at: new Date().toISOString(),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
|
|
220
|
+
fs.appendFileSync(ledgerPath, `${JSON.stringify(record)}\n`, 'utf8');
|
|
221
|
+
} catch {
|
|
222
|
+
// A ledger that cannot be written is a lost measurement, never a
|
|
223
|
+
// reason to affect the turn. Phase 0 is advisory by construction.
|
|
224
|
+
}
|
|
225
|
+
return EXIT_OK;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The concern runs in-process via `CONCERN_REGISTRY`, but the manifest's
|
|
230
|
+
* `script:` path is also the SPAWN FALLBACK — and without this guard that path
|
|
231
|
+
* imports the module, calls nothing, and exits 0. A silent no-op on a
|
|
232
|
+
* capture-only concern is invisible by construction: the ledger simply stays
|
|
233
|
+
* empty and the baseline reads as "no contacts". Caught by running the hook
|
|
234
|
+
* against a real envelope rather than by any unit test, which is why the probe
|
|
235
|
+
* is worth the two minutes.
|
|
236
|
+
*/
|
|
237
|
+
function _isCliEntry(): boolean {
|
|
238
|
+
if (process.argv[1] === undefined) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
242
|
+
if (import.meta.url === argvUrl) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
247
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
248
|
+
return here === argv;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (_isCliEntry()) {
|
|
255
|
+
process.exit(main());
|
|
256
|
+
}
|