@event4u/agent-config 1.21.0 → 1.23.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.
Files changed (130) hide show
  1. package/.agent-src/commands/agents/cleanup.md +31 -17
  2. package/.agent-src/commands/bug-fix.md +1 -0
  3. package/.agent-src/commands/bug-investigate.md +1 -0
  4. package/.agent-src/commands/challenge-me/vision.md +348 -0
  5. package/.agent-src/commands/challenge-me/with-docs.md +333 -0
  6. package/.agent-src/commands/challenge-me.md +61 -0
  7. package/.agent-src/commands/commit/in-chunks.md +30 -10
  8. package/.agent-src/commands/commit.md +46 -6
  9. package/.agent-src/commands/compress.md +19 -13
  10. package/.agent-src/commands/cost-report.md +120 -0
  11. package/.agent-src/commands/council/default.md +64 -17
  12. package/.agent-src/commands/create-pr/description-only.md +8 -0
  13. package/.agent-src/commands/create-pr.md +99 -80
  14. package/.agent-src/commands/feature/plan.md +13 -7
  15. package/.agent-src/commands/grill-me.md +38 -0
  16. package/.agent-src/commands/judge/steps.md +1 -1
  17. package/.agent-src/commands/memory/add.md +16 -8
  18. package/.agent-src/commands/memory/promote.md +17 -9
  19. package/.agent-src/commands/optimize/rtk.md +16 -11
  20. package/.agent-src/commands/prepare-for-review.md +12 -6
  21. package/.agent-src/commands/project-analyze.md +31 -20
  22. package/.agent-src/commands/review-changes.md +24 -15
  23. package/.agent-src/commands/roadmap/ai-council.md +183 -0
  24. package/.agent-src/commands/roadmap/create.md +20 -10
  25. package/.agent-src/commands/roadmap/process-full.md +58 -0
  26. package/.agent-src/commands/roadmap/process-phase.md +69 -0
  27. package/.agent-src/commands/roadmap/process-step.md +57 -0
  28. package/.agent-src/commands/roadmap.md +44 -16
  29. package/.agent-src/commands/threat-model.md +1 -0
  30. package/.agent-src/contexts/augment-infrastructure.md +1 -1
  31. package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +53 -18
  32. package/.agent-src/contexts/contracts/frugality-charter.md +57 -0
  33. package/.agent-src/contexts/execution/roadmap-process-loop.md +125 -0
  34. package/.agent-src/contexts/skills-and-commands.md +1 -1
  35. package/.agent-src/rules/architecture.md +9 -0
  36. package/.agent-src/rules/ask-when-uncertain.md +3 -13
  37. package/.agent-src/rules/caveman-speak.md +78 -0
  38. package/.agent-src/rules/direct-answers.md +5 -14
  39. package/.agent-src/rules/improve-before-implement.md +1 -0
  40. package/.agent-src/rules/invite-challenge.md +71 -0
  41. package/.agent-src/rules/markdown-safe-codeblocks.md +6 -7
  42. package/.agent-src/rules/no-cheap-questions.md +4 -14
  43. package/.agent-src/rules/token-efficiency.md +5 -7
  44. package/.agent-src/skills/adr-create/SKILL.md +197 -0
  45. package/.agent-src/skills/adversarial-review/SKILL.md +1 -0
  46. package/.agent-src/skills/agent-docs-writing/SKILL.md +23 -1
  47. package/.agent-src/skills/ai-council/SKILL.md +132 -8
  48. package/.agent-src/skills/bug-analyzer/SKILL.md +1 -0
  49. package/.agent-src/skills/command-writing/SKILL.md +23 -0
  50. package/.agent-src/skills/context-authoring/SKILL.md +23 -0
  51. package/.agent-src/skills/conventional-commits-writing/SKILL.md +23 -0
  52. package/.agent-src/skills/guideline-writing/SKILL.md +22 -0
  53. package/.agent-src/skills/persona-writing/SKILL.md +153 -0
  54. package/.agent-src/skills/readme-writing/SKILL.md +20 -0
  55. package/.agent-src/skills/readme-writing-package/SKILL.md +19 -0
  56. package/.agent-src/skills/roadmap-management/SKILL.md +7 -7
  57. package/.agent-src/skills/roadmap-writing/SKILL.md +157 -0
  58. package/.agent-src/skills/rule-writing/SKILL.md +22 -0
  59. package/.agent-src/skills/script-writing/SKILL.md +226 -0
  60. package/.agent-src/skills/skill-writing/SKILL.md +23 -0
  61. package/.agent-src/skills/systematic-debugging/SKILL.md +22 -2
  62. package/.agent-src/skills/technical-specification/SKILL.md +58 -1
  63. package/.agent-src/skills/test-driven-development/SKILL.md +24 -0
  64. package/.agent-src/skills/threat-modeling/SKILL.md +1 -0
  65. package/.agent-src/templates/agent-settings.md +87 -3
  66. package/.agent-src/templates/command.md +30 -9
  67. package/.agent-src/templates/roadmaps.md +10 -2
  68. package/.agent-src/templates/rule.md +8 -0
  69. package/.agent-src/templates/skill.md +49 -0
  70. package/.claude-plugin/marketplace.json +14 -2
  71. package/AGENTS.md +3 -3
  72. package/CHANGELOG.md +73 -0
  73. package/README.md +5 -5
  74. package/config/agent-settings.template.yml +22 -0
  75. package/docs/architecture.md +4 -4
  76. package/docs/contracts/command-clusters.md +45 -1
  77. package/docs/customization.md +72 -0
  78. package/docs/decisions/ADR-003-flat-cluster-subs-and-colon-syntax.md +126 -0
  79. package/docs/decisions/INDEX.md +15 -0
  80. package/docs/getting-started.md +2 -2
  81. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +27 -19
  82. package/docs/guidelines/agent-infra/carve-out-predicates.md +17 -0
  83. package/docs/guidelines/agent-infra/mcp-request-signing.md +199 -0
  84. package/docs/guidelines/agent-infra/naming.md +1 -1
  85. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +11 -4
  86. package/package.json +1 -1
  87. package/scripts/_lib/__init__.py +5 -0
  88. package/scripts/_lib/script_output.py +140 -0
  89. package/scripts/_phase2_shim_helper.py +1 -1
  90. package/scripts/adr/regenerate_index.py +79 -0
  91. package/scripts/ai_council/one_off_archive/2026-05/_one_off_add_quiet.py +149 -0
  92. package/scripts/ai_council/one_off_archive/2026-05/_one_off_inject_quiet_flag.py +33 -0
  93. package/scripts/ai_council/one_off_archive/2026-05/_one_off_measure_v2.sh +36 -0
  94. package/scripts/ai_council/one_off_archive/2026-05/_one_off_measure_verbosity.sh +26 -0
  95. package/scripts/ai_council/one_off_archive/2026-05/_one_off_per_task.sh +41 -0
  96. package/scripts/ai_council/one_off_archive/2026-05/_one_off_silent_taskfiles.py +98 -0
  97. package/scripts/check_augmentignore.py +4 -1
  98. package/scripts/check_command_count_messaging.py +4 -1
  99. package/scripts/check_compressed_paths.py +4 -1
  100. package/scripts/check_council_layout.py +4 -1
  101. package/scripts/check_council_references.py +4 -1
  102. package/scripts/check_iron_law_prominence.py +3 -1
  103. package/scripts/check_md_language.py +3 -1
  104. package/scripts/check_memory_proposal.py +3 -1
  105. package/scripts/check_public_catalog_links.py +4 -1
  106. package/scripts/check_reply_consistency.py +8 -2
  107. package/scripts/check_roadmap_trackable.py +4 -1
  108. package/scripts/compile_router.py +27 -0
  109. package/scripts/compress.py +33 -19
  110. package/scripts/cost/budget.mjs +152 -0
  111. package/scripts/cost/track.mjs +144 -0
  112. package/scripts/council_cli.py +127 -10
  113. package/scripts/first-run.sh +3 -9
  114. package/scripts/install-hooks.sh +19 -1
  115. package/scripts/install.py +17 -12
  116. package/scripts/install.sh +19 -8
  117. package/scripts/lint_examples.py +6 -2
  118. package/scripts/lint_handoffs.py +4 -1
  119. package/scripts/lint_load_context.py +4 -1
  120. package/scripts/lint_roadmap_complexity.py +6 -2
  121. package/scripts/lint_rule_interactions.py +4 -1
  122. package/scripts/lint_rule_tiers.py +4 -1
  123. package/scripts/measure_frugality_savings.py +164 -0
  124. package/scripts/migrate_command_suggestions.py +2 -2
  125. package/scripts/runtime_dispatcher.py +11 -0
  126. package/scripts/schemas/command.schema.json +5 -0
  127. package/scripts/schemas/rule.schema.json +5 -0
  128. package/scripts/schemas/skill.schema.json +5 -0
  129. package/scripts/skill_linter.py +208 -3
  130. package/.agent-src/commands/roadmap/execute.md +0 -109
