@event4u/agent-config 2.9.0 → 2.10.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/rules/no-roadmap-references.md +19 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +32 -3
- package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +147 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +28 -0
- package/README.md +31 -11
- package/config/agent-settings.template.yml +28 -0
- package/docs/contracts/decision-trace-v1.md +30 -0
- package/docs/contracts/hook-architecture-v1.md +46 -0
- package/docs/contracts/memory-visibility-v1.md +33 -0
- package/docs/contracts/settings-sync-yaml-subset.md +138 -0
- package/docs/readme-split-plan.md +102 -0
- package/package.json +1 -1
- package/scripts/_cli/cmd_settings_check.py +171 -0
- package/scripts/agent-config +40 -0
- package/scripts/chat_history.py +19 -0
- package/scripts/check_council_references.py +46 -5
- package/scripts/hooks/dispatch_hook.py +5 -1
- package/scripts/hooks/replay_hook.py +144 -0
- package/scripts/hooks/state_io.py +24 -1
- package/scripts/hooks_doctor.py +184 -0
- package/scripts/lint_hook_concern_budget.py +203 -0
- package/scripts/roadmap_progress_hook.py +11 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
stability: beta
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Settings-sync YAML subset
|
|
6
|
+
|
|
7
|
+
**Purpose.** Pin the YAML feature set that `.agent-settings.yml` and
|
|
8
|
+
`config/agent-settings.template.yml` may use, so contributors can cite a
|
|
9
|
+
contract instead of inferring it from
|
|
10
|
+
[`scripts/sync_yaml_rt.py`](../../scripts/sync_yaml_rt.py) source. The
|
|
11
|
+
sync engine ([ADR](adr-settings-sync-engine.md)) is a custom stdlib-only
|
|
12
|
+
round-trip parser/emitter; staying inside the subset below is what
|
|
13
|
+
keeps user-line preservation (every byte of every user line round-trips
|
|
14
|
+
unchanged unless the merger explicitly edits the key).
|
|
15
|
+
|
|
16
|
+
Authoritative source: this document. The module docstring of
|
|
17
|
+
`sync_yaml_rt.py` mirrors it; on drift, this file wins and the docstring
|
|
18
|
+
is corrected to match.
|
|
19
|
+
|
|
20
|
+
## Supported
|
|
21
|
+
|
|
22
|
+
### Document shape
|
|
23
|
+
|
|
24
|
+
- One YAML document per file. No `---` or `...` document separators.
|
|
25
|
+
- UTF-8. CRLF and LF line endings — both accepted, preserved per-line.
|
|
26
|
+
|
|
27
|
+
### Mappings (sections)
|
|
28
|
+
|
|
29
|
+
- Block-style mappings only (`key: value` on its own line).
|
|
30
|
+
- Indent: 2- or 4-space, **no tabs** in indent.
|
|
31
|
+
- Nested mappings unlimited in depth (the template uses 3 levels —
|
|
32
|
+
e.g. `chat_history.archive.cleanup_after_days`).
|
|
33
|
+
- Duplicate keys at the same level: **last wins** (the later line
|
|
34
|
+
carries the value; the earlier entry is replaced).
|
|
35
|
+
|
|
36
|
+
### Scalars (values)
|
|
37
|
+
|
|
38
|
+
- Bare scalars: `enabled`, `42`, `true`, `~`, `null`, `None`.
|
|
39
|
+
- Single-quoted strings: `'literal text'`.
|
|
40
|
+
- Double-quoted strings: `"literal text"`.
|
|
41
|
+
- Bools, ints, `~` / `null` / `None` are kept **verbatim** — the
|
|
42
|
+
parser does not normalise `True` → `true` or `null` → `~`.
|
|
43
|
+
|
|
44
|
+
### Lists (sequences of scalars)
|
|
45
|
+
|
|
46
|
+
- Block-style lists:
|
|
47
|
+
```yaml
|
|
48
|
+
allowlist:
|
|
49
|
+
- foo
|
|
50
|
+
- bar
|
|
51
|
+
```
|
|
52
|
+
Indent inside the list must be consistent.
|
|
53
|
+
- Inline-flow lists, **flat only**: `[a, b, c]`.
|
|
54
|
+
- List items are scalars only. Nested mappings inside a list item are
|
|
55
|
+
**not** supported (see below).
|
|
56
|
+
|
|
57
|
+
### Comments and blank lines
|
|
58
|
+
|
|
59
|
+
- `#`-comments — full-line and inline (`key: value # comment`). Both
|
|
60
|
+
preserved verbatim, including leading whitespace and the gap before
|
|
61
|
+
`#`.
|
|
62
|
+
- Blank lines preserved verbatim — the engine never collapses them.
|
|
63
|
+
|
|
64
|
+
## Not supported (parser raises `ValueError` with a line number)
|
|
65
|
+
|
|
66
|
+
The following YAML features are out of contract. A user file that uses
|
|
67
|
+
any of them surfaces as `ValueError` from `scripts/sync_yaml_rt.py:sync`,
|
|
68
|
+
which `scripts/sync_agent_settings.py` catches and reports as **exit
|
|
69
|
+
code 2** with a line-numbered message.
|
|
70
|
+
|
|
71
|
+
- **Anchors and aliases** — `&name`, `*name`.
|
|
72
|
+
- **Multi-document streams** — `---` / `...` separators.
|
|
73
|
+
- **Nested flow mappings** — `key: {nested: value}` inline. Block-style
|
|
74
|
+
nested mappings are fine; flow-style nested mappings are not.
|
|
75
|
+
- **Nested mappings inside list items** — `- name: foo` followed by
|
|
76
|
+
indented children. Lists hold scalars only.
|
|
77
|
+
- **Complex keys** — `? [composite, key]: value`.
|
|
78
|
+
- **Tagged scalars** — `!!str 42`, `!Custom value`.
|
|
79
|
+
- **Multiline scalar styles** — `|` (literal) and `>` (folded) block
|
|
80
|
+
scalars.
|
|
81
|
+
- **Tabs in indent** — even one tab character in indent.
|
|
82
|
+
- **Mixed indent inside a block** — every child of a parent must share
|
|
83
|
+
the same indent.
|
|
84
|
+
|
|
85
|
+
Pinned by `tests/test_sync_round_trip.py` (34 tests) — every
|
|
86
|
+
not-supported feature has at least one fixture that asserts the
|
|
87
|
+
`ValueError` message.
|
|
88
|
+
|
|
89
|
+
## Test pinning
|
|
90
|
+
|
|
91
|
+
- Verbatim round-trip: `tests/test_sync_round_trip.py::test_user_block_round_trip_is_idempotent`, `::test_three_level_idempotent`.
|
|
92
|
+
- Out-of-subset rejection: same file, fixtures under
|
|
93
|
+
`tests/fixtures/sync_yaml_rt/` named `bad_*.yml`.
|
|
94
|
+
- CLI exit code on malformed input:
|
|
95
|
+
`tests/test_sync_agent_settings.py::test_malformed_user_yaml_exits_2_with_message`.
|
|
96
|
+
|
|
97
|
+
Any parser change is gated on those tests staying green. New fixtures
|
|
98
|
+
for new features land under `tests/fixtures/sync_yaml_rt/`.
|
|
99
|
+
|
|
100
|
+
## Why this subset (and why it is fixed)
|
|
101
|
+
|
|
102
|
+
The driving requirement from
|
|
103
|
+
[`layered-settings`](../guidelines/agent-infra/layered-settings.md) is
|
|
104
|
+
**verbatim user-line preservation**. `ruamel.yaml` and PyYAML both
|
|
105
|
+
re-emit through their own emitters, which normalises whitespace,
|
|
106
|
+
quoting, and blank-line placement. A stdlib parser limited to this
|
|
107
|
+
subset gives byte-identity across two consecutive syncs — the property
|
|
108
|
+
the merger relies on for additive insertion.
|
|
109
|
+
|
|
110
|
+
Out-of-subset YAML therefore is not a parser bug; it is a contract
|
|
111
|
+
violation by the user file. The friendly `ValueError` and exit code 2
|
|
112
|
+
are the contract's failure surface.
|
|
113
|
+
|
|
114
|
+
## Revisit triggers
|
|
115
|
+
|
|
116
|
+
This subset is **fixed** until one of the
|
|
117
|
+
[ADR revisit triggers](adr-settings-sync-engine.md#revisit-triggers)
|
|
118
|
+
fires — namely:
|
|
119
|
+
|
|
120
|
+
1. `.agent-settings.yml` schema gains a YAML feature outside the subset
|
|
121
|
+
(anchors, multi-doc, complex keys, nested flow mappings) — the cost
|
|
122
|
+
of extending the parser exceeds the cost of adopting `ruamel.yaml`.
|
|
123
|
+
2. The verbatim-preservation contract is relaxed — the driver for the
|
|
124
|
+
custom parser is gone.
|
|
125
|
+
3. The 0-dep posture for Python tooling is dropped at the package level
|
|
126
|
+
— the marginal cost of one more dep collapses.
|
|
127
|
+
4. A maintenance bug surfaces in the engine that ruamel's mature spec
|
|
128
|
+
coverage would have prevented.
|
|
129
|
+
|
|
130
|
+
A new ADR (with successor link) is required to change the subset; this
|
|
131
|
+
document is updated in the same commit.
|
|
132
|
+
|
|
133
|
+
## See also
|
|
134
|
+
|
|
135
|
+
- [`docs/contracts/adr-settings-sync-engine.md`](adr-settings-sync-engine.md) — decision record for the stdlib-only engine.
|
|
136
|
+
- [`docs/guidelines/agent-infra/layered-settings.md`](../guidelines/agent-infra/layered-settings.md) § Sync rules — the additive-merge-with-user-line-preservation contract this subset implements.
|
|
137
|
+
- [`scripts/sync_yaml_rt.py`](../../scripts/sync_yaml_rt.py) — implementation; module docstring mirrors this file.
|
|
138
|
+
- [`scripts/sync_agent_settings.py`](../../scripts/sync_agent_settings.py) — CLI driver and exit-code contract.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# README three-audience split — plan
|
|
2
|
+
|
|
3
|
+
Annotated outline for `P2.2a` in
|
|
4
|
+
[`road-to-proof-not-features.md`](../agents/roadmaps/road-to-proof-not-features.md).
|
|
5
|
+
Decides the **information architecture**, not the prose. No content
|
|
6
|
+
rewrite happens in this step; `P2.2b` applies the mapping.
|
|
7
|
+
|
|
8
|
+
## Target headings (top of README, in order)
|
|
9
|
+
|
|
10
|
+
1. **Use it in your project** — anchor `#use-it`
|
|
11
|
+
2. **Prove it** — anchor `#prove-it`
|
|
12
|
+
3. **Contribute** — anchor `#contribute`
|
|
13
|
+
|
|
14
|
+
Each branch opens with one paragraph + one primary CTA. AI Council is
|
|
15
|
+
not mentioned in any branch (verified by `P3.4`).
|
|
16
|
+
|
|
17
|
+
### Anchor-stability promise
|
|
18
|
+
|
|
19
|
+
`P2.2b` must keep these existing anchors intact so external inbound
|
|
20
|
+
links survive:
|
|
21
|
+
|
|
22
|
+
| Anchor today | Lives under (new) | Why |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `#quickstart` | `#use-it` | npm/composer search results, social links |
|
|
25
|
+
| `#supported-tools` | `#use-it` | most-cited section on the web |
|
|
26
|
+
| `#what-your-agent-is-asked-to-do` | `#prove-it` | linked from blog posts |
|
|
27
|
+
| `#documentation` | `#use-it` | docs portal entry |
|
|
28
|
+
| `#development` | `#contribute` | contributor guides |
|
|
29
|
+
|
|
30
|
+
Other section anchors may be renamed; `lint-readme` checks the table
|
|
31
|
+
above and the three new audience anchors only.
|
|
32
|
+
|
|
33
|
+
## Block-by-block mapping
|
|
34
|
+
|
|
35
|
+
Every existing top-of-README block, in source order, mapped to
|
|
36
|
+
exactly one branch. "Drop" = block is retired; "Move" = relocated as-
|
|
37
|
+
is; "Reframe" = block stays but its lead-in / CTA changes (still no
|
|
38
|
+
copy rewrite in this step — the reframe direction is decided here,
|
|
39
|
+
applied in `P2.2b`).
|
|
40
|
+
|
|
41
|
+
| # | Block (current heading) | Lines | Branch | Action | Notes |
|
|
42
|
+
|---|---|---|---|---|---|
|
|
43
|
+
| 1 | Title + tagline + stats badge | 1–13 | — | Keep above branches | Survives unchanged; counts updated by `update_readme_counts`. |
|
|
44
|
+
| 2 | `## Start here` (three-paths table) | 15–25 | — | **Drop** | Replaced by the three branch sections themselves; rows map cleanly: `/onboard` → Use, `task ci` → Contribute, `task generate-tools` → Use. |
|
|
45
|
+
| 3 | `## Quickstart` lead-in | 27–39 | Use it | Move | Becomes the opening paragraph under `#use-it`. |
|
|
46
|
+
| 4 | `### For teams (recommended)` | 40–79 | Use it | Move | Primary CTA for `#use-it`. |
|
|
47
|
+
| 5 | `### Pick specific AIs` | 81–101 | Use it | Move | Stays under Quickstart subtree. |
|
|
48
|
+
| 6 | `#### Global install` | 103–124 | Use it | Move | Subsection of Pick specific AIs. |
|
|
49
|
+
| 7 | `### For individual use (optional)` | 126–144 | Use it | Move | Alternate install path. |
|
|
50
|
+
| 8 | `### Self-hosted MCP on Cloudflare` | 146–226 | Use it | Move | Operator install path; deep but consumer-facing. |
|
|
51
|
+
| 9 | `#### Lock your Worker behind Bearer` | 196–213 | Use it | Move | Subsection of MCP block; stays nested. |
|
|
52
|
+
| 10 | `### Optional: persistent agent memory` | 228–247 | Use it | Move | Companion package install. |
|
|
53
|
+
| 11 | `## 2-minute demo: /implement-ticket` | 251–285 | Prove it | Move | Flagship evidence surface. Primary CTA for `#prove-it`. |
|
|
54
|
+
| 12 | `### Sibling entrypoint: /work` | 287–316 | Prove it | Move | Same engine, second envelope. |
|
|
55
|
+
| 13 | `### Product UI track` | 318–347 | Prove it | Move | Third evidence surface. |
|
|
56
|
+
| 14 | `## What your agent is asked to do` | 351–365 | Prove it | Move | Intent table — proof of behaviour, not features. |
|
|
57
|
+
| 15 | `## What this package is — and what it isn't` | 369–398 | Prove it | Move | Scope-honesty surface; loadbearing for the "proof" framing. |
|
|
58
|
+
| 16 | `## You don't need everything` (cost profiles) | 402–423 | Prove it | Reframe | Currently sits as "feature" prose; the new framing is "proof that the package shrinks to fit". |
|
|
59
|
+
| 17 | `## Who this is for` (stack coverage) | 427–439 | Prove it | Move | Honest depth claim — also evidence-side. |
|
|
60
|
+
| 18 | `## Featured Skills` | 443–462 | Use it | Move | Catalog teaser → consumer surface. |
|
|
61
|
+
| 19 | `## Featured Commands` | 466–481 | Use it | Move | Catalog teaser → consumer surface. |
|
|
62
|
+
| 20 | `## Supported Tools / Project-installed` | 487–527 | Use it | Move | Per-tool install matrix. |
|
|
63
|
+
| 21 | `## Supported Tools / Plugin-installed` | 529–541 | Use it | Move | Subsection. |
|
|
64
|
+
| 22 | `## Supported Tools / Cloud / Hosted-agent` | 543–558 | Use it | Move | Subsection. |
|
|
65
|
+
| 23 | `## Core Principles` | 562–570 | Prove it | Move | Behavioural floor — proof-side. |
|
|
66
|
+
| 24 | `## Documentation` (index table) | 574–589 | Use it | Move | Doc portal entry. |
|
|
67
|
+
| 25 | `### Maintainer telemetry (opt-in)` | 591–608 | Contribute | Move | Engagement measurement — maintainer / contributor surface. |
|
|
68
|
+
| 26 | `### Context-aware command suggestion` | 610–629 | Use it | Move | Consumer-facing feature toggle. |
|
|
69
|
+
| 27 | `## Development` | 633–642 | Contribute | Move | Primary CTA for `#contribute`. |
|
|
70
|
+
| 28 | `## Requirements` | 644–649 | Use it | Move | Install gate — Use-side, not Contribute. |
|
|
71
|
+
| 29 | `## License` | 651–653 | — | Keep at bottom | Footer; outside the three branches. |
|
|
72
|
+
|
|
73
|
+
## Branch outlines (post-migration shape)
|
|
74
|
+
|
|
75
|
+
### `## Use it in your project`
|
|
76
|
+
|
|
77
|
+
Opening paragraph: one-line "Two minutes from npx to a better-behaved
|
|
78
|
+
agent." Primary CTA: `npx @event4u/agent-config init`. Children:
|
|
79
|
+
Quickstart subtree (#3–#7), MCP operator path (#8–#9), optional memory
|
|
80
|
+
(#10), Featured Skills + Commands (#18–#19), Supported Tools (#20–#22),
|
|
81
|
+
Documentation (#24), Command suggestion (#26), Requirements (#28).
|
|
82
|
+
|
|
83
|
+
### `## Prove it`
|
|
84
|
+
|
|
85
|
+
Opening paragraph: one-line "What the agent actually does, with
|
|
86
|
+
evidence." Primary CTA: `/implement-ticket` demo (#11). Children:
|
|
87
|
+
`/work` (#12), Product UI track (#13), Intent table (#14), Scope
|
|
88
|
+
statement (#15), Cost profiles reframed (#16), Stack coverage (#17),
|
|
89
|
+
Core Principles (#23).
|
|
90
|
+
|
|
91
|
+
### `## Contribute`
|
|
92
|
+
|
|
93
|
+
Opening paragraph: one-line "Editing rules, skills, commands — the
|
|
94
|
+
contributor loop." Primary CTA: `task ci` (#27). Children: Maintainer
|
|
95
|
+
telemetry (#25). External links: `CONTRIBUTING.md`, `AGENTS.md`,
|
|
96
|
+
`docs/development.md`.
|
|
97
|
+
|
|
98
|
+
## Verification (P2.2c preview)
|
|
99
|
+
|
|
100
|
+
Grep-based test asserts `## Use it in your project`, `## Prove it`,
|
|
101
|
+
`## Contribute` appear in that order. `lint-readme` keeps anchor
|
|
102
|
+
stability for the rows in the Anchor-stability promise table.
|
package/package.json
CHANGED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""``agent-config settings:check`` — validate ``.agent-settings.yml`` against the supported YAML subset.
|
|
2
|
+
|
|
3
|
+
Read-only. Implements P3.2 of road-to-proof-not-features.md. The contract
|
|
4
|
+
this checks against is pinned in
|
|
5
|
+
``docs/contracts/settings-sync-yaml-subset.md``; out-of-subset constructs
|
|
6
|
+
cause :class:`sync_yaml_rt` to raise ``ValueError`` during a sync. This
|
|
7
|
+
CLI surfaces the same findings *before* a sync runs, so users can fix
|
|
8
|
+
their file without watching the merge fail.
|
|
9
|
+
|
|
10
|
+
Output line format::
|
|
11
|
+
|
|
12
|
+
line:N <kind> <verdict> <fix hint>
|
|
13
|
+
|
|
14
|
+
Exit codes:
|
|
15
|
+
|
|
16
|
+
* ``0`` — file is inside the supported subset (or absent and ``--allow-missing``).
|
|
17
|
+
* ``1`` — one or more findings (verdict ``not supported``).
|
|
18
|
+
* ``2`` — file absent (without ``--allow-missing``) or unreadable.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# Imported lazily inside ``main`` so a missing engine cannot break ``--help``.
|
|
28
|
+
|
|
29
|
+
DEFAULT_PATH = ".agent-settings.yml"
|
|
30
|
+
|
|
31
|
+
# Out-of-subset patterns detected by a line-level pre-scan. Each rule is
|
|
32
|
+
# (label, regex, fix hint). The regex is applied to the *stripped* body
|
|
33
|
+
# of each non-comment line so leading indent does not affect matching.
|
|
34
|
+
_PRESCAN_RULES: tuple[tuple[str, re.Pattern[str], str], ...] = (
|
|
35
|
+
(
|
|
36
|
+
"multi-doc separator",
|
|
37
|
+
re.compile(r"^(---|\.\.\.)\s*(#.*)?$"),
|
|
38
|
+
"remove the separator — one YAML document per file only.",
|
|
39
|
+
),
|
|
40
|
+
(
|
|
41
|
+
"complex key",
|
|
42
|
+
re.compile(r"^\?\s"),
|
|
43
|
+
"rewrite as a plain ``key: value`` mapping line.",
|
|
44
|
+
),
|
|
45
|
+
(
|
|
46
|
+
"block-scalar indicator",
|
|
47
|
+
re.compile(r":\s*[|>][+-]?\s*(#.*)?$"),
|
|
48
|
+
"inline the value as a single-line quoted scalar.",
|
|
49
|
+
),
|
|
50
|
+
(
|
|
51
|
+
"tagged scalar",
|
|
52
|
+
re.compile(r":\s*!!?[A-Za-z_]"),
|
|
53
|
+
"remove the ``!tag``; the parser does not honour it.",
|
|
54
|
+
),
|
|
55
|
+
(
|
|
56
|
+
"anchor / alias",
|
|
57
|
+
re.compile(r":\s*[&*][A-Za-z_]"),
|
|
58
|
+
"expand the anchor inline — anchors / aliases are not supported.",
|
|
59
|
+
),
|
|
60
|
+
(
|
|
61
|
+
"nested flow-mapping",
|
|
62
|
+
re.compile(r":\s*\{[^}]*:[^}]*\}"),
|
|
63
|
+
"rewrite as a block-style nested mapping (indented child keys).",
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _scan_line(stripped: str) -> tuple[str, str] | None:
|
|
69
|
+
if not stripped or stripped.startswith("#"):
|
|
70
|
+
return None
|
|
71
|
+
for label, pattern, hint in _PRESCAN_RULES:
|
|
72
|
+
if pattern.search(stripped):
|
|
73
|
+
return label, hint
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _scan_text(text: str) -> list[dict]:
|
|
78
|
+
findings: list[dict] = []
|
|
79
|
+
for lineno, raw in enumerate(text.splitlines(), 1):
|
|
80
|
+
stripped = raw.strip()
|
|
81
|
+
if "\t" in raw[: len(raw) - len(raw.lstrip(" \t"))]:
|
|
82
|
+
findings.append({
|
|
83
|
+
"line": lineno,
|
|
84
|
+
"kind": "tab in indent",
|
|
85
|
+
"verdict": "not supported",
|
|
86
|
+
"hint": "replace leading tabs with 2 or 4 spaces.",
|
|
87
|
+
})
|
|
88
|
+
continue
|
|
89
|
+
hit = _scan_line(stripped)
|
|
90
|
+
if hit is not None:
|
|
91
|
+
label, hint = hit
|
|
92
|
+
findings.append({
|
|
93
|
+
"line": lineno,
|
|
94
|
+
"kind": label,
|
|
95
|
+
"verdict": "not supported",
|
|
96
|
+
"hint": hint,
|
|
97
|
+
})
|
|
98
|
+
return findings
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _format(finding: dict) -> str:
|
|
102
|
+
return (
|
|
103
|
+
f" ❌ line:{finding['line']:<4} "
|
|
104
|
+
f"{finding['kind']:<22} {finding['verdict']:<14} {finding['hint']}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _parse(argv: list[str]) -> argparse.Namespace:
|
|
109
|
+
parser = argparse.ArgumentParser(
|
|
110
|
+
prog="agent-config settings:check",
|
|
111
|
+
description=(
|
|
112
|
+
"Validate .agent-settings.yml against the supported YAML subset "
|
|
113
|
+
"(docs/contracts/settings-sync-yaml-subset.md). Read-only."
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument("--path", default=DEFAULT_PATH,
|
|
117
|
+
help=f"target settings file (default: ./{DEFAULT_PATH})")
|
|
118
|
+
parser.add_argument("--allow-missing", action="store_true",
|
|
119
|
+
help="exit 0 when the file is absent (CI-friendly)")
|
|
120
|
+
parser.add_argument("--quiet", action="store_true",
|
|
121
|
+
help="suppress non-essential output")
|
|
122
|
+
return parser.parse_args(argv)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main(argv: list[str]) -> int:
|
|
126
|
+
opts = _parse(argv)
|
|
127
|
+
target = Path(opts.path)
|
|
128
|
+
if not target.is_file():
|
|
129
|
+
if opts.allow_missing:
|
|
130
|
+
if not opts.quiet:
|
|
131
|
+
print(f"✅ {target}: file absent (allow-missing).")
|
|
132
|
+
return 0
|
|
133
|
+
print(f"❌ {target}: file not found.", file=sys.stderr)
|
|
134
|
+
print(" Run `./agent-config sync-agent-settings` to create it.", file=sys.stderr)
|
|
135
|
+
return 2
|
|
136
|
+
try:
|
|
137
|
+
text = target.read_text(encoding="utf-8")
|
|
138
|
+
except OSError as exc:
|
|
139
|
+
print(f"❌ {target}: cannot read: {exc}", file=sys.stderr)
|
|
140
|
+
return 2
|
|
141
|
+
|
|
142
|
+
findings = _scan_text(text)
|
|
143
|
+
if not findings:
|
|
144
|
+
# Final gate: run the round-trip parser to catch anything the
|
|
145
|
+
# pre-scan missed (mismatched indent, malformed mapping lines).
|
|
146
|
+
from scripts import sync_yaml_rt as _rt # noqa: PLC0415
|
|
147
|
+
try:
|
|
148
|
+
_rt.parse(text)
|
|
149
|
+
except ValueError as exc:
|
|
150
|
+
findings.append({
|
|
151
|
+
"line": 0, "kind": "parser",
|
|
152
|
+
"verdict": "not supported", "hint": str(exc),
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
if not findings:
|
|
156
|
+
if not opts.quiet:
|
|
157
|
+
print(f"✅ {target}: inside the supported subset "
|
|
158
|
+
"(docs/contracts/settings-sync-yaml-subset.md).")
|
|
159
|
+
return 0
|
|
160
|
+
print(f"❌ {target}: {len(findings)} finding(s) outside the supported subset.",
|
|
161
|
+
file=sys.stderr)
|
|
162
|
+
for finding in findings:
|
|
163
|
+
print(_format(finding), file=sys.stderr)
|
|
164
|
+
print("", file=sys.stderr)
|
|
165
|
+
print(" Contract: docs/contracts/settings-sync-yaml-subset.md",
|
|
166
|
+
file=sys.stderr)
|
|
167
|
+
return 1
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
sys.exit(main(sys.argv[1:]))
|
package/scripts/agent-config
CHANGED
|
@@ -127,10 +127,22 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
|
|
|
127
127
|
(experimental — beta gates: docs/contracts/mcp-beta-criteria.md)
|
|
128
128
|
roadmap:progress Regenerate agents/roadmaps-progress.md from open roadmaps
|
|
129
129
|
roadmap:progress-check Fail if agents/roadmaps-progress.md is stale (for CI)
|
|
130
|
+
settings:check Validate .agent-settings.yml against the YAML-subset contract
|
|
131
|
+
(docs/contracts/settings-sync-yaml-subset.md). Read-only.
|
|
132
|
+
Exit 0 clean, 1 finding(s), 2 file absent / unreadable.
|
|
130
133
|
hooks:install Install the pre-commit roadmap-progress hook
|
|
131
134
|
(use --print to dump it, --force to overwrite an existing hook)
|
|
132
135
|
hooks:status Print the runtime hook matrix (per-platform install + bindings)
|
|
133
136
|
Flags: --format json|table, --strict (CI), --project-root <path>
|
|
137
|
+
hooks:doctor Diagnose hook health: concerns + fail-open/closed posture,
|
|
138
|
+
last dispatcher feedback per concern, missing trampolines.
|
|
139
|
+
Wraps hooks:status. Read-only.
|
|
140
|
+
Flags: --format json|table, --strict (CI), --project-root <path>
|
|
141
|
+
hooks:replay Replay a fixture through the universal dispatcher with
|
|
142
|
+
AGENT_CONFIG_REPLAY=1 (no writes under agents/state/).
|
|
143
|
+
Usage: hooks:replay --platform <name> --event <event>
|
|
144
|
+
--payload <path|event-name> [--native-event <native>]
|
|
145
|
+
[--manifest <path>] [--json] [--dry-run]
|
|
134
146
|
migrate-state Migrate a legacy .implement-ticket-state.json file
|
|
135
147
|
to the v1 .work-state.json schema (preserves .bak)
|
|
136
148
|
memory:lookup Retrieve memory entries (text or JSON envelope)
|
|
@@ -217,7 +229,9 @@ Examples (Tier 2):
|
|
|
217
229
|
./agent-config mcp:setup
|
|
218
230
|
./agent-config mcp:run
|
|
219
231
|
./agent-config roadmap:progress
|
|
232
|
+
./agent-config settings:check
|
|
220
233
|
./agent-config hooks:install
|
|
234
|
+
./agent-config hooks:replay --platform augment --event post_tool_use --payload post_tool_use --json
|
|
221
235
|
./agent-config migrate-state
|
|
222
236
|
./agent-config memory:lookup --types domain-invariants --key billing
|
|
223
237
|
./agent-config memory:signal --type architecture-decision --path src/Foo.php --body "…"
|
|
@@ -507,6 +521,20 @@ cmd_hooks_status() {
|
|
|
507
521
|
exec python3 "$script" "$@"
|
|
508
522
|
}
|
|
509
523
|
|
|
524
|
+
cmd_hooks_doctor() {
|
|
525
|
+
require_python3
|
|
526
|
+
local script
|
|
527
|
+
script="$(resolve_script "scripts/hooks_doctor.py")" || return 1
|
|
528
|
+
exec python3 "$script" "$@"
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
cmd_hooks_replay() {
|
|
532
|
+
require_python3
|
|
533
|
+
local script
|
|
534
|
+
script="$(resolve_script "scripts/hooks/replay_hook.py")" || return 1
|
|
535
|
+
exec python3 "$script" "$@"
|
|
536
|
+
}
|
|
537
|
+
|
|
510
538
|
cmd_chat_history_checkpoint() {
|
|
511
539
|
require_python3
|
|
512
540
|
local script
|
|
@@ -676,6 +704,15 @@ cmd_validate() {
|
|
|
676
704
|
exec env PYTHONPATH="$PACKAGE_ROOT" python3 -m scripts._cli.cmd_validate "$@"
|
|
677
705
|
}
|
|
678
706
|
|
|
707
|
+
# `agent-config settings:check` — read-only YAML-subset validator for
|
|
708
|
+
# `.agent-settings.yml` (P3.2 of road-to-proof-not-features.md). Contract
|
|
709
|
+
# pinned in docs/contracts/settings-sync-yaml-subset.md. Exit 0 clean,
|
|
710
|
+
# 1 finding(s), 2 file absent / unreadable.
|
|
711
|
+
cmd_settings_check() {
|
|
712
|
+
require_python3
|
|
713
|
+
exec env PYTHONPATH="$PACKAGE_ROOT" python3 -m scripts._cli.cmd_settings_check "$@"
|
|
714
|
+
}
|
|
715
|
+
|
|
679
716
|
# `agent-config uninstall` — remove bridge markers (project) or lockfile
|
|
680
717
|
# entries (global). Idempotent. Pass `--purge` to also delete deployed
|
|
681
718
|
# content directories under user-scope anchors (destructive). See
|
|
@@ -744,6 +781,8 @@ main() {
|
|
|
744
781
|
context-hygiene:hook) cmd_context_hygiene_hook "$@" ;;
|
|
745
782
|
dispatch:hook) cmd_dispatch_hook "$@" ;;
|
|
746
783
|
hooks:status) cmd_hooks_status "$@" ;;
|
|
784
|
+
hooks:doctor) cmd_hooks_doctor "$@" ;;
|
|
785
|
+
hooks:replay) cmd_hooks_replay "$@" ;;
|
|
747
786
|
telemetry:record) cmd_telemetry_record "$@" ;;
|
|
748
787
|
telemetry:status) cmd_telemetry_status "$@" ;;
|
|
749
788
|
telemetry:report) cmd_telemetry_report "$@" ;;
|
|
@@ -757,6 +796,7 @@ main() {
|
|
|
757
796
|
export) cmd_export "$@" ;;
|
|
758
797
|
sync) cmd_sync "$@" ;;
|
|
759
798
|
validate) cmd_validate "$@" ;;
|
|
799
|
+
settings:check) cmd_settings_check "$@" ;;
|
|
760
800
|
uninstall) cmd_uninstall "$@" ;;
|
|
761
801
|
prune) cmd_prune "$@" ;;
|
|
762
802
|
doctor) cmd_doctor "$@" ;;
|
package/scripts/chat_history.py
CHANGED
|
@@ -48,6 +48,15 @@ SCHEMA_VERSION = 4
|
|
|
48
48
|
DEFAULT_MAX_SESSIONS = 5
|
|
49
49
|
VALID_FREQS = {"per_turn", "per_phase", "per_tool"}
|
|
50
50
|
VALID_OVERFLOW = {"rotate", "compress"}
|
|
51
|
+
|
|
52
|
+
# Replay-mode signal — when set, every write to the on-disk transcript
|
|
53
|
+
# is a no-op. Honoured per `docs/contracts/hook-architecture-v1.md`
|
|
54
|
+
# § Replay mode so fixture dispatches never mutate real session state.
|
|
55
|
+
REPLAY_ENV_VAR = "AGENT_CONFIG_REPLAY"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_replay_mode() -> bool:
|
|
59
|
+
return os.environ.get(REPLAY_ENV_VAR, "").strip() == "1"
|
|
51
60
|
_WS_RE = re.compile(r"\s+")
|
|
52
61
|
SESSION_ID_LEN = 16
|
|
53
62
|
SESSION_ID_UNKNOWN = "<unknown>"
|
|
@@ -247,6 +256,8 @@ def init(freq: str = "per_phase", *,
|
|
|
247
256
|
raise ValueError(f"freq must be one of {sorted(VALID_FREQS)}")
|
|
248
257
|
p = path or file_path()
|
|
249
258
|
header = _build_header(freq)
|
|
259
|
+
if _is_replay_mode():
|
|
260
|
+
return header
|
|
250
261
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
251
262
|
with p.open("w", encoding="utf-8") as fh:
|
|
252
263
|
fh.write(json.dumps(header, ensure_ascii=False) + "\n")
|
|
@@ -318,6 +329,8 @@ def append(entry: dict[str, Any], *, path: Path | None = None,
|
|
|
318
329
|
entry["s"] = session
|
|
319
330
|
elif "s" not in entry and _session_tag_enabled():
|
|
320
331
|
entry["s"] = _last_body_session_id(p)
|
|
332
|
+
if _is_replay_mode():
|
|
333
|
+
return
|
|
321
334
|
with p.open("a", encoding="utf-8") as fh:
|
|
322
335
|
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
323
336
|
|
|
@@ -328,7 +341,11 @@ def _atomic_write_text(p: Path, text: str) -> None:
|
|
|
328
341
|
Multiple processes writing to the same target use disjoint tmp paths
|
|
329
342
|
(PID + uuid), so concurrent writes no longer collide on a shared
|
|
330
343
|
``.tmp`` file. The final ``replace`` is atomic on POSIX.
|
|
344
|
+
|
|
345
|
+
Under `AGENT_CONFIG_REPLAY=1` the call is a no-op.
|
|
331
346
|
"""
|
|
347
|
+
if _is_replay_mode():
|
|
348
|
+
return
|
|
332
349
|
tmp = p.with_suffix(
|
|
333
350
|
f"{p.suffix}.{os.getpid()}.{uuid.uuid4().hex[:8]}.tmp",
|
|
334
351
|
)
|
|
@@ -405,6 +422,8 @@ def prepend_entries(entries: list[dict[str, Any]], *,
|
|
|
405
422
|
|
|
406
423
|
|
|
407
424
|
def clear(*, path: Path | None = None) -> None:
|
|
425
|
+
if _is_replay_mode():
|
|
426
|
+
return
|
|
408
427
|
p = path or file_path()
|
|
409
428
|
if p.exists():
|
|
410
429
|
p.unlink()
|
|
@@ -14,8 +14,10 @@ council files. Directory mentions and placeholder paths
|
|
|
14
14
|
output-path convention, not a live reference.
|
|
15
15
|
|
|
16
16
|
Forbidden hits in this codebase exist today (kernel-membership ADRs
|
|
17
|
-
cite real session JSONs as decision traces).
|
|
18
|
-
|
|
17
|
+
cite real session JSONs as decision traces). Two source/target shapes
|
|
18
|
+
are exempt structurally — see STRUCTURAL_CARVEOUTS below — because
|
|
19
|
+
they encode immutable decision provenance, not transient drafting
|
|
20
|
+
state. Anything else needs an inline pragma at the end of the line:
|
|
19
21
|
|
|
20
22
|
`agents/council-sessions/...json` <!-- council-ref-allowed: <reason> -->
|
|
21
23
|
|
|
@@ -82,6 +84,31 @@ ALLOWLIST_FILES: frozenset[str] = frozenset({
|
|
|
82
84
|
|
|
83
85
|
INLINE_PRAGMA = re.compile(r"<!--\s*council-ref-allowed:[^>]*-->")
|
|
84
86
|
|
|
87
|
+
# Structural carve-outs — (source_pattern, target_pattern) pairs where
|
|
88
|
+
# the reference is immutable decision provenance rather than transient
|
|
89
|
+
# drafting state. Driven by the 2026-05-14 P3.4 council round
|
|
90
|
+
# (agents/council-sessions/2026-05-14-p3-4-references/synthesis.md).
|
|
91
|
+
#
|
|
92
|
+
# Each entry: source file matches `source` regex AND the captured
|
|
93
|
+
# reference path matches `target` regex → reference is allowed without
|
|
94
|
+
# an inline pragma.
|
|
95
|
+
STRUCTURAL_CARVEOUTS: tuple[tuple[re.Pattern[str], re.Pattern[str]], ...] = (
|
|
96
|
+
# (a) evaluation-context → council-question:
|
|
97
|
+
# the question file is a frozen function-parameter / spend-gate
|
|
98
|
+
# input, not a documentation link.
|
|
99
|
+
(
|
|
100
|
+
re.compile(r"^agents/contexts/evaluation-[^/]+\.md$"),
|
|
101
|
+
re.compile(r"^agents/council-questions/[^/]+\.md$"),
|
|
102
|
+
),
|
|
103
|
+
# (b) contract → council-session-synthesis:
|
|
104
|
+
# the synthesis file is the audit-trail receipt the contract cites
|
|
105
|
+
# as decision provenance; the contract inlines the decision body.
|
|
106
|
+
(
|
|
107
|
+
re.compile(r"^docs/contracts/[^/]+\.md$"),
|
|
108
|
+
re.compile(r"^agents/council-sessions/[^/]+/synthesis\.md$"),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
|
|
85
112
|
|
|
86
113
|
def _is_allowlisted(rel: str) -> bool:
|
|
87
114
|
if rel in ALLOWLIST_FILES:
|
|
@@ -89,8 +116,17 @@ def _is_allowlisted(rel: str) -> bool:
|
|
|
89
116
|
return any(rel.startswith(prefix) for prefix in ALLOWLIST_PREFIXES)
|
|
90
117
|
|
|
91
118
|
|
|
119
|
+
def _is_structurally_allowed(source_rel: str, target_capture: str) -> bool:
|
|
120
|
+
"""True when (source, target) matches a structural carve-out pair."""
|
|
121
|
+
for src_re, tgt_re in STRUCTURAL_CARVEOUTS:
|
|
122
|
+
if src_re.match(source_rel) and tgt_re.match(target_capture):
|
|
123
|
+
return True
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
92
127
|
def _scan_file(path: Path) -> list[tuple[int, str]]:
|
|
93
128
|
findings: list[tuple[int, str]] = []
|
|
129
|
+
rel = path.as_posix()
|
|
94
130
|
try:
|
|
95
131
|
text = path.read_text(encoding="utf-8")
|
|
96
132
|
except (OSError, UnicodeDecodeError):
|
|
@@ -99,6 +135,8 @@ def _scan_file(path: Path) -> list[tuple[int, str]]:
|
|
|
99
135
|
if INLINE_PRAGMA.search(line):
|
|
100
136
|
continue
|
|
101
137
|
for m in PATTERN.finditer(line):
|
|
138
|
+
if _is_structurally_allowed(rel, m.group(0)):
|
|
139
|
+
continue
|
|
102
140
|
findings.append((ln, m.group(0)))
|
|
103
141
|
return findings
|
|
104
142
|
|
|
@@ -136,10 +174,13 @@ def main() -> int:
|
|
|
136
174
|
print(
|
|
137
175
|
"\nRule: .agent-src/rules/no-roadmap-references.md (council clause)\n"
|
|
138
176
|
"Fix: inline the convergence summary (members + date) instead of\n"
|
|
139
|
-
"linking the file.
|
|
177
|
+
"linking the file. Two source/target shapes are exempt structurally\n"
|
|
178
|
+
"(evaluation-context → council-question, contract →\n"
|
|
179
|
+
"council-session-synthesis) — see STRUCTURAL_CARVEOUTS in this\n"
|
|
180
|
+
"script. Otherwise append "
|
|
140
181
|
"<!-- council-ref-allowed: <reason> --> on the same line to\n"
|
|
141
|
-
"suppress when the reference is genuinely required (ADR
|
|
142
|
-
"
|
|
182
|
+
"suppress when the reference is genuinely required (ADR decision\n"
|
|
183
|
+
"trace)."
|
|
143
184
|
)
|
|
144
185
|
return 1
|
|
145
186
|
|
|
@@ -37,7 +37,7 @@ MANIFEST_PATH = REPO_ROOT / "scripts" / "hook_manifest.yaml"
|
|
|
37
37
|
# Lazy import — we want this module to be importable even if the
|
|
38
38
|
# hooks package state_io has changed (test isolation).
|
|
39
39
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
40
|
-
from state_io import atomic_write_json, feedback_dir # noqa: E402
|
|
40
|
+
from state_io import atomic_write_json, feedback_dir, is_replay_mode # noqa: E402
|
|
41
41
|
|
|
42
42
|
EXIT_ALLOW = 0
|
|
43
43
|
EXIT_BLOCK = 1
|
|
@@ -272,6 +272,10 @@ def _write_feedback(envelope: dict, session_id: str, entries: list[dict],
|
|
|
272
272
|
not control flow. We only swallow IO errors here; fail-open
|
|
273
273
|
matches the dispatcher's overall posture.
|
|
274
274
|
"""
|
|
275
|
+
# Replay mode skips feedback emission entirely so fixture replays
|
|
276
|
+
# never create per-session dirs under agents/state/.dispatcher/.
|
|
277
|
+
if is_replay_mode():
|
|
278
|
+
return
|
|
275
279
|
workspace = envelope.get("workspace_root") or str(Path.cwd())
|
|
276
280
|
state_root = Path(workspace) / "agents" / "state"
|
|
277
281
|
fb_dir = feedback_dir(state_root, session_id)
|