@event4u/agent-config 1.22.0 → 1.24.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/.agent-src/commands/agents/cleanup.md +31 -17
- package/.agent-src/commands/analyze-reference-repo.md +3 -0
- package/.agent-src/commands/commit/in-chunks.md +30 -10
- package/.agent-src/commands/commit.md +46 -6
- package/.agent-src/commands/compress.md +19 -13
- package/.agent-src/commands/cost-report.md +120 -0
- package/.agent-src/commands/create-pr/description-only.md +8 -0
- package/.agent-src/commands/create-pr.md +95 -80
- package/.agent-src/commands/feature/plan.md +13 -7
- package/.agent-src/commands/memory/add.md +16 -8
- package/.agent-src/commands/memory/promote.md +17 -9
- package/.agent-src/commands/optimize/rtk.md +16 -11
- package/.agent-src/commands/prepare-for-review.md +12 -6
- package/.agent-src/commands/project-analyze.md +31 -20
- package/.agent-src/commands/review-changes.md +24 -15
- package/.agent-src/commands/roadmap/create.md +14 -9
- package/.agent-src/commands/roadmap/process-full.md +41 -1
- package/.agent-src/contexts/contracts/frugality-charter.md +57 -0
- package/.agent-src/contexts/execution/roadmap-process-loop.md +29 -6
- package/.agent-src/rules/architecture.md +9 -0
- package/.agent-src/rules/ask-when-uncertain.md +3 -13
- package/.agent-src/rules/caveman-speak.md +78 -0
- package/.agent-src/rules/direct-answers.md +5 -14
- package/.agent-src/rules/markdown-safe-codeblocks.md +6 -7
- package/.agent-src/rules/no-cheap-questions.md +4 -14
- package/.agent-src/rules/roadmap-progress-sync.md +37 -3
- package/.agent-src/rules/token-efficiency.md +5 -7
- package/.agent-src/skills/adr-create/SKILL.md +197 -0
- package/.agent-src/skills/agent-docs-writing/SKILL.md +23 -1
- package/.agent-src/skills/command-writing/SKILL.md +23 -0
- package/.agent-src/skills/context-authoring/SKILL.md +23 -0
- package/.agent-src/skills/conventional-commits-writing/SKILL.md +23 -0
- package/.agent-src/skills/guideline-writing/SKILL.md +22 -0
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +9 -0
- package/.agent-src/skills/markitdown/SKILL.md +239 -0
- package/.agent-src/skills/persona-writing/SKILL.md +153 -0
- package/.agent-src/skills/readme-writing/SKILL.md +20 -0
- package/.agent-src/skills/readme-writing-package/SKILL.md +19 -0
- package/.agent-src/skills/roadmap-writing/SKILL.md +157 -0
- package/.agent-src/skills/rule-writing/SKILL.md +22 -0
- package/.agent-src/skills/script-writing/SKILL.md +226 -0
- package/.agent-src/skills/skill-writing/SKILL.md +23 -0
- package/.agent-src/skills/test-driven-development/SKILL.md +24 -0
- package/.agent-src/skills/universal-project-analysis/SKILL.md +8 -0
- package/.agent-src/templates/agent-settings.md +73 -0
- package/.agent-src/templates/command.md +15 -10
- package/.agent-src/templates/rule.md +6 -0
- package/.agent-src/templates/skill.md +32 -0
- package/.claude-plugin/marketplace.json +10 -4
- package/AGENTS.md +14 -3
- package/CHANGELOG.md +61 -0
- package/README.md +5 -5
- package/docs/architecture.md +4 -4
- package/docs/catalog.md +25 -8
- package/docs/customization.md +72 -0
- package/docs/decisions/INDEX.md +15 -0
- package/docs/getting-started.md +2 -2
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +27 -19
- package/docs/guidelines/agent-infra/carve-out-predicates.md +17 -0
- package/docs/guidelines/agent-infra/mcp-request-signing.md +199 -0
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +11 -4
- package/package.json +1 -1
- package/scripts/_lib/__init__.py +5 -0
- package/scripts/_lib/script_output.py +140 -0
- package/scripts/adr/regenerate_index.py +79 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_add_quiet.py +149 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_inject_quiet_flag.py +33 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_measure_v2.sh +36 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_measure_verbosity.sh +26 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_per_task.sh +41 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_silent_taskfiles.py +98 -0
- package/scripts/check_augmentignore.py +4 -1
- package/scripts/check_command_count_messaging.py +4 -1
- package/scripts/check_compressed_paths.py +4 -1
- package/scripts/check_council_layout.py +4 -1
- package/scripts/check_council_references.py +4 -1
- package/scripts/check_iron_law_prominence.py +3 -1
- package/scripts/check_md_language.py +3 -1
- package/scripts/check_memory_proposal.py +3 -1
- package/scripts/check_public_catalog_links.py +4 -1
- package/scripts/check_reply_consistency.py +8 -2
- package/scripts/check_roadmap_trackable.py +4 -1
- package/scripts/compile_router.py +27 -0
- package/scripts/compress.py +33 -19
- package/scripts/cost/budget.mjs +152 -0
- package/scripts/cost/track.mjs +144 -0
- package/scripts/first-run.sh +3 -9
- package/scripts/install-hooks.sh +19 -1
- package/scripts/install.py +17 -12
- package/scripts/install.sh +19 -8
- package/scripts/lint_examples.py +6 -2
- package/scripts/lint_handoffs.py +4 -1
- package/scripts/lint_load_context.py +4 -1
- package/scripts/lint_roadmap_complexity.py +6 -2
- package/scripts/lint_rule_interactions.py +4 -1
- package/scripts/lint_rule_tiers.py +4 -1
- package/scripts/measure_frugality_savings.py +164 -0
- package/scripts/measure_markitdown_lift.py +127 -0
- package/scripts/runtime_dispatcher.py +11 -0
- package/scripts/skill_linter.py +207 -2
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# MCP Request Signing (HMAC-SHA256)
|
|
2
|
+
|
|
3
|
+
Reference guideline for signing JSON-RPC requests crossing any non-stdio
|
|
4
|
+
MCP transport — HTTP, SSE, WebSocket, anything routable. Stdio over a
|
|
5
|
+
trusted parent-child pipe is **outside** the scope of this guideline; only
|
|
6
|
+
network-exposed transports require signing.
|
|
7
|
+
|
|
8
|
+
Lands ahead of any HTTP-MCP transport so the security floor is in place
|
|
9
|
+
when one becomes a real consumer use case (see
|
|
10
|
+
[`road-to-mcp-server.md`](../../../agents/roadmaps/road-to-mcp-server.md)
|
|
11
|
+
Phase 4 D4 — allowlist).
|
|
12
|
+
|
|
13
|
+
Adapted from
|
|
14
|
+
[`ruvnet/ruflo`](https://github.com/ruvnet/ruflo) — commit
|
|
15
|
+
[`1dd1db1`](https://github.com/ruvnet/ruflo/blob/1dd1db1ec2572ce68f6805dff98c177b5771cbf9/ruflo/src/mcp-bridge/mcp-stdio-kernel.js)
|
|
16
|
+
`ruflo/src/mcp-bridge/mcp-stdio-kernel.js` — `CRYPTO_SEG`. The full
|
|
17
|
+
Express bridge (`index.js`, ~1.6k LOC) stays authoritative-link only;
|
|
18
|
+
this guideline forks the **primitive**, not the runtime.
|
|
19
|
+
|
|
20
|
+
## When signing is mandatory
|
|
21
|
+
|
|
22
|
+
- Any HTTP / SSE / WebSocket MCP transport — the wire is shared with
|
|
23
|
+
arbitrary callers.
|
|
24
|
+
- Any cross-host stdio bridge (parent and child on different machines).
|
|
25
|
+
- Any MCP server reachable from a browser context.
|
|
26
|
+
|
|
27
|
+
## When signing is **not** required
|
|
28
|
+
|
|
29
|
+
- Plain stdio MCP server invoked as a child process by one trusted client
|
|
30
|
+
on the same host. The OS pipe is the trust boundary.
|
|
31
|
+
- Local-only loopback served behind a Unix domain socket with `0700`
|
|
32
|
+
permissions on the socket file.
|
|
33
|
+
|
|
34
|
+
If unsure → sign. The cost is one HMAC per request.
|
|
35
|
+
|
|
36
|
+
## Signing pattern (~30 LOC reference)
|
|
37
|
+
|
|
38
|
+
`KERNEL_SECRET` is a per-installation shared secret loaded from env or a
|
|
39
|
+
secrets store. Never commit it. `randomUUID()` is Node's
|
|
40
|
+
`crypto.randomUUID`.
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import { createHmac, randomUUID } from 'node:crypto';
|
|
44
|
+
|
|
45
|
+
const KERNEL_SECRET = process.env.MCP_KERNEL_SECRET;
|
|
46
|
+
const KERNEL_ID = `mcp-kernel-${process.pid}`;
|
|
47
|
+
|
|
48
|
+
function signRequest(payload) {
|
|
49
|
+
const timestamp = Date.now();
|
|
50
|
+
const nonce = randomUUID();
|
|
51
|
+
const data = `${timestamp}:${nonce}:${JSON.stringify(payload)}`;
|
|
52
|
+
const signature = createHmac('sha256', KERNEL_SECRET)
|
|
53
|
+
.update(data)
|
|
54
|
+
.digest('hex');
|
|
55
|
+
return { timestamp, nonce, signature, kernelId: KERNEL_ID };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// On every outbound MCP request:
|
|
59
|
+
const sig = signRequest(body);
|
|
60
|
+
headers['X-MCP-Kernel'] = sig.kernelId;
|
|
61
|
+
headers['X-MCP-Signature'] = sig.signature;
|
|
62
|
+
headers['X-MCP-Timestamp'] = String(sig.timestamp);
|
|
63
|
+
headers['X-MCP-Nonce'] = sig.nonce;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Header names are project-namespaced; the upstream Ruflo file uses
|
|
67
|
+
`X-RVF-*`, the convention here is `X-MCP-*`.
|
|
68
|
+
|
|
69
|
+
## Verification pattern (server-side counterpart)
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
73
|
+
|
|
74
|
+
const KERNEL_SECRET = process.env.MCP_KERNEL_SECRET;
|
|
75
|
+
const MAX_SKEW_MS = 5 * 60 * 1000; // 5 min
|
|
76
|
+
const seenNonces = new Map(); // nonce -> expiresAt
|
|
77
|
+
|
|
78
|
+
function verifyRequest(headers, rawBody) {
|
|
79
|
+
const ts = Number(headers['x-mcp-timestamp']);
|
|
80
|
+
const nonce = headers['x-mcp-nonce'];
|
|
81
|
+
const sig = headers['x-mcp-signature'];
|
|
82
|
+
if (!ts || !nonce || !sig) return false;
|
|
83
|
+
if (Math.abs(Date.now() - ts) > MAX_SKEW_MS) return false;
|
|
84
|
+
if (seenNonces.has(nonce)) return false; // replay
|
|
85
|
+
const data = `${ts}:${nonce}:${rawBody}`;
|
|
86
|
+
const expected = createHmac('sha256', KERNEL_SECRET).update(data).digest();
|
|
87
|
+
const got = Buffer.from(sig, 'hex');
|
|
88
|
+
if (got.length !== expected.length) return false;
|
|
89
|
+
if (!timingSafeEqual(got, expected)) return false;
|
|
90
|
+
seenNonces.set(nonce, Date.now() + MAX_SKEW_MS);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`timingSafeEqual` is non-negotiable — `===` on a hex string is a timing
|
|
96
|
+
oracle. The nonce store must evict on `expiresAt` to bound memory; a
|
|
97
|
+
plain `setInterval` sweep every minute is enough.
|
|
98
|
+
|
|
99
|
+
## Threat model
|
|
100
|
+
|
|
101
|
+
| Threat | Mitigation in this pattern |
|
|
102
|
+
|---|---|
|
|
103
|
+
| **Replay** — attacker captures a valid request and resends it | `nonce` + `seenNonces` set; replays inside the skew window are rejected, replays outside it fail the timestamp check |
|
|
104
|
+
| **MITM (non-stdio)** — wire-level rewrite | HMAC over `${ts}:${nonce}:${body}` — any payload tamper invalidates the signature; pair with TLS in production |
|
|
105
|
+
| **Clock skew abuse** — long-lived request | `MAX_SKEW_MS = 5 min` rejects out-of-window timestamps |
|
|
106
|
+
| **Timing oracle on signature compare** | `timingSafeEqual`, never `===` |
|
|
107
|
+
| **Secret exfil via repo / log** | `KERNEL_SECRET` from env or secrets store; never log raw headers; redact `X-MCP-Signature` in any audit trail |
|
|
108
|
+
| **Allowlist bypass** | Signing **does not** authorize what's called — pair with the allowlist enforced at server boot ([`road-to-mcp-server.md`](../../../agents/roadmaps/road-to-mcp-server.md) Phase 4 **D4**); a valid signature on a non-allowlisted tool name still rejects |
|
|
109
|
+
|
|
110
|
+
## Citation hooks
|
|
111
|
+
|
|
112
|
+
- [`road-to-mcp-server.md`](../../../agents/roadmaps/road-to-mcp-server.md)
|
|
113
|
+
**Phase 4 D4** — allowlist enforced at server boot. Signing layers
|
|
114
|
+
*under* the allowlist: verify signature → look up tool in allowlist →
|
|
115
|
+
execute. Both gates must pass.
|
|
116
|
+
- [`road-to-mcp-server.md`](../../../agents/roadmaps/road-to-mcp-server.md)
|
|
117
|
+
**Phase 6 F2 / F3** — SSE transport, cloud bundle. These are the
|
|
118
|
+
triggers that make this guideline load-bearing; until then it is
|
|
119
|
+
reference material for the deferred-with-trigger HTTP-bridge slot of
|
|
120
|
+
[`road-to-ruflo-adoption.md`](../../../agents/roadmaps/road-to-ruflo-adoption.md)
|
|
121
|
+
Phase 2 P2.1.
|
|
122
|
+
|
|
123
|
+
## Operational notes
|
|
124
|
+
|
|
125
|
+
- **Secret rotation** — rotate `MCP_KERNEL_SECRET` on a fixed cadence
|
|
126
|
+
(90 days minimum). Both client and server reload from env on the next
|
|
127
|
+
request; in-flight requests fail and retry with the new secret.
|
|
128
|
+
- **Multi-client deployments** — give every client kernel a distinct
|
|
129
|
+
`KERNEL_ID` so logs attribute to a source even though all use the
|
|
130
|
+
same shared secret.
|
|
131
|
+
- **Don't sign `tools/list`** — `tools/list` is read-only metadata; it
|
|
132
|
+
can stay unsigned in deployments where the metadata itself is public.
|
|
133
|
+
`tools/call` must always be signed.
|
|
134
|
+
|
|
135
|
+
## Out-of-scope
|
|
136
|
+
|
|
137
|
+
- The full Express bridge in `ruflo/src/mcp-bridge/index.js` (~1.6k LOC,
|
|
138
|
+
HTTP routing, SSE streaming, auth proxying) — authoritative-link only,
|
|
139
|
+
not forked. If we ever need an HTTP-MCP server, build on this
|
|
140
|
+
guideline + the host's web framework, not on Ruflo's runtime.
|
|
141
|
+
- Asymmetric signing (Ed25519, ECDSA). HMAC-SHA256 is sufficient for
|
|
142
|
+
shared-secret deployments. Asymmetric is only worth the complexity
|
|
143
|
+
when keys cross trust boundaries the shared-secret model can't
|
|
144
|
+
represent.
|
|
145
|
+
|
|
146
|
+
## Appendix — HTTP-bridge `stdio-kernel` pattern (reference)
|
|
147
|
+
|
|
148
|
+
Portable shape of Ruflo's `mcp-stdio-kernel.js` (~250 LOC), on hand for
|
|
149
|
+
the day a real HTTP-MCP consumer surfaces (`road-to-mcp-server.md`
|
|
150
|
+
Phase 6 F2 / F3). Full file stays **authoritative-link only**:
|
|
151
|
+
[`mcp-stdio-kernel.js`](https://github.com/ruvnet/ruflo/blob/1dd1db1ec2572ce68f6805dff98c177b5771cbf9/ruflo/src/mcp-bridge/mcp-stdio-kernel.js).
|
|
152
|
+
|
|
153
|
+
**Trigger to inline more:** both — (a) Phase 1 ships stdio prompt fetch
|
|
154
|
+
in ≥1 confirmed client, (b) ≥1 consumer surfaces a concrete HTTP-MCP
|
|
155
|
+
use case. Until then, this appendix + upstream link is the adoption.
|
|
156
|
+
|
|
157
|
+
### Pattern shape
|
|
158
|
+
|
|
159
|
+
The kernel sits between the HTTP transport and the spawned stdio MCP
|
|
160
|
+
child. Inbound: HTTP → `verifyRequest` → JSON-RPC onto child stdin.
|
|
161
|
+
Outbound: child stdout → parsed → signed response → HTTP.
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
client ──HTTP──▶ kernel.verify ──stdin──▶ stdio MCP child
|
|
165
|
+
client ◀─HTTP── kernel.sign ◀─stdout── stdio MCP child
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Six load-bearing pieces:
|
|
169
|
+
|
|
170
|
+
1. **Process supervisor** — spawn with `stdio: ['pipe', 'pipe', 'inherit']`,
|
|
171
|
+
restart on exit with bounded backoff, SIGTERM on shutdown.
|
|
172
|
+
2. **JSON-RPC framing** — newline-delimited JSON; buffer partial
|
|
173
|
+
stdout reads; drop unparseable frames with a logged warning, never
|
|
174
|
+
crash the bridge.
|
|
175
|
+
3. **Request-id correlation** — kernel generates outbound `id`, maps
|
|
176
|
+
`kernelId → clientId`; slow calls can't cross-talk between HTTP
|
|
177
|
+
clients.
|
|
178
|
+
4. **Verification gate** — `verifyRequest` (above) before stdin write;
|
|
179
|
+
failure is a 401, never a 500; never log the raw signature.
|
|
180
|
+
5. **Allowlist enforcement** — after verify, look up the JSON-RPC
|
|
181
|
+
`method` in the boot-time allowlist (`road-to-mcp-server.md` **D4**).
|
|
182
|
+
Non-allowlisted → JSON-RPC `-32601 Method not found`; no enumeration
|
|
183
|
+
leak.
|
|
184
|
+
6. **Backpressure** — bound the in-flight queue per kernel (Ruflo
|
|
185
|
+
uses 32); beyond it, return `429`. Otherwise a flood OOMs the child.
|
|
186
|
+
|
|
187
|
+
### Out of this appendix
|
|
188
|
+
|
|
189
|
+
Express routes / middleware / SSE upgrade — host web framework.
|
|
190
|
+
Ruflo marketplace + `mcp__claude-flow__*` tools — never adopted (see
|
|
191
|
+
`road-to-ruflo-adoption.md` Sunset path). Multi-tenant routing —
|
|
192
|
+
out-of-scope until a consumer surfaces a tenancy requirement.
|
|
193
|
+
|
|
194
|
+
### Citation hooks
|
|
195
|
+
|
|
196
|
+
- `road-to-mcp-server.md` **Phase 6 F2 / F3** — SSE / cloud-bundle work
|
|
197
|
+
starts here; the upstream link is the authoritative source.
|
|
198
|
+
- `road-to-ruflo-adoption.md` **P2.1** — landed this appendix; full
|
|
199
|
+
bridge fork stays out-of-scope unless the dual trigger fires.
|
|
@@ -9,10 +9,17 @@ _Origin: migrated from `.agent-src.uncompressed/rules/roadmap-progress-sync.md`
|
|
|
9
9
|
|
|
10
10
|
# Roadmap Progress Sync
|
|
11
11
|
|
|
12
|
-
> **Enforced by
|
|
13
|
-
>
|
|
14
|
-
>
|
|
15
|
-
>
|
|
12
|
+
> **Enforced by (defence in depth):**
|
|
13
|
+
> 1. [`scripts/roadmap_progress_hook.py`](../../scripts/roadmap_progress_hook.py)
|
|
14
|
+
> on Augment + Claude Code (`PostToolUse`) — auto-regen on write.
|
|
15
|
+
> 2. `.git/hooks/pre-commit` (installed by `scripts/install-hooks.sh`) —
|
|
16
|
+
> blocks any commit whose staged set touches `agents/roadmaps/` or
|
|
17
|
+
> `agents/roadmaps-progress.md` while the dashboard is stale.
|
|
18
|
+
> 3. `task ci` runs `roadmap-progress-check` so a PR cannot land with a
|
|
19
|
+
> stale dashboard even if local hooks were bypassed.
|
|
20
|
+
>
|
|
21
|
+
> Hook is primary; the prose below is the specification the hook
|
|
22
|
+
> implements and the fallback when the platform has no hook surface.
|
|
16
23
|
|
|
17
24
|
## Iron Law — dashboard sync
|
|
18
25
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Verbosity-aware print router for scripts/*.py.
|
|
2
|
+
|
|
3
|
+
Phase 10 of road-to-token-frugality. Single source of truth for how
|
|
4
|
+
maintenance scripts emit progress, success, warnings, and errors.
|
|
5
|
+
|
|
6
|
+
Resolution order (first wins):
|
|
7
|
+
1. AGENT_SCRIPT_VERBOSITY env var (silent | minimal | verbose)
|
|
8
|
+
2. SCRIPT_OUTPUT_VERBOSE=1 alias (== verbose)
|
|
9
|
+
3. .agent-settings.yml verbosity.script_output
|
|
10
|
+
4. Default: minimal
|
|
11
|
+
|
|
12
|
+
Once resolved, the level is exported back into AGENT_SCRIPT_VERBOSITY
|
|
13
|
+
so child processes inherit the same level (Phase 10.1c). Explicit
|
|
14
|
+
--quiet flags on the child still win at the call site.
|
|
15
|
+
|
|
16
|
+
Levels:
|
|
17
|
+
silent = stderr only; success() drops; info() drops; warn() drops
|
|
18
|
+
minimal = success() collapsed to one end-of-run summary; info() drops
|
|
19
|
+
verbose = pre-Phase-10 behaviour, every call prints
|
|
20
|
+
|
|
21
|
+
error() always writes to stderr regardless of level. Iron-Law surfaces
|
|
22
|
+
(release confirms, install secrets prompts) bypass this module and use
|
|
23
|
+
plain print() so they cannot be silenced.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Final
|
|
31
|
+
|
|
32
|
+
VALID_LEVELS: Final[tuple[str, ...]] = ("silent", "minimal", "verbose")
|
|
33
|
+
DEFAULT_LEVEL: Final[str] = "minimal"
|
|
34
|
+
ENV_VAR: Final[str] = "AGENT_SCRIPT_VERBOSITY"
|
|
35
|
+
ENV_ALIAS: Final[str] = "SCRIPT_OUTPUT_VERBOSE"
|
|
36
|
+
SETTINGS_FILE: Final[str] = ".agent-settings.yml"
|
|
37
|
+
|
|
38
|
+
_resolved_level: str | None = None
|
|
39
|
+
_pending_summary: list[str] = []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _read_settings_level(settings_path: Path) -> str | None:
|
|
43
|
+
"""Read verbosity.script_output from .agent-settings.yml.
|
|
44
|
+
|
|
45
|
+
Returns None when the file is missing, PyYAML is unavailable, or
|
|
46
|
+
the key is absent. Errors fall through to the default level.
|
|
47
|
+
"""
|
|
48
|
+
if not settings_path.is_file():
|
|
49
|
+
return None
|
|
50
|
+
try:
|
|
51
|
+
import yaml # type: ignore[import-untyped]
|
|
52
|
+
except ImportError:
|
|
53
|
+
return None
|
|
54
|
+
try:
|
|
55
|
+
with settings_path.open(encoding="utf-8") as fh:
|
|
56
|
+
data = yaml.safe_load(fh) or {}
|
|
57
|
+
except (OSError, yaml.YAMLError):
|
|
58
|
+
return None
|
|
59
|
+
section = data.get("verbosity") if isinstance(data, dict) else None
|
|
60
|
+
if not isinstance(section, dict):
|
|
61
|
+
return None
|
|
62
|
+
value = section.get("script_output")
|
|
63
|
+
if isinstance(value, str) and value in VALID_LEVELS:
|
|
64
|
+
return value
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resolve_level(settings_path: Path | None = None) -> str:
|
|
69
|
+
"""Resolve and cache the active verbosity level.
|
|
70
|
+
|
|
71
|
+
First call wins; subsequent calls return the cached value so the
|
|
72
|
+
process is internally consistent. Tests reset via reset_level().
|
|
73
|
+
"""
|
|
74
|
+
global _resolved_level
|
|
75
|
+
if _resolved_level is not None:
|
|
76
|
+
return _resolved_level
|
|
77
|
+
|
|
78
|
+
env_value = os.environ.get(ENV_VAR, "").strip().lower()
|
|
79
|
+
if env_value in VALID_LEVELS:
|
|
80
|
+
_resolved_level = env_value
|
|
81
|
+
elif os.environ.get(ENV_ALIAS, "").strip() == "1":
|
|
82
|
+
_resolved_level = "verbose"
|
|
83
|
+
else:
|
|
84
|
+
path = settings_path or Path(SETTINGS_FILE)
|
|
85
|
+
_resolved_level = _read_settings_level(path) or DEFAULT_LEVEL
|
|
86
|
+
|
|
87
|
+
# Inheritance: export resolved level so child processes see it.
|
|
88
|
+
os.environ[ENV_VAR] = _resolved_level
|
|
89
|
+
return _resolved_level
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def reset_level() -> None:
|
|
93
|
+
"""Clear the cached level. Test helper."""
|
|
94
|
+
global _resolved_level
|
|
95
|
+
_resolved_level = None
|
|
96
|
+
_pending_summary.clear()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def info(message: str) -> None:
|
|
100
|
+
"""Per-step progress note. Drops at silent + minimal."""
|
|
101
|
+
if resolve_level() == "verbose":
|
|
102
|
+
print(message)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def success(message: str) -> None:
|
|
106
|
+
"""Per-step success. At minimal collected for end-of-run summary;
|
|
107
|
+
at verbose printed immediately; at silent dropped."""
|
|
108
|
+
level = resolve_level()
|
|
109
|
+
if level == "verbose":
|
|
110
|
+
print(message)
|
|
111
|
+
elif level == "minimal":
|
|
112
|
+
_pending_summary.append(message)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def warn(message: str) -> None:
|
|
116
|
+
"""Warning. Stderr at all levels except silent."""
|
|
117
|
+
if resolve_level() != "silent":
|
|
118
|
+
print(message, file=sys.stderr)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def error(message: str) -> None:
|
|
122
|
+
"""Error. Always stderr regardless of level."""
|
|
123
|
+
print(message, file=sys.stderr)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def flush_summary(headline: str | None = None) -> None:
|
|
127
|
+
"""Emit the pending success() summary at end-of-run.
|
|
128
|
+
|
|
129
|
+
No-op at verbose (already printed) and silent (suppressed).
|
|
130
|
+
Use the explicit `headline` arg to override the auto-pick.
|
|
131
|
+
"""
|
|
132
|
+
level = resolve_level()
|
|
133
|
+
if level != "minimal" or not _pending_summary:
|
|
134
|
+
return
|
|
135
|
+
if headline:
|
|
136
|
+
print(headline)
|
|
137
|
+
else:
|
|
138
|
+
# Default: print the last collected line as the headline.
|
|
139
|
+
print(_pending_summary[-1])
|
|
140
|
+
_pending_summary.clear()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regenerate INDEX.md for an ADR directory. Parses ADR-*.md frontmatter
|
|
3
|
+
(adr/status/date/decision/supersedes), writes INDEX.md, splits legacy
|
|
4
|
+
non-numbered ADRs into an Unnumbered table, hard-fails on duplicate
|
|
5
|
+
numbers, filename/frontmatter mismatch, or broken supersedes links."""
|
|
6
|
+
import argparse, re, sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
NAMED = re.compile(r"^ADR-(\d{3})-([a-z0-9-]+)\.md$")
|
|
10
|
+
FM = re.compile(r"^---\n(.*?)\n---", re.DOTALL)
|
|
11
|
+
FIELD = re.compile(r"^([a-z_]+):\s*(.+?)\s*$", re.MULTILINE)
|
|
12
|
+
HEAD = "| # | Title | Status | Date | Supersedes |\n|---|---|---|---|---|"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def fm(t):
|
|
16
|
+
m = FM.search(t)
|
|
17
|
+
return {k: v.strip(" \"'") for k, v in FIELD.findall(m.group(1))} if m else {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def scan(d):
|
|
21
|
+
num, leg, errs, seen = [], [], [], {}
|
|
22
|
+
for p in sorted(d.glob("ADR-*.md")):
|
|
23
|
+
if p.name == "INDEX.md": continue
|
|
24
|
+
meta, m = fm(p.read_text(encoding="utf-8")), NAMED.match(p.name)
|
|
25
|
+
if not m:
|
|
26
|
+
leg.append({"path": p.name, **meta}); continue
|
|
27
|
+
n = m.group(1)
|
|
28
|
+
if meta.get("adr") and meta["adr"].lstrip("0") != n.lstrip("0"):
|
|
29
|
+
errs.append(f"{p.name}: adr={meta['adr']} != filename {n}")
|
|
30
|
+
if n in seen: errs.append(f"ADR-{n} duplicate: {p.name} and {seen[n]}")
|
|
31
|
+
seen[n] = p.name
|
|
32
|
+
num.append({"num": n, "slug": m.group(2), "path": p.name, **meta})
|
|
33
|
+
nums = {r["num"] for r in num}
|
|
34
|
+
for r in num:
|
|
35
|
+
s = r.get("supersedes", "—")
|
|
36
|
+
if s and s != "—":
|
|
37
|
+
t = s.replace("ADR-", "").lstrip("0").zfill(3)
|
|
38
|
+
if t not in nums: errs.append(f"{r['path']}: supersedes ADR-{t} not found")
|
|
39
|
+
return num, leg, errs
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def row(r):
|
|
43
|
+
title = r.get("decision", r.get("slug", "—")).replace("-", " ").title()
|
|
44
|
+
label = f"ADR-{r['num']}" if "num" in r else r["path"][:-3]
|
|
45
|
+
return (f"| [{label}]({r['path']}) | {title} | {r.get('status','—')} "
|
|
46
|
+
f"| {r.get('date','—')} | {r.get('supersedes','—')} |")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def render(num, leg):
|
|
50
|
+
out = ["# ADR Index", "", "_Auto-generated by `scripts/adr/regenerate_index.py`. Do not edit._", ""]
|
|
51
|
+
if not num and not leg: return "\n".join(out + ["No ADRs yet.", ""])
|
|
52
|
+
out += [HEAD, *(row(r) for r in num)]
|
|
53
|
+
if leg: out += ["", "## Unnumbered (legacy)", "", HEAD, *(row(r) for r in leg)]
|
|
54
|
+
return "\n".join(out + [""])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main():
|
|
58
|
+
ap = argparse.ArgumentParser(description="Regenerate ADR INDEX.md")
|
|
59
|
+
ap.add_argument("--dir", default="docs/adr/")
|
|
60
|
+
ap.add_argument("--check", action="store_true", help="exit 1 if INDEX.md is stale")
|
|
61
|
+
a = ap.parse_args()
|
|
62
|
+
d = Path(a.dir)
|
|
63
|
+
if not d.is_dir():
|
|
64
|
+
print(f"adr-dir not found: {d}", file=sys.stderr); return 2
|
|
65
|
+
num, leg, errs = scan(d)
|
|
66
|
+
for e in errs: print(f"error: {e}", file=sys.stderr)
|
|
67
|
+
if errs: return 2
|
|
68
|
+
rendered, idx = render(num, leg), d / "INDEX.md"
|
|
69
|
+
if a.check:
|
|
70
|
+
cur = idx.read_text(encoding="utf-8") if idx.exists() else ""
|
|
71
|
+
if cur != rendered: print(f"stale: {idx}", file=sys.stderr); return 1
|
|
72
|
+
return 0
|
|
73
|
+
idx.write_text(rendered, encoding="utf-8")
|
|
74
|
+
print(f"wrote {idx} ({len(num)} numbered, {len(leg)} legacy)")
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
sys.exit(main())
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""One-off: add --quiet flag to every check_*/lint_* script that lacks one.
|
|
3
|
+
|
|
4
|
+
Target pattern (the canonical example is check_one_off_location.py):
|
|
5
|
+
- argparse parser exists
|
|
6
|
+
- parser.add_argument("--quiet", action="store_true", help="Only print on failure")
|
|
7
|
+
- success print lines matching `print(...✅...)` are wrapped:
|
|
8
|
+
if not args.quiet:
|
|
9
|
+
print("✅ ...")
|
|
10
|
+
|
|
11
|
+
For scripts without argparse, fall back to a minimal sys.argv probe.
|
|
12
|
+
Reports a manual-review list for anything that doesn't match the simple pattern.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
SCRIPTS = Path("scripts")
|
|
20
|
+
SUCCESS_RE = re.compile(r'^(\s*)(print\((?:f)?["\'].*\u2705.*\))\s*$')
|
|
21
|
+
# Accept any parser var name (parser, ap, p, …) — capture both lhs (args var) and rhs (parser var).
|
|
22
|
+
PARSE_ARGS_RE = re.compile(
|
|
23
|
+
r"^(\s*)([A-Za-z_][A-Za-z_0-9]*)\s*=\s*([A-Za-z_][A-Za-z_0-9]*)\.parse_args\(.*\)\s*$"
|
|
24
|
+
)
|
|
25
|
+
TOP_IMPORT_RE = re.compile(r"^(?:import |from )\S")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def has_quiet_flag(text: str) -> bool:
|
|
29
|
+
return '"--quiet"' in text or "'--quiet'" in text
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def has_argparse(text: str) -> bool:
|
|
33
|
+
return "argparse" in text and ".add_argument(" in text and ".parse_args(" in text
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def patch_argparse_script(text: str) -> tuple[str, int]:
|
|
37
|
+
"""Insert --quiet arg before parse_args() call; gate ✅ print lines.
|
|
38
|
+
|
|
39
|
+
Returns (new_text, n_prints_gated). Caller checks both for sanity.
|
|
40
|
+
"""
|
|
41
|
+
lines = text.splitlines(keepends=True)
|
|
42
|
+
out: list[str] = []
|
|
43
|
+
inserted = False
|
|
44
|
+
n_gated = 0
|
|
45
|
+
parse_args_var: str | None = None
|
|
46
|
+
|
|
47
|
+
for line in lines:
|
|
48
|
+
if not inserted:
|
|
49
|
+
m = PARSE_ARGS_RE.match(line.rstrip("\n"))
|
|
50
|
+
if m:
|
|
51
|
+
indent = m.group(1)
|
|
52
|
+
parse_args_var = m.group(2) # lhs (args var)
|
|
53
|
+
parser_var = m.group(3) # rhs (parser var: parser/ap/p/…)
|
|
54
|
+
out.append(
|
|
55
|
+
f'{indent}{parser_var}.add_argument("--quiet", action="store_true", '
|
|
56
|
+
'help="Only print on failure")\n'
|
|
57
|
+
)
|
|
58
|
+
inserted = True
|
|
59
|
+
out.append(line)
|
|
60
|
+
|
|
61
|
+
if not inserted or parse_args_var is None:
|
|
62
|
+
return text, 0
|
|
63
|
+
|
|
64
|
+
# Now rewrite: gate success prints behind `if not <var>.quiet:`
|
|
65
|
+
text2 = "".join(out)
|
|
66
|
+
lines2 = text2.splitlines(keepends=True)
|
|
67
|
+
out2: list[str] = []
|
|
68
|
+
for line in lines2:
|
|
69
|
+
m = SUCCESS_RE.match(line.rstrip("\n"))
|
|
70
|
+
if m:
|
|
71
|
+
indent, stmt = m.group(1), m.group(2)
|
|
72
|
+
out2.append(f"{indent}if not {parse_args_var}.quiet:\n")
|
|
73
|
+
out2.append(f"{indent} {stmt}\n")
|
|
74
|
+
n_gated += 1
|
|
75
|
+
else:
|
|
76
|
+
out2.append(line)
|
|
77
|
+
return "".join(out2), n_gated
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def patch_plain_script(text: str) -> tuple[str, int]:
|
|
81
|
+
"""For scripts without argparse: add a sys.argv probe near the top.
|
|
82
|
+
|
|
83
|
+
Inserts after the last `import` or `from` line. Gates ✅ prints behind QUIET.
|
|
84
|
+
"""
|
|
85
|
+
lines = text.splitlines(keepends=True)
|
|
86
|
+
last_import = -1
|
|
87
|
+
for i, line in enumerate(lines):
|
|
88
|
+
# Only TOP-LEVEL imports (column 0) — skip nested imports inside try/def.
|
|
89
|
+
if TOP_IMPORT_RE.match(line):
|
|
90
|
+
last_import = i
|
|
91
|
+
if last_import < 0:
|
|
92
|
+
return text, 0
|
|
93
|
+
if "import sys" not in text:
|
|
94
|
+
lines.insert(last_import + 1, "import sys\n")
|
|
95
|
+
last_import += 1
|
|
96
|
+
lines.insert(last_import + 1, '\nQUIET = "--quiet" in sys.argv\n')
|
|
97
|
+
|
|
98
|
+
n_gated = 0
|
|
99
|
+
out: list[str] = []
|
|
100
|
+
for line in lines:
|
|
101
|
+
m = SUCCESS_RE.match(line.rstrip("\n"))
|
|
102
|
+
if m:
|
|
103
|
+
indent, stmt = m.group(1), m.group(2)
|
|
104
|
+
out.append(f"{indent}if not QUIET:\n")
|
|
105
|
+
out.append(f"{indent} {stmt}\n")
|
|
106
|
+
n_gated += 1
|
|
107
|
+
else:
|
|
108
|
+
out.append(line)
|
|
109
|
+
return "".join(out), n_gated
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main() -> int:
|
|
113
|
+
targets = sorted(SCRIPTS.glob("check_*.py")) + sorted(SCRIPTS.glob("lint_*.py"))
|
|
114
|
+
skipped, patched, manual = [], [], []
|
|
115
|
+
|
|
116
|
+
for f in targets:
|
|
117
|
+
text = f.read_text()
|
|
118
|
+
if has_quiet_flag(text):
|
|
119
|
+
skipped.append(f.name)
|
|
120
|
+
continue
|
|
121
|
+
if has_argparse(text):
|
|
122
|
+
new, n = patch_argparse_script(text)
|
|
123
|
+
if n == 0 or new == text:
|
|
124
|
+
manual.append((f.name, f"argparse but no ✅-print gated (n={n})"))
|
|
125
|
+
continue
|
|
126
|
+
f.write_text(new)
|
|
127
|
+
patched.append((f.name, n))
|
|
128
|
+
else:
|
|
129
|
+
new, n = patch_plain_script(text)
|
|
130
|
+
if n == 0 or new == text:
|
|
131
|
+
manual.append((f.name, "plain but no ✅-print to gate"))
|
|
132
|
+
continue
|
|
133
|
+
f.write_text(new)
|
|
134
|
+
patched.append((f.name, n))
|
|
135
|
+
|
|
136
|
+
print(f"Skipped (already had --quiet): {len(skipped)}")
|
|
137
|
+
for s in skipped:
|
|
138
|
+
print(f" · {s}")
|
|
139
|
+
print(f"\nPatched: {len(patched)}")
|
|
140
|
+
for name, n in patched:
|
|
141
|
+
print(f" · {name} (gated {n} print(s))")
|
|
142
|
+
print(f"\nManual review needed: {len(manual)}")
|
|
143
|
+
for name, why in manual:
|
|
144
|
+
print(f" · {name}: {why}")
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""One-off: inject {{.QUIET_FLAG}} into Taskfile cmds for --quiet-aware scripts."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# Scripts that accept --quiet (verified: 20 total = 3 pre-existing + 17 patched).
|
|
8
|
+
QUIET_AWARE = {
|
|
9
|
+
"check_always_budget", "check_one_off_location", "check_safety_floor_untouched",
|
|
10
|
+
"check_augmentignore", "check_command_count_messaging", "check_compressed_paths",
|
|
11
|
+
"check_council_layout", "check_council_references", "check_iron_law_prominence",
|
|
12
|
+
"check_md_language", "check_memory_proposal", "check_public_catalog_links",
|
|
13
|
+
"check_reply_consistency", "check_roadmap_trackable",
|
|
14
|
+
"lint_examples", "lint_handoffs", "lint_load_context", "lint_roadmap_complexity",
|
|
15
|
+
"lint_rule_interactions", "lint_rule_tiers",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
TASKFILES = sorted(Path("taskfiles").glob("*.yml")) + [Path("Taskfile.yml")]
|
|
19
|
+
patched = 0
|
|
20
|
+
for tf in TASKFILES:
|
|
21
|
+
text = tf.read_text()
|
|
22
|
+
new = text
|
|
23
|
+
for name in QUIET_AWARE:
|
|
24
|
+
# Match: cmd: python3 scripts/<name>.py [args]
|
|
25
|
+
# Insert {{.QUIET_FLAG}} after the script path, before any other args.
|
|
26
|
+
pat = re.compile(rf"(python3 scripts/{name}\.py)(\s|$)(?!.*QUIET_FLAG)")
|
|
27
|
+
new, n = pat.subn(r"\1 {{.QUIET_FLAG}}\2", new)
|
|
28
|
+
if n:
|
|
29
|
+
patched += n
|
|
30
|
+
if new != text:
|
|
31
|
+
tf.write_text(new)
|
|
32
|
+
print(f" patched: {tf}")
|
|
33
|
+
print(f"\nTotal injections: {patched}")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Phase 10.7 baseline — runs only verbosity-aware patched-script tasks.
|
|
3
|
+
# Skips broken-on-dirty-tree tasks (consistency, check-index, validate-schema).
|
|
4
|
+
# This subset is what actually demonstrates the --quiet effect.
|
|
5
|
+
TASKS="check-compressed-paths check-refs check-portability lint-roadmap-complexity check-public-catalog-links check-command-count check-cluster-patterns lint-rule-interactions lint-load-context check-context-paths check-no-roadmap-refs check-council-references lint-one-off-age check-reply-consistency check-iron-law-prominence check-always-budget check-one-off-location lint-rule-budget lint-skills lint-rule-tiers lint-handoffs lint-marketplace lint-examples"
|
|
6
|
+
|
|
7
|
+
run() {
|
|
8
|
+
local label=$1
|
|
9
|
+
local level=$2
|
|
10
|
+
local out=$3
|
|
11
|
+
AGENT_SCRIPT_VERBOSITY=$level task $TASKS > "$out" 2>&1
|
|
12
|
+
local lines=$(wc -l < "$out" | tr -d ' ')
|
|
13
|
+
local chars=$(wc -c < "$out" | tr -d ' ')
|
|
14
|
+
echo "$label: lines=$lines chars=$chars"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
echo "=== MINIMAL ==="
|
|
18
|
+
run "MINIMAL" minimal /tmp/ci-min.log
|
|
19
|
+
echo ""
|
|
20
|
+
echo "=== VERBOSE ==="
|
|
21
|
+
run "VERBOSE" verbose /tmp/ci-vrb.log
|
|
22
|
+
|
|
23
|
+
ML=$(wc -l < /tmp/ci-min.log | tr -d ' ')
|
|
24
|
+
MC=$(wc -c < /tmp/ci-min.log | tr -d ' ')
|
|
25
|
+
VL=$(wc -l < /tmp/ci-vrb.log | tr -d ' ')
|
|
26
|
+
VC=$(wc -c < /tmp/ci-vrb.log | tr -d ' ')
|
|
27
|
+
|
|
28
|
+
echo ""
|
|
29
|
+
python3 -c "
|
|
30
|
+
ml=$ML; vl=$VL; mc=$MC; vc=$VC
|
|
31
|
+
dl=(vl-ml)/vl*100 if vl else 0
|
|
32
|
+
dc=(vc-mc)/vc*100 if vc else 0
|
|
33
|
+
print(f'Lines: {ml} -> {vl}, reduction={dl:.1f}% (target >=40%)')
|
|
34
|
+
print(f'Chars: {mc} -> {vc}, reduction={dc:.1f}%')
|
|
35
|
+
print('verdict:', 'MET' if dl >= 40 else f'MISSED ({dl:.1f}% < 40%)')
|
|
36
|
+
"
|