@event4u/agent-config 9.31.0 → 9.33.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 +146 -0
- package/dist/agent-src/commands/review/routing.md +3 -4
- package/dist/agent-src/contexts/communication/rules-auto/reply-close-mechanics.md +65 -0
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +32 -1
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +3 -0
- package/dist/agent-src/rules/delegation-policy.md +17 -1
- package/dist/agent-src/scripts/roadmap_gates.ts +107 -3
- package/dist/agent-src/skills/docker/SKILL.md +11 -0
- package/dist/agent-src/skills/laravel/SKILL.md +11 -0
- package/dist/agent-src/skills/review-routing/SKILL.md +23 -6
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/ownership-map.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/delivery_state.ts +6 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +332 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +106 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +50 -15
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +49 -15
- package/dist/agent-src/templates/scripts/work_engine/state.ts +74 -0
- package/dist/agent-src/templates/scripts/work_engine/state_io.ts +6 -0
- package/dist/cli/registry.js +4 -4
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-2WELHO3P.js +582 -0
- package/dist/cli-delegate/chunk-HTIU3W56.js +66 -0
- package/dist/cli-delegate/{chunk-ITIQSGOQ.js → chunk-MA4GFWDY.js} +7 -4
- package/dist/cli-delegate/{chunk-2WCLVDWC.js → chunk-PMQ6Z3T3.js} +1642 -165
- package/dist/cli-delegate/chunk-PZWRPRWP.js +38 -0
- package/dist/cli-delegate/{chunk-PFBV6XRT.js → chunk-Q26HT4QT.js} +1 -63
- package/dist/cli-delegate/{chunk-LVRYBKTI.js → chunk-YPAWXXXR.js} +158 -0
- package/dist/cli-delegate/cmd_conformance.js +308 -227
- package/dist/cli-delegate/cmd_doctor.js +4 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +160 -58
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +30 -5
- package/dist/cli-delegate/cmd_settings_get.js +4 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_settings_set.js +5 -3
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +8 -8
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1136 -690
- package/dist/install/install.mjs +7 -4
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3688 -1159
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +107 -3
- package/src/agent-src/templates/scripts/ownership-map.example.yml +1 -1
- package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +6 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +332 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +106 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +50 -15
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +49 -15
- package/src/agent-src/templates/scripts/work_engine/state.ts +74 -0
- package/src/agent-src/templates/scripts/work_engine/state_io.ts +6 -0
- package/src/config/cost-parity-budget.json +167 -0
- package/src/config/gate-coverage.yml +44 -5
- package/src/config/quorum-attendance-budget.json +39 -0
- package/src/scripts/_cli/cmd_doctor.ts +69 -1
- package/src/scripts/_cli/cmd_session_recycle.ts +44 -3
- package/src/scripts/_cli/handoff_generate.ts +36 -1
- package/src/scripts/_cli/handoff_sessions.ts +140 -9
- package/src/scripts/_dispatch.bash +5 -3
- package/src/scripts/_lib/carrier_divergence.ts +98 -27
- package/src/scripts/_lib/envelope_grounding.ts +206 -0
- package/src/scripts/_lib/host_capability.ts +79 -0
- package/src/scripts/_lib/orchestration_record.ts +45 -0
- package/src/scripts/_lib/session_eol.ts +85 -0
- package/src/scripts/_lib/subagent_capsule.ts +256 -1
- package/src/scripts/ai_council/clients.ts +31 -0
- package/src/scripts/ai_council/events_log.ts +128 -6
- package/src/scripts/ai_council/orchestrator.ts +12 -3
- package/src/scripts/ai_council/pricing.ts +48 -1
- package/src/scripts/ai_council/quorum.ts +20 -0
- package/src/scripts/ai_council/session.ts +11 -0
- package/src/scripts/ask_transport.ts +20 -1
- package/src/scripts/cache_realization_report.ts +38 -0
- package/src/scripts/check_release_highlights.ts +7 -1
- package/src/scripts/check_secret_leak.test.ts +43 -0
- package/src/scripts/check_secret_leak.ts +12 -0
- package/src/scripts/conformance_scan.ts +105 -0
- package/src/scripts/cost/track.mjs +39 -0
- package/src/scripts/cost_summary.ts +104 -1
- package/src/scripts/council_cli.ts +131 -13
- package/src/scripts/dispatch_economy_report.ts +5 -2
- package/src/scripts/handoff_context_hook.ts +66 -18
- package/src/scripts/hooks/session_eol_hook.ts +12 -12
- package/src/scripts/install.ts +20 -6
- package/src/scripts/lint_budget_ownership.ts +81 -0
- package/src/scripts/lint_skill_frontmatter_safety.ts +55 -1
- package/src/scripts/lint_workflow_paths.ts +384 -0
- package/src/scripts/preamble_byte_census.ts +9 -1
- package/src/scripts/report_carrier_divergence.ts +104 -29
- package/src/scripts/report_conformance_funnel.ts +51 -4
- package/src/scripts/routing_doctor.ts +25 -2
- package/src/scripts/schemas/dod.schema.json +45 -0
- package/src/scripts/schemas/subagent.schema.json +3 -2
- package/dist/cli-delegate/chunk-RVGBH4XK.js +0 -142
|
@@ -77,7 +77,14 @@ import { InvalidModeError, resolve_global_mode } from './ai_council/modes.js';
|
|
|
77
77
|
import { resolveMemberTransport } from './ai_council/transport_resolver.js';
|
|
78
78
|
import { absentReasonFromCliFailure, classifyCliFailure, type AbsentReason } from './ai_council/transport_resolver.js';
|
|
79
79
|
import { evaluateQuorum, type QuorumResult } from './ai_council/quorum.js';
|
|
80
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
appendEvent,
|
|
82
|
+
appendQuorumEvent,
|
|
83
|
+
type QuorumAbsence,
|
|
84
|
+
type QuorumCommand,
|
|
85
|
+
type QuorumDispatch,
|
|
86
|
+
type QuorumEventPhase,
|
|
87
|
+
} from './ai_council/events_log.js';
|
|
81
88
|
import {
|
|
82
89
|
type ClassificationResult,
|
|
83
90
|
type SizeFitVerdict,
|
|
@@ -576,6 +583,16 @@ interface BuildMembersOptions {
|
|
|
576
583
|
* "null = never evaluated" convention.
|
|
577
584
|
*/
|
|
578
585
|
quorum_out?: { result: QuorumResult | null } | null;
|
|
586
|
+
/**
|
|
587
|
+
* Which CLI path is constructing — recorded on the `pre_run` quorum
|
|
588
|
+
* event so a consumer can exclude the paths that never run a pass.
|
|
589
|
+
* `estimate` is a spend-free preview and `run` may still abort at the
|
|
590
|
+
* necessity or size-fit gate after this point, so a `pre_run` line is
|
|
591
|
+
* evidence that construction happened, never that a pass followed;
|
|
592
|
+
* attendance rates are computed over `post_run` lines for exactly that
|
|
593
|
+
* reason. Defaults to `run` for a caller that does not say.
|
|
594
|
+
*/
|
|
595
|
+
command?: QuorumCommand;
|
|
579
596
|
/**
|
|
580
597
|
* Test-only injection point, mirroring the `settings`/`members`/`table`
|
|
581
598
|
* DI pattern the CLI commands already use. Production callers never set
|
|
@@ -630,6 +647,52 @@ function _quorum_setting_from(ai: Dict): QuorumSetting {
|
|
|
630
647
|
return 'majority';
|
|
631
648
|
}
|
|
632
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Emit one `quorum_result` line for a resolved quorum.
|
|
652
|
+
*
|
|
653
|
+
* Both `evaluateQuorum` call sites route through here, tagged by `phase`, so
|
|
654
|
+
* a solo-concluded pass is distinguishable from a full-attendance one in the
|
|
655
|
+
* event log — the gap `road-to-always-on-orchestration`'s Risk 6 asserted was
|
|
656
|
+
* already closed and was not.
|
|
657
|
+
*
|
|
658
|
+
* The emit sits at the call sites rather than inside `_postRunQuorum` on
|
|
659
|
+
* purpose: that function is exported and unit-tested as a pure derivation,
|
|
660
|
+
* and a write buried in it would make every test of it a log writer.
|
|
661
|
+
*
|
|
662
|
+
* `absent` entries arrive as the CLI's own `{member, reason, detail}` dicts;
|
|
663
|
+
* `detail` is dropped by `appendQuorumEvent`'s input type, which cannot carry
|
|
664
|
+
* free-form text at all.
|
|
665
|
+
*/
|
|
666
|
+
function _emitQuorumEvent(
|
|
667
|
+
phase: QuorumEventPhase,
|
|
668
|
+
quorum: QuorumResult,
|
|
669
|
+
absent: readonly Dict[],
|
|
670
|
+
ctx: {
|
|
671
|
+
command: QuorumCommand;
|
|
672
|
+
dispatch?: QuorumDispatch;
|
|
673
|
+
/** Enabled members before `--single` filtering; defaults to the roster that ran. */
|
|
674
|
+
configuredTotal?: number;
|
|
675
|
+
lens?: string;
|
|
676
|
+
invocation?: string;
|
|
677
|
+
},
|
|
678
|
+
): void {
|
|
679
|
+
appendQuorumEvent({
|
|
680
|
+
lens: ctx.lens ?? '',
|
|
681
|
+
invocation: ctx.invocation ?? '',
|
|
682
|
+
phase,
|
|
683
|
+
command: ctx.command,
|
|
684
|
+
dispatch: ctx.dispatch ?? 'full',
|
|
685
|
+
configuredTotal: ctx.configuredTotal ?? quorum.total,
|
|
686
|
+
result: quorum,
|
|
687
|
+
absent: absent.map(
|
|
688
|
+
(a): QuorumAbsence => ({
|
|
689
|
+
member: String(a['member'] ?? ''),
|
|
690
|
+
reason: (a['reason'] as QuorumAbsence['reason']) ?? 'unavailable',
|
|
691
|
+
}),
|
|
692
|
+
),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
633
696
|
/**
|
|
634
697
|
* Re-derive quorum AFTER the provider calls, over what was actually USABLE —
|
|
635
698
|
* never what merely CONSTRUCTED (M3, independent-review finding). `members`
|
|
@@ -762,6 +825,18 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
762
825
|
// that caller shape.
|
|
763
826
|
let total_enabled = 0;
|
|
764
827
|
let absent_count = 0;
|
|
828
|
+
// Every absence, regardless of whether the caller asked for the
|
|
829
|
+
// human-readable `skipped` out-param — the quorum event needs the
|
|
830
|
+
// member/reason pairs even when `skipped` is `null`, and reconstructing
|
|
831
|
+
// them from `absent_count` alone is impossible.
|
|
832
|
+
const absent_entries: Dict[] = [];
|
|
833
|
+
const record_absent = (entry: Dict): void => {
|
|
834
|
+
absent_count += 1;
|
|
835
|
+
absent_entries.push(entry);
|
|
836
|
+
if (skipped !== null) {
|
|
837
|
+
skipped.push(entry);
|
|
838
|
+
}
|
|
839
|
+
};
|
|
765
840
|
for (const name of Object.keys(members_cfg)) {
|
|
766
841
|
const cfg = ((members_cfg[name] as Dict) || {}) as Dict;
|
|
767
842
|
if (!_pyBool(cfg['enabled'])) {
|
|
@@ -834,10 +909,7 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
834
909
|
reason: resolved.absentReason ?? 'unavailable',
|
|
835
910
|
detail,
|
|
836
911
|
};
|
|
837
|
-
|
|
838
|
-
if (skipped !== null) {
|
|
839
|
-
skipped.push(entry);
|
|
840
|
-
}
|
|
912
|
+
record_absent(entry);
|
|
841
913
|
_stderr(`[council] SKIP ${name}: ${detail}\n`);
|
|
842
914
|
continue;
|
|
843
915
|
}
|
|
@@ -877,10 +949,7 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
877
949
|
}
|
|
878
950
|
const detail = exc.message;
|
|
879
951
|
const entry: Dict = { member: name, reason: 'no_auth', detail };
|
|
880
|
-
|
|
881
|
-
if (skipped !== null) {
|
|
882
|
-
skipped.push(entry);
|
|
883
|
-
}
|
|
952
|
+
record_absent(entry);
|
|
884
953
|
_stderr(`[council] SKIP ${name}: ${detail}\n`);
|
|
885
954
|
continue;
|
|
886
955
|
}
|
|
@@ -910,10 +979,7 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
910
979
|
reason: 'binary_missing',
|
|
911
980
|
detail,
|
|
912
981
|
};
|
|
913
|
-
|
|
914
|
-
if (skipped !== null) {
|
|
915
|
-
skipped.push(entry);
|
|
916
|
-
}
|
|
982
|
+
record_absent(entry);
|
|
917
983
|
_stderr(`[council] SKIP ${name}: ${detail}\n`);
|
|
918
984
|
continue;
|
|
919
985
|
}
|
|
@@ -935,6 +1001,15 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
935
1001
|
if (quorum_out !== null) {
|
|
936
1002
|
const present = total_enabled - absent_count;
|
|
937
1003
|
quorum_out.result = evaluateQuorum(total_enabled, present, _quorum_setting_from(ai));
|
|
1004
|
+
// Construction-time attendance. `lens` / `invocation` stay empty
|
|
1005
|
+
// here on purpose: neither is known at member-construction time,
|
|
1006
|
+
// and `invocation_mode` (api / cli / manual) is a different axis
|
|
1007
|
+
// that would read as the same field under a shared name.
|
|
1008
|
+
// `command` IS recorded, because a preview and an aborted run both
|
|
1009
|
+
// reach this line and neither is a pass.
|
|
1010
|
+
_emitQuorumEvent('pre_run', quorum_out.result, absent_entries, {
|
|
1011
|
+
command: opts.command ?? 'run',
|
|
1012
|
+
});
|
|
938
1013
|
}
|
|
939
1014
|
if (members.length === 0) {
|
|
940
1015
|
if (skipped && skipped.length > 0) {
|
|
@@ -1703,6 +1778,10 @@ function cmd_estimate(
|
|
|
1703
1778
|
siblings_overrides: _parse_siblings_overrides(_getattr<string[] | null>(args, 'siblings', null)),
|
|
1704
1779
|
skipped,
|
|
1705
1780
|
quorum_out,
|
|
1781
|
+
// A cost preview spends nothing and runs no pass. The line is
|
|
1782
|
+
// still written — tagged, so a consumer excludes it deliberately
|
|
1783
|
+
// instead of never learning it was there.
|
|
1784
|
+
command: 'estimate',
|
|
1706
1785
|
});
|
|
1707
1786
|
}
|
|
1708
1787
|
if (table === null) {
|
|
@@ -2365,6 +2444,13 @@ function cmd_run(
|
|
|
2365
2444
|
quorum_out,
|
|
2366
2445
|
});
|
|
2367
2446
|
}
|
|
2447
|
+
// Measured, never assumed: `_apply_solo_dispatch` escalates back to the
|
|
2448
|
+
// full roster on three paths (empty fallback chain, no chain member
|
|
2449
|
+
// runtime-present, the picked member vanished). `--single` is therefore a
|
|
2450
|
+
// request, not an outcome, and a `dispatch` field written from the flag
|
|
2451
|
+
// would claim a solo pass that did not happen — contaminating the one
|
|
2452
|
+
// split the solo-conclusion rate depends on.
|
|
2453
|
+
const roster_before_dispatch = members.length;
|
|
2368
2454
|
if (_getattr(args, 'single', false)) {
|
|
2369
2455
|
const [filtered, solo_banner] = _apply_solo_dispatch(members);
|
|
2370
2456
|
members = filtered;
|
|
@@ -2373,6 +2459,8 @@ function cmd_run(
|
|
|
2373
2459
|
}
|
|
2374
2460
|
_emit_shadow_slo_banner();
|
|
2375
2461
|
}
|
|
2462
|
+
const dispatch_shape: QuorumDispatch =
|
|
2463
|
+
members.length < roster_before_dispatch ? 'single' : 'full';
|
|
2376
2464
|
if (table === null) {
|
|
2377
2465
|
// Anchor to the PROJECT root — the module default writes into the
|
|
2378
2466
|
// installed package dir when run from a consumer (pollutes the npm
|
|
@@ -2538,9 +2626,30 @@ function cmd_run(
|
|
|
2538
2626
|
// (mid-flight failures appended to the same array construction-time
|
|
2539
2627
|
// skips already populate, so `payload['absent_members']` needs no
|
|
2540
2628
|
// separate wiring either).
|
|
2629
|
+
// The pre-run reading, before it is overwritten — `build_members` set it
|
|
2630
|
+
// from `total_enabled`, i.e. the enabled roster before `--single` filtering
|
|
2631
|
+
// and before any construction failure. Without carrying it forward, a pass
|
|
2632
|
+
// with 3 configured members where 2 fail to construct and 1 answers emits
|
|
2633
|
+
// `{total: 1, present: 1}` and reads as full attendance.
|
|
2634
|
+
const configured_total = quorum_out.result?.total ?? null;
|
|
2541
2635
|
const post_run = _postRunQuorum(members, responses, ai_cfg);
|
|
2542
2636
|
quorum_out.result = post_run.quorum;
|
|
2543
2637
|
skipped.push(...post_run.absent);
|
|
2638
|
+
// Attendance telemetry for the reading that actually decided the pass.
|
|
2639
|
+
// `post_run.absent` is the mid-flight set only; the construction-time
|
|
2640
|
+
// skips already went out under `phase: 'pre_run'` from `build_members`,
|
|
2641
|
+
// and merging them here would double-count a member absent in both.
|
|
2642
|
+
_emitQuorumEvent('post_run', post_run.quorum, post_run.absent, {
|
|
2643
|
+
command: 'run',
|
|
2644
|
+
// Whether the roster actually shrank — see `dispatch_shape`. Without
|
|
2645
|
+
// this the line is byte-identical to a configured one-member council,
|
|
2646
|
+
// and the solo-conclusion rate cannot make the one distinction it
|
|
2647
|
+
// exists for.
|
|
2648
|
+
dispatch: dispatch_shape,
|
|
2649
|
+
...(configured_total !== null ? { configuredTotal: configured_total } : {}),
|
|
2650
|
+
lens: question.mode,
|
|
2651
|
+
invocation: String(_getattr(args, 'invocation', 'agent')),
|
|
2652
|
+
});
|
|
2544
2653
|
// Phase 4.1 — verdict → handoff envelope. Same tally `render()` computes
|
|
2545
2654
|
// internally for the Vote Tally block (mirrored here, not imported from
|
|
2546
2655
|
// there, because `render()` re-derives it from the SAVED payload on a
|
|
@@ -2817,12 +2926,21 @@ function cmd_debate(
|
|
|
2817
2926
|
const advisor_plans = _build_advisor_plans(ai_cfg, REPO_ROOT);
|
|
2818
2927
|
const explicit_overrides = _parse_model_overrides(_getattr<string[] | null>(args, 'model', null));
|
|
2819
2928
|
const skipped: Dict[] = [];
|
|
2929
|
+
// Debate spends the same providers as `run`, so its attendance belongs in
|
|
2930
|
+
// the same log. It carries the construction-time half only: there is no
|
|
2931
|
+
// `_postRunQuorum` on this path, and adding one would need a usable-response
|
|
2932
|
+
// definition for a multi-round debate that this change does not have. The
|
|
2933
|
+
// gap is declared in quorum-attendance-budget.json rather than left for a
|
|
2934
|
+
// reader to discover from a missing row.
|
|
2935
|
+
const quorum_out: { result: QuorumResult | null } = { result: null };
|
|
2820
2936
|
if (members === null) {
|
|
2821
2937
|
members = build_members(settings, {
|
|
2822
2938
|
invocation_mode: args.mode_override,
|
|
2823
2939
|
model_overrides: _advisor_model_overrides(advisor_plans, explicit_overrides),
|
|
2824
2940
|
siblings_overrides: _parse_siblings_overrides(_getattr<string[] | null>(args, 'siblings', null)),
|
|
2825
2941
|
skipped,
|
|
2942
|
+
quorum_out,
|
|
2943
|
+
command: 'debate',
|
|
2826
2944
|
});
|
|
2827
2945
|
}
|
|
2828
2946
|
if (table === null) {
|
|
@@ -54,7 +54,9 @@ const _HERE = fileURLToPath(import.meta.url);
|
|
|
54
54
|
// 2-up resolution cache_realization_report.ts uses at the same depth).
|
|
55
55
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
56
56
|
const REGISTRATION_FILE = path.join(REPO_ROOT, 'src', 'config', 'dispatch-economy-metrics.json');
|
|
57
|
-
|
|
57
|
+
/** Exported so `doctor --anatomy` resolves the same audit dir this script does. */
|
|
58
|
+
export const DEFAULT_AUDIT_DIR = path.join('agents', 'runtime', 'state', 'audit');
|
|
59
|
+
export { DEFAULT_PROJECTS_ROOT };
|
|
58
60
|
|
|
59
61
|
/** Reviewer attribution off audit-line agent_combo — id-shaped types only. */
|
|
60
62
|
const REVIEWER_TYPE_RE = /review|judge|validator/i;
|
|
@@ -375,7 +377,8 @@ function fmtTokens(n: number | null): string {
|
|
|
375
377
|
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(Math.round(n));
|
|
376
378
|
}
|
|
377
379
|
|
|
378
|
-
|
|
380
|
+
/** Exported so `doctor --anatomy` renders the same text this script prints. */
|
|
381
|
+
export function renderText(r: Report): string {
|
|
379
382
|
const lines: string[] = [];
|
|
380
383
|
lines.push('dispatch-economy report (registered: ' + r.registration.registered_at + ' · review by: ' + r.registration.review_by + ')');
|
|
381
384
|
lines.push(`window: ${r.window.max_age_days}d · ${r.window.transcript_records} deduped records · ${r.window.legs} legs`);
|
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
* - Never blocks: exit 0 on every path; failures are silent (stderr note).
|
|
18
18
|
* - `AGENT_CONFIG_REPLAY=1` → no-op (replay fixtures never mutate state).
|
|
19
19
|
*
|
|
20
|
+
* Data-never-instruction (road-to-cost-parity-3 Phase 2.6/2.8): every block
|
|
21
|
+
* this concern emits is wrapped by `wrapAsPriorSessionData` — an explicit
|
|
22
|
+
* boundary plus a label naming it as prior-session DATA — and a block that
|
|
23
|
+
* somehow lacks that marker is refused rather than injected. That is the
|
|
24
|
+
* GATEABLE half. The other half, that a marked block is *treated* as data
|
|
25
|
+
* rather than followed, is model-carried and `enforced_by: none`; the guard
|
|
26
|
+
* below must never be read as covering it.
|
|
27
|
+
*
|
|
20
28
|
* Reads the dispatcher JSON envelope on stdin
|
|
21
29
|
* (`{platform, event, payload, workspace_root, …}`).
|
|
22
30
|
*/
|
|
@@ -26,13 +34,19 @@ import * as path from 'node:path';
|
|
|
26
34
|
import process from 'node:process';
|
|
27
35
|
import { fileURLToPath } from 'node:url';
|
|
28
36
|
|
|
37
|
+
import { collectRepoAnchor, describeDrift } from './_lib/envelope_grounding.js';
|
|
29
38
|
import { readHookStdin } from './hooks/hook_stdin.js';
|
|
30
39
|
import {
|
|
31
40
|
RECYCLE_CONSUMED_REL,
|
|
32
41
|
RECYCLE_ENVELOPE_REL,
|
|
33
42
|
RECYCLE_MAX_AGE_HOURS,
|
|
34
43
|
} from './_lib/recycle_envelope_paths.js';
|
|
35
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
hasBoundaryMarker,
|
|
46
|
+
scanEnvelopeDirectives,
|
|
47
|
+
validateRecycleEnvelope,
|
|
48
|
+
wrapAsPriorSessionData,
|
|
49
|
+
} from './_lib/subagent_capsule.js';
|
|
36
50
|
|
|
37
51
|
export const HANDOFF_CONTEXT_REL = path.join('agents', 'runtime', 'state', 'handoff-context.md');
|
|
38
52
|
export const MAX_AGE_HOURS = 48;
|
|
@@ -53,6 +67,23 @@ export interface ConsumeDecision {
|
|
|
53
67
|
context?: string;
|
|
54
68
|
}
|
|
55
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The 2.8a gate, as one choke point both consumers pass through: a block
|
|
72
|
+
* WITHOUT its prior-session boundary and label never becomes an injection.
|
|
73
|
+
* Exported so a fixture can prove the refusal on an arbitrary block —
|
|
74
|
+
* otherwise the only way to reach this branch would be to break the
|
|
75
|
+
* wrapper, and an unreachable guard is an untested one.
|
|
76
|
+
*/
|
|
77
|
+
export function guardedInjection(block: string, reason: string): ConsumeDecision {
|
|
78
|
+
if (!hasBoundaryMarker(block)) {
|
|
79
|
+
return {
|
|
80
|
+
action: 'discard',
|
|
81
|
+
reason: 'refused: injected block carries no prior-session boundary marker',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return { action: 'inject', reason, context: block };
|
|
85
|
+
}
|
|
86
|
+
|
|
56
87
|
/**
|
|
57
88
|
* Consume the one-shot handoff file: inject when fresh, discard when stale.
|
|
58
89
|
* BOTH outcomes remove the file — the handoff never survives a second
|
|
@@ -92,14 +123,12 @@ export function consume_handoff_context(root: string, now: Date = new Date()): C
|
|
|
92
123
|
}
|
|
93
124
|
|
|
94
125
|
const sourceSession = text.match(/^Source-Session: (.+)$/m)?.[1]?.trim() ?? 'unknown';
|
|
95
|
-
const block =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
'</handoff-context>',
|
|
100
|
-
].join('\n');
|
|
126
|
+
const block = wrapAsPriorSessionData(text, {
|
|
127
|
+
kind: `handoff session=${sourceSession}`,
|
|
128
|
+
source: HANDOFF_CONTEXT_REL,
|
|
129
|
+
});
|
|
101
130
|
remove(); // consume-once: the file never outlives its first injection
|
|
102
|
-
return
|
|
131
|
+
return guardedInjection(block, `handoff consumed (session=${sourceSession})`);
|
|
103
132
|
}
|
|
104
133
|
|
|
105
134
|
/**
|
|
@@ -117,7 +146,12 @@ export function consume_handoff_context(root: string, now: Date = new Date()): C
|
|
|
117
146
|
* - consume-on-read, MOVED not copied: every outcome except `absent`
|
|
118
147
|
* relocates the file to `recycle-envelope.consumed.json` — it can never
|
|
119
148
|
* leak into a second session, while the last envelope stays
|
|
120
|
-
* inspectable for debugging
|
|
149
|
+
* inspectable for debugging;
|
|
150
|
+
* - drift (Phase 3.2): the envelope's recorded repo identity + branch +
|
|
151
|
+
* HEAD are compared against the tree it lands in, and any mismatch LEADS
|
|
152
|
+
* the injected block. Never a silent stale resume;
|
|
153
|
+
* - focus (Phase 3.4): `AGENT_RESUME_FOCUS` narrows what the successor
|
|
154
|
+
* attacks first — the consumer-side mirror of `next_task`.
|
|
121
155
|
*/
|
|
122
156
|
export function consume_recycle_envelope(root: string, now: Date = new Date()): ConsumeDecision {
|
|
123
157
|
const target = process.env.AGENT_RECYCLE_ENVELOPE_FILE || path.join(root, RECYCLE_ENVELOPE_REL);
|
|
@@ -178,16 +212,30 @@ export function consume_recycle_envelope(root: string, now: Date = new Date()):
|
|
|
178
212
|
return { action: 'discard', reason: 'recycle envelope workspace does not resolve' };
|
|
179
213
|
}
|
|
180
214
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
215
|
+
// Drift first: a stale resume against the wrong tree is the failure the
|
|
216
|
+
// reader must see before anything else in the block.
|
|
217
|
+
const drift = describeDrift(envelope, collectRepoAnchor(root));
|
|
218
|
+
// Proposal fields carrying an imperative LEAD the block as a stop
|
|
219
|
+
// notice — surfaced, never executed, never silently stripped.
|
|
220
|
+
const warnings = scanEnvelopeDirectives(envelope);
|
|
221
|
+
const focus = (process.env.AGENT_RESUME_FOCUS ?? '').trim();
|
|
222
|
+
const block = wrapAsPriorSessionData(JSON.stringify(envelope, null, 2), {
|
|
223
|
+
kind: 'recycle-envelope',
|
|
224
|
+
source: RECYCLE_ENVELOPE_REL,
|
|
225
|
+
warnings: [
|
|
226
|
+
...drift,
|
|
227
|
+
...(focus ? [`FOCUS: attack "${focus}" first — the rest of this envelope is context.`] : []),
|
|
228
|
+
'Re-derive everything under not_carried_forward from source before trusting it.',
|
|
229
|
+
...warnings,
|
|
230
|
+
],
|
|
231
|
+
});
|
|
189
232
|
consume(); // moved, not copied: the envelope never outlives its first injection
|
|
190
|
-
return
|
|
233
|
+
return guardedInjection(
|
|
234
|
+
block,
|
|
235
|
+
warnings.length > 0
|
|
236
|
+
? `recycle envelope consumed (${warnings.length} directive warning(s) surfaced)`
|
|
237
|
+
: 'recycle envelope consumed',
|
|
238
|
+
);
|
|
191
239
|
}
|
|
192
240
|
|
|
193
241
|
// ---------------------------------------------------------------------
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
* reduces to a non-blocking warn on claude. `AGENT_CONFIG_REPLAY=1` → no-op.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import * as crypto from 'node:crypto';
|
|
32
31
|
import * as fs from 'node:fs';
|
|
33
32
|
import * as path from 'node:path';
|
|
34
33
|
import process from 'node:process';
|
|
@@ -38,6 +37,8 @@ import recycleThresholdConfig from '../../config/recycle-threshold-budget.json';
|
|
|
38
37
|
|
|
39
38
|
import {
|
|
40
39
|
emptyCounters,
|
|
40
|
+
eolSessionKey,
|
|
41
|
+
eolStateFile,
|
|
41
42
|
readNewCompleteLines,
|
|
42
43
|
scanEolSlice,
|
|
43
44
|
type EolCounters,
|
|
@@ -73,20 +74,19 @@ function str(value: JsonValue | undefined): string {
|
|
|
73
74
|
return typeof value === 'string' ? value : '';
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function stateFile(workspaceRoot: string, sessionKey: string): string {
|
|
81
|
-
return path.join(stateDir(workspaceRoot), `${sessionKey}.json`);
|
|
82
|
-
}
|
|
77
|
+
/** Re-exported so existing importers keep one path; derivation lives in the lib. */
|
|
78
|
+
export const stateFile = eolStateFile;
|
|
83
79
|
|
|
84
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Same derivation as `end_review_nudge_hook.deriveSessionKey` — hashed, never
|
|
82
|
+
* raw. The hashing itself is `eolSessionKey`, shared with the readers so a
|
|
83
|
+
* reader can never key a state file differently than the writer wrote it.
|
|
84
|
+
*/
|
|
85
85
|
export function deriveSessionKey(envelope: JsonObject, payload: JsonObject): string {
|
|
86
|
-
|
|
86
|
+
return eolSessionKey(
|
|
87
87
|
str(envelope['session_id'] as JsonValue | undefined) ||
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
str((payload['transcript_path'] ?? payload['transcriptPath']) as JsonValue | undefined),
|
|
89
|
+
);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/** Load prior per-session state; any read/shape error reads as "fresh session". */
|
package/src/scripts/install.ts
CHANGED
|
@@ -975,13 +975,17 @@ function ensure_agent_settings(
|
|
|
975
975
|
success(`${SETTINGS_FILE} created (rule_loading_tier=${profile}${suffix})`);
|
|
976
976
|
}
|
|
977
977
|
|
|
978
|
-
function ensure_vscode_bridge(
|
|
978
|
+
function ensure_vscode_bridge(
|
|
979
|
+
project_root: string,
|
|
980
|
+
package_type: string,
|
|
981
|
+
force: boolean,
|
|
982
|
+
): Record<string, unknown>[] {
|
|
979
983
|
const plugin_paths: Record<string, string> = {
|
|
980
984
|
npm: './node_modules/@event4u/agent-config/plugin/agent-config',
|
|
981
985
|
};
|
|
982
986
|
const plugin_path = plugin_paths[package_type] ?? './plugin/agent-config';
|
|
983
987
|
const bridge = { 'chat.pluginLocations': { [plugin_path]: true } };
|
|
984
|
-
merge_json_file(
|
|
988
|
+
return merge_json_file(
|
|
985
989
|
path.join(project_root, '.vscode', 'settings.json'),
|
|
986
990
|
bridge,
|
|
987
991
|
force,
|
|
@@ -1043,8 +1047,11 @@ function _remove_legacy_augment_trampolines(): void {
|
|
|
1043
1047
|
fs.unlinkSync(legacy);
|
|
1044
1048
|
skip(`removed legacy ~/.augment/hooks/${name}`);
|
|
1045
1049
|
}
|
|
1046
|
-
} catch {
|
|
1047
|
-
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
// A swallowed failure leaves a stale trampoline that no manifest entry
|
|
1052
|
+
// covers, so uninstall cannot reach it. Non-fatal and semantics-neutral:
|
|
1053
|
+
// the removal is still best-effort, it is simply no longer silent.
|
|
1054
|
+
warn(`could not remove legacy ${legacy}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1048
1055
|
}
|
|
1049
1056
|
}
|
|
1050
1057
|
}
|
|
@@ -5075,8 +5082,15 @@ function _main_project_install(
|
|
|
5075
5082
|
const merged_keys_by_tool: Record<string, Record<string, unknown>[]> = {};
|
|
5076
5083
|
|
|
5077
5084
|
if (!opts.skip_bridges) {
|
|
5078
|
-
|
|
5079
|
-
|
|
5085
|
+
// `.vscode/settings.json` and `.augment/settings.json` are the two bridges
|
|
5086
|
+
// written unconditionally, so neither belongs to a selected tool. The
|
|
5087
|
+
// lockfile keys `merged_keys` by tool name (`_VALID_TOOLS`), so both are
|
|
5088
|
+
// recorded under the substrate entry `augment` — see
|
|
5089
|
+
// `docs/contracts/install-layout.md` § Untracked surfaces for the residual.
|
|
5090
|
+
merged_keys_by_tool['augment'] = [
|
|
5091
|
+
...ensure_vscode_bridge(project_root, package_type, opts.force),
|
|
5092
|
+
...ensure_augment_bridge(project_root, opts.force),
|
|
5093
|
+
];
|
|
5080
5094
|
if (_is_tool_enabled(tools, 'claude-code')) {
|
|
5081
5095
|
merged_keys_by_tool['claude-code'] = ensure_claude_bridge(project_root, opts.force);
|
|
5082
5096
|
}
|
|
@@ -15,6 +15,16 @@
|
|
|
15
15
|
* are allowed but not required — the top-level owner/review_by covers the
|
|
16
16
|
* file.
|
|
17
17
|
*
|
|
18
|
+
* ROW-LEVEL schema, opt-in (road-to-cost-parity-0-program § 1.2b/1.3): a
|
|
19
|
+
* budget file that declares a `row_schema` block additionally has every row
|
|
20
|
+
* of every top-level array checked for the fields it names — in practice
|
|
21
|
+
* `source` (so a baseline figure without a traceable origin cannot enter the
|
|
22
|
+
* file) plus the three honest-null fields. The honest-null contract is the
|
|
23
|
+
* part that binds: a row that records a `revised_from` without a
|
|
24
|
+
* `revision_evidence` pointer fails the file, because a target that moved
|
|
25
|
+
* without published evidence is precisely the drift the clause exists to
|
|
26
|
+
* refuse. Files that declare no `row_schema` are untouched by this half.
|
|
27
|
+
*
|
|
18
28
|
* Exit codes: 0 clean · 1 findings · 2 internal error.
|
|
19
29
|
*/
|
|
20
30
|
import * as fs from 'node:fs';
|
|
@@ -41,6 +51,76 @@ export function checkBudgetDoc(relPath: string, doc: Record<string, unknown>): s
|
|
|
41
51
|
return errors;
|
|
42
52
|
}
|
|
43
53
|
|
|
54
|
+
const DEFAULT_ROW_FIELDS = [
|
|
55
|
+
'id',
|
|
56
|
+
'unit',
|
|
57
|
+
'baseline',
|
|
58
|
+
'source',
|
|
59
|
+
'revisable',
|
|
60
|
+
'revision_evidence',
|
|
61
|
+
'revised_from',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function isRow(value: unknown): value is Record<string, unknown> {
|
|
65
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Row-level checks for budget files that opt in via a `row_schema` block.
|
|
70
|
+
* Returns [] for every file that does not declare one.
|
|
71
|
+
*/
|
|
72
|
+
export function checkBudgetRows(relPath: string, doc: Record<string, unknown>): string[] {
|
|
73
|
+
const schema = doc['row_schema'];
|
|
74
|
+
if (!isRow(schema)) return [];
|
|
75
|
+
|
|
76
|
+
const declared = schema['required_row_fields'];
|
|
77
|
+
const required =
|
|
78
|
+
Array.isArray(declared) && declared.every((f) => typeof f === 'string')
|
|
79
|
+
? (declared as string[])
|
|
80
|
+
: DEFAULT_ROW_FIELDS;
|
|
81
|
+
|
|
82
|
+
const errors: string[] = [];
|
|
83
|
+
if (typeof doc['schema_version'] !== 'number') {
|
|
84
|
+
errors.push(`${relPath}: \`row_schema\` declared but no numeric top-level \`schema_version\``);
|
|
85
|
+
}
|
|
86
|
+
if (typeof doc['registered_at'] !== 'string' || !DATE_RE.test(doc['registered_at'])) {
|
|
87
|
+
errors.push(`${relPath}: \`row_schema\` declared but no valid top-level \`registered_at\` (YYYY-MM-DD)`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
91
|
+
if (!Array.isArray(value)) continue;
|
|
92
|
+
value.forEach((row, index) => {
|
|
93
|
+
if (!isRow(row)) return;
|
|
94
|
+
const id = typeof row['id'] === 'string' ? row['id'] : `#${index}`;
|
|
95
|
+
const where = `${relPath}: ${key}[${index}] (${id})`;
|
|
96
|
+
|
|
97
|
+
for (const field of required) {
|
|
98
|
+
if (!(field in row)) errors.push(`${where}: missing \`${field}\``);
|
|
99
|
+
}
|
|
100
|
+
if ('source' in row) {
|
|
101
|
+
const source = row['source'];
|
|
102
|
+
if (typeof source !== 'string' || source.trim() === '') {
|
|
103
|
+
errors.push(`${where}: empty \`source\` — a baseline figure needs a traceable origin`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if ('revisable' in row && row['revisable'] !== true) {
|
|
107
|
+
errors.push(`${where}: \`revisable\` must be true — an unrevisable target is not honest-null`);
|
|
108
|
+
}
|
|
109
|
+
// The honest-null contract: a moved target must publish what moved it.
|
|
110
|
+
const revisedFrom = row['revised_from'];
|
|
111
|
+
if (revisedFrom !== null && revisedFrom !== undefined) {
|
|
112
|
+
const evidence = row['revision_evidence'];
|
|
113
|
+
if (typeof evidence !== 'string' || evidence.trim() === '') {
|
|
114
|
+
errors.push(
|
|
115
|
+
`${where}: sets \`revised_from\` without a \`revision_evidence\` pointer — a revision without published evidence`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return errors;
|
|
122
|
+
}
|
|
123
|
+
|
|
44
124
|
export function budgetFiles(dir: string = CONFIG_DIR): string[] {
|
|
45
125
|
try {
|
|
46
126
|
return fs
|
|
@@ -85,6 +165,7 @@ export function main(): number {
|
|
|
85
165
|
continue;
|
|
86
166
|
}
|
|
87
167
|
errors.push(...checkBudgetDoc(rel, doc));
|
|
168
|
+
errors.push(...checkBudgetRows(rel, doc));
|
|
88
169
|
const review = doc['review_by'];
|
|
89
170
|
if (typeof review === 'string' && DATE_RE.test(review) && review < today) {
|
|
90
171
|
process.stderr.write(
|