@event4u/agent-config 9.32.0 → 9.34.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 +85 -72
- package/dist/agent-src/commands/agent-handoff.md +15 -3
- 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/orchestration-telemetry.md +3 -0
- package/dist/agent-src/rules/delegation-policy.md +6 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
- package/dist/agent-src/scripts/roadmap_gates.ts +107 -3
- package/dist/agent-src/skills/corpus-grounding/SKILL.md +21 -12
- package/dist/agent-src/skills/design-tokens/SKILL.md +15 -0
- 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/react-shadcn-ui/SKILL.md +24 -0
- package/dist/agent-src/skills/review-routing/SKILL.md +23 -6
- package/dist/agent-src/skills/rule-writing/SKILL.md +23 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +80 -2
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +5 -3
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +24 -0
- 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/cli/registry.js +4 -4
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-HTIU3W56.js +66 -0
- package/dist/cli-delegate/{chunk-2WELHO3P.js → chunk-I5EAFRII.js} +35 -0
- package/dist/cli-delegate/{chunk-ITIQSGOQ.js → chunk-MA4GFWDY.js} +7 -4
- package/dist/cli-delegate/{chunk-PFBV6XRT.js → chunk-Q26HT4QT.js} +1 -63
- package/dist/cli-delegate/{chunk-542DZR4P.js → chunk-REMEZSVP.js} +1660 -146
- package/dist/cli-delegate/{chunk-3EWYC3YK.js → chunk-UEZ7D35A.js} +4 -3
- package/dist/cli-delegate/{chunk-LVRYBKTI.js → chunk-YPAWXXXR.js} +158 -0
- package/dist/cli-delegate/cmd_conformance.js +256 -226
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +21 -28
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +20 -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 +17 -17
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +2 -2
- package/dist/discovery/dormancy-report.md +17 -0
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +3 -3
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +3 -3
- package/dist/hooks/dispatch.js +114 -7
- package/dist/install/install.mjs +7 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3755 -1168
- package/docs/guidelines/agent-infra/failure-signatures.md +47 -12
- package/package.json +2 -2
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +35 -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/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 +25 -4
- package/src/scripts/_dispatch.bash +5 -3
- package/src/scripts/_lib/env_kill_switch.ts +37 -0
- package/src/scripts/_lib/orchestration_record.ts +45 -0
- package/src/scripts/_lib/security_lint.ts +18 -4
- package/src/scripts/_lib/self_repair.ts +60 -0
- package/src/scripts/_lib/self_repair_store.ts +65 -5
- package/src/scripts/_lib/subagent_capsule.ts +78 -1
- package/src/scripts/ai_council/clients.ts +31 -0
- package/src/scripts/ai_council/events_log.ts +130 -8
- package/src/scripts/ai_council/orchestrator.ts +50 -3
- package/src/scripts/ai_council/pricing.ts +48 -1
- package/src/scripts/ai_council/prompts.ts +165 -3
- package/src/scripts/ai_council/quorum.ts +30 -0
- package/src/scripts/ai_council/session.ts +11 -0
- package/src/scripts/ai_team/review_gate.ts +2 -2
- package/src/scripts/ask_transport.ts +20 -1
- package/src/scripts/build_discovery_manifest.ts +165 -0
- package/src/scripts/cache_realization_report.ts +38 -0
- package/src/scripts/check_claims.ts +51 -2
- package/src/scripts/check_release_highlights.ts +7 -1
- package/src/scripts/ci_time_ratio.ts +3 -1
- 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/discovery_graph.ts +201 -27
- package/src/scripts/dispatch_economy_report.ts +5 -2
- package/src/scripts/dispatch_r2_reviewer.ts +102 -1
- package/src/scripts/install.ts +20 -6
- package/src/scripts/lint_budget_ownership.ts +81 -0
- package/src/scripts/lint_handoffs.ts +92 -2
- package/src/scripts/lint_hedge_words.ts +317 -0
- package/src/scripts/lint_mcp_config_security.ts +1 -1
- package/src/scripts/lint_skill_frontmatter_safety.ts +56 -2
- 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 +10 -0
- package/src/scripts/report_conformance_funnel.ts +40 -0
- package/src/scripts/schemas/subagent.schema.json +3 -2
- package/src/scripts/self_repair_cli.ts +26 -1
- package/src/scripts/skill_linter.ts +58 -2
|
@@ -12,18 +12,53 @@ probability cause.
|
|
|
12
12
|
|
|
13
13
|
## Lookup
|
|
14
14
|
|
|
15
|
-
| Signature (what you see) | Likely cause | First check |
|
|
16
|
-
|
|
17
|
-
| **Tool/agent loop** — same tool called >2× with near-identical args, no progress | Missing stop condition; reading without acting; ambiguous state | Stop. State the hypothesis in one line. Act or ask — do not re-read. (`context-hygiene` 15/25 rule) |
|
|
18
|
-
| **HTTP 429 / "rate limit" / "quota"** | Too-frequent calls; shared quota; missing backoff | Inspect response headers (`Retry-After`, `X-RateLimit-*`); add exponential backoff; check if a loop is firing the calls |
|
|
19
|
-
| **`ECONNREFUSED` / connection refused** | Service not up; wrong host/port; container not started | Is the service running? (`docker compose ps`, health endpoint); is the port right? (`lsof -i :PORT`); is it bound to `127.0.0.1` vs `0.0.0.0`? |
|
|
20
|
-
| **`ENOENT` / "no such file or directory"** | Wrong cwd; path relative vs absolute; file not generated yet | Print the *resolved absolute* path; check cwd; confirm a prior step actually wrote the file |
|
|
21
|
-
| **Timeout / hang** | Blocking I/O; deadlock; awaiting a condition that never holds | What is it waiting on? Add a log right before the blocking call; bound the wait; check for a missing `await`/`return` |
|
|
22
|
-
| **`OOM` / killed (137) / heap out of memory** | Unbounded collection; loading all rows; leak in a loop | What grows per iteration? Stream/paginate instead of loading all; check `--max-old-space-size` only after finding the growth |
|
|
23
|
-
| **`EACCES` / permission denied** | Wrong file mode; running as wrong user; read-only mount | `ls -l` the target; check the process user; is the path a read-only volume? |
|
|
24
|
-
| **Flaky test (passes alone, fails in suite)** | Shared state; order dependence; real clock/random; unawaited async | Run the test in isolation vs in suite; look for shared fixtures, global state, `Date.now()`/random, missing `await` |
|
|
25
|
-
| **Works locally, fails in CI** | Env/config drift; missing service; absent secret; path/case sensitivity | Diff env (`printenv`); is the service/secret present in CI? Linux is case-sensitive — check filename casing |
|
|
26
|
-
| **Intermittent / "sometimes"** | Race; timing; network; non-deterministic order | Reproduce with condition-based waiting, not fixed sleeps (`systematic-debugging` § Condition-based waiting); log timestamps |
|
|
15
|
+
| ID | Signature (what you see) | Likely cause | First check |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `tool-loop` | **Tool/agent loop** — same tool called >2× with near-identical args, no progress | Missing stop condition; reading without acting; ambiguous state | Stop. State the hypothesis in one line. Act or ask — do not re-read. (`context-hygiene` 15/25 rule) |
|
|
18
|
+
| `rate-limited` | **HTTP 429 / "rate limit" / "quota"** | Too-frequent calls; shared quota; missing backoff | Inspect response headers (`Retry-After`, `X-RateLimit-*`); add exponential backoff; check if a loop is firing the calls |
|
|
19
|
+
| `connection-refused` | **`ECONNREFUSED` / connection refused** | Service not up; wrong host/port; container not started | Is the service running? (`docker compose ps`, health endpoint); is the port right? (`lsof -i :PORT`); is it bound to `127.0.0.1` vs `0.0.0.0`? |
|
|
20
|
+
| `path-not-found` | **`ENOENT` / "no such file or directory"** | Wrong cwd; path relative vs absolute; file not generated yet | Print the *resolved absolute* path; check cwd; confirm a prior step actually wrote the file |
|
|
21
|
+
| `hang` | **Timeout / hang** | Blocking I/O; deadlock; awaiting a condition that never holds | What is it waiting on? Add a log right before the blocking call; bound the wait; check for a missing `await`/`return` |
|
|
22
|
+
| `out-of-memory` | **`OOM` / killed (137) / heap out of memory** | Unbounded collection; loading all rows; leak in a loop | What grows per iteration? Stream/paginate instead of loading all; check `--max-old-space-size` only after finding the growth |
|
|
23
|
+
| `permission-denied` | **`EACCES` / permission denied** | Wrong file mode; running as wrong user; read-only mount | `ls -l` the target; check the process user; is the path a read-only volume? |
|
|
24
|
+
| `order-dependent-test` | **Flaky test (passes alone, fails in suite)** | Shared state; order dependence; real clock/random; unawaited async | Run the test in isolation vs in suite; look for shared fixtures, global state, `Date.now()`/random, missing `await` |
|
|
25
|
+
| `local-ci-drift` | **Works locally, fails in CI** | Env/config drift; missing service; absent secret; path/case sensitivity | Diff env (`printenv`); is the service/secret present in CI? Linux is case-sensitive — check filename casing |
|
|
26
|
+
| `race` | **Intermittent / "sometimes"** | Race; timing; network; non-deterministic order | Reproduce with condition-based waiting, not fixed sleeps (`systematic-debugging` § Condition-based waiting); log timestamps |
|
|
27
|
+
| `stale-capability-claim` | **Budget exhausted plus repeated attempts at a tool that is not available** | A capability the session claimed for itself instead of resolving — the council, a hook slot, a subagent primitive, an MCP server assumed present | Ask the live verb, not the tree: `agent-config hooks:status` for slot bindings, `agent-config routing:doctor` for per-field capability **provenance** (`registry` / `live-probe` / `default`), `agent-config council:status` for council availability. A `false` from `default` records that nobody answered, not that the host cannot |
|
|
28
|
+
|
|
29
|
+
### Naming convention for IDs
|
|
30
|
+
|
|
31
|
+
An id is a short kebab handle so a signature can be **cited by name** — the
|
|
32
|
+
first column was prose, so nothing could reference a row.
|
|
33
|
+
|
|
34
|
+
One rule governs new ids: **where a row's signature is a stable string the
|
|
35
|
+
tooling already prints, the id is that string verbatim** rather than a kebab
|
|
36
|
+
synonym. `skill_linter.ts` emits a closed set of `Issue` codes —
|
|
37
|
+
`missing_section`, `missing_conditional_section`, `invalid_execution_handler` —
|
|
38
|
+
and a row about one of those is looked up by the code the reader just saw in the
|
|
39
|
+
output. No current row is of that class; the convention is stated for the next
|
|
40
|
+
one, so the table does not accumulate two names for the same failure.
|
|
41
|
+
|
|
42
|
+
## Discrimination drills
|
|
43
|
+
|
|
44
|
+
A row's "first check" is only worth following if the documented signature is what
|
|
45
|
+
actually appears. Each drill breaks the named layer and asserts the documented
|
|
46
|
+
symptom — run it once when you doubt a row, or when adding one. They are text a
|
|
47
|
+
human runs; nothing here is a CI gate.
|
|
48
|
+
|
|
49
|
+
| ID | Drill — break this, expect that |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `tool-loop` | Ask for a fact that is genuinely absent from the tree (a config key nothing defines). Expect repeated searches with shrinking variation and no edit — that shape, not an error message, is the signature. |
|
|
52
|
+
| `rate-limited` | Fire the provider's own rate limit with a tight loop against a cheap endpoint. Expect `429` **plus** `Retry-After`; a `429` with no such header is a different failure (a gateway, not the API). |
|
|
53
|
+
| `connection-refused` | Stop the service and call it (`docker compose stop <svc>`). Expect `ECONNREFUSED` immediately. If it hangs instead, the host resolves but nothing listens — that is `hang`, not this row. |
|
|
54
|
+
| `path-not-found` | Run a path-taking script from a different cwd. Expect `ENOENT` naming a path that is *almost* right — the near-miss is the tell that the path is relative, not missing. |
|
|
55
|
+
| `hang` | `await` a promise nothing resolves. Expect no output and no exit, distinguishable from `race` by being 100% reproducible. |
|
|
56
|
+
| `out-of-memory` | Accumulate rows in an array inside a loop over a large fixture. Expect the heap message and exit 137 — and note that the reported line is where allocation failed, not where the growth is. |
|
|
57
|
+
| `permission-denied` | `chmod 000` a fixture and read it. Expect `EACCES`, not `ENOENT` — confusing the two sends you looking for a missing file that is right there. |
|
|
58
|
+
| `order-dependent-test` | Add a module-level mutable to two tests in one file. Expect green alone, red in suite. Reversing the file order should move the failure — if it does not, the cause is shared *external* state, not order. |
|
|
59
|
+
| `local-ci-drift` | Rename a fixture's casing (`Foo.ts` → `foo.ts`) and import the old name. Expect green on macOS, red on Linux CI. The generalisation: anything the local filesystem is lenient about. |
|
|
60
|
+
| `race` | Replace a condition-based wait with a fixed `sleep` shorter than the operation. Expect intermittent failure whose rate moves with machine load — load-sensitivity is what separates this from `order-dependent-test`. |
|
|
61
|
+
| `stale-capability-claim` | Assert a capability without asking for it, then compare against the live verb. `agent-config routing:doctor` prints provenance per field; a field whose provenance is `default` was never established by anything — which is exactly the state a confident self-claim is indistinguishable from. |
|
|
27
62
|
|
|
28
63
|
## How to use
|
|
29
64
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
3
|
"mcpName": "io.github.event4u-app/agent-config",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.34.0",
|
|
5
5
|
"description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including \"zero runtime daemon\".",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"build:mcp-catalog": "./scripts-run src/scripts/build_mcp_catalog --write --strict",
|
|
78
78
|
"build": "npm run build:cli && npm run build:install-bundle && npm run build:hooks && npm run build:mcp-bundle && npm run build:cli-delegate && npm run build:ui",
|
|
79
79
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.scripts.json",
|
|
80
|
-
"lint:ts": "eslint 'src/**/*.ts'",
|
|
80
|
+
"lint:ts": "eslint --cache 'src/**/*.ts'",
|
|
81
81
|
"test:ts": "vitest run",
|
|
82
82
|
"lint:publint": "publint",
|
|
83
83
|
"prepack": "npm run build && npm run build:discovery && npm run build:mcp-manifest && node src/scripts/prepack-check.mjs",
|
|
@@ -128,12 +128,42 @@ function _branch_touched_paths(root: string, base: string): Set<string> | null {
|
|
|
128
128
|
return out;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Paths where a repo-relative path is DATA, not a live reference — so
|
|
133
|
+
* rewriting it falsifies a record instead of keeping a link resolvable.
|
|
134
|
+
*
|
|
135
|
+
* Two classes, and the second is why this predicate exists at all:
|
|
136
|
+
*
|
|
137
|
+
* - `agents/evidence/**` — a committed record of what was true, or of what a
|
|
138
|
+
* reviewer was shown, at one point in time. Retroactively editing it makes it
|
|
139
|
+
* claim something that did not happen, which is the failure
|
|
140
|
+
* `evaluator-independence` exists to prevent ("the prompt ships with the
|
|
141
|
+
* verdict" is worthless if a later sweep can edit the prompt).
|
|
142
|
+
* - `*.patch` anywhere — a patch's `diff --git a/… b/…` and `+++ b/…` lines are
|
|
143
|
+
* the payload. Rewriting them detaches the patch from the commit it records
|
|
144
|
+
* AND makes `git apply` target a path the diff never created.
|
|
145
|
+
*
|
|
146
|
+
* Measured 2026-08-11: archiving one roadmap rewrote four frozen artefacts
|
|
147
|
+
* under `agents/evidence/reviews/`, including the `diff.patch` headers of a
|
|
148
|
+
* recorded review input. Nothing caught it — the sweep reported it as
|
|
149
|
+
* "4 ref(s) migrated", which reads like success.
|
|
150
|
+
*/
|
|
151
|
+
const _FROZEN_RECORD_PREFIXES: readonly string[] = ['agents/evidence/'];
|
|
152
|
+
|
|
153
|
+
/** Is `rel` a frozen record whose path strings must not be rewritten? */
|
|
154
|
+
export function _is_frozen_record(rel: string): boolean {
|
|
155
|
+
const norm = rel.split(path.sep).join('/');
|
|
156
|
+
return _FROZEN_RECORD_PREFIXES.some((p) => norm.startsWith(p)) || norm.endsWith('.patch');
|
|
157
|
+
}
|
|
158
|
+
|
|
131
159
|
/**
|
|
132
160
|
* Rewrite full-path references `old_rel` → `new_rel` in tracked files.
|
|
133
161
|
*
|
|
134
162
|
* Only the exact repo-relative path is rewritten (bare-filename mentions are
|
|
135
163
|
* left alone). The archived file's own path never matches because the search
|
|
136
|
-
* string is the un-archived path.
|
|
164
|
+
* string is the un-archived path. Frozen records are skipped — see
|
|
165
|
+
* `_is_frozen_record`; a link left dangling in a historical record is correct,
|
|
166
|
+
* because the record describes a tree that no longer exists.
|
|
137
167
|
*/
|
|
138
168
|
function _inbound_ref_rewrite(
|
|
139
169
|
root: string,
|
|
@@ -165,6 +195,10 @@ function _inbound_ref_rewrite(
|
|
|
165
195
|
// skip the roadmap file itself
|
|
166
196
|
continue;
|
|
167
197
|
}
|
|
198
|
+
if (_is_frozen_record(rel)) {
|
|
199
|
+
// A record of a past state — rewriting it falsifies the record.
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
168
202
|
const fp = path.join(root, rel);
|
|
169
203
|
let text: string;
|
|
170
204
|
try {
|
|
@@ -18,10 +18,34 @@
|
|
|
18
18
|
* Blocking a pipeline on "the user has an open decision" would turn a
|
|
19
19
|
* visibility aid into a second thing to fight.
|
|
20
20
|
*
|
|
21
|
+
* `--reply` is the same projection shaped for the END OF A CHAT REPLY rather
|
|
22
|
+
* than a terminal pane, and it exists because the pull channel above is only
|
|
23
|
+
* read by someone who already knows to ask. Transcript forensics over three
|
|
24
|
+
* consumer sessions found five "what do I do now?" follow-ups, every one of
|
|
25
|
+
* them next to a blocker the agent HAD reported — as a file reference, as a
|
|
26
|
+
* bare option count, or as a choice of substitute work. What the same corpus
|
|
27
|
+
* also shows is that a tool's output gets carried into the reply verbatim
|
|
28
|
+
* (the dashboard's step count appears word-for-word inside eight option
|
|
29
|
+
* blocks). So the fix is not to make the agent format better from memory; it
|
|
30
|
+
* is to hand it the finished text. See ADR-222.
|
|
31
|
+
*
|
|
32
|
+
* Two properties carry the contract mechanically:
|
|
33
|
+
* - Nothing owned by the user open ⇒ EMPTY output. "No blocker → no block"
|
|
34
|
+
* stops being a judgement call, so the command is safe to call
|
|
35
|
+
* unconditionally at reply-close.
|
|
36
|
+
* - Exactly ONE blocker is rendered in full — the one that unblocks most.
|
|
37
|
+
* Every other one is a single trailing line. The user is never handed a
|
|
38
|
+
* flat list to weigh themselves.
|
|
39
|
+
*
|
|
40
|
+
* Labels are English like the rest of the CLI surface; the agent mirrors them
|
|
41
|
+
* into the user's language when it lifts them into a reply, exactly as it does
|
|
42
|
+
* for `Recommendation:` / `Empfehlung:`.
|
|
43
|
+
*
|
|
21
44
|
* Invocation (from project root):
|
|
22
45
|
* ./agent-config gates # decisions owned by the user
|
|
23
46
|
* ./agent-config gates --all # every open blocker, grouped by owner
|
|
24
47
|
* ./agent-config gates --json # machine-readable
|
|
48
|
+
* ./agent-config gates --reply # reply-close form; empty when none
|
|
25
49
|
*/
|
|
26
50
|
|
|
27
51
|
import * as path from 'node:path';
|
|
@@ -209,6 +233,76 @@ function render(entries: readonly Entry[], all: boolean): string {
|
|
|
209
233
|
return lines.join('\n') + '\n';
|
|
210
234
|
}
|
|
211
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Reply-close form: the one blocking decision in full, the rest as one line.
|
|
238
|
+
*
|
|
239
|
+
* Deliberately narrower than `render()`. `Status` and `Owner` are roadmap-file
|
|
240
|
+
* metadata that a reader of a reply cannot act on, so they are dropped; what
|
|
241
|
+
* survives is what the user has to DO and how they will know it is done. The
|
|
242
|
+
* five-field shape stays where it is enforced — in the roadmap file — and does
|
|
243
|
+
* not follow the blocker into prose it would only pad.
|
|
244
|
+
*
|
|
245
|
+
* Returns '' when nothing is owned by the user, so the caller can append the
|
|
246
|
+
* result unconditionally and get silence when silence is correct.
|
|
247
|
+
*/
|
|
248
|
+
function renderReply(entries: readonly Entry[]): string {
|
|
249
|
+
const mine = entries.filter((e) => needsUser(e.blocker.owner));
|
|
250
|
+
if (mine.length === 0) {
|
|
251
|
+
return '';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// `collectEntries` already sorted by unblocking weight, so the first entry
|
|
255
|
+
// IS the one that matters most. Picking it here rather than asking the
|
|
256
|
+
// caller to choose is the point: a flat list is the failure mode.
|
|
257
|
+
const lead = mine[0] as Entry;
|
|
258
|
+
const rest = mine.slice(1);
|
|
259
|
+
const lines: string[] = [];
|
|
260
|
+
|
|
261
|
+
lines.push(`Blocked: ${lead.blocker.blocks} — ${lead.roadmapRel}`);
|
|
262
|
+
|
|
263
|
+
const steps = regroupTodo(lead.blocker.todo);
|
|
264
|
+
const doLabel = isLegacy(lead.blocker) ? 'Blocked until:' : 'Do:';
|
|
265
|
+
if (steps.length === 0) {
|
|
266
|
+
lines.push(
|
|
267
|
+
`${doLabel} (no steps recorded — the blocker entry needs a **What to do:** list)`,
|
|
268
|
+
);
|
|
269
|
+
} else if (steps.length === 1) {
|
|
270
|
+
lines.push(...wrap(`${doLabel} ${steps[0] as string}`, WIDTH, ' '));
|
|
271
|
+
} else {
|
|
272
|
+
// Emitted as authored, never re-numbered: real blocker entries already
|
|
273
|
+
// carry their own `1.` / `2.` markers (the CI-enforced `What to do:`
|
|
274
|
+
// shape), so adding our own would produce "1. 1. …". An entry that
|
|
275
|
+
// arrives unnumbered stays unnumbered — that is a fact about the
|
|
276
|
+
// roadmap entry, and papering over it here would hide it from the
|
|
277
|
+
// person who could fix it.
|
|
278
|
+
lines.push(`${doLabel}`);
|
|
279
|
+
for (const s of steps) {
|
|
280
|
+
lines.push(...wrap(` ${s}`, WIDTH, ' '));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
lines.push(...wrap(`Done when: ${lead.blocker.resolvedWhen}`, WIDTH, ' '));
|
|
284
|
+
|
|
285
|
+
if (rest.length > 0) {
|
|
286
|
+
// A count, not a roster. Naming all of them reproduces the exact
|
|
287
|
+
// failure this form exists to end — "zwölf Entscheidungen bei Dir …
|
|
288
|
+
// rund 850 Zeilen", which the transcript shows produced no decision at
|
|
289
|
+
// all. The one that blocks is written out above; the rest are one
|
|
290
|
+
// command away, and saying so is shorter than listing them and more
|
|
291
|
+
// honest than hiding them.
|
|
292
|
+
lines.push('');
|
|
293
|
+
lines.push(
|
|
294
|
+
...wrap(
|
|
295
|
+
`${rest.length} other decision${rest.length !== 1 ? 's' : ''} also wait${rest.length === 1 ? 's' : ''} on you, ` +
|
|
296
|
+
'none blocking this — `agent-config gates`.',
|
|
297
|
+
WIDTH,
|
|
298
|
+
' ',
|
|
299
|
+
),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return lines.join('\n') + '\n';
|
|
304
|
+
}
|
|
305
|
+
|
|
212
306
|
function renderJson(entries: readonly Entry[], all: boolean): string {
|
|
213
307
|
const pick = all ? entries : entries.filter((e) => needsUser(e.blocker.owner));
|
|
214
308
|
return (
|
|
@@ -270,18 +364,28 @@ function main(argv?: readonly string[]): number {
|
|
|
270
364
|
const args = argv ?? process.argv.slice(2);
|
|
271
365
|
const all = args.includes('--all');
|
|
272
366
|
const json = args.includes('--json');
|
|
367
|
+
const reply = args.includes('--reply');
|
|
273
368
|
|
|
274
369
|
const repoRoot = _resolveRepoRoot(process.cwd());
|
|
275
370
|
const roadmapRoot = path.join(repoRoot, 'agents', 'roadmaps');
|
|
276
371
|
if (!_isDir(roadmapRoot)) {
|
|
372
|
+
// `--reply` stays silent here too: a project without roadmaps has no
|
|
373
|
+
// blocker to hand over, and a reply-close line saying so would be the
|
|
374
|
+
// ceremony the form exists to avoid.
|
|
277
375
|
process.stdout.write(
|
|
278
|
-
|
|
376
|
+
reply
|
|
377
|
+
? ''
|
|
378
|
+
: json
|
|
379
|
+
? '{"needsYou":0,"other":0,"blockers":[]}\n'
|
|
380
|
+
: 'No roadmaps directory — nothing to report.\n',
|
|
279
381
|
);
|
|
280
382
|
return 0;
|
|
281
383
|
}
|
|
282
384
|
|
|
283
385
|
const entries = collectEntries(roadmapRoot);
|
|
284
|
-
process.stdout.write(
|
|
386
|
+
process.stdout.write(
|
|
387
|
+
reply ? renderReply(entries) : json ? renderJson(entries, all) : render(entries, all),
|
|
388
|
+
);
|
|
285
389
|
return 0;
|
|
286
390
|
}
|
|
287
391
|
|
|
@@ -308,5 +412,5 @@ if (_isCliEntry()) {
|
|
|
308
412
|
process.exitCode = main();
|
|
309
413
|
}
|
|
310
414
|
|
|
311
|
-
export { main, needsUser, regroupTodo, wrap, collectEntries, render, renderJson };
|
|
415
|
+
export { main, needsUser, regroupTodo, wrap, collectEntries, render, renderJson, renderReply };
|
|
312
416
|
export type { Entry };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Copy this file to `.github/ownership-map.yml` (preferred) or
|
|
4
4
|
# `agents/ownership-map.yml` and adapt the entries to your stack.
|
|
5
5
|
# Omitting the file entirely is fine — routing falls back to the
|
|
6
|
-
#
|
|
6
|
+
# common role vocabulary in `skill:review-routing`.
|
|
7
7
|
#
|
|
8
8
|
# First match wins per file. List narrower globs before broader ones.
|
|
9
9
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Committed target table for the road-to-cost-parity-* family (part 0, Phase 1). Registers what a session costs today, what the family is steering toward, and what the rule payload actually is PER HOST — the correction that matters most, because every diet target in the source drafts was set against one aggregate number that describes no host. Nothing here changes behaviour: no carrier ships for these thresholds (part 0 Phase 4.5), and no gate reads the target columns. What IS gated is the file's own shape — lint_budget_ownership rejects a row without a `source`, and rejects a `revised_from` without `revision_evidence`, so a number cannot enter or move without a traceable origin.",
|
|
3
|
+
"schema_version": 1,
|
|
4
|
+
"registered_at": "2026-08-10",
|
|
5
|
+
"owner": "maintainer",
|
|
6
|
+
"review_by": "2026-11-10",
|
|
7
|
+
"row_schema": {
|
|
8
|
+
"_comment": "Reading of part 0 § 1.2b: `schema_version` / `registered_at` / `owner` / `review_by` are FILE-level (as in every sibling budget file — a per-row copy would be four constants repeated N times), and every ROW carries `source` plus the three honest-null fields from § 1.3. The validator enforces exactly that split.",
|
|
9
|
+
"required_row_fields": ["id", "unit", "baseline", "source", "revisable", "revision_evidence", "revised_from"],
|
|
10
|
+
"honest_null_contract": "A target bends to a measurement, never to a preference. `revisable: true` says the number is revisable at all; `revised_from` records what it used to be once it moves; `revision_evidence` must then point at the evidence note that forced the move. The validator fails the file when `revised_from` is set and `revision_evidence` is empty — a revision without published evidence is the one form of drift this schema exists to refuse."
|
|
11
|
+
},
|
|
12
|
+
"measurement": {
|
|
13
|
+
"_comment": "Both measurement rules from part 0's Context are fields here, not prose: a rule that lives only in a paragraph is a rule the next author does not inherit.",
|
|
14
|
+
"measurement_date": "2026-08-10",
|
|
15
|
+
"commit": "c073d5732fb890bbf8b41ec879f62f651c7ba293",
|
|
16
|
+
"projection_regenerated": true,
|
|
17
|
+
"regeneration_command": "task sync && task generate-tools — verified no-op afterwards (git status clean), so the payload rows below measure the commit and not when someone last ran a generator",
|
|
18
|
+
"emitter_generation": "real, frontmatter-less projection files (25 of 110 .claude/rules entries carry a `paths:` block; the rest carry no frontmatter). NOT the older emitter that symlinked .claude/rules/* into dist/agent-src/rules/ and so inherited dist's full frontmatter. This is the THIRD variable behind the 92-vs-110 file-count spread: a projection count is a fact about which emitter last wrote the tree, on top of how stale that tree is.",
|
|
19
|
+
"carrier_divergence_admitted": false,
|
|
20
|
+
"carrier_divergence_reason": "report_carrier_divergence compares a globally installed carrier against the project projection on disk; both can be stale independently, so it measures relative cache staleness rather than any property of the source. It may diagnose a behind-the-times install; it may never gate a phase, and no row here is derived from it."
|
|
21
|
+
},
|
|
22
|
+
"session_cost_targets": [
|
|
23
|
+
{
|
|
24
|
+
"id": "median_final_context_tokens",
|
|
25
|
+
"unit": "billable input tokens of the last main-chain assistant record (per cc_transcript.ts)",
|
|
26
|
+
"baseline": 519349,
|
|
27
|
+
"interim": 300000,
|
|
28
|
+
"target": 200000,
|
|
29
|
+
"direction": "lower_is_better",
|
|
30
|
+
"source": "agents/evidence/analysis/token-economy-recycling-phase1.md:45 — median over 201 sessions with parseable final usage (:44) out of 205 non-empty (:10)",
|
|
31
|
+
"revisable": true,
|
|
32
|
+
"revision_evidence": "",
|
|
33
|
+
"revised_from": null
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "p90_final_context_tokens",
|
|
37
|
+
"unit": "billable input tokens of the last main-chain assistant record",
|
|
38
|
+
"baseline": 807937,
|
|
39
|
+
"interim": 550000,
|
|
40
|
+
"target": 400000,
|
|
41
|
+
"direction": "lower_is_better",
|
|
42
|
+
"source": "agents/evidence/analysis/token-economy-recycling-phase1.md:45 — same 201-session corpus",
|
|
43
|
+
"revisable": true,
|
|
44
|
+
"revision_evidence": "",
|
|
45
|
+
"revised_from": null
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "auto_compact_incidence",
|
|
49
|
+
"unit": "share of sessions with >= 1 auto-compaction event",
|
|
50
|
+
"baseline": 0.112,
|
|
51
|
+
"interim": 0.05,
|
|
52
|
+
"target": 0.0,
|
|
53
|
+
"direction": "lower_is_better",
|
|
54
|
+
"source": "agents/evidence/analysis/token-economy-recycling-phase1.md:59 — 23 of 205 sessions, 31 events (:60), all trigger:\"auto\"",
|
|
55
|
+
"revisable": true,
|
|
56
|
+
"revision_evidence": "",
|
|
57
|
+
"revised_from": null
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "late_early_per_call_cost_ratio",
|
|
61
|
+
"unit": "median billable input per call over the last 20 calls / the first 20 calls of a session",
|
|
62
|
+
"baseline": 2.1,
|
|
63
|
+
"interim": 1.6,
|
|
64
|
+
"target": 1.3,
|
|
65
|
+
"direction": "lower_is_better",
|
|
66
|
+
"source": "agents/evidence/analysis/token-economy-recycling-phase1.md:69-75 — 145 sessions with >= 200 main-chain calls; median early 279,859 vs late 594,985; p90 ratio 3.3x",
|
|
67
|
+
"revisable": true,
|
|
68
|
+
"revision_evidence": "",
|
|
69
|
+
"revised_from": null
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"payload_rows": [
|
|
73
|
+
{
|
|
74
|
+
"_comment": "Per host, never one number. The aggregate the source drafts dieted against (\"419 KB always-loaded layer\") describes no host that exists: it is the whole rule tree, and only 9 of 116 rules are type: always.",
|
|
75
|
+
"id": "src_rules_maintained",
|
|
76
|
+
"unit": "bytes across src/rules/*.md",
|
|
77
|
+
"baseline": 424896,
|
|
78
|
+
"files": 116,
|
|
79
|
+
"target_bytes": null,
|
|
80
|
+
"target_owner": "none — this is the maintained source, not a delivered payload; it shrinks only as a consequence of a diet elsewhere",
|
|
81
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration (see `measurement`)",
|
|
82
|
+
"revisable": true,
|
|
83
|
+
"revision_evidence": "",
|
|
84
|
+
"revised_from": null
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "dist_agent_src_rules",
|
|
88
|
+
"unit": "bytes across dist/agent-src/rules/*.md",
|
|
89
|
+
"baseline": 420051,
|
|
90
|
+
"files": 115,
|
|
91
|
+
"target_bytes": null,
|
|
92
|
+
"target_owner": "none — the tracked projection source. It is the ONLY rule-payload figure that is stable across checkouts, because it is committed; every other row below is regenerated and therefore machine-dependent.",
|
|
93
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration",
|
|
94
|
+
"revisable": true,
|
|
95
|
+
"revision_evidence": "",
|
|
96
|
+
"revised_from": null
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "augment_rules_payload",
|
|
100
|
+
"unit": "bytes across .augment/rules/*.md",
|
|
101
|
+
"baseline": 420051,
|
|
102
|
+
"files": 115,
|
|
103
|
+
"target_bytes": null,
|
|
104
|
+
"target_owner": "src/config/budgets.yml § standing_rule_delivery (total_cap_tokens 110000) already owns the delivered-rule ceiling. This family does not open a second, byte-denominated target for the same surface — a metric with two owners is a metric with none.",
|
|
105
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration; mirrors dist/ by symlink",
|
|
106
|
+
"revisable": true,
|
|
107
|
+
"revision_evidence": "",
|
|
108
|
+
"revised_from": null
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "claude_rules_payload",
|
|
112
|
+
"unit": "bytes across .claude/rules/*.md",
|
|
113
|
+
"baseline": 339035,
|
|
114
|
+
"files": 110,
|
|
115
|
+
"target_bytes": null,
|
|
116
|
+
"target_owner": "src/config/budgets.yml § standing_rule_delivery — same ceiling, same reason as the augment row",
|
|
117
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration; 25 of 110 files carry a `paths:` block, the rest none",
|
|
118
|
+
"revisable": true,
|
|
119
|
+
"revision_evidence": "",
|
|
120
|
+
"revised_from": null
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"_comment": "CORRECTION to part 0's triage table, which recorded these two as \"~3 KB / ~0 KB\" and therefore as already-lean hosts needing no target. Re-measured post-regeneration they are 408 KB and 330 KB — a full payload each. The earlier reading is a stale-projection artefact of exactly the class measurement rule 1 warns about: both carriers are UNTRACKED and written only by `task generate-tools`, so a checkout that has not run it reads them as near-empty. Risk 3's mitigation (\"the lean hosts need no target\") does not survive the re-measure and is not encoded here.",
|
|
124
|
+
"id": "clinerules_payload",
|
|
125
|
+
"unit": "bytes across .clinerules/*.md",
|
|
126
|
+
"baseline": 417703,
|
|
127
|
+
"files": 111,
|
|
128
|
+
"target_bytes": null,
|
|
129
|
+
"target_owner": "unowned — a diet for this host belongs to the parked rule-payload part of this family, not to part 0",
|
|
130
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration; untracked, generated by task generate-tools",
|
|
131
|
+
"revisable": true,
|
|
132
|
+
"revision_evidence": "",
|
|
133
|
+
"revised_from": null
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "windsurfrules_payload",
|
|
137
|
+
"unit": "bytes of the single .windsurfrules file",
|
|
138
|
+
"baseline": 338160,
|
|
139
|
+
"files": 1,
|
|
140
|
+
"target_bytes": null,
|
|
141
|
+
"target_owner": "unowned — see the clinerules row; same correction, same parked owner",
|
|
142
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration; untracked, generated by task generate-tools",
|
|
143
|
+
"revisable": true,
|
|
144
|
+
"revision_evidence": "",
|
|
145
|
+
"revised_from": null
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"id": "gemini_md_payload",
|
|
149
|
+
"unit": "bytes of GEMINI.md",
|
|
150
|
+
"baseline": 2982,
|
|
151
|
+
"files": 1,
|
|
152
|
+
"target_bytes": null,
|
|
153
|
+
"target_owner": "none — genuinely lean, and the only host in this table for which that is true. It is a pointer file, not a rule payload.",
|
|
154
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration",
|
|
155
|
+
"revisable": true,
|
|
156
|
+
"revision_evidence": "",
|
|
157
|
+
"revised_from": null
|
|
158
|
+
}
|
|
159
|
+
],
|
|
160
|
+
"anchors": {
|
|
161
|
+
"premium_band_tokens": 200000,
|
|
162
|
+
"rationale": "Above ~200k a session sits in the premium pricing band, which is why the median target is set there rather than at a round fraction of the baseline.",
|
|
163
|
+
"known_noise_problem": "187 of 205 observed sessions end at or above 200k, so a session-end hint at this threshold would fire on ~91% of sessions — a constant line, not an advisory. The 200k figure is a TARGET here and nothing else.",
|
|
164
|
+
"no_carrier_ships": "Part 0 § 4.5: no session-end advisory ships for this anchor. The existing 800k recycle threshold (src/config/recycle-threshold-budget.json) stays the only strong line, and any carrier for 200k is a separate, separately-argued change.",
|
|
165
|
+
"source": "agents/evidence/analysis/token-economy-recycling-phase1.md — session-final distribution (:45) over the 205-session corpus (:10)"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -40,13 +40,16 @@
|
|
|
40
40
|
# visible, or it repeats the sin it exists to catch.
|
|
41
41
|
# canary optional mutation recipe (Phase 7) — see CANARY below
|
|
42
42
|
#
|
|
43
|
-
# COVERAGE — THE HONEST DENOMINATOR (re-measured 2026-08-
|
|
43
|
+
# COVERAGE — THE HONEST DENOMINATOR (re-measured 2026-08-10)
|
|
44
44
|
# ----------------------------------------------------------
|
|
45
|
-
#
|
|
45
|
+
# 251 `lint_*` / `check_*` / `audit_*` scripts live under `src/scripts/`
|
|
46
46
|
# (the count is computed, not asserted — `check_gate_coverage.count_gate_scripts`).
|
|
47
|
-
# **
|
|
48
|
-
# CI-identical argv, and all
|
|
47
|
+
# **33 of them emit the machine-readable `scanned: <N>` line under their
|
|
48
|
+
# CI-identical argv, and all 33 are listed below.** The manifest is therefore
|
|
49
49
|
# complete with respect to its own contract, and covers ~13 % of the population.
|
|
50
|
+
# (The 2026-08-05 reading said "32 … and all 31 are listed": the two halves of
|
|
51
|
+
# one sentence disagreed by one, which is the drift this paragraph exists to
|
|
52
|
+
# make visible. Both are recounted above, not adjusted toward each other.)
|
|
50
53
|
#
|
|
51
54
|
# Two revisions of this header have now gone stale the same way. It claimed
|
|
52
55
|
# "8 of 211", then "22 of 223" while 26 entries were listed. Both halves drift
|
|
@@ -98,7 +101,7 @@
|
|
|
98
101
|
# which is the shape this repo tells a gate author to use — writes the line from
|
|
99
102
|
# inside the library, so its own source never matches and the test cannot see it.
|
|
100
103
|
# Live split: **1 unregistered gate emits the inline shape (`check_gate_coverage`
|
|
101
|
-
# itself), and
|
|
104
|
+
# itself), and 205 of 251 gate scripts emit through the helper.** So for the
|
|
102
105
|
# recommended shape the sentence above is false, and the list CAN fall behind.
|
|
103
106
|
#
|
|
104
107
|
# Not widened here, deliberately. Widening the regex to include the helper turns
|
|
@@ -557,6 +560,42 @@ gates:
|
|
|
557
560
|
untouched original stayed clean. Floor at 450 against a live 503 leaves
|
|
558
561
|
room for skill retirement without redding the change that retires one.
|
|
559
562
|
|
|
563
|
+
- id: lint_workflow_paths
|
|
564
|
+
argv: []
|
|
565
|
+
min_scanned: 20
|
|
566
|
+
corpus: "every workflow under .github/workflows (28 at baseline); the unit is the file, and each file's `paths:` / `paths-ignore:` entries are resolved against `git ls-files`"
|
|
567
|
+
status: enforced
|
|
568
|
+
canary:
|
|
569
|
+
class: undetected-antipattern
|
|
570
|
+
path: .github/workflows/zz-canary-workflow-paths.yml
|
|
571
|
+
content: |
|
|
572
|
+
name: Canary
|
|
573
|
+
on:
|
|
574
|
+
pull_request:
|
|
575
|
+
paths:
|
|
576
|
+
- "retired/tree/that/never/existed/**"
|
|
577
|
+
jobs:
|
|
578
|
+
noop:
|
|
579
|
+
runs-on: ubuntu-latest
|
|
580
|
+
steps:
|
|
581
|
+
- run: echo canary
|
|
582
|
+
note: >
|
|
583
|
+
Registered STRICT on day one for the same reason `lint_example_fences`
|
|
584
|
+
was, and on the same evidence standard: the classification pass ran
|
|
585
|
+
BEFORE the gate existed and returned **20 dead entries across 6 workflow
|
|
586
|
+
files**, each classified by hand into one of four causes — a tree retired
|
|
587
|
+
by ADR-051, a gitignored projection that can never appear in a diff, two
|
|
588
|
+
paths moved by the Python-to-TS migration, and bare root paths whose real
|
|
589
|
+
files sit under `src/`. All 20 were repaired in the change that added the
|
|
590
|
+
gate, so the corpus is verified empty rather than unexamined, and an
|
|
591
|
+
advisory gate over it could only ever be ignored. Floor at 20 against a
|
|
592
|
+
live 28 so retiring a workflow does not red the change that retires it.
|
|
593
|
+
Worth recording that the highest-severity hit was invisible to the hand
|
|
594
|
+
census and only the gate found it: `release-validation.yml` filtered on a
|
|
595
|
+
root `marketplace.json` that has never existed, so a version drift in the
|
|
596
|
+
real manifest did not re-trigger the `version-consistency` job whose
|
|
597
|
+
entire purpose is catching that drift.
|
|
598
|
+
|
|
560
599
|
- id: check_depth_budget
|
|
561
600
|
argv: []
|
|
562
601
|
min_scanned: 140
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Pre-registered council ATTENDANCE metrics (road-to-inbox-harvest-2026-08-b Phase 1.4). Registered here rather than in roadmap prose because a roadmap is archived when it closes and a metric outlives it — the sibling budget files (hook-token-budget.json, recycle-threshold-budget.json) already carry the owner / review_by / honest-gap discipline this reuses. The instrument is the quorum_result line appendQuorumEvent writes to agents/runtime/council/events.log (schema v2); nothing here is measured yet, and no threshold is committed before data exists.",
|
|
3
|
+
"schema_version": 1,
|
|
4
|
+
"registered_at": "2026-08-10",
|
|
5
|
+
"owner": "maintainer",
|
|
6
|
+
"review_by": "2026-11-10",
|
|
7
|
+
"unit": "quorum_result event lines in agents/runtime/council/events.log",
|
|
8
|
+
"privacy": "a quorum_result line carries {phase, command, dispatch, verdict, threshold, configured_total, total, present, solo, absent[{member, reason}]} — counts, closed vocabularies, and member ids only. The type has no field able to hold free-form content, so the CLI's own `detail` string (provider error text, which can embed paths and prompt fragments) is dropped at the boundary rather than scrubbed.",
|
|
9
|
+
"phase_split": "A pass emits `pre_run` (build_members — who could be constructed) and, on the `run` path, `post_run` (_postRunQuorum — who actually returned a usable response). They disagree whenever a member constructs and then fails mid-flight, so a rate computed over both double-counts. A `pre_run` line is NOT evidence a pass ran: `command: estimate` is a spend-free preview, and a `run` / `debate` line is written before the `--confirm` gate (which defaults to false and returns before any provider call) and before the necessity and size-fit gates. A `post_run` line is the only line that proves providers were called — which is why every rate below is post_run-scoped, and why `command` exists at all.",
|
|
10
|
+
"attendance_rate": {
|
|
11
|
+
"definition": "of the members that entered the pass, how many answered: present / total per post_run pass. Bounded by 1 by construction (evaluateQuorum clamps present into [0, total]).",
|
|
12
|
+
"instrument": "quorum_result lines, phase=post_run, command=run, fields present + total",
|
|
13
|
+
"threshold": "none committed before data — baseline first, threshold at review date"
|
|
14
|
+
},
|
|
15
|
+
"roster_shortfall_rate": {
|
|
16
|
+
"definition": "how often the council shrank BEFORE the pass: share of post_run passes with total < configured_total, plus the size of the gap. This is the half attendance_rate cannot see — a pass with 3 configured, 2 failing to construct and 1 answering reads 1/1 = 100 % attendance and a shortfall of 2. Both numbers are needed and neither substitutes for the other; a single ratio over mixed rosters is the error this pair replaces.",
|
|
17
|
+
"instrument": "quorum_result lines, phase=post_run, command=run, configured_total vs total",
|
|
18
|
+
"threshold": "none committed before data — baseline first, threshold at review date",
|
|
19
|
+
"not_an_error": "total > configured_total is legitimate and not a shortfall: `--siblings` fans ONE enabled config entry into N clients, so the client roster can exceed the entry count. configured_total counts enabled config ENTRIES; total counts CLIENTS. Compare them for direction, never divide one by the other."
|
|
20
|
+
},
|
|
21
|
+
"solo_conclusion_rate": {
|
|
22
|
+
"definition": "share of post_run passes with `solo: true` — written by the isSoloConcluded predicate in quorum.ts, never re-derived from the numbers here, so one change to the rule cannot leave this definition stale. ceil(n/2) makes 1-of-2 a legitimate conclusion; this measures how often that legitimacy is used, not whether it should be. THREE cases carry solo: true and they are not the same finding — a degraded multi-member council (configured_total > 1), a council configured with one member (configured_total = 1, dispatch = full), and a deliberate --single dispatch (dispatch = single). Split on dispatch and configured_total before reading the rate.",
|
|
23
|
+
"instrument": "quorum_result lines, phase=post_run, command=run, field solo, split by dispatch + configured_total",
|
|
24
|
+
"threshold": "DECIDED 2026-08-11 against real data — see ADR-224 (docs/decisions/ADR-224-gate-scoped-solo-attendance-floor.md). The quorum-solo-floor blocker pre-registered three outcomes — a third CLI member, a gate-scoped min_present: 2, or a published null under 5 %. Read at 2026-08-11T09:30Z with the definition above: 1 of 8 post_run/command=run passes carried a degraded solo conclusion = 12.5 %, so the under-5 % null did not fire and the gate-scoped floor was chosen (council 2/2). It was decided on the degraded case only; a --single pass is the operator asking for one voice, not the council failing to assemble. Still no numeric threshold is committed here, deliberately: at n=8 the 95 % interval around 1/8 spans roughly 0.3 %-53 %, so the rate established urgency and not certainty, and ADR-224's review trigger reopens the null at n=40. What this metric measures does NOT change; once the floor lands, how often it FIRES is a second and different question needing its own instrument."
|
|
25
|
+
},
|
|
26
|
+
"absent_reason_distribution": {
|
|
27
|
+
"definition": "count per reason over absent[] entries — no_binary / no_auth / timeout / quota (AbsentReason) plus the two runtime fallbacks the CLI writes directly, `unavailable` (transport resolved to nothing) and `binary_missing` (a CLI client refused to construct). Distinguishes a council degraded by configuration from one degraded by provider behaviour, which have opposite fixes.",
|
|
28
|
+
"instrument": "quorum_result lines, absent[].reason, split by phase (a pre_run absence is configuration; a post_run absence is mid-flight). Excluding `command: estimate` is NOT sufficient: `run` and `debate` also reach construction and return at the `--confirm` gate, which defaults to false. The reliable filter is a pre_run line whose pass produced a matching post_run line; a pre_run line with no post_run partner reached construction and stopped there, whatever its command says.",
|
|
29
|
+
"threshold": "none committed before data — the distribution IS the finding at review date",
|
|
30
|
+
"merge_before_reading": "`no_binary` and `binary_missing` are two spellings of one condition — the CLI binary is unusable — written by different code paths (transport_resolver's static classification vs the CLI client's construction failure). They are recorded separately because that is what the tree emits, not because they are separate findings; merge them before reading the distribution, or the largest bucket is split in two."
|
|
31
|
+
},
|
|
32
|
+
"honest_null_consequence": "If the log accumulates and the solo-conclusion rate sits near zero, that is the published answer to the quorum-solo-floor blocker and 1.5 is cancelled against it — not quietly dropped. If attendance data never accumulates at all (nobody runs a metered council pass in the window), that is reported as an unmeasured metric at the review date, never as a passing one.",
|
|
33
|
+
"honest_gaps": [
|
|
34
|
+
"`lens` and `invocation` are empty on every pre_run line — neither is known at member-construction time — so any per-lens breakdown is post_run-only.",
|
|
35
|
+
"`command: debate` carries the pre_run half ONLY. There is no _postRunQuorum on the debate path, and defining `usable response` for a multi-round debate is a separate question this registration does not answer. So debate contributes to absent_reason_distribution and to nothing else: attendance_rate and solo_conclusion_rate cover the `run` path, and a reader must not read them as covering every metered pass.",
|
|
36
|
+
"A pre_run line proves construction happened, never that a pass followed — the `--confirm` gate (default false), the necessity gate and the size-fit gate all abort afterwards and nothing on the line records it. Any count over pre_run lines is an upper bound; pair it with a post_run line before treating it as a pass.",
|
|
37
|
+
"The CLI emit wiring itself has no test. The two modules are covered directly, but nothing exercises build_members / cmd_run / cmd_debate end-to-end against a captured log — and `_emitQuorumEvent` forwards no AppendEventOptions, so there is no target to capture into. Two defects in this file's first version (a `dispatch` written from the flag rather than the outcome, and a rate mixing config entries with clients) were found by review rather than by a test, which is what that gap costs."
|
|
38
|
+
]
|
|
39
|
+
}
|