@@ -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.
@@ -41,7 +41,7 @@ source: package
41
41
  | Pattern | When | Examples |
42
42
  |---|---|---|
43
43
  | `{verb}-{target}` | Action commands | `create-pr`, `fix-ci`, `commit` |
44
- | `{target}-{verb}` | Target-first grouping | `roadmap-create`, `roadmap-execute` |
44
+ | `{target}-{verb}` | Target-first grouping | `roadmap-create` (legacy atomic; current cluster form is `/roadmap:create` and `/roadmap:process-step|phase|full`) |
45
45
  | `{scope}-{action}` | Scoped actions | `optimize-agents`, `review-changes` |
46
46
 
47
47
  ### Guidelines
@@ -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:** [`scripts/roadmap_progress_hook.py`](../../scripts/roadmap_progress_hook.py)
13
- > on Augment + Claude Code (`PostToolUse`). Hook is primary; the prose
14
- > below is the specification the hook implements and the fallback when
15
- > the platform has no hook surface.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "Shared agent configuration \u2014 skills, rules, commands, guidelines, and templates for AI coding tools",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -0,0 +1,5 @@
1
+ # scripts/_lib — shared helpers for scripts/*.py.
2
+ #
3
+ # Phase 10 of road-to-token-frugality. The first member is
4
+ # `script_output` — a thin verbosity-aware print router that reads
5
+ # `.agent-settings.yml` and the `AGENT_SCRIPT_VERBOSITY` env var.
@@ -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()
@@ -25,7 +25,7 @@ PHASE2_SHIMS: list[tuple[str, str]] = [
25
25
  ("propose-memory", "memory propose"),
26
26
  # roadmap cluster
27
27
  ("roadmap-create", "roadmap create"),
28
- ("roadmap-execute", "roadmap execute"),
28
+ ("roadmap-execute", "roadmap process-phase"),
29
29
  # module cluster
30
30
  ("module-create", "module create"),
31
31
  ("module-explore", "module explore"),
@@ -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}")