@event4u/agent-config 13.0.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 +160 -333
- package/README.md +1 -1
- 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-spawn-contract.md +29 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
- package/dist/agent-src/rules/design-review-after-ui-write.md +25 -8
- package/dist/agent-src/rules/ui-audit-gate.md +25 -8
- 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 +53 -2
- package/dist/agent-src/skills/ai-council/SKILL.md +61 -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/roadmap-management/SKILL.md +2 -2
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
- 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-N7FEST6D.js → chunk-7PZ4FSZB.js} +351 -174
- package/dist/cli-delegate/{chunk-3B72LSZL.js → chunk-F3YUF7SA.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +224 -104
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +40 -18
- 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 +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +2018 -1320
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1652 -892
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +1 -1
- package/dist/scripts/condense.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/package.json +1 -1
- 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 +53 -2
- package/src/config/ci-local-parity.yml +13 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +111 -5
- package/src/config/gate-violation-baselines.json +2 -2
- 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/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 +20 -0
- package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
- 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 +80 -143
- 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/capture_skill_catalogue.ts +264 -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/condense.ts +1 -1
- package/src/scripts/council_cli.ts +141 -191
- package/src/scripts/dispatch_r2_reviewer.ts +8 -1
- package/src/scripts/hook_manifest.yaml +72 -5
- package/src/scripts/hooks/concern_registry.ts +4 -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/turn_end_gate_hook.ts +119 -26
- package/src/scripts/hooks_doctor.ts +112 -0
- package/src/scripts/interruption_report.ts +443 -0
- package/src/scripts/lint_evidence_artifacts.ts +359 -0
- package/src/scripts/lint_roadmap_blockers.ts +65 -0
- package/src/scripts/lint_roadmap_complexity.ts +7 -1
- package/src/scripts/release.ts +38 -53
- package/src/scripts/rule_activation_census.ts +292 -0
- package/src/scripts/schemas/evaluator-output.schema.json +60 -0
- package/src/scripts/schemas/review-findings.schema.json +31 -0
- 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/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
package/src/scripts/condense.ts
CHANGED
|
@@ -1297,7 +1297,7 @@ function _CLAUDE_COMMANDS_DIR(): string {
|
|
|
1297
1297
|
return path.join(MODULE_STATE.PROJECT_ROOT, '.claude', 'commands');
|
|
1298
1298
|
}
|
|
1299
1299
|
|
|
1300
|
-
function _parse_frontmatter(content: string): [Record<string, unknown>, string] {
|
|
1300
|
+
export function _parse_frontmatter(content: string): [Record<string, unknown>, string] {
|
|
1301
1301
|
if (!content.startsWith('---')) {
|
|
1302
1302
|
return [{}, content];
|
|
1303
1303
|
}
|
|
@@ -48,7 +48,9 @@ import {
|
|
|
48
48
|
PerplexityCliClient,
|
|
49
49
|
XAIClient,
|
|
50
50
|
XAICliClient,
|
|
51
|
+
CLI_CONSUMER_COUNCIL,
|
|
51
52
|
load_anthropic_key,
|
|
53
|
+
load_cli_call_attribution,
|
|
52
54
|
load_cli_call_counts,
|
|
53
55
|
load_openai_key,
|
|
54
56
|
quota_summary_line,
|
|
@@ -64,27 +66,38 @@ import { format_install_hints } from './ai_council/cli_hints.js';
|
|
|
64
66
|
import {
|
|
65
67
|
type AdvisorConfig,
|
|
66
68
|
type CouncilConfig,
|
|
67
|
-
type QuorumSetting,
|
|
68
69
|
COUNCIL_CONFIG_ENV,
|
|
69
70
|
COUNCIL_CONFIG_USER_GLOBAL_REL,
|
|
70
71
|
CouncilConfigError,
|
|
71
72
|
load_council_config,
|
|
72
73
|
resolve_api_key,
|
|
74
|
+
resolve_cli_call_caps,
|
|
73
75
|
resolve_config_path,
|
|
74
76
|
} from './ai_council/config.js';
|
|
75
77
|
import { AuthCache, select_solo_member } from './ai_council/solo_dispatch.js';
|
|
76
78
|
import { InvalidModeError, resolve_global_mode } from './ai_council/modes.js';
|
|
77
79
|
import { resolveMemberTransport } from './ai_council/transport_resolver.js';
|
|
78
|
-
import {
|
|
79
|
-
import { evaluateQuorum,
|
|
80
|
+
import { classifyCliFailure, type AbsentReason } from './ai_council/transport_resolver.js';
|
|
81
|
+
import { evaluateQuorum, type QuorumResult } from './ai_council/quorum.js';
|
|
80
82
|
import {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
} from './ai_council/
|
|
83
|
+
_emitQuorumEvent,
|
|
84
|
+
_format_quorum_line,
|
|
85
|
+
_postRunQuorum,
|
|
86
|
+
_quorum_min_present_from,
|
|
87
|
+
_quorum_setting_from,
|
|
88
|
+
} from './ai_council/quorum_wiring.js';
|
|
89
|
+
import { formatQualificationLine, type MemberQualification } from './ai_council/qualification.js';
|
|
90
|
+
import {
|
|
91
|
+
absenceReasonFor,
|
|
92
|
+
attendanceGate,
|
|
93
|
+
countableSeats,
|
|
94
|
+
qualificationJson,
|
|
95
|
+
qualificationStatusLines,
|
|
96
|
+
qualifySeat,
|
|
97
|
+
recordRoundObservations,
|
|
98
|
+
} from './ai_council/qualification_wiring.js';
|
|
99
|
+
import { PROBE_STORE_RELPATH, readProbeStore, type ProbeStore } from './ai_council/probe_store.js';
|
|
100
|
+
import { appendEvent, type QuorumCommand, type QuorumDispatch } from './ai_council/events_log.js';
|
|
88
101
|
import {
|
|
89
102
|
type ClassificationResult,
|
|
90
103
|
type SizeFitVerdict,
|
|
@@ -609,6 +622,18 @@ interface BuildMembersOptions {
|
|
|
609
622
|
* state that differs per machine and per CI run.
|
|
610
623
|
*/
|
|
611
624
|
environment_report?: EnvironmentReport | null;
|
|
625
|
+
/**
|
|
626
|
+
* Recorded provider observations. Supplied ⇒ presence is gated on the
|
|
627
|
+
* qualification ladder; omitted ⇒ not evaluated at all, and presence keeps
|
|
628
|
+
* its constructibility-only meaning. Why omission is the safe default:
|
|
629
|
+
* `qualification_wiring.ts` § The injection contract.
|
|
630
|
+
*/
|
|
631
|
+
probe_store?: ProbeStore | null;
|
|
632
|
+
/**
|
|
633
|
+
* Out-param: the verdict per enabled member, in roster order. Empty from a
|
|
634
|
+
* caller that passed no store means "not evaluated" — NOT "all qualified".
|
|
635
|
+
*/
|
|
636
|
+
qualification_out?: MemberQualification[] | null;
|
|
612
637
|
}
|
|
613
638
|
|
|
614
639
|
/**
|
|
@@ -632,172 +657,6 @@ function _member_api_key_present(cfg: Dict): boolean | undefined {
|
|
|
632
657
|
}
|
|
633
658
|
}
|
|
634
659
|
|
|
635
|
-
/**
|
|
636
|
-
* `ai['quorum']` as forwarded by `_synthesize_ai_council_block` (already
|
|
637
|
-
* validated by `config.ts::_build_quorum` on that path) — or, for a caller
|
|
638
|
-
* that hands `build_members` a hand-built dict bypassing the loader
|
|
639
|
-
* entirely, a defensive re-check that fails soft to `'majority'` rather
|
|
640
|
-
* than throwing. `build_members` is not the config-validation layer;
|
|
641
|
-
* `evaluateQuorum` degrades an unrecognisable setting toward
|
|
642
|
-
* `inconclusive`, never toward a false `concluded`, so failing soft here
|
|
643
|
-
* costs nothing on the safety side.
|
|
644
|
-
*/
|
|
645
|
-
function _quorum_setting_from(ai: Dict): QuorumSetting {
|
|
646
|
-
const raw = ai['quorum'];
|
|
647
|
-
if (raw === 'majority') {
|
|
648
|
-
return 'majority';
|
|
649
|
-
}
|
|
650
|
-
if (typeof raw === 'number' && Number.isInteger(raw) && raw >= 1) {
|
|
651
|
-
return raw;
|
|
652
|
-
}
|
|
653
|
-
return 'majority';
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* `ai['quorum_min_present']` with the same fail-soft posture as
|
|
658
|
-
* `_quorum_setting_from`: the loader has already validated it on the normal
|
|
659
|
-
* path, and a hand-built dict that bypasses the loader falls back to the
|
|
660
|
-
* ADR-224 default rather than throwing.
|
|
661
|
-
*
|
|
662
|
-
* Failing soft costs nothing on the safety side here, and less than it does
|
|
663
|
-
* for `quorum`: this value feeds a counterfactual boolean that no gate reads,
|
|
664
|
-
* so the worst outcome of a wrong fallback is one mis-recorded telemetry line,
|
|
665
|
-
* never a pass that concluded when it should not have.
|
|
666
|
-
*/
|
|
667
|
-
function _quorum_min_present_from(ai: Dict): number {
|
|
668
|
-
const raw = ai['quorum_min_present'];
|
|
669
|
-
if (typeof raw === 'number' && Number.isInteger(raw) && raw >= 1) {
|
|
670
|
-
return raw;
|
|
671
|
-
}
|
|
672
|
-
return SOLO_FLOOR_MIN_PRESENT;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Emit one `quorum_result` line for a resolved quorum.
|
|
677
|
-
*
|
|
678
|
-
* Both `evaluateQuorum` call sites route through here, tagged by `phase`, so
|
|
679
|
-
* a solo-concluded pass is distinguishable from a full-attendance one in the
|
|
680
|
-
* event log — the gap `road-to-always-on-orchestration`'s Risk 6 asserted was
|
|
681
|
-
* already closed and was not.
|
|
682
|
-
*
|
|
683
|
-
* The emit sits at the call sites rather than inside `_postRunQuorum` on
|
|
684
|
-
* purpose: that function is exported and unit-tested as a pure derivation,
|
|
685
|
-
* and a write buried in it would make every test of it a log writer.
|
|
686
|
-
*
|
|
687
|
-
* `absent` entries arrive as the CLI's own `{member, reason, detail}` dicts;
|
|
688
|
-
* `detail` is dropped by `appendQuorumEvent`'s input type, which cannot carry
|
|
689
|
-
* free-form text at all.
|
|
690
|
-
*/
|
|
691
|
-
function _emitQuorumEvent(
|
|
692
|
-
phase: QuorumEventPhase,
|
|
693
|
-
quorum: QuorumResult,
|
|
694
|
-
absent: readonly Dict[],
|
|
695
|
-
ctx: {
|
|
696
|
-
command: QuorumCommand;
|
|
697
|
-
dispatch?: QuorumDispatch;
|
|
698
|
-
/** Enabled members before `--single` filtering; defaults to the roster that ran. */
|
|
699
|
-
configuredTotal?: number;
|
|
700
|
-
lens?: string;
|
|
701
|
-
invocation?: string;
|
|
702
|
-
/**
|
|
703
|
-
* The ADR-224 shadow floor for this pass. `gateClass` is deliberately
|
|
704
|
-
* NOT in this context object: no CLI path declares itself gate-class,
|
|
705
|
-
* so a parameter here would be one no caller ever sets, and its
|
|
706
|
-
* absence is what the `false` on every line honestly records.
|
|
707
|
-
*/
|
|
708
|
-
minPresent?: number;
|
|
709
|
-
},
|
|
710
|
-
): void {
|
|
711
|
-
appendQuorumEvent({
|
|
712
|
-
lens: ctx.lens ?? '',
|
|
713
|
-
invocation: ctx.invocation ?? '',
|
|
714
|
-
phase,
|
|
715
|
-
command: ctx.command,
|
|
716
|
-
dispatch: ctx.dispatch ?? 'full',
|
|
717
|
-
configuredTotal: ctx.configuredTotal ?? quorum.total,
|
|
718
|
-
result: quorum,
|
|
719
|
-
...(ctx.minPresent !== undefined ? { minPresent: ctx.minPresent } : {}),
|
|
720
|
-
absent: absent.map(
|
|
721
|
-
(a): QuorumAbsence => ({
|
|
722
|
-
member: String(a['member'] ?? ''),
|
|
723
|
-
reason: (a['reason'] as QuorumAbsence['reason']) ?? 'unavailable',
|
|
724
|
-
}),
|
|
725
|
-
),
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
/**
|
|
730
|
-
* Re-derive quorum AFTER the provider calls, over what was actually USABLE —
|
|
731
|
-
* never what merely CONSTRUCTED (M3, independent-review finding). `members`
|
|
732
|
-
* and `responses` must be index-aligned (`consult()`'s own contract: one
|
|
733
|
-
* `CouncilResponse` per member, from the final round); `members.length` is
|
|
734
|
-
* the `n` — deliberately the roster that actually ran (post `--single` /
|
|
735
|
-
* `--siblings` filtering), never the full config roster a filtered run
|
|
736
|
-
* never attempted. A response carrying `.error` (or a missing response
|
|
737
|
-
* entry altogether) counts as absent, classified through the same
|
|
738
|
-
* `AbsentReason` vocabulary a static (pre-call) resolution failure uses, so
|
|
739
|
-
* one artefact buckets every absence — construction-time and mid-flight —
|
|
740
|
-
* under one taxonomy.
|
|
741
|
-
*/
|
|
742
|
-
function _postRunQuorum(
|
|
743
|
-
members: ExternalAIClient[],
|
|
744
|
-
responses: CouncilResponse[],
|
|
745
|
-
ai_cfg: Dict,
|
|
746
|
-
): { quorum: QuorumResult; absent: Dict[] } {
|
|
747
|
-
const absent: Dict[] = [];
|
|
748
|
-
let present = 0;
|
|
749
|
-
for (let i = 0; i < members.length; i++) {
|
|
750
|
-
const m = members[i] as ExternalAIClient;
|
|
751
|
-
const r = responses[i];
|
|
752
|
-
// Round 7 § 5.2 — attendance is a NON-EMPTY answer, not the absence of an
|
|
753
|
-
// error. `!r.error` alone counted a member that returned zero bytes, and
|
|
754
|
-
// that is not hypothetical: a 290 s curl timeout returned an empty body
|
|
755
|
-
// with no error set in two sessions (`9fc9ba3e`, `4ac2f7ac`) and the
|
|
756
|
-
// banner printed `2/2 present` — a single-voice verdict presented as
|
|
757
|
-
// convergence, on a paid run. An empty answer contributes nothing to a
|
|
758
|
-
// quorum by definition, whatever the transport thought of it.
|
|
759
|
-
if (r !== undefined && !r.error && r.text.trim() !== '') {
|
|
760
|
-
present += 1;
|
|
761
|
-
continue;
|
|
762
|
-
}
|
|
763
|
-
const raw = r?.error ?? (r !== undefined && r.text.trim() === '' ? 'empty response body' : 'no response');
|
|
764
|
-
const failure = classifyCliFailure(raw);
|
|
765
|
-
absent.push({
|
|
766
|
-
member: m.name,
|
|
767
|
-
reason: absentReasonFromCliFailure(failure) ?? 'unavailable',
|
|
768
|
-
detail: raw,
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
return { quorum: evaluateQuorum(members.length, present, _quorum_setting_from(ai_cfg)), absent };
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
/**
|
|
775
|
-
* One-line k-of-n banner, mirrored from `orchestrator.ts::_render_quorum_line`
|
|
776
|
-
* but for the CLI's own stdout stream rather than the rendered report —
|
|
777
|
-
* "attendance is telemetry, never a silent drop" applies to the estimate
|
|
778
|
-
* preview too, not only to a completed pass.
|
|
779
|
-
*/
|
|
780
|
-
function _format_quorum_line(q: QuorumResult, phase?: QuorumEventPhase): string {
|
|
781
|
-
// The phase tag is not decoration. A degraded run prints attendance TWICE —
|
|
782
|
-
// the pre-run reading before the estimate table, the post-run reading after
|
|
783
|
-
// the consult — and the two disagree by construction: `2/2 present …
|
|
784
|
-
// concluded` then `0/2 present … INCONCLUSIVE`. Same prefix, opposite
|
|
785
|
-
// content, and until 2026-08-12 neither line said which was which, so an
|
|
786
|
-
// operator skimming (or anything grepping `council:quorum ·`) could take the
|
|
787
|
-
// stale one. The event log had solved this from the start with an explicit
|
|
788
|
-
// `phase` field; stdout had not. Optional, because the estimate path prints
|
|
789
|
-
// exactly one line and a tag there would claim a distinction it does not make.
|
|
790
|
-
const tag = phase === undefined ? '' : `${phase === 'pre_run' ? 'before the run' : 'after the run'} · `;
|
|
791
|
-
const verdict = q.status === 'concluded' ? 'concluded' : 'INCONCLUSIVE — release gate holds';
|
|
792
|
-
// Round 7 § 5.3 — a degraded pass says so. `1/2 present, needed 1 —
|
|
793
|
-
// concluded` is literally true and reads as agreement; with a threshold of 1
|
|
794
|
-
// a solo answer concludes, and nothing in the line distinguishes "both
|
|
795
|
-
// members agreed" from "one member answered". The counts were always there;
|
|
796
|
-
// what was missing is the word that stops a reader inferring convergence.
|
|
797
|
-
const degraded =
|
|
798
|
-
q.present < q.total ? ` ⚠️ DEGRADED — ${String(q.total - q.present)} member(s) did not answer; this is not convergence.` : '';
|
|
799
|
-
return `council:quorum · ${tag}${q.present}/${q.total} present, needed ${q.threshold} — ${verdict}.${degraded}`;
|
|
800
|
-
}
|
|
801
660
|
|
|
802
661
|
function build_members(settings: Dict, opts: BuildMembersOptions = {}): ExternalAIClient[] {
|
|
803
662
|
const invocation_mode = opts.invocation_mode ?? null;
|
|
@@ -805,6 +664,9 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
805
664
|
const siblings_overrides = opts.siblings_overrides ?? null;
|
|
806
665
|
const skipped = opts.skipped ?? null;
|
|
807
666
|
const quorum_out = opts.quorum_out ?? null;
|
|
667
|
+
const probe_store = opts.probe_store ?? null;
|
|
668
|
+
const qualification_out = opts.qualification_out ?? null;
|
|
669
|
+
const qualifications: MemberQualification[] = [];
|
|
808
670
|
// Read-only, per-process-memoised (`detectEnvironment` caches the
|
|
809
671
|
// no-argument call) — cheap to call once per `build_members` invocation.
|
|
810
672
|
// A caller may inject a fixed report (tests only); production leaves
|
|
@@ -837,10 +699,16 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
837
699
|
// dropped the configured default for every caller that hands this
|
|
838
700
|
// exported function a raw `.ai-council.yml` dict.
|
|
839
701
|
const global_mode = resolve_global_mode(ai);
|
|
702
|
+
// ENFORCED cap source. This function accepts BOTH config shapes (see the
|
|
703
|
+
// `resolve_global_mode` note above), and for a RAW `.ai-council.yml` dict a
|
|
704
|
+
// commented-out `cli_call_budget:` leaves the map empty — the lookup below
|
|
705
|
+
// used to fall back to `null`, which `ask()` reads as uncapped while still
|
|
706
|
+
// booking. Resolution runs through `resolve_cli_call_caps`, the same
|
|
707
|
+
// authority `cmd_quota` uses, so uncapped is unreachable by omission.
|
|
840
708
|
const cli_budget_cfg = _isDict(ai) ? ((ai['cli_call_budget'] as Dict) || {}) : {};
|
|
841
|
-
const cli_caps =
|
|
842
|
-
?
|
|
843
|
-
|
|
709
|
+
const cli_caps = resolve_cli_call_caps(
|
|
710
|
+
_isDict(cli_budget_cfg) ? cli_budget_cfg['max_calls_per_day'] : undefined,
|
|
711
|
+
);
|
|
844
712
|
const cli_warn_at = _isDict(cli_budget_cfg)
|
|
845
713
|
? _pyFloat(cli_budget_cfg['warn_at'] ?? 0.8, 0.8)
|
|
846
714
|
: 0.8;
|
|
@@ -928,6 +796,20 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
928
796
|
// same pattern.
|
|
929
797
|
...(api_key_present !== undefined ? { apiKeyPresent: api_key_present } : {}),
|
|
930
798
|
});
|
|
799
|
+
// Qualification runs on the resolved transport, before any branch
|
|
800
|
+
// decides whether this seat constructs — ONE call site, because the
|
|
801
|
+
// branches differ in how a member is built, not in what would qualify
|
|
802
|
+
// it. `||` matches the construction path below (R2 finding 11).
|
|
803
|
+
if (probe_store !== null) {
|
|
804
|
+
qualifications.push(
|
|
805
|
+
qualifySeat(
|
|
806
|
+
name,
|
|
807
|
+
resolved,
|
|
808
|
+
(overrides[name] as string | undefined) || (cfg['model'] as string | undefined) || null,
|
|
809
|
+
probe_store,
|
|
810
|
+
),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
931
813
|
if (name in siblings) {
|
|
932
814
|
if (resolved.transport !== 'api') {
|
|
933
815
|
throw new CouncilDisabledError(
|
|
@@ -1015,7 +897,7 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
1015
897
|
members.push(
|
|
1016
898
|
_construct_cli_member(name, model, {
|
|
1017
899
|
binary: (cfg['binary'] as string | null) ?? null,
|
|
1018
|
-
max_calls_per_day:
|
|
900
|
+
max_calls_per_day: cli_caps[name] as number,
|
|
1019
901
|
warn_at: cli_warn_at,
|
|
1020
902
|
}),
|
|
1021
903
|
);
|
|
@@ -1055,6 +937,26 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
1055
937
|
);
|
|
1056
938
|
}
|
|
1057
939
|
}
|
|
940
|
+
// Phase 3 step 4 — presence is gated on qualification, and the roster is
|
|
941
|
+
// NOT. Dropping an unqualified seat from `total` would lower the threshold
|
|
942
|
+
// (`ceil(n/2)`) and make a short pass EASIER to conclude, which inverts the
|
|
943
|
+
// intent; withholding it from `present` is what makes the pass report being
|
|
944
|
+
// short. A seat already recorded absent is excluded here so it is not
|
|
945
|
+
// subtracted twice.
|
|
946
|
+
//
|
|
947
|
+
// Routed through `record_absent`, never subtracted separately — that keeps
|
|
948
|
+
// the event's `total - present == absent.length` invariant. The seat is
|
|
949
|
+
// still dispatched: see `qualification_wiring.ts::attendanceGate`.
|
|
950
|
+
const gate = attendanceGate(qualifications, new Set(absent_entries.map((e) => String(e['member']))));
|
|
951
|
+
for (const q of gate.toRecordAbsent) {
|
|
952
|
+
record_absent({ member: q.name, reason: absenceReasonFor(q), detail: formatQualificationLine(q) });
|
|
953
|
+
}
|
|
954
|
+
if (gate.noticeLine !== null) {
|
|
955
|
+
_stderr(`${gate.noticeLine}\n`);
|
|
956
|
+
}
|
|
957
|
+
if (qualification_out !== null) {
|
|
958
|
+
qualification_out.push(...qualifications);
|
|
959
|
+
}
|
|
1058
960
|
if (quorum_out !== null) {
|
|
1059
961
|
const present = total_enabled - absent_count;
|
|
1060
962
|
quorum_out.result = evaluateQuorum(total_enabled, present, _quorum_setting_from(ai));
|
|
@@ -1239,7 +1141,13 @@ function _construct_cli_member(
|
|
|
1239
1141
|
const warn_at = opts.warn_at ?? 0.8;
|
|
1240
1142
|
if (name in _CLI_FACTORY) {
|
|
1241
1143
|
const [cls, default_model] = _CLI_FACTORY[name] as [
|
|
1242
|
-
new (opts: {
|
|
1144
|
+
new (opts: {
|
|
1145
|
+
model: string;
|
|
1146
|
+
binary?: string | null;
|
|
1147
|
+
max_calls_per_day?: number | null;
|
|
1148
|
+
warn_at?: number;
|
|
1149
|
+
consumer?: string;
|
|
1150
|
+
}) => CliClient,
|
|
1243
1151
|
string,
|
|
1244
1152
|
string,
|
|
1245
1153
|
];
|
|
@@ -1248,6 +1156,9 @@ function _construct_cli_member(
|
|
|
1248
1156
|
binary,
|
|
1249
1157
|
max_calls_per_day,
|
|
1250
1158
|
warn_at,
|
|
1159
|
+
// Declared at the construction site — the client cannot know its
|
|
1160
|
+
// caller. The council half of the two enumerated consumers.
|
|
1161
|
+
consumer: CLI_CONSUMER_COUNCIL,
|
|
1251
1162
|
});
|
|
1252
1163
|
}
|
|
1253
1164
|
throw new CouncilDisabledError(
|
|
@@ -1836,6 +1747,7 @@ function cmd_estimate(
|
|
|
1836
1747
|
siblings_overrides: _parse_siblings_overrides(_getattr<string[] | null>(args, 'siblings', null)),
|
|
1837
1748
|
skipped,
|
|
1838
1749
|
quorum_out,
|
|
1750
|
+
probe_store: readProbeStore(REPO_ROOT),
|
|
1839
1751
|
// A cost preview spends nothing and runs no pass. The line is
|
|
1840
1752
|
// still written — tagged, so a consumer excludes it deliberately
|
|
1841
1753
|
// instead of never learning it was there.
|
|
@@ -2401,6 +2313,14 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
|
|
|
2401
2313
|
// because only one of the two is fixed by writing a config.
|
|
2402
2314
|
const configured = cfg !== null && cfg.enabled && enabledMembers.length > 0;
|
|
2403
2315
|
|
|
2316
|
+
// `configured` above is a boolean off the config file — exactly what let a
|
|
2317
|
+
// dead seat report as healthy. It stays (it answers a real question) but is
|
|
2318
|
+
// no longer the only thing this command says.
|
|
2319
|
+
const probeStore = readProbeStore(REPO_ROOT);
|
|
2320
|
+
const qualifications = enabledMembers.map(([name, m]) =>
|
|
2321
|
+
qualifySeat(name, _resolvedTransportFor(name, m), m.model, probeStore),
|
|
2322
|
+
);
|
|
2323
|
+
|
|
2404
2324
|
if (_getattr(args, 'json', false)) {
|
|
2405
2325
|
_stdout(
|
|
2406
2326
|
`${JSON.stringify({
|
|
@@ -2427,8 +2347,14 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
|
|
|
2427
2347
|
}),
|
|
2428
2348
|
),
|
|
2429
2349
|
ignored_transport_keys: cfg?.ignored_transport_keys ?? [],
|
|
2350
|
+
qualification: qualificationJson(qualifications),
|
|
2351
|
+
qualified_members: countableSeats(qualifications),
|
|
2430
2352
|
parse_error,
|
|
2353
|
+
// Scoped to CONFIG (ADR-104); the probe read is named beside
|
|
2354
|
+
// it — `qualification_wiring.ts` § What the status surface consulted.
|
|
2355
|
+
project_tree_consulted_for_config: false,
|
|
2431
2356
|
project_tree_consulted: false,
|
|
2357
|
+
probe_store_path: PROBE_STORE_RELPATH,
|
|
2432
2358
|
})}\n`,
|
|
2433
2359
|
);
|
|
2434
2360
|
return 0;
|
|
@@ -2458,6 +2384,11 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
|
|
|
2458
2384
|
const detail = t.available ? t.transport : `unavailable — ${t.reason ?? 'no usable transport'}`;
|
|
2459
2385
|
_stdout(` transport ${name}: ${detail}${billing}\n`);
|
|
2460
2386
|
}
|
|
2387
|
+
// The four-value verdict per seat, then the advice — TWO warnings, so
|
|
2388
|
+
// an `unavailable` seat is not told to re-run (R2 finding 12).
|
|
2389
|
+
for (const line of qualificationStatusLines(qualifications)) {
|
|
2390
|
+
_stdout(`${line}\n`);
|
|
2391
|
+
}
|
|
2461
2392
|
if (cfg.ignored_transport_keys.length > 0) {
|
|
2462
2393
|
_stdout('\n');
|
|
2463
2394
|
_stdout(' Ignored transport keys — transport is resolved, not configured:\n');
|
|
@@ -2468,7 +2399,7 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
|
|
|
2468
2399
|
}
|
|
2469
2400
|
}
|
|
2470
2401
|
_stdout('\n');
|
|
2471
|
-
_stdout(' The project tree is NEVER consulted (ADR-104). No project file —\n');
|
|
2402
|
+
_stdout(' The project tree is NEVER consulted FOR CONFIG (ADR-104). No project file —\n');
|
|
2472
2403
|
_stdout(' `.agent-settings.yml` included — indicates council availability in\n');
|
|
2473
2404
|
_stdout(' either direction. Do not infer it from one; run this instead.\n');
|
|
2474
2405
|
if (!configured) {
|
|
@@ -2559,6 +2490,7 @@ function cmd_run(
|
|
|
2559
2490
|
siblings_overrides: _parse_siblings_overrides(_getattr<string[] | null>(args, 'siblings', null)),
|
|
2560
2491
|
skipped,
|
|
2561
2492
|
quorum_out,
|
|
2493
|
+
probe_store: readProbeStore(REPO_ROOT),
|
|
2562
2494
|
});
|
|
2563
2495
|
}
|
|
2564
2496
|
// Measured, never assumed: `_apply_solo_dispatch` escalates back to the
|
|
@@ -2773,6 +2705,7 @@ function cmd_run(
|
|
|
2773
2705
|
const post_run = _postRunQuorum(members, responses, ai_cfg);
|
|
2774
2706
|
quorum_out.result = post_run.quorum;
|
|
2775
2707
|
skipped.push(...post_run.absent);
|
|
2708
|
+
recordRoundObservations(REPO_ROOT, members, responses, classifyCliFailure);
|
|
2776
2709
|
// Attendance telemetry for the reading that actually decided the pass.
|
|
2777
2710
|
// `post_run.absent` is the mid-flight set only; the construction-time
|
|
2778
2711
|
// skips already went out under `phase: 'pre_run'` from `build_members`,
|
|
@@ -3092,6 +3025,7 @@ function cmd_debate(
|
|
|
3092
3025
|
siblings_overrides: _parse_siblings_overrides(_getattr<string[] | null>(args, 'siblings', null)),
|
|
3093
3026
|
skipped,
|
|
3094
3027
|
quorum_out,
|
|
3028
|
+
probe_store: readProbeStore(REPO_ROOT),
|
|
3095
3029
|
command: 'debate',
|
|
3096
3030
|
});
|
|
3097
3031
|
}
|
|
@@ -3305,6 +3239,12 @@ function cmd_debate(
|
|
|
3305
3239
|
throw exc;
|
|
3306
3240
|
}
|
|
3307
3241
|
|
|
3242
|
+
// Debate consumes qualification, so it produces observations too —
|
|
3243
|
+
// otherwise a debate-only operator's seats stay `unknown` forever
|
|
3244
|
+
// (R2 finding 14). Not a post-run quorum: recording and re-deriving are
|
|
3245
|
+
// different jobs, and the debate path deliberately has no quorum.
|
|
3246
|
+
recordRoundObservations(REPO_ROOT, members, all_rounds[all_rounds.length - 1] ?? [], classifyCliFailure);
|
|
3247
|
+
|
|
3308
3248
|
let actual_total = 0.0;
|
|
3309
3249
|
for (const rnd of [...all_rounds, restate_responses]) {
|
|
3310
3250
|
for (const r of rnd) {
|
|
@@ -3450,7 +3390,13 @@ function cmd_quota(args: Args, opts: { settings?: Dict | null } = {}): number {
|
|
|
3450
3390
|
const s = opts.settings !== undefined && opts.settings !== null ? opts.settings : load_settings();
|
|
3451
3391
|
const ai_cfg = _isDict(s) ? ((s['ai_council'] as Dict) || {}) : {};
|
|
3452
3392
|
const cli_budget_cfg = _isDict(ai_cfg) ? ((ai_cfg['cli_call_budget'] as Dict) || {}) : {};
|
|
3453
|
-
|
|
3393
|
+
// REPORTED cap source — the same authority the gate uses. It used to read
|
|
3394
|
+
// the settings mapping directly and print "no providers have a configured
|
|
3395
|
+
// cap", which was misleading: the gate seeds a default for every provider,
|
|
3396
|
+
// so an operator reading "no cap" was capped and already booked past it.
|
|
3397
|
+
const caps = resolve_cli_call_caps(
|
|
3398
|
+
_isDict(cli_budget_cfg) ? cli_budget_cfg['max_calls_per_day'] : undefined,
|
|
3399
|
+
);
|
|
3454
3400
|
const warn_at = _isDict(cli_budget_cfg) ? _pyFloat(cli_budget_cfg['warn_at'] ?? 0.8, 0.8) : 0.8;
|
|
3455
3401
|
|
|
3456
3402
|
if (_getattr<string | null>(args, 'reset', null)) {
|
|
@@ -3465,13 +3411,9 @@ function cmd_quota(args: Args, opts: { settings?: Dict | null } = {}): number {
|
|
|
3465
3411
|
}
|
|
3466
3412
|
|
|
3467
3413
|
const counts = load_cli_call_counts();
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
'cli_call_budget.max_calls_per_day cap.\n',
|
|
3472
|
-
);
|
|
3473
|
-
return 0;
|
|
3474
|
-
}
|
|
3414
|
+
const attribution = load_cli_call_attribution();
|
|
3415
|
+
// No empty-caps branch: the resolver seeds every provider, so there is no
|
|
3416
|
+
// configuration in which this command has nothing to report.
|
|
3475
3417
|
for (const provider of _pySortedStr(Object.keys(caps))) {
|
|
3476
3418
|
const limit = _pyInt(caps[provider]);
|
|
3477
3419
|
const used = _pyInt(counts[provider] ?? 0, 0);
|
|
@@ -3482,7 +3424,15 @@ function cmd_quota(args: Args, opts: { settings?: Dict | null } = {}): number {
|
|
|
3482
3424
|
} else if (ratio >= warn_at) {
|
|
3483
3425
|
status = 'warn';
|
|
3484
3426
|
}
|
|
3485
|
-
|
|
3427
|
+
// Named when the sidecar knows them. An empty suffix is the honest
|
|
3428
|
+
// reading for a bucket booked before attribution existed — not a claim
|
|
3429
|
+
// that nobody spent it.
|
|
3430
|
+
const perConsumer = attribution[provider] ?? {};
|
|
3431
|
+
const consumers = _pySortedStr(Object.keys(perConsumer));
|
|
3432
|
+
const by = consumers.length > 0
|
|
3433
|
+
? ` · by ${consumers.map((c) => `${c} ${_pyInt(perConsumer[c] ?? 0, 0)}`).join(' + ')}`
|
|
3434
|
+
: '';
|
|
3435
|
+
_stdout(`council:quota · ${provider} · ${used}/${limit} · ${status}${by}\n`);
|
|
3486
3436
|
}
|
|
3487
3437
|
return 0;
|
|
3488
3438
|
}
|
|
@@ -685,12 +685,19 @@ function findingsSkeleton(args: {
|
|
|
685
685
|
return [
|
|
686
686
|
`# Findings: ${args.slug}`,
|
|
687
687
|
`<!-- completion-review: v1 | reviewed: ${args.reviewedDate} | scope: ${args.scopeHash} | diff: ${args.headSha} | reviewer: r2-fresh-subagent-${args.slug} | prompt_hash: ${args.promptHash} -->`,
|
|
688
|
+
// Set at CREATION, never inferred later: an inferred type reads filename
|
|
689
|
+
// and location, which are exactly the signals that already fail to
|
|
690
|
+
// distinguish an input from a binding. This artifact binds a scope from
|
|
691
|
+
// its first byte, so `current-binding` is what it IS — an empty table is
|
|
692
|
+
// a RESULT, and a reviewer returning zero findings flips the type to
|
|
693
|
+
// `honest-null` in the same edit that replaces the table.
|
|
694
|
+
`<!-- evidence-type: v1 | type: current-binding | declared: ${args.reviewedDate} -->`,
|
|
688
695
|
'',
|
|
689
696
|
args.manifest,
|
|
690
697
|
'',
|
|
691
698
|
'| # | Severity | File:Line | Finding | Status | Reason/Ref |',
|
|
692
699
|
'|---|----------|-----------|---------|--------|------------|',
|
|
693
|
-
'<!-- reviewer fills the table; 0 findings => replace the table with the exact honest-null line per docs/contracts/plan-review-gates.md §2.3 -->',
|
|
700
|
+
'<!-- reviewer fills the table; 0 findings => replace the table with the exact honest-null line per docs/contracts/plan-review-gates.md §2.3 AND change the evidence-type to `honest-null` per docs/contracts/evidence-artifact-types.md §4 -->',
|
|
694
701
|
'',
|
|
695
702
|
].join('\n');
|
|
696
703
|
}
|
|
@@ -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: []
|
|
@@ -474,6 +489,16 @@ concerns:
|
|
|
474
489
|
args: []
|
|
475
490
|
fail_closed: false
|
|
476
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
|
|
477
502
|
# road-to-conformance-round5 Phase 3 — the FIRST concern that can refuse a
|
|
478
503
|
# turn-END. Round 5 measured the reason: both blocking carriers reached zero
|
|
479
504
|
# violations, neither advisory carrier did, and 19 language violations
|
|
@@ -557,6 +582,43 @@ concerns:
|
|
|
557
582
|
args: []
|
|
558
583
|
fail_closed: false
|
|
559
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
|
|
560
622
|
# road-to-subagent-lifecycle-integrity Phase 1 (Steps 2+3) — the subagent
|
|
561
623
|
# lifecycle ledger. Binds ONLY the two new events; on every other slot it is
|
|
562
624
|
# not listed at all. Appends `subagent_start` / `subagent_stop` lines to
|
|
@@ -665,22 +727,27 @@ roles:
|
|
|
665
727
|
# is on it: a worker was handed a bounded slice by an orchestrator that
|
|
666
728
|
# already chose the approach, so a routing pointer there advises nobody who
|
|
667
729
|
# can act on it and costs the dispatch anyway.
|
|
668
|
-
|
|
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]
|
|
669
736
|
|
|
670
737
|
platforms:
|
|
671
738
|
augment:
|
|
672
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]
|
|
673
740
|
session_end: [chat-history, memory-learn, session-register]
|
|
674
741
|
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
675
|
-
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]
|
|
676
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]
|
|
677
744
|
|
|
678
745
|
claude:
|
|
679
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]
|
|
680
747
|
session_end: [chat-history, memory-learn, session-register]
|
|
681
|
-
stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, turn-end-gate, self-repair, session-register, session-eol]
|
|
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]
|
|
682
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]
|
|
683
|
-
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]
|
|
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]
|
|
684
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]
|
|
685
752
|
# `language-mirror` — round-5 § 6.1. `pre_compact` sets a pin-lost marker;
|
|
686
753
|
# the `post_tool_use` entry above re-emits the pin exactly once while that
|
|
@@ -734,7 +801,7 @@ platforms:
|
|
|
734
801
|
# still costs the dispatch, and its entire output is thrown away.
|
|
735
802
|
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
736
803
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
737
|
-
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]
|
|
738
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]
|
|
739
806
|
# subagent-ledger IS bound here, unlike the two nudges above, and the
|
|
740
807
|
# difference is the delivery channel rather than the platform: the nudges
|