@event4u/agent-config 15.0.0 → 16.1.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +301 -347
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +77 -29
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +97 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/rules/user-interaction.md +31 -30
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +50 -0
- package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +188 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/dist/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
- package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
- package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-P6ZHP5W2.js} +15 -90
- package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +918 -325
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +5 -3
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +6 -6
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +3 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +14 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +87 -40
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +2601 -1073
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2008 -1481
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1612 -667
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_lock.js +1 -1
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/io/yamlIO.js +50 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +17 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +26 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/src/config/agent-settings.template.yml +139 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +18 -5
- package/src/config/continuity-surface.json +28 -0
- package/src/config/gate-coverage.yml +146 -13
- package/src/config/gate-violation-baselines.json +6 -6
- package/src/config/hook-token-budget.json +2 -0
- package/src/config/host-injection-effect.json +1 -1
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/preamble-payload-budget.json +12 -7
- package/src/config/preamble-payload-exceptions.json +28 -0
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +4 -3
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +90 -0
- package/src/scripts/_dispatch.bash +3 -1
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/base_ref_payload.ts +315 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/dropped_decision.ts +142 -0
- package/src/scripts/_lib/forge_protection.ts +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/installed_lock.ts +1 -1
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +402 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
- package/src/scripts/_lib/release_findings_ingest.ts +324 -0
- package/src/scripts/_lib/release_highlights.ts +199 -10
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
- package/src/scripts/_lib/review_baseline.ts +254 -0
- package/src/scripts/_lib/review_skipped_record.ts +61 -1
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/turn_end_refusals.ts +51 -4
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -0
- package/src/scripts/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/check_beta_review_markers.ts +35 -4
- package/src/scripts/check_enforcement_matrix.ts +624 -0
- package/src/scripts/check_finding_dispositions.ts +167 -16
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_held_object_arrivals.ts +381 -0
- package/src/scripts/check_preamble_payload_budget.ts +474 -124
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- package/src/scripts/check_release_published.ts +1 -1
- package/src/scripts/check_reply_consistency.ts +86 -17
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/closure_scan.ts +333 -0
- package/src/scripts/compile_hook_manifest.ts +43 -3
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +127 -12
- package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
- package/src/scripts/hooks/evidence_independence.ts +96 -0
- package/src/scripts/hooks/memory_recall_hook.ts +540 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
- package/src/scripts/hooks/review_baseline_hook.ts +130 -0
- package/src/scripts/hooks/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
- package/src/scripts/hooks_doctor.ts +89 -1
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +107 -207
- package/src/scripts/interruption_report.ts +8 -2
- package/src/scripts/janitor.ts +15 -1
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_hook_manifest.ts +11 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +61 -0
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/measure_turn_end_gate.ts +302 -5
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +66 -162
- package/src/scripts/release_drill.ts +460 -15
- package/src/scripts/release_env.ts +1 -1
- package/src/scripts/release_publication.ts +52 -1
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_held_object_arrivals.ts +456 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/sync_agent_settings.ts +140 -18
- package/src/scripts/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -0
- package/src/server/io/yamlIO.ts +48 -0
- package/src/server/schemas/settings.ts +31 -7
- package/src/shared/missionExecution.ts +126 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* is deliberately not another reminder. It is a check at the point of
|
|
10
10
|
* delivery that can say no.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* FIVE detectors ride on one guard, because building the unsafe part twice
|
|
13
13
|
* is how a second detector becomes a second outage:
|
|
14
14
|
*
|
|
15
15
|
* A — promissory closing (FC-5, 20 measured occurrences)
|
|
@@ -22,11 +22,18 @@
|
|
|
22
22
|
* only, so tool activity is invisible to A and B.)
|
|
23
23
|
* D — completion claim (a claim of done carrying no fresh evidence; landed
|
|
24
24
|
* under conformance round 7 § Phase 1.)
|
|
25
|
+
* E — pending decision (an earlier reply in the SAME user turn put numbered
|
|
26
|
+
* options to the user and the closing reply carries
|
|
27
|
+
* none — the turn asked and then dropped the ask.
|
|
28
|
+
* Read off `assistantTurnTexts`, which is why that
|
|
29
|
+
* field exists: every other detector reads
|
|
30
|
+
* `lastAssistant` alone and therefore cannot see a
|
|
31
|
+
* question that was asked one reply earlier.)
|
|
25
32
|
*
|
|
26
|
-
* TWO of the
|
|
33
|
+
* TWO of the five are CONDITIONAL, and saying "unconditional" here would be the
|
|
27
34
|
* same stale-header defect this block corrects below. A and D are the
|
|
28
35
|
* completion-adjacent pair an open subagent dispatch excuses, so `main()` runs
|
|
29
|
-
* them only when `dispatchOpen` is false; B and
|
|
36
|
+
* them only when `dispatchOpen` is false; B, C and E run on every turn-end. That
|
|
30
37
|
* narrowing is deliberate (Phase 3 Step 2, narrowed again by R2 round 2) and is
|
|
31
38
|
* the third allow path, alongside the two re-entrancy layers — see the
|
|
32
39
|
* per-detector list in `main()`.
|
|
@@ -35,7 +42,7 @@
|
|
|
35
42
|
* below has carried four since round 7. `DETECTOR_IDS` in
|
|
36
43
|
* `_lib/turn_end_refusals.ts` is read off that union rather than off this
|
|
37
44
|
* comment for exactly that reason, and the count is corrected here rather than
|
|
38
|
-
* left as a stale header the next reader has to disbelieve.
|
|
45
|
+
* left as a stale header the next reader has to disbelieve. E made it five.
|
|
39
46
|
*
|
|
40
47
|
* ## Removal condition
|
|
41
48
|
*
|
|
@@ -157,6 +164,23 @@ import {
|
|
|
157
164
|
// agreement invisible when the producer's layout moved. A builder makes the move
|
|
158
165
|
// a type error.
|
|
159
166
|
import { statePathFor as ciStatePathFor } from '../before_complete_hook.js';
|
|
167
|
+
// The spec-backed options-block reading, imported rather than re-derived:
|
|
168
|
+
// `user-interaction` Iron Law 1's definition of an ask — a block PLUS its
|
|
169
|
+
// recommendation line — lives in exactly one place and detector E reads it from
|
|
170
|
+
// there.
|
|
171
|
+
//
|
|
172
|
+
// WHAT KEEPS IT FROM RUNNING ON IMPORT, stated precisely because the obvious
|
|
173
|
+
// answer is wrong: that module is `_isCliEntry`-guarded, but WITHOUT the
|
|
174
|
+
// `__AGENT_CONFIG_BUNDLE__` early-out this file carries. Inside
|
|
175
|
+
// `dist/hooks/dispatch.js` every module shares one `import.meta.url`, so the
|
|
176
|
+
// guard alone would not settle it; what does is the `.__direct__` argv rewrite
|
|
177
|
+
// in the `build:hooks` banner. Named here so a future change to that banner
|
|
178
|
+
// does not silently start running a lint's `main()` on every hook dispatch.
|
|
179
|
+
// Detector E, extracted to `_lib` where its prose costs no ratchet debt.
|
|
180
|
+
// Re-exported so every existing importer of this module is unchanged.
|
|
181
|
+
import { detectDroppedDecision } from '../_lib/dropped_decision.js';
|
|
182
|
+
|
|
183
|
+
export { detectDroppedDecision };
|
|
160
184
|
import { isSafeTranscriptPath } from './end_review_nudge_hook.js';
|
|
161
185
|
import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
|
|
162
186
|
import { readHookStdin } from './hook_stdin.js';
|
|
@@ -190,7 +214,13 @@ const EXIT_ALLOW = 0;
|
|
|
190
214
|
export const TRANSCRIPT_READ_MAX_BYTES = 8 * 1024 * 1024; // 8 MB
|
|
191
215
|
|
|
192
216
|
/** Detector identity, used in the state marker and the refusal text. */
|
|
193
|
-
export type DetectorId =
|
|
217
|
+
export type DetectorId =
|
|
218
|
+
| 'promissory'
|
|
219
|
+
| 'language'
|
|
220
|
+
| 'verification'
|
|
221
|
+
| 'completion'
|
|
222
|
+
| 'pending-decision'
|
|
223
|
+
| 'untested';
|
|
194
224
|
|
|
195
225
|
export interface Finding {
|
|
196
226
|
detector: DetectorId;
|
|
@@ -723,6 +753,110 @@ export function detectUnverifiedEdit(toolCalls: readonly ToolCall[]): Finding |
|
|
|
723
753
|
};
|
|
724
754
|
}
|
|
725
755
|
|
|
756
|
+
// ---------------------------------------------------------------------------
|
|
757
|
+
// Detector F — a completion claim over production code no test accompanies
|
|
758
|
+
// ---------------------------------------------------------------------------
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* WHY THIS EXISTS, and why detector C did not already cover it.
|
|
762
|
+
*
|
|
763
|
+
* Detector C asks "did ANY verification command run after the last edit". It is
|
|
764
|
+
* satisfied by `eslint`. That is deliberate — see `isVerificationCommand`, which
|
|
765
|
+
* is narrow about what counts as a command and says nothing about what the
|
|
766
|
+
* command COVERS. So a turn can write four hundred lines of feature code, run
|
|
767
|
+
* the linter, claim done, and pass every guard in this file.
|
|
768
|
+
*
|
|
769
|
+
* Measured consequence, reported by the maintainer 2026-09-11 about a consumer
|
|
770
|
+
* project: a feature shipped whose detail view crashed on open, whose flyouts
|
|
771
|
+
* were broken, and for which the maintainer then had to hand-enumerate what
|
|
772
|
+
* should have been exercised — every view, every CRUD path, drag-and-drop,
|
|
773
|
+
* mobile, filters. Their words: *"Solche Fehler können nur auftreten, weil Du
|
|
774
|
+
* nicht TDD gearbeitet ... hast."* Nothing in the suite refused that turn,
|
|
775
|
+
* because nothing in the suite asks whether the change is TESTED — only whether
|
|
776
|
+
* something ran.
|
|
777
|
+
*
|
|
778
|
+
* THE SIGNAL, and why it is this one. "Is the change covered" is undecidable
|
|
779
|
+
* from a transcript: coverage lives in the consumer's tooling, which this hook
|
|
780
|
+
* cannot run. What IS decidable is the cheapest possible proxy, and it happens
|
|
781
|
+
* to be the exact shape of the reported failure — **production source changed,
|
|
782
|
+
* no test file touched, done claimed**. A proxy is worth shipping here precisely
|
|
783
|
+
* because the population it flags is small and the failure it catches is total:
|
|
784
|
+
* a feature with zero new test lines is not under-tested, it is untested.
|
|
785
|
+
*
|
|
786
|
+
* WHY IT IS GATED ON THE CLAIM rather than on the edit. Firing on every edit
|
|
787
|
+
* would refuse the first turn of every red-green-refactor cycle — the discipline
|
|
788
|
+
* it exists to encourage — and a guard that fires on the majority of turns is a
|
|
789
|
+
* guard that gets switched off. The obligation belongs to the CLAIM, which is
|
|
790
|
+
* also where `verify-before-complete` puts it and where detector D already sits.
|
|
791
|
+
*
|
|
792
|
+
* WHAT IT CANNOT DO. It cannot tell a good test from a token one: a turn that
|
|
793
|
+
* touches any test file clears it. That is a deliberate floor rather than an
|
|
794
|
+
* oversight — `testing-anti-patterns` owns assertion quality, and a guard that
|
|
795
|
+
* tried to judge it from a path would be judging something it cannot see. The
|
|
796
|
+
* honest claim is narrow: this refuses the case where the agent wrote NO test at
|
|
797
|
+
* all and said it was finished.
|
|
798
|
+
*/
|
|
799
|
+
|
|
800
|
+
/** Extensions that carry behaviour a test can be owed for. */
|
|
801
|
+
const _SOURCE_RE = /\.(?:ts|tsx|js|jsx|mjs|cjs|vue|svelte|php|py|rb|go|rs|java|kt|swift|cs|scala|ex|exs)$/i;
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Paths that ARE tests, across the conventions this suite's stacks use.
|
|
805
|
+
*
|
|
806
|
+
* Deliberately generous: a false NEGATIVE here (calling a test file a test) only
|
|
807
|
+
* silences the detector, while a false positive would refuse a turn that did
|
|
808
|
+
* write its tests — the direction that gets a guard disabled.
|
|
809
|
+
*/
|
|
810
|
+
const _TEST_PATH_RE =
|
|
811
|
+
/(?:^|\/)(?:tests?|specs?|__tests__|__specs__|e2e|cypress|playwright|features)\//i;
|
|
812
|
+
const _TEST_FILE_RE =
|
|
813
|
+
/(?:\.(?:test|spec)\.[a-z]+$)|(?:_test\.[a-z]+$)|(?:(?:^|\/)test_[^/]+$)|(?:Test\.php$)|(?:Spec\.php$)|(?:\.feature$)/i;
|
|
814
|
+
|
|
815
|
+
function _isTestPath(p: string): boolean {
|
|
816
|
+
return _TEST_PATH_RE.test(p) || _TEST_FILE_RE.test(p);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function _isProductionSource(p: string): boolean {
|
|
820
|
+
return _SOURCE_RE.test(p) && !_isTestPath(p);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Fire when the turn changed production code, touched no test, and said done.
|
|
825
|
+
*
|
|
826
|
+
* All three conditions are load-bearing. Drop the first and it fires on a docs
|
|
827
|
+
* turn; drop the second and it fires on a turn that did its job; drop the third
|
|
828
|
+
* and it fires mid-cycle on the red step of red-green-refactor.
|
|
829
|
+
*/
|
|
830
|
+
export function detectUntestedChange(reply: string, toolCalls: readonly ToolCall[]): Finding | null {
|
|
831
|
+
const edited = toolCalls
|
|
832
|
+
.filter((c) => _EDIT_TOOLS.has(c.name) && c.path !== undefined)
|
|
833
|
+
.map((c) => c.path as string);
|
|
834
|
+
const source = edited.filter(_isProductionSource);
|
|
835
|
+
if (source.length === 0) return null;
|
|
836
|
+
if (edited.some(_isTestPath)) return null;
|
|
837
|
+
|
|
838
|
+
// The claim gate, reusing detector D's pair rather than a second dialect of
|
|
839
|
+
// "done" — two lists of completion phrasings would drift, and the negation
|
|
840
|
+
// check is what keeps an honest "noch nicht fertig" from being refused.
|
|
841
|
+
const prose = visibleProse(reply);
|
|
842
|
+
const m = _COMPLETION_RE.exec(prose);
|
|
843
|
+
if (!m) return null;
|
|
844
|
+
if (_NEGATED_CLAIM_RE.test(_lineAround(prose, m.index))) return null;
|
|
845
|
+
|
|
846
|
+
const shown = source.slice(0, 3).join(', ');
|
|
847
|
+
return {
|
|
848
|
+
detector: 'untested',
|
|
849
|
+
evidence: shown + (source.length > 3 ? ` (+${String(source.length - 3)} more)` : ''),
|
|
850
|
+
reason:
|
|
851
|
+
'a completion claim over production code this turn changed, with NO test file ' +
|
|
852
|
+
'touched anywhere in the turn — running a linter is not evidence the change ' +
|
|
853
|
+
'works. Write the test that would have caught the failure, and enumerate the ' +
|
|
854
|
+
'states the change actually has (every view, every CRUD path, the empty and ' +
|
|
855
|
+
'error states) rather than the happy one. If a test genuinely does not belong ' +
|
|
856
|
+
'here, say which file carries the coverage instead',
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
726
860
|
// ---------------------------------------------------------------------------
|
|
727
861
|
// Re-entrancy — the guard, keyed on the turn, not on the reply
|
|
728
862
|
// ---------------------------------------------------------------------------
|
|
@@ -811,6 +945,7 @@ function markRefusedTurn(
|
|
|
811
945
|
sessionKey: string,
|
|
812
946
|
turnOrdinal: number,
|
|
813
947
|
detectors: readonly DetectorId[],
|
|
948
|
+
promptId: string,
|
|
814
949
|
): void {
|
|
815
950
|
if (is_replay_mode()) return;
|
|
816
951
|
const file = sessionStateFile(workspaceRoot, sessionKey);
|
|
@@ -835,6 +970,7 @@ function markRefusedTurn(
|
|
|
835
970
|
turnOrdinal,
|
|
836
971
|
at: new Date().toISOString(),
|
|
837
972
|
version,
|
|
973
|
+
promptId,
|
|
838
974
|
}) as unknown as Record<string, unknown>,
|
|
839
975
|
);
|
|
840
976
|
} catch {
|
|
@@ -868,6 +1004,20 @@ export interface ToolCall {
|
|
|
868
1004
|
|
|
869
1005
|
export interface TranscriptTail {
|
|
870
1006
|
lastAssistant: string;
|
|
1007
|
+
/**
|
|
1008
|
+
* Every assistant text of the CURRENT turn, in order — reset at each genuine
|
|
1009
|
+
* user prompt exactly like `toolCalls`, and for the same reason: a question
|
|
1010
|
+
* asked three turns ago was answered, a question asked earlier in THIS turn
|
|
1011
|
+
* was not. Detector E reads it; the other four read `lastAssistant`, which is
|
|
1012
|
+
* always the last element when this is non-empty.
|
|
1013
|
+
*
|
|
1014
|
+
* A tool-only assistant entry contributes NOTHING here. `_messageText`
|
|
1015
|
+
* returns null for it and the collector skips it, so the ordinary shape of a
|
|
1016
|
+
* working turn — prose, tool call, prose — produces two elements and not
|
|
1017
|
+
* three. That is the whole false-positive surface of detector E, and it is
|
|
1018
|
+
* closed here rather than in the detector.
|
|
1019
|
+
*/
|
|
1020
|
+
assistantTurnTexts: string[];
|
|
871
1021
|
/**
|
|
872
1022
|
* How many GENUINE user prompts the transcript carries — harness-injected
|
|
873
1023
|
* user-role entries excluded via `isSyntheticPrompt`. This is the turn's
|
|
@@ -899,7 +1049,12 @@ export function readTranscriptTail(
|
|
|
899
1049
|
transcriptPath: string,
|
|
900
1050
|
opts: { homeDir?: string; maxBytes?: number } = {},
|
|
901
1051
|
): TranscriptTail {
|
|
902
|
-
const empty: TranscriptTail = {
|
|
1052
|
+
const empty: TranscriptTail = {
|
|
1053
|
+
lastAssistant: '',
|
|
1054
|
+
turnOrdinal: 0,
|
|
1055
|
+
toolCalls: [],
|
|
1056
|
+
assistantTurnTexts: [],
|
|
1057
|
+
};
|
|
903
1058
|
if (!transcriptPath || !isSafeTranscriptPath(transcriptPath, opts)) return empty;
|
|
904
1059
|
let lines: string[];
|
|
905
1060
|
try {
|
|
@@ -919,6 +1074,7 @@ export function readTranscriptTail(
|
|
|
919
1074
|
let lastAssistant = '';
|
|
920
1075
|
let turnOrdinal = 0;
|
|
921
1076
|
let toolCalls: ToolCall[] = [];
|
|
1077
|
+
let assistantTurnTexts: string[] = [];
|
|
922
1078
|
for (const rawLine of lines) {
|
|
923
1079
|
const line = rawLine.trim();
|
|
924
1080
|
if (!line) continue;
|
|
@@ -944,12 +1100,13 @@ export function readTranscriptTail(
|
|
|
944
1100
|
// only a tool_use block has no text at all, so `continue`-ing on a null
|
|
945
1101
|
// text would drop exactly the entries this detector exists to see.
|
|
946
1102
|
if (role === 'assistant') {
|
|
947
|
-
toolCalls.push(...
|
|
1103
|
+
toolCalls.push(...extractToolCalls(content));
|
|
948
1104
|
}
|
|
949
1105
|
const text = _messageText(content);
|
|
950
1106
|
if (text === null) continue;
|
|
951
1107
|
if (role === 'assistant') {
|
|
952
1108
|
lastAssistant = text;
|
|
1109
|
+
assistantTurnTexts.push(text);
|
|
953
1110
|
} else if (!isSyntheticPrompt(text)) {
|
|
954
1111
|
turnOrdinal += 1;
|
|
955
1112
|
// A genuine user prompt starts a new turn, so the previous turn's
|
|
@@ -957,17 +1114,34 @@ export function readTranscriptTail(
|
|
|
957
1114
|
// run three turns ago would vouch for an edit made now — the
|
|
958
1115
|
// "fresh" in edit-without-FRESH-verification is this line.
|
|
959
1116
|
toolCalls = [];
|
|
1117
|
+
// Same boundary, same reason, for detector E: an options block the
|
|
1118
|
+
// user has since replied to is answered, not dropped. Resetting on
|
|
1119
|
+
// the identical line is what makes "in the SAME user turn" true of
|
|
1120
|
+
// the array rather than merely intended.
|
|
1121
|
+
assistantTurnTexts = [];
|
|
960
1122
|
}
|
|
961
1123
|
}
|
|
962
|
-
return {
|
|
1124
|
+
return {
|
|
1125
|
+
lastAssistant: lastAssistant.trim(),
|
|
1126
|
+
turnOrdinal,
|
|
1127
|
+
toolCalls,
|
|
1128
|
+
assistantTurnTexts,
|
|
1129
|
+
};
|
|
963
1130
|
}
|
|
964
1131
|
|
|
965
1132
|
/**
|
|
966
1133
|
* Extract this entry's tool calls, keeping only name, shell command and target
|
|
967
1134
|
* path. A tool input can hold a whole file body; nothing but those three fields
|
|
968
1135
|
* is carried forward.
|
|
1136
|
+
*
|
|
1137
|
+
* Exported because `measure_turn_end_gate` scores detectors C and E over a real
|
|
1138
|
+
* transcript corpus and therefore has to rebuild the same `ToolCall[]` the gate
|
|
1139
|
+
* sees. A second extractor there would be a second dialect of "what the turn
|
|
1140
|
+
* did", and the measurement would then be of that dialect rather than of the
|
|
1141
|
+
* shipped gate — the exact population-parity defect that script's own header
|
|
1142
|
+
* documents twice.
|
|
969
1143
|
*/
|
|
970
|
-
function
|
|
1144
|
+
export function extractToolCalls(content: unknown): ToolCall[] {
|
|
971
1145
|
if (!Array.isArray(content)) return [];
|
|
972
1146
|
const out: ToolCall[] = [];
|
|
973
1147
|
for (const blk of content) {
|
|
@@ -1074,11 +1248,30 @@ export function main(): number {
|
|
|
1074
1248
|
// in production while its comment claimed it was enforced — the same
|
|
1075
1249
|
// fixed-the-definition-not-the-caller shape as two other findings in that
|
|
1076
1250
|
// round, which is why this line exists rather than a default parameter.
|
|
1077
|
-
const { lastAssistant, turnOrdinal, toolCalls } = readTranscriptTail(
|
|
1078
|
-
|
|
1079
|
-
|
|
1251
|
+
const { lastAssistant, turnOrdinal, toolCalls, assistantTurnTexts } = readTranscriptTail(
|
|
1252
|
+
transcriptPath,
|
|
1253
|
+
{ maxBytes: TRANSCRIPT_READ_MAX_BYTES },
|
|
1254
|
+
);
|
|
1080
1255
|
if (!lastAssistant) return EXIT_ALLOW;
|
|
1081
1256
|
|
|
1257
|
+
// The closing reply comes from the field the host hands us when it supplies
|
|
1258
|
+
// one, not only from a file the host writes asynchronously. The transcript
|
|
1259
|
+
// may lag the in-memory conversation, and a tail that is one assistant entry
|
|
1260
|
+
// short is silence for `detectDroppedDecision`, whose early return needs two
|
|
1261
|
+
// texts — i.e. the detector goes quiet in exactly the shape it exists for.
|
|
1262
|
+
// `suggestion_capture_hook.ts` reads the same field for the same reason.
|
|
1263
|
+
// Appended, never substituted: `lastAssistant` and the four other detectors
|
|
1264
|
+
// are untouched, and a host that supplies no such field is unchanged.
|
|
1265
|
+
const payloadClosing = str(
|
|
1266
|
+
(payload['last_assistant_message'] ?? payload['lastAssistantMessage']) as
|
|
1267
|
+
| JsonValue
|
|
1268
|
+
| undefined,
|
|
1269
|
+
);
|
|
1270
|
+
const closingFromPayload = payloadClosing !== '' && !assistantTurnTexts.includes(payloadClosing);
|
|
1271
|
+
const turnTexts = closingFromPayload
|
|
1272
|
+
? [...assistantTurnTexts, payloadClosing]
|
|
1273
|
+
: assistantTurnTexts;
|
|
1274
|
+
|
|
1082
1275
|
// Layer 2 — keyed on the turn's ORDINAL, never on the prompt's text.
|
|
1083
1276
|
// A host that sends no `session_id` shares one bucket AND one small-integer
|
|
1084
1277
|
// ordinal namespace, so an unrelated session whose ordinal matches a stored
|
|
@@ -1092,6 +1285,13 @@ export function main(): number {
|
|
|
1092
1285
|
// producer keys that on the raw `session_id`. Passing `sessionKey` here would
|
|
1093
1286
|
// read a path nothing writes — the same shape as the STATE_FILE break this
|
|
1094
1287
|
// parameter exists to close.
|
|
1288
|
+
// The host's own id for the prompt being processed. Read here and recorded
|
|
1289
|
+
// beside the ordinal in the refusal marker — never used as the guard key.
|
|
1290
|
+
// The ordinal's drift is what both re-entrancy layers exist for, so a later
|
|
1291
|
+
// reading needs the pair to tell a drifted ordinal from a second prompt.
|
|
1292
|
+
const promptId = str(
|
|
1293
|
+
(payload['prompt_id'] ?? payload['promptId']) as JsonValue | undefined,
|
|
1294
|
+
);
|
|
1095
1295
|
const rawSessionId = str(envelope['session_id'] as JsonValue | undefined) || '';
|
|
1096
1296
|
const sessionKey = deriveSessionKey(rawSessionId || 'unknown-session');
|
|
1097
1297
|
if (alreadyRefusedTurn(workspaceRoot, sessionKey, turnOrdinal)) return EXIT_ALLOW;
|
|
@@ -1129,6 +1329,22 @@ export function main(): number {
|
|
|
1129
1329
|
dispatchOpen
|
|
1130
1330
|
? null
|
|
1131
1331
|
: detectCompletionClaim(lastAssistant, readCiSettled(workspaceRoot, rawSessionId)),
|
|
1332
|
+
// Detector E is UNCONDITIONAL, and that is the opposite choice from A
|
|
1333
|
+
// and D one line above rather than an oversight. An open dispatch
|
|
1334
|
+
// excuses a promissory closing because the work is genuinely still
|
|
1335
|
+
// running; it excuses nothing about a question the user was already
|
|
1336
|
+
// asked. Worse: a dispatch — or the `end-review-nudge` stop concern
|
|
1337
|
+
// that produced the measured failure — IS the continuation that drops
|
|
1338
|
+
// the decision, so narrowing E the same way would silence it in exactly
|
|
1339
|
+
// the case it exists for.
|
|
1340
|
+
detectDroppedDecision(turnTexts, closingFromPayload),
|
|
1341
|
+
// Detector F takes the OTHER side of that same question, and the two
|
|
1342
|
+
// sitting adjacent is the clearest place to say why. E fires on a
|
|
1343
|
+
// question already asked, which an open dispatch cannot excuse. F fires
|
|
1344
|
+
// on a completion CLAIM, and a turn waiting on a subagent has not
|
|
1345
|
+
// finished — so its closing is not the claim F is about. Same slot,
|
|
1346
|
+
// opposite trigger, opposite treatment of `dispatchOpen`.
|
|
1347
|
+
dispatchOpen ? null : detectUntestedChange(lastAssistant, toolCalls),
|
|
1132
1348
|
]) {
|
|
1133
1349
|
if (f) findings.push(f);
|
|
1134
1350
|
}
|
|
@@ -1139,6 +1355,7 @@ export function main(): number {
|
|
|
1139
1355
|
sessionKey,
|
|
1140
1356
|
turnOrdinal,
|
|
1141
1357
|
findings.map((f) => f.detector),
|
|
1358
|
+
promptId,
|
|
1142
1359
|
);
|
|
1143
1360
|
|
|
1144
1361
|
const lines = findings.map(
|
|
@@ -1147,8 +1364,9 @@ export function main(): number {
|
|
|
1147
1364
|
process.stderr.write(
|
|
1148
1365
|
`turn-end-gate: REFUSED — this turn is not finished.\n${lines.join('\n')}\n` +
|
|
1149
1366
|
' Do the promised work now, correct the language, run the ' +
|
|
1150
|
-
'verification the edit needs,
|
|
1151
|
-
'it — then end
|
|
1367
|
+
'verification the edit needs, read the CI verdict before claiming ' +
|
|
1368
|
+
'it, or re-present the options block this turn dropped — then end ' +
|
|
1369
|
+
'the turn.\n' +
|
|
1152
1370
|
' This turn will not be refused a second time.\n',
|
|
1153
1371
|
);
|
|
1154
1372
|
return EXIT_BLOCK;
|
|
@@ -40,10 +40,12 @@ import * as hooks_status from "./hooks_status.js";
|
|
|
40
40
|
import {
|
|
41
41
|
DETECTOR_IDS,
|
|
42
42
|
collectRefusalStats,
|
|
43
|
+
emptyCounts,
|
|
43
44
|
readInstallBoundary,
|
|
44
45
|
type InstallBoundary,
|
|
45
46
|
type RefusalStats,
|
|
46
47
|
} from "./_lib/turn_end_refusals.js";
|
|
48
|
+
import { ledgerWritable, readDelivered } from "./_lib/obligations.js";
|
|
47
49
|
|
|
48
50
|
// src/scripts/hooks_doctor.ts → parents[2] (../../..) is the repo root,
|
|
49
51
|
// mirroring the Python `Path(__file__).resolve().parents[2]`.
|
|
@@ -190,10 +192,35 @@ export interface DoctorPayload {
|
|
|
190
192
|
* concern's health is already read, so this is where the number belongs.
|
|
191
193
|
*/
|
|
192
194
|
turn_end_refusals: RefusalStats;
|
|
195
|
+
/**
|
|
196
|
+
* The obligation ledger's two readiness figures.
|
|
197
|
+
*
|
|
198
|
+
* Reported here and nowhere else, because the doctor is where a concern's
|
|
199
|
+
* health is already read and the ledger's failure mode is silence: it never
|
|
200
|
+
* throws, never refuses and never fails a turn, so an unwritable ledger
|
|
201
|
+
* looks exactly like a session that triggered no rules. These two lines are
|
|
202
|
+
* the difference between those, and they are the ONLY consumer the ledger
|
|
203
|
+
* has — reporting a count is not acting on the rows.
|
|
204
|
+
*/
|
|
205
|
+
obligation_ledger: ObligationLedgerStats;
|
|
193
206
|
/** The machine's install stamp, for reading the version split below it. */
|
|
194
207
|
install_boundary: InstallBoundary;
|
|
195
208
|
}
|
|
196
209
|
|
|
210
|
+
/** What the doctor can say about the ledger without interpreting a single row. */
|
|
211
|
+
export interface ObligationLedgerStats {
|
|
212
|
+
/** Probed by writing and removing a sentinel, not by reading mode bits. */
|
|
213
|
+
writable: boolean;
|
|
214
|
+
/**
|
|
215
|
+
* Rows for THIS session, or `null` when no session id was resolvable.
|
|
216
|
+
*
|
|
217
|
+
* `null` and `0` are different answers and are rendered differently: `0`
|
|
218
|
+
* means this session was delivered no rule, `null` means the doctor could
|
|
219
|
+
* not tell which session it is in and so has nothing to count.
|
|
220
|
+
*/
|
|
221
|
+
rows_this_session: number | null;
|
|
222
|
+
}
|
|
223
|
+
|
|
197
224
|
/** Build the doctor payload — JSON-serialisable. */
|
|
198
225
|
export function collect(
|
|
199
226
|
project_root: string,
|
|
@@ -268,7 +295,9 @@ export function collect(
|
|
|
268
295
|
turn_end_refusals = {
|
|
269
296
|
sessionsWithRefusals: 0,
|
|
270
297
|
total: 0,
|
|
271
|
-
|
|
298
|
+
// Derived, never a literal: a fifth detector made the old four-key object
|
|
299
|
+
// a type error here and a silent `NaN` in the aggregator it mirrors.
|
|
300
|
+
byDetector: emptyCounts(),
|
|
272
301
|
byPeriod: [],
|
|
273
302
|
legacyRecords: 0,
|
|
274
303
|
unversionedRecords: 0,
|
|
@@ -285,10 +314,67 @@ export function collect(
|
|
|
285
314
|
trampolines,
|
|
286
315
|
dispatch_issues: issues,
|
|
287
316
|
turn_end_refusals,
|
|
317
|
+
obligation_ledger: collectLedgerStats(project_root),
|
|
288
318
|
install_boundary: readInstallBoundary(),
|
|
289
319
|
};
|
|
290
320
|
}
|
|
291
321
|
|
|
322
|
+
/**
|
|
323
|
+
* The ledger's two figures, never throwing.
|
|
324
|
+
*
|
|
325
|
+
* The session id comes from the host's own environment variable, which is the
|
|
326
|
+
* same source the concerns key their state on. Absent means the doctor is
|
|
327
|
+
* being run outside a session — by a human at a shell, most often — and the
|
|
328
|
+
* honest answer is `null` rather than 0, because there is no session whose
|
|
329
|
+
* rows could be counted.
|
|
330
|
+
*/
|
|
331
|
+
export function collectLedgerStats(project_root: string): ObligationLedgerStats {
|
|
332
|
+
let writable = false;
|
|
333
|
+
try {
|
|
334
|
+
writable = ledgerWritable(project_root);
|
|
335
|
+
} catch {
|
|
336
|
+
writable = false;
|
|
337
|
+
}
|
|
338
|
+
const session = (process.env["CLAUDE_CODE_SESSION_ID"] ?? "").trim();
|
|
339
|
+
if (session === "") return { writable, rows_this_session: null };
|
|
340
|
+
try {
|
|
341
|
+
return { writable, rows_this_session: readDelivered(project_root, session).length };
|
|
342
|
+
} catch {
|
|
343
|
+
return { writable, rows_this_session: null };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The ledger section.
|
|
349
|
+
*
|
|
350
|
+
* Deliberately states what a row is NOT. The council lock this ledger sits
|
|
351
|
+
* under says installation proves availability and not exposure, so a reader
|
|
352
|
+
* who sees a healthy row count and concludes the rules were followed has made
|
|
353
|
+
* the one claim these records may never support. The line is cheap; the
|
|
354
|
+
* misreading it prevents is the whole reason the ledger needed a non-goal.
|
|
355
|
+
*/
|
|
356
|
+
export function _render_ledger(payload: DoctorPayload): string[] {
|
|
357
|
+
const s = payload.obligation_ledger;
|
|
358
|
+
const lines: string[] = [];
|
|
359
|
+
lines.push("Obligation ledger");
|
|
360
|
+
lines.push("-".repeat(60));
|
|
361
|
+
lines.push(
|
|
362
|
+
`${s.writable ? "✅ " : "❌ "}writable ${
|
|
363
|
+
s.writable ? "agents/runtime/state/obligations/" : "cannot write agents/runtime/state/obligations/"
|
|
364
|
+
}`,
|
|
365
|
+
);
|
|
366
|
+
if (s.rows_this_session === null) {
|
|
367
|
+
lines.push("· rows this session: no session id resolved — nothing to count");
|
|
368
|
+
} else {
|
|
369
|
+
lines.push(`· rows this session: ${s.rows_this_session}`);
|
|
370
|
+
}
|
|
371
|
+
lines.push(
|
|
372
|
+
" Rows record that an emitter ran. They are not evidence that a rule",
|
|
373
|
+
);
|
|
374
|
+
lines.push(" reached the model, and may not be read as compliance.");
|
|
375
|
+
return lines;
|
|
376
|
+
}
|
|
377
|
+
|
|
292
378
|
function _strVal(v: JsonValue | undefined): string {
|
|
293
379
|
return v === undefined || v === null ? "undefined" : String(v);
|
|
294
380
|
}
|
|
@@ -412,6 +498,8 @@ export function _render_table(payload: DoctorPayload): string {
|
|
|
412
498
|
}
|
|
413
499
|
lines.push("");
|
|
414
500
|
lines.push(..._render_refusals(payload));
|
|
501
|
+
lines.push("");
|
|
502
|
+
lines.push(..._render_ledger(payload));
|
|
415
503
|
// Dispatch-issues detail — last 20 grouped by concern.
|
|
416
504
|
if (payload.dispatch_issues.length > 0) {
|
|
417
505
|
lines.push("");
|
|
@@ -73,11 +73,22 @@ export const PLATFORM_BRIDGES: Record<string, [string, string]> = {
|
|
|
73
73
|
copilot: ["", "rule-only fallback — this package binds no hook here"],
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
+
/** The shape of a host's user-facing question primitive. */
|
|
77
|
+
export type AskShape = "native" | "text";
|
|
78
|
+
|
|
76
79
|
export interface PlatformRow {
|
|
77
80
|
platform: string;
|
|
78
81
|
status: string;
|
|
79
82
|
bridge_path: string | null;
|
|
80
83
|
fallback_only: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* DECLARED in the manifest, never probed. A host with no row reads `text`:
|
|
86
|
+
* the conservative answer, and the one that is true of every host that has
|
|
87
|
+
* not said otherwise. Absent must not read as "unknown" here — an unknown
|
|
88
|
+
* that the contract then treats as native is exactly the silent degrade the
|
|
89
|
+
* row exists to prevent.
|
|
90
|
+
*/
|
|
91
|
+
ask: AskShape;
|
|
81
92
|
bindings: Record<string, string[]>;
|
|
82
93
|
hint: string | null;
|
|
83
94
|
}
|
|
@@ -122,6 +133,7 @@ export function collect(project_root: string, manifest: JsonObject): StatusMatri
|
|
|
122
133
|
const blockRaw = platforms[platform];
|
|
123
134
|
const block = _isObject(blockRaw) ? blockRaw : {};
|
|
124
135
|
const fallback_only = Boolean(block["fallback_only"]);
|
|
136
|
+
const ask: AskShape = block["ask"] === "native" ? "native" : "text";
|
|
125
137
|
const bindings: Record<string, string[]> = {};
|
|
126
138
|
if (!fallback_only) {
|
|
127
139
|
for (const [ev, c] of Object.entries(block)) {
|
|
@@ -138,6 +150,7 @@ export function collect(project_root: string, manifest: JsonObject): StatusMatri
|
|
|
138
150
|
status,
|
|
139
151
|
bridge_path: rel || null,
|
|
140
152
|
fallback_only,
|
|
153
|
+
ask,
|
|
141
154
|
bindings,
|
|
142
155
|
hint: ["missing", "empty", "degraded", "n/a"].includes(status)
|
|
143
156
|
? hint
|
|
@@ -166,6 +179,12 @@ export function _render_table(matrix: StatusMatrix): string {
|
|
|
166
179
|
head += ` (${row.bridge_path})`;
|
|
167
180
|
}
|
|
168
181
|
lines.push(head);
|
|
182
|
+
lines.push(
|
|
183
|
+
` ask → ${row.ask}` +
|
|
184
|
+
(row.ask === "native"
|
|
185
|
+
? " (structured-ask tool; the recommendation is its default option)"
|
|
186
|
+
: " (numbered text block — the fallback, and it says so)"),
|
|
187
|
+
);
|
|
169
188
|
if (row.fallback_only) {
|
|
170
189
|
lines.push(
|
|
171
190
|
" degraded: rule-only fallback " +
|