@event4u/agent-config 9.35.0 → 10.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 +95 -142
- package/agents/templates/.ai-council.yml.example +31 -51
- package/dist/agent-src/commands/analyze/conformance.md +15 -4
- package/dist/agent-src/commands/analyze/reference-repo.md +135 -42
- package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +53 -6
- package/dist/agent-src/rules/improve-before-implement.md +1 -1
- package/dist/agent-src/rules/session-canary.md +28 -0
- package/dist/agent-src/rules/ui-audit-gate.md +35 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +2 -2
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +84 -12
- package/dist/agent-src/skills/fe-design/evals/triggers.json +11 -3
- package/dist/agent-src/skills/prediction-pool-optimizer/SKILL.md +5 -5
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +24 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +29 -0
- package/dist/agent-src/templates/AGENTS.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-2TGMGLBL.js → chunk-2H5SE7DU.js} +35 -5
- package/dist/cli-delegate/{chunk-DS6BLDMG.js → chunk-3JUTT26I.js} +8 -3
- package/dist/cli-delegate/{chunk-ZAXH3ZWU.js → chunk-HJSKTMIF.js} +14 -13
- package/dist/cli-delegate/{chunk-QEEB4LHO.js → chunk-LRXJK6GN.js} +1 -1
- package/dist/cli-delegate/{chunk-736JCWLM.js → chunk-M2DPGFZ4.js} +10 -0
- package/dist/cli-delegate/{chunk-H6F2CQFI.js → chunk-NX5AXSLD.js} +3 -3
- package/dist/cli-delegate/{chunk-AG47LAGQ.js → chunk-TVGXH7VA.js} +1 -1
- package/dist/cli-delegate/{chunk-55RI5UXK.js → chunk-XT2YTJCZ.js} +22 -18
- package/dist/cli-delegate/{chunk-JEJABGKB.js → chunk-XTC5ARB6.js} +16 -12
- 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 +1038 -197
- package/dist/cli-delegate/cmd_doctor.js +4 -4
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- 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 +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_refresh.js +1 -1
- 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 +9 -2
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +14 -14
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.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 +654 -411
- package/dist/install/install.mjs +39 -29
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1580 -617
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +39 -25
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +15 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/routes/wizard.js +23 -3
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/server/schemas/settings.js +11 -6
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +16 -9
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +46 -4
- package/package.json +2 -2
- package/src/agent-src/templates/AGENTS.md +1 -1
- package/src/config/agent-settings.template.yml +38 -36
- package/src/config/ci-local-parity.yml +44 -0
- package/src/config/gate-coverage.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +32 -0
- package/src/scripts/_cli/cmd_migrate.ts +26 -3
- package/src/scripts/_cli/cmd_refresh.ts +29 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +40 -2
- package/src/scripts/_lib/agent_settings.ts +39 -25
- package/src/scripts/_lib/environment_detector.ts +52 -1
- package/src/scripts/_lib/hook_settings.ts +79 -0
- package/src/scripts/_lib/ui_surface.ts +97 -0
- package/src/scripts/ai_council/config.ts +62 -24
- package/src/scripts/before_complete_hook.ts +39 -0
- package/src/scripts/capture_skill_catalogue.ts +483 -0
- package/src/scripts/check_bridge_derivation.ts +89 -3
- package/src/scripts/check_ci_local_parity.ts +74 -0
- package/src/scripts/check_references.ts +15 -1
- package/src/scripts/condense.ts +16 -2
- package/src/scripts/conformance_scan.ts +200 -9
- package/src/scripts/council_cli.ts +95 -3
- package/src/scripts/hook_manifest.yaml +30 -9
- package/src/scripts/hooks/block_no_verify.ts +82 -9
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/design_slop_hook.ts +1 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +138 -105
- package/src/scripts/hooks/ui_route_nudge_hook.ts +250 -0
- package/src/scripts/lint_roadmap_complexity.ts +60 -0
- package/src/scripts/lint_rule_skill_pack_reach.ts +263 -0
- package/src/scripts/probe_promissory_closing.ts +180 -0
- package/src/scripts/probe_session_canary.ts +180 -0
- package/src/scripts/report_consultation_rate.ts +358 -0
- package/src/scripts/report_skill_activation.ts +5 -1
- package/src/server/schemas/settings.ts +14 -13
- /package/dist/agent-src/skills/design-system-capture/{reference → references}/design-system-json.md +0 -0
- /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/ev-fixtures.md +0 -0
- /package/dist/agent-src/skills/prediction-pool-optimizer/{reference → references}/odds-and-bonus.md +0 -0
- /package/dist/agent-src/skills/react-shadcn-ui/{reference → references}/registry.md +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui_surface — the ONE definition of "this path is a UI surface".
|
|
3
|
+
*
|
|
4
|
+
* Three consumers need the same answer and were about to disagree about it:
|
|
5
|
+
* the anti-slop PreToolUse hook (which had the only copy), the UI-turn
|
|
6
|
+
* definition the catalogue analyzer counts against, and the route nudge.
|
|
7
|
+
* A UI-turn rate measured against one extension set and a nudge fired against
|
|
8
|
+
* another would produce a consultation rate that no fix could ever move,
|
|
9
|
+
* because the denominator and the trigger would be different populations.
|
|
10
|
+
*
|
|
11
|
+
* `.blade.php` is deliberately part of the set and was absent from the hook's
|
|
12
|
+
* original regex: a two-part extension does not match a `\.(…)$` alternation,
|
|
13
|
+
* so every Blade template — the single largest UI surface in a Laravel
|
|
14
|
+
* consumer — read as a non-UI file. That is why `isUiPath` matches on the
|
|
15
|
+
* path tail rather than on `path.extname`, which returns `.php` for
|
|
16
|
+
* `page.blade.php` and would silently reproduce the same hole.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Single-segment UI extensions, without the leading dot. */
|
|
20
|
+
export const UI_EXTENSIONS = [
|
|
21
|
+
'html',
|
|
22
|
+
'htm',
|
|
23
|
+
'css',
|
|
24
|
+
'scss',
|
|
25
|
+
'sass',
|
|
26
|
+
'less',
|
|
27
|
+
'vue',
|
|
28
|
+
'svelte',
|
|
29
|
+
'astro',
|
|
30
|
+
'jsx',
|
|
31
|
+
'tsx',
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Multi-segment UI suffixes. These cannot be expressed as an extension
|
|
36
|
+
* alternation — `path.extname('x.blade.php')` is `.php`.
|
|
37
|
+
*/
|
|
38
|
+
export const UI_COMPOUND_SUFFIXES = ['.blade.php'] as const;
|
|
39
|
+
|
|
40
|
+
/** Equivalent to the historical hook regex, plus the compound suffixes. */
|
|
41
|
+
export const UI_EXT = new RegExp(
|
|
42
|
+
`(\\.(${UI_EXTENSIONS.join('|')})|${UI_COMPOUND_SUFFIXES.map((s) => s.replace(/\./g, '\\.')).join('|')})$`,
|
|
43
|
+
'i',
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/** True when `p` names a file on a UI surface. Case-insensitive. */
|
|
47
|
+
export function isUiPath(p: string): boolean {
|
|
48
|
+
return UI_EXT.test(p);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Directory fragments that mark a UI tree across frameworks. The two UI rules
|
|
53
|
+
* carried only the Laravel pair (`resources/views/`, `resources/js/`), so a
|
|
54
|
+
* React, Vue, Next or Svelte consumer writing to `src/components/` matched no
|
|
55
|
+
* path trigger at all. Kept as fragments, not anchored prefixes: a consumer's
|
|
56
|
+
* UI tree is rarely at the repo root.
|
|
57
|
+
*
|
|
58
|
+
* `app/` is deliberately NOT in this list although it is the Next.js router
|
|
59
|
+
* directory. In a Laravel consumer `app/` is the entire backend, so the
|
|
60
|
+
* fragment would fire on every controller, model and job — a trigger that
|
|
61
|
+
* over-fires on the largest directory of a whole ecosystem is worse than the
|
|
62
|
+
* gap it closes. Next.js route files are caught by extension instead
|
|
63
|
+
* (`page.tsx`, `layout.tsx` match `isUiPath`), which is the precise signal.
|
|
64
|
+
*/
|
|
65
|
+
export const UI_PATH_FRAGMENTS = [
|
|
66
|
+
'resources/views/',
|
|
67
|
+
'resources/js/',
|
|
68
|
+
// `components/` already subsumes `src/components/` under substring
|
|
69
|
+
// matching — listing both here would be dead weight. The rules' own
|
|
70
|
+
// `path_prefix` list keeps both because matching there is anchored.
|
|
71
|
+
'components/',
|
|
72
|
+
'pages/',
|
|
73
|
+
] as const;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fragments that sit INSIDE a UI tree and are not UI. Checked first, because a
|
|
77
|
+
* substring fragment cannot express "pages/ but not pages/api/" on its own —
|
|
78
|
+
* and `pages/api/` is server-only Next.js code, the exact over-fire the `app/`
|
|
79
|
+
* exclusion above exists to avoid. Leaving it in would have put backend files
|
|
80
|
+
* in the pre-registered UI-turn denominator, which is worse than the nudge
|
|
81
|
+
* firing on them: a rate is harder to un-break than a warning.
|
|
82
|
+
*/
|
|
83
|
+
export const UI_TREE_EXCLUSIONS = ['pages/api/'] as const;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* True when `p` sits inside a conventional UI tree.
|
|
87
|
+
*
|
|
88
|
+
* Note this does NOT require a UI extension — that is the point: a `.js` file
|
|
89
|
+
* under `resources/js/` is UI. The cost is that a non-code file in a UI tree
|
|
90
|
+
* (a README, a fixture) also matches; callers that need the narrower answer
|
|
91
|
+
* combine this with `isUiPath`.
|
|
92
|
+
*/
|
|
93
|
+
export function isUiTreePath(p: string): boolean {
|
|
94
|
+
const normalized = p.replace(/\\/g, '/').toLowerCase();
|
|
95
|
+
if (UI_TREE_EXCLUSIONS.some((fragment) => normalized.includes(fragment))) return false;
|
|
96
|
+
return UI_PATH_FRAGMENTS.some((fragment) => normalized.includes(fragment));
|
|
97
|
+
}
|
|
@@ -639,6 +639,20 @@ export interface CouncilConfig {
|
|
|
639
639
|
readonly low_impact: LowImpactConfig;
|
|
640
640
|
readonly lens_overrides: LensOverridesConfig;
|
|
641
641
|
readonly source_path: PathLike | null;
|
|
642
|
+
/**
|
|
643
|
+
* Transport keys the config file still carries and the loader deliberately
|
|
644
|
+
* IGNORED — dotted paths, e.g. `defaults.mode`, `members.anthropic.mode`.
|
|
645
|
+
*
|
|
646
|
+
* Transport is resolved per machine per invocation, never configured: the
|
|
647
|
+
* chain is `cli → api → unavailable`, and an airgapped host forces the
|
|
648
|
+
* `api` rung. Deleting the keys from the schema outright would have made
|
|
649
|
+
* every pre-existing installation fail to load, so a stale key is
|
|
650
|
+
* accepted, ignored, and reported once — the migration IS the ignore.
|
|
651
|
+
*
|
|
652
|
+
* Empty on a config that never carried one, which is the shape the current
|
|
653
|
+
* template ships.
|
|
654
|
+
*/
|
|
655
|
+
readonly ignored_transport_keys: readonly string[];
|
|
642
656
|
}
|
|
643
657
|
|
|
644
658
|
// ── default factories (dataclass defaults) ─────────────────────────
|
|
@@ -796,7 +810,11 @@ export function _build_config(raw: Dict, source_path: PathLike): CouncilConfig {
|
|
|
796
810
|
throw new CouncilConfigError('`enabled` must be a bool.');
|
|
797
811
|
}
|
|
798
812
|
|
|
799
|
-
const
|
|
813
|
+
const ignored_transport_keys: string[] = [];
|
|
814
|
+
const defaults = _build_defaults(
|
|
815
|
+
_asDict(_getOr(raw, 'defaults', {})),
|
|
816
|
+
ignored_transport_keys,
|
|
817
|
+
);
|
|
800
818
|
const cost_budget = _build_cost_budget(
|
|
801
819
|
_asDict(_getOr(raw, 'cost_budget', {})),
|
|
802
820
|
);
|
|
@@ -809,7 +827,12 @@ export function _build_config(raw: Dict, source_path: PathLike): CouncilConfig {
|
|
|
809
827
|
for (const [name, cfg] of Object.entries(members_raw)) {
|
|
810
828
|
members.set(
|
|
811
829
|
name,
|
|
812
|
-
_build_member(
|
|
830
|
+
_build_member(
|
|
831
|
+
name,
|
|
832
|
+
_asDict(_orEmpty(cfg)),
|
|
833
|
+
defaults.mode,
|
|
834
|
+
ignored_transport_keys,
|
|
835
|
+
),
|
|
813
836
|
);
|
|
814
837
|
}
|
|
815
838
|
|
|
@@ -920,6 +943,7 @@ export function _build_config(raw: Dict, source_path: PathLike): CouncilConfig {
|
|
|
920
943
|
low_impact,
|
|
921
944
|
lens_overrides,
|
|
922
945
|
source_path,
|
|
946
|
+
ignored_transport_keys,
|
|
923
947
|
};
|
|
924
948
|
}
|
|
925
949
|
|
|
@@ -1508,23 +1532,36 @@ function _build_consensus_scoring(d: Dict): ConsensusScoringConfig {
|
|
|
1508
1532
|
|
|
1509
1533
|
const _VALID_MEMBER_MODES: ReadonlySet<string> = new Set(['cli', 'api']);
|
|
1510
1534
|
|
|
1511
|
-
function _build_defaults(d: Dict): DefaultsConfig {
|
|
1535
|
+
function _build_defaults(d: Dict, ignored: string[] = []): DefaultsConfig {
|
|
1512
1536
|
if (!_isDict(d)) {
|
|
1513
1537
|
throw new CouncilConfigError('`defaults` must be a mapping.');
|
|
1514
1538
|
}
|
|
1515
|
-
//
|
|
1516
|
-
//
|
|
1517
|
-
//
|
|
1518
|
-
//
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1539
|
+
// Transport is RESOLVED, never configured.
|
|
1540
|
+
//
|
|
1541
|
+
// The CLI-first flip (road-to-always-on-orchestration Phase 3.1) changed
|
|
1542
|
+
// only what a config that OMITS `mode` receives. Every installation that
|
|
1543
|
+
// spelled the key out — which the wizard did, and which every pre-flip
|
|
1544
|
+
// template shipped — kept its old `api` value forever, silently, and went
|
|
1545
|
+
// on paying per token while a subscription CLI sat unused on the same
|
|
1546
|
+
// machine. A default that only fires on an absent key does not migrate
|
|
1547
|
+
// anybody; it just makes the docs read better than the behaviour.
|
|
1548
|
+
//
|
|
1549
|
+
// So the key is no longer read. `auto` is the only value this loader
|
|
1550
|
+
// produces, and `transport_resolver` picks the concrete rung per machine
|
|
1551
|
+
// per invocation (`cli → api → unavailable`, airgap forcing `api`). A file
|
|
1552
|
+
// still carrying `mode:` loads fine and is reported via
|
|
1553
|
+
// `ignored_transport_keys` — see `CouncilConfig`.
|
|
1554
|
+
// `null` as the sentinel, not `undefined`: `_get`'s default parameter is
|
|
1555
|
+
// typed `Json`, which excludes `undefined`. A config spelling `mode: null`
|
|
1556
|
+
// is indistinguishable from an absent key here, and that is correct — a
|
|
1557
|
+
// null transport is not a pin worth reporting.
|
|
1558
|
+
if (_get(d, 'mode', null) !== null) {
|
|
1559
|
+
ignored.push('defaults.mode');
|
|
1560
|
+
}
|
|
1561
|
+
const mode = 'auto';
|
|
1525
1562
|
// `member_mode` (step-9 P8 · U1) — global preference for solo /
|
|
1526
|
-
// CLI-mode invocations. Narrower set than `defaults.mode`
|
|
1527
|
-
// `manual` makes no sense as a per-member dispatch default.
|
|
1563
|
+
// CLI-mode invocations. Narrower set than the retired `defaults.mode`
|
|
1564
|
+
// because `manual` makes no sense as a per-member dispatch default.
|
|
1528
1565
|
const member_mode = _get(d, 'member_mode', 'cli');
|
|
1529
1566
|
if (!(_isStr(member_mode) && _VALID_MEMBER_MODES.has(member_mode))) {
|
|
1530
1567
|
throw new CouncilConfigError(
|
|
@@ -1726,6 +1763,7 @@ function _build_member(
|
|
|
1726
1763
|
// a future caller that omits the third argument observes the same
|
|
1727
1764
|
// doctrine rather than silently reverting to the retired `api` default.
|
|
1728
1765
|
default_mode = 'auto',
|
|
1766
|
+
ignored: string[] = [],
|
|
1729
1767
|
): MemberConfig {
|
|
1730
1768
|
if (!_VALID_PROVIDERS.has(name)) {
|
|
1731
1769
|
throw new CouncilConfigError(
|
|
@@ -1738,15 +1776,15 @@ function _build_member(
|
|
|
1738
1776
|
const model = _pyTruthy(modelVal) ? (modelVal as string) : '';
|
|
1739
1777
|
const api_key_ref_raw = _get(cfg, 'api_key_ref', null);
|
|
1740
1778
|
const api_key_ref = api_key_ref_raw === undefined ? null : api_key_ref_raw;
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
const effective_mode =
|
|
1779
|
+
// Per-member transport is no longer configurable either — same reasoning
|
|
1780
|
+
// as `_build_defaults`. A member that pinned `mode: api` would reintroduce
|
|
1781
|
+
// exactly the silent-spend path the global key was removed for, one
|
|
1782
|
+
// provider at a time. Recorded as ignored, never read.
|
|
1783
|
+
if (_get(cfg, 'mode', null) !== null) {
|
|
1784
|
+
ignored.push(`members.${name}.mode`);
|
|
1785
|
+
}
|
|
1786
|
+
const member_mode: string | null = null;
|
|
1787
|
+
const effective_mode = default_mode;
|
|
1750
1788
|
const binary_raw = _get(cfg, 'binary', null);
|
|
1751
1789
|
const binary = binary_raw === undefined ? null : binary_raw;
|
|
1752
1790
|
if (binary !== null) {
|
|
@@ -113,6 +113,14 @@ function _empty_state(): StateDict {
|
|
|
113
113
|
verified_this_turn: false,
|
|
114
114
|
ci_saw_pending: false,
|
|
115
115
|
nonevidence_this_turn: 0,
|
|
116
|
+
// Round 7 § Phase 1 — the last CI read, SESSION-scoped on purpose. Every
|
|
117
|
+
// other counter here is turn-scoped because a stale POSITIVE would wrongly
|
|
118
|
+
// vouch ("I verified"). This one is a NEGATIVE ("CI was not settled"), and a
|
|
119
|
+
// stale negative only ever refuses more often — so surviving the turn
|
|
120
|
+
// boundary preserves the freshness invariant rather than bypassing it. It
|
|
121
|
+
// has to survive it: the measured failure is a completion claim in a LATER
|
|
122
|
+
// turn than the poll it rests on.
|
|
123
|
+
ci_last: null,
|
|
116
124
|
checked_at: _now(),
|
|
117
125
|
};
|
|
118
126
|
}
|
|
@@ -303,6 +311,16 @@ function _update(state: StateDict, event: string, envelope: StateDict): StateDic
|
|
|
303
311
|
// Session boundary — reset session-scoped counters.
|
|
304
312
|
state["session_id"] = session_id;
|
|
305
313
|
state["verifications_this_session"] = 0;
|
|
314
|
+
// R2 finding 1 (high). `ci_last` is SESSION-scoped, and until this line
|
|
315
|
+
// nothing cleared it: `_reset_turn` clears the turn-scoped witness,
|
|
316
|
+
// `_empty_state` sets it null only for a state file that does not exist yet,
|
|
317
|
+
// and `_load_state` merges the persisted value over that default. So session
|
|
318
|
+
// A polled CI, saw pending, and ended; session B — a doc-only session that
|
|
319
|
+
// never touched CI — said "Fertig" and was REFUSED, contradicting both the
|
|
320
|
+
// consumer's own comment ("a session that never polled CI must never be
|
|
321
|
+
// refused for it") and the negative case the roadmap pinned. Session-scoped
|
|
322
|
+
// has to mean cleared at the boundary, or it means never cleared.
|
|
323
|
+
state["ci_last"] = null;
|
|
306
324
|
state = _reset_turn(state, session_id);
|
|
307
325
|
}
|
|
308
326
|
|
|
@@ -338,6 +356,27 @@ function _update(state: StateDict, event: string, envelope: StateDict): StateDic
|
|
|
338
356
|
} else {
|
|
339
357
|
counts = false; // unrecognised or unreadable — never read as settled
|
|
340
358
|
}
|
|
359
|
+
// Round 7 § 1.1 — the session-scoped negative the turn-end consumer reads.
|
|
360
|
+
//
|
|
361
|
+
// R2 finding 3 (medium), and it was the sharpest one: this said `settled:
|
|
362
|
+
// pending === 0` and CLAIMED "the same discrimination `counts` makes
|
|
363
|
+
// above". It was not. `counts` requires `!vacuous && ci_saw_pending`;
|
|
364
|
+
// `pending === 0` alone drops both. A post-push poll that reads a stale
|
|
365
|
+
// all-pass table returns pending 0 — verbatim the FC-3b failure documented
|
|
366
|
+
// twenty lines up — and recorded `settled: true`, so the completion
|
|
367
|
+
// detector went SILENT in exactly the premature-claim case it exists for.
|
|
368
|
+
// A detector defeated by the false settle it was built to catch is worse
|
|
369
|
+
// than no detector, because it reports coverage.
|
|
370
|
+
//
|
|
371
|
+
// Now it reuses `counts` itself, which is that discrimination rather than
|
|
372
|
+
// a paraphrase of it. `pending === null` (unreadable, and "no checks
|
|
373
|
+
// reported") stays not-a-settle by construction.
|
|
374
|
+
state["ci_last"] = {
|
|
375
|
+
at: _now(),
|
|
376
|
+
command: cmd.slice(0, 512),
|
|
377
|
+
pending,
|
|
378
|
+
settled: pending === 0 && counts === true,
|
|
379
|
+
};
|
|
341
380
|
}
|
|
342
381
|
|
|
343
382
|
state["last_verification"] = {
|