@event4u/agent-config 1.14.0 → 1.15.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/agent-handoff.md +1 -1
- package/.agent-src/commands/bug-fix.md +2 -2
- package/.agent-src/commands/chat-history-checkpoint.md +2 -2
- package/.agent-src/commands/chat-history-clear.md +1 -1
- package/.agent-src/commands/chat-history-resume.md +2 -2
- package/.agent-src/commands/chat-history.md +2 -2
- package/.agent-src/commands/check-current-md.md +43 -32
- package/.agent-src/commands/commit-in-chunks.md +43 -23
- package/.agent-src/commands/compress.md +34 -2
- package/.agent-src/commands/feature-roadmap.md +2 -2
- package/.agent-src/commands/fix-portability.md +2 -2
- package/.agent-src/commands/onboard.md +14 -5
- package/.agent-src/commands/optimize-augmentignore.md +9 -0
- package/.agent-src/commands/refine-ticket.md +9 -7
- package/.agent-src/commands/review-changes.md +35 -8
- package/.agent-src/commands/roadmap-create.md +13 -2
- package/.agent-src/commands/roadmap-execute.md +9 -7
- package/.agent-src/commands/set-cost-profile.md +8 -0
- package/.agent-src/commands/sync-agent-settings.md +9 -0
- package/.agent-src/commands/tests-execute.md +2 -3
- package/.agent-src/rules/artifact-engagement-recording.md +1 -1
- package/.agent-src/rules/augment-portability.md +56 -37
- package/.agent-src/rules/chat-history-cadence.md +109 -0
- package/.agent-src/rules/chat-history-ownership.md +123 -0
- package/.agent-src/rules/chat-history-visibility.md +96 -0
- package/.agent-src/rules/cli-output-handling.md +1 -1
- package/.agent-src/rules/command-suggestion.md +3 -2
- package/.agent-src/rules/commit-policy.md +44 -34
- package/.agent-src/rules/direct-answers.md +1 -1
- package/.agent-src/rules/language-and-tone.md +19 -15
- package/.agent-src/rules/non-destructive-by-default.md +18 -18
- package/.agent-src/rules/roadmap-progress-sync.md +133 -74
- package/.agent-src/rules/role-mode-adherence.md +1 -1
- package/.agent-src/rules/size-enforcement.md +2 -1
- package/.agent-src/rules/user-interaction.md +28 -4
- package/.agent-src/scripts/update_roadmap_progress.py +56 -4
- package/.agent-src/skills/blade-ui/SKILL.md +29 -10
- package/.agent-src/skills/command-writing/SKILL.md +15 -4
- package/.agent-src/skills/existing-ui-audit/SKILL.md +24 -9
- package/.agent-src/skills/fe-design/SKILL.md +20 -15
- package/.agent-src/skills/file-editor/SKILL.md +9 -0
- package/.agent-src/skills/livewire/SKILL.md +26 -7
- package/.agent-src/skills/refine-ticket/SKILL.md +30 -24
- package/.agent-src/skills/roadmap-management/SKILL.md +22 -16
- package/.agent-src/skills/skill-writing/SKILL.md +3 -3
- package/.agent-src/skills/upstream-contribute/SKILL.md +2 -2
- package/.agent-src/templates/agent-settings.md +1 -1
- package/.agent-src/templates/roadmaps.md +9 -8
- package/.agent-src/templates/scripts/memory_lookup.py +1 -1
- package/.agent-src/templates/scripts/work_engine/__init__.py +2 -2
- package/.agent-src/templates/scripts/work_engine/cli.py +64 -461
- package/.agent-src/templates/scripts/work_engine/cli_args.py +116 -0
- package/.agent-src/templates/scripts/work_engine/delivery_state.py +3 -3
- package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/implement.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/memory.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/plan.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/report.py +1 -1
- package/.agent-src/templates/scripts/work_engine/dispatcher.py +1 -1
- package/.agent-src/templates/scripts/work_engine/emitters.py +43 -0
- package/.agent-src/templates/scripts/work_engine/errors.py +19 -0
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +76 -0
- package/.agent-src/templates/scripts/work_engine/input_builders.py +163 -0
- package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +34 -2
- package/.agent-src/templates/scripts/work_engine/persona_policy.py +1 -1
- package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +1 -1
- package/.agent-src/templates/scripts/work_engine/state_io.py +202 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +6 -4
- package/CHANGELOG.md +83 -8
- package/README.md +24 -23
- package/docs/MIGRATION.md +122 -0
- package/docs/architecture.md +83 -34
- package/docs/contracts/STABILITY.md +95 -0
- package/docs/contracts/adr-chat-history-split.md +132 -0
- package/docs/contracts/adr-command-suggestion.md +146 -0
- package/docs/contracts/adr-implement-ticket-runtime.md +122 -0
- package/docs/contracts/adr-product-ui-track.md +384 -0
- package/docs/contracts/adr-prompt-driven-execution.md +187 -0
- package/docs/contracts/agent-memory-contract.md +149 -0
- package/docs/contracts/artifact-engagement-flow.md +262 -0
- package/docs/contracts/command-clusters.md +126 -0
- package/docs/contracts/command-suggestion-flow.md +148 -0
- package/docs/contracts/implement-ticket-flow.md +628 -0
- package/docs/contracts/linear-ai-rules-inclusion.md +143 -0
- package/docs/contracts/linear-ai-three-layers.md +131 -0
- package/docs/contracts/rule-interactions.md +107 -0
- package/docs/contracts/rule-interactions.yml +142 -0
- package/docs/contracts/ui-stack-extension.md +236 -0
- package/docs/contracts/ui-track-flow.md +338 -0
- package/docs/getting-started.md +2 -2
- package/docs/installation.md +42 -6
- package/docs/migrations/commands-1.15.0.md +112 -0
- package/docs/ui-track-mental-model.md +121 -0
- package/package.json +1 -1
- package/scripts/build_linear_digest.py +4 -4
- package/scripts/check_portability.py +2 -0
- package/scripts/check_public_links.py +185 -0
- package/scripts/check_references.py +1 -0
- package/scripts/lint_no_new_atomic_commands.py +179 -0
- package/scripts/lint_rule_interactions.py +149 -0
- package/scripts/memory_lookup.py +1 -1
- package/scripts/release.py +297 -64
- package/scripts/skill_linter.py +14 -0
- package/scripts/update_counts.py +10 -0
- package/.agent-src/rules/chat-history.md +0 -200
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Command migration — 1.15.0
|
|
2
|
+
|
|
3
|
+
> **Audience:** consumers of `event4u/agent-config` upgrading from
|
|
4
|
+
> `1.14.x` to `1.15.0`.
|
|
5
|
+
> **Authoritative spec:** [`docs/contracts/command-clusters.md`](../contracts/command-clusters.md).
|
|
6
|
+
|
|
7
|
+
`1.15.0` collapses 15 atomic commands into 3 verb clusters
|
|
8
|
+
(`fix`, `optimize`, `feature`) to reduce command-palette
|
|
9
|
+
fragmentation. **Old commands keep working through the entire
|
|
10
|
+
1.15.x cycle**; they emit a one-line deprecation warning on
|
|
11
|
+
invocation and are removed in `1.16.0`.
|
|
12
|
+
|
|
13
|
+
## Summary table
|
|
14
|
+
|
|
15
|
+
| Old command | New invocation | Removed in |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `/fix-ci` | `/fix ci` | 1.16.0 |
|
|
18
|
+
| `/fix-pr-comments` | `/fix pr` | 1.16.0 |
|
|
19
|
+
| `/fix-pr-bot-comments` | `/fix pr-bots` | 1.16.0 |
|
|
20
|
+
| `/fix-pr-developer-comments` | `/fix pr-developers` | 1.16.0 |
|
|
21
|
+
| `/fix-portability` | `/fix portability` | 1.16.0 |
|
|
22
|
+
| `/fix-references` | `/fix refs` | 1.16.0 |
|
|
23
|
+
| `/fix-seeder` | `/fix seeder` | 1.16.0 |
|
|
24
|
+
| `/optimize-agents` | `/optimize agents` | 1.16.0 |
|
|
25
|
+
| `/optimize-augmentignore` | `/optimize augmentignore` | 1.16.0 |
|
|
26
|
+
| `/optimize-rtk-filters` | `/optimize rtk` | 1.16.0 |
|
|
27
|
+
| `/optimize-skills` | `/optimize skills` | 1.16.0 |
|
|
28
|
+
| `/feature-explore` | `/feature explore` | 1.16.0 |
|
|
29
|
+
| `/feature-plan` | `/feature plan` | 1.16.0 |
|
|
30
|
+
| `/feature-refactor` | `/feature refactor` | 1.16.0 |
|
|
31
|
+
| `/feature-roadmap` | `/feature roadmap` | 1.16.0 |
|
|
32
|
+
|
|
33
|
+
## What changes for you
|
|
34
|
+
|
|
35
|
+
**Nothing breaks in 1.15.x.** Old slugs continue to work — the agent
|
|
36
|
+
recognises them, dispatches to the new cluster command, and prints
|
|
37
|
+
one warning line at the top of the reply:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
⚠️ /fix-ci is deprecated; use /fix ci instead.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Your existing custom skills, rules, or agents/roadmaps that
|
|
44
|
+
reference old slugs do not need to change in 1.15.x.** Update at
|
|
45
|
+
your own pace.
|
|
46
|
+
|
|
47
|
+
## What to update before 1.16.0
|
|
48
|
+
|
|
49
|
+
Search your project for any direct references to the old slugs and
|
|
50
|
+
swap them for the new invocation:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Find references in agents/, docs/, README, custom rules
|
|
54
|
+
rg -n '/(fix|optimize|feature)-[a-z-]+' \
|
|
55
|
+
agents/ docs/ README.md AGENTS.md .github/ 2>/dev/null
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Common spots:
|
|
59
|
+
|
|
60
|
+
- `agents/roadmaps/*.md` — roadmap steps that name commands
|
|
61
|
+
- `agents/contexts/*.md` — context docs cross-linking commands
|
|
62
|
+
- Custom skills/rules under `.augment/` overrides
|
|
63
|
+
- Internal team docs / runbooks / onboarding pages
|
|
64
|
+
|
|
65
|
+
## Why this changed
|
|
66
|
+
|
|
67
|
+
The atomic-command surface had grown to 77 commands by 1.14.0. Three
|
|
68
|
+
prefix families (`fix-*`, `optimize-*`, `feature-*`) accounted for 15
|
|
69
|
+
of those — same verb, same invocation pattern, only the noun
|
|
70
|
+
differed. Collapsing the families into clusters:
|
|
71
|
+
|
|
72
|
+
- Reduces palette noise (15 → 3 top-level entries).
|
|
73
|
+
- Makes new sub-commands additive (`/fix tests` ships without a new
|
|
74
|
+
top-level slug).
|
|
75
|
+
- Holds the line: `scripts/lint_no_new_atomic_commands.py` fails CI
|
|
76
|
+
if a new atomic command lands without a `cluster:` field declared
|
|
77
|
+
in [`docs/contracts/command-clusters.md`](../contracts/command-clusters.md).
|
|
78
|
+
|
|
79
|
+
## Phase 2 (deferred)
|
|
80
|
+
|
|
81
|
+
A second wave of collapses (`chat-history`, `agents`, `memory`,
|
|
82
|
+
`roadmap`, `module`, `tests`, `context`, `override`, `copilot-agents`,
|
|
83
|
+
`commit`, `judge`, `create-pr`) is scheduled after 1.15.0 ships and
|
|
84
|
+
the deprecation cycle for Phase 1 closes. Tracked in
|
|
85
|
+
[`agents/roadmaps/road-to-governance-cleanup.md`](../../agents/roadmaps/road-to-governance-cleanup.md)
|
|
86
|
+
§ F2.
|
|
87
|
+
|
|
88
|
+
## Related rule split — `chat-history` (post-1.15.0)
|
|
89
|
+
|
|
90
|
+
The monolithic `rules/chat-history.md` was split into three sibling
|
|
91
|
+
`always` rules in the post-1.15.0 optimization phase:
|
|
92
|
+
|
|
93
|
+
- [`chat-history-ownership`](../../.agent-src/rules/chat-history-ownership.md) — sole owner of file I/O + first-turn handshake.
|
|
94
|
+
- [`chat-history-cadence`](../../.agent-src/rules/chat-history-cadence.md) — when to persist (SessionStart / StepEnd / append boundaries).
|
|
95
|
+
- [`chat-history-visibility`](../../.agent-src/rules/chat-history-visibility.md) — heartbeat marker contract for user-facing reporting.
|
|
96
|
+
|
|
97
|
+
Decision record: [`docs/contracts/adr-chat-history-split.md`](../contracts/adr-chat-history-split.md).
|
|
98
|
+
Cross-references in commands and contexts now point to
|
|
99
|
+
`chat-history-ownership` as the entry point.
|
|
100
|
+
|
|
101
|
+
## Rollback
|
|
102
|
+
|
|
103
|
+
If a deprecation warning blocks tooling that screen-scrapes agent
|
|
104
|
+
output, suppress it for the 1.15.x cycle by setting
|
|
105
|
+
`commands.deprecation_warnings: false` in `.agent-settings.yml`.
|
|
106
|
+
The setting disappears in 1.16.0 along with the shims.
|
|
107
|
+
|
|
108
|
+
## See also
|
|
109
|
+
|
|
110
|
+
- [`docs/contracts/command-clusters.md`](../contracts/command-clusters.md) — locked cluster spec.
|
|
111
|
+
- [`docs/contracts/STABILITY.md`](../contracts/STABILITY.md) — public-surface stability tiers.
|
|
112
|
+
- [`agents/roadmaps/archive/road-to-post-pr29-optimize.md`](../../agents/roadmaps/archive/road-to-post-pr29-optimize.md) — P0.8 anchor for this migration.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
stability: stable
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# UI Track — Mental Model (1 page)
|
|
6
|
+
|
|
7
|
+
> **Audience:** anyone driving `/work` or `/implement-ticket` on a UI-shaped
|
|
8
|
+
> prompt, or reading code that touches `state.directive_set`.
|
|
9
|
+
> **Not a contract.** For shapes, schemas, and slot wiring see
|
|
10
|
+
> [`ui-track-flow.md`](contracts/ui-track-flow.md) and
|
|
11
|
+
> [`adr-product-ui-track.md`](contracts/adr-product-ui-track.md).
|
|
12
|
+
> **Not a roadmap.** Phased delivery lives in
|
|
13
|
+
> [`road-to-product-ui-track.md`](../agents/roadmaps/archive/road-to-product-ui-track.md)
|
|
14
|
+
> and [`road-to-visual-review-loop.md`](../agents/roadmaps/archive/road-to-visual-review-loop.md).
|
|
15
|
+
> This page is the picture you keep in your head while the engine runs.
|
|
16
|
+
|
|
17
|
+
## The three (+1) directive sets
|
|
18
|
+
|
|
19
|
+
| Set | Shape | Halt budget |
|
|
20
|
+
|---|---|---:|
|
|
21
|
+
| `backend` | `refine → memory → analyze → plan → implement → test → verify → report` | depends on confidence band |
|
|
22
|
+
| `ui` | `audit → design → apply → review → polish → report` | **2** (audit pick + design sign-off) |
|
|
23
|
+
| `ui-trivial` | `refine → apply → test → report` | **0** on the happy path |
|
|
24
|
+
| `mixed` | `refine → memory → analyze → contract → ui → stitch → verify → report` | inherits both |
|
|
25
|
+
|
|
26
|
+
The first three are sibling routes; `mixed` stitches `backend` and `ui`
|
|
27
|
+
in one envelope. The dispatcher picks one and refuses to switch
|
|
28
|
+
mid-flight.
|
|
29
|
+
|
|
30
|
+
## When to pick which
|
|
31
|
+
|
|
32
|
+
| Signal | Set |
|
|
33
|
+
|---|---|
|
|
34
|
+
| No UI keywords, no UI envelope, prompt edits services / migrations / tests / config | `backend` |
|
|
35
|
+
| New component / screen / partial; "improve this dashboard"; "build a settings panel"; major edit to a screen | `ui` |
|
|
36
|
+
| Bounded edit, **provably** ≤ 1 file, ≤ 5 changed lines, no new component, no new state, no new dependency | `ui-trivial` |
|
|
37
|
+
| One prompt that adds a backend endpoint **and** the screen that consumes it | `mixed` |
|
|
38
|
+
|
|
39
|
+
The classifier picks; the agent does not override the pick silently.
|
|
40
|
+
A misclassified `ui-trivial` that grows during edit must reclassify
|
|
41
|
+
**loudly** (stop, restart at `audit`) — never quietly upgrade in place.
|
|
42
|
+
|
|
43
|
+
## What the agent must never do
|
|
44
|
+
|
|
45
|
+
1. **Skip the audit.** No new component, screen, or partial without
|
|
46
|
+
`state.ui_audit` populated (or `greenfield_decision` recorded).
|
|
47
|
+
Defense-in-depth: dispatcher refuses *and* `ui-audit-before-build`
|
|
48
|
+
refuses, even when the engine is not in the loop.
|
|
49
|
+
2. **Render the UI.** The engine never opens a browser, never takes a
|
|
50
|
+
screenshot, never runs axe. Rendering and a11y scanning happen
|
|
51
|
+
out-of-process; the engine consumes the `preview_envelope` /
|
|
52
|
+
`a11y` envelope written by the skill.
|
|
53
|
+
3. **Edit microcopy.** The design brief is **locked**. `apply` reads
|
|
54
|
+
strings verbatim. `<placeholder>`, `lorem`, `todo:`, `tbd`, `xxx`
|
|
55
|
+
are rejected at the producer (design) **and** the consumer (apply).
|
|
56
|
+
4. **Loop polish indefinitely.** Hard ceiling is 2 rounds, +1 with
|
|
57
|
+
the explicit Extend pick (one-shot, never returns). Round 4 is
|
|
58
|
+
rejected on disk regardless of flags.
|
|
59
|
+
5. **Confuse "ship as-is" with "review clean".** `review_clean=False`
|
|
60
|
+
plus `findings=[]` is a malformed envelope, not a green light.
|
|
61
|
+
|
|
62
|
+
## Where each set stops
|
|
63
|
+
|
|
64
|
+
| Set | Stops cleanly when … | Stops with a halt when … |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `ui` | `report` written; design brief + audit + apply + review + polish all `SUCCESS` | audit ambiguous · greenfield decision missing · design unconfirmed · review dirty at ceiling · a11y violation un-accepted · preview render failed |
|
|
67
|
+
| `ui-trivial` | `report` written; classifier preconditions held throughout | preconditions fail mid-flight → reclassify to `ui` (loud halt) |
|
|
68
|
+
| `mixed` | `stitch` joins both subtrees; `verify` + `report` clean | either subtree halts → mixed waits, never auto-completes the other |
|
|
69
|
+
|
|
70
|
+
## What "audit" actually means
|
|
71
|
+
|
|
72
|
+
A non-empty `state.ui_audit` carrying **at least one of**:
|
|
73
|
+
|
|
74
|
+
- `components_found` — `[{path, name, kind, similarity?}]` from
|
|
75
|
+
[`existing-ui-audit`](../.agent-src/skills/existing-ui-audit/SKILL.md).
|
|
76
|
+
- `greenfield: true` plus `greenfield_decision ∈ {scaffold, bare, external_reference}`.
|
|
77
|
+
- Legacy `components` alias — same shape.
|
|
78
|
+
|
|
79
|
+
Empty dict, `null`, or a dict without those keys is **not** an audit.
|
|
80
|
+
The gate fires; the dispatcher emits `@agent-directive: existing-ui-audit`
|
|
81
|
+
instead of advancing.
|
|
82
|
+
|
|
83
|
+
## What "design locked" actually means
|
|
84
|
+
|
|
85
|
+
The brief carries `layout`, `components`, `states`, `microcopy`,
|
|
86
|
+
`a11y`. State coverage requires `empty`, `loading`, `error`, `success`,
|
|
87
|
+
`disabled`. `apply` does not re-decide microcopy — it copies strings.
|
|
88
|
+
"The button label feels off" is a **new** design pass, not a polish
|
|
89
|
+
fix.
|
|
90
|
+
|
|
91
|
+
## Polish termination — subjective vs objective
|
|
92
|
+
|
|
93
|
+
| Findings at ceiling | Halt branch | User options |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| Non-a11y only | `polish_ceiling_reached` | ship as-is · abort · hand off |
|
|
96
|
+
| Includes a11y violation | `polish_a11y_blocking` | extend (one-shot, sets `extension_used=True`) · accept (rule ids land in `accepted_violations`) · abort |
|
|
97
|
+
|
|
98
|
+
Pre-existing a11y violations recorded in `state.ui_audit.a11y_baseline`
|
|
99
|
+
stay informational and never block.
|
|
100
|
+
|
|
101
|
+
## Stack dispatch (apply / review / polish)
|
|
102
|
+
|
|
103
|
+
`state.stack.frontend` decides which skill bundle runs:
|
|
104
|
+
|
|
105
|
+
| Stack | Skill bundle |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `blade-livewire-flux` | `flux` + `livewire` + `blade-ui` |
|
|
108
|
+
| `react-shadcn` | `react-shadcn-ui` |
|
|
109
|
+
| `vue` | `ui-apply-vue` |
|
|
110
|
+
| `plain` (or unknown) | `blade-ui` + Tailwind base |
|
|
111
|
+
|
|
112
|
+
The directive set stays `ui`; only the skill changes. Adding a new
|
|
113
|
+
stack ships as a new skill bundle and a recipe — see
|
|
114
|
+
[`ui-stack-extension.md`](contracts/ui-stack-extension.md).
|
|
115
|
+
|
|
116
|
+
## See also
|
|
117
|
+
|
|
118
|
+
- [`adr-product-ui-track.md`](contracts/adr-product-ui-track.md) — *why* this shape.
|
|
119
|
+
- [`ui-track-flow.md`](contracts/ui-track-flow.md) — slot-by-slot contract.
|
|
120
|
+
- [`ui-stack-extension.md`](contracts/ui-stack-extension.md) — adding a stack.
|
|
121
|
+
- [`ui-audit-before-build`](../.agent-src/rules/ui-audit-before-build.md) — the always-on rule that mirrors the dispatcher gate.
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ Concatenates a curated set of cloud-safe rules from
|
|
|
10
10
|
personal.md — empty stub for individual preferences
|
|
11
11
|
|
|
12
12
|
Per-rule inclusion + mode is the source of truth in
|
|
13
|
-
`
|
|
13
|
+
`docs/contracts/linear-ai-rules-inclusion.md`. This script encodes the
|
|
14
14
|
same lists so a drift between the two surfaces is caught by the digest
|
|
15
15
|
audit (Phase 3 Step 4) — the markdown doc is the human-readable spec,
|
|
16
16
|
this script is the executable.
|
|
@@ -44,7 +44,7 @@ from pathlib import Path
|
|
|
44
44
|
ROOT = Path(__file__).resolve().parent.parent
|
|
45
45
|
# Compressed source is the shipped form — denser, sharper section
|
|
46
46
|
# structure; better fit for a guidance field than the verbose authoring
|
|
47
|
-
# layer. The inclusion list at
|
|
47
|
+
# layer. The inclusion list at docs/contracts/linear-ai-rules-inclusion.md
|
|
48
48
|
# remains the human-readable spec.
|
|
49
49
|
SOURCE = ROOT / ".agent-src" / "rules"
|
|
50
50
|
OUT_DIR = ROOT / "dist" / "linear"
|
|
@@ -64,7 +64,7 @@ class RuleEntry:
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
# Workspace digest — universal coding posture. Maps 1:1 to the
|
|
67
|
-
# "Workspace digest" table in
|
|
67
|
+
# "Workspace digest" table in docs/contracts/linear-ai-rules-inclusion.md.
|
|
68
68
|
WORKSPACE: list[RuleEntry] = [
|
|
69
69
|
RuleEntry("ask-when-uncertain"),
|
|
70
70
|
RuleEntry("commit-conventions"),
|
|
@@ -169,7 +169,7 @@ def render_digest(layer: str, entries: list[RuleEntry]) -> tuple[str, dict]:
|
|
|
169
169
|
parts.append(
|
|
170
170
|
"> Auto-generated by `scripts/build_linear_digest.py` from "
|
|
171
171
|
"`.agent-src/rules/` (compressed source) plus the inclusion list "
|
|
172
|
-
"at `
|
|
172
|
+
"at `docs/contracts/linear-ai-rules-inclusion.md`. Do not edit "
|
|
173
173
|
"this file by hand — re-run `task build-linear-digest` to "
|
|
174
174
|
"regenerate.\n"
|
|
175
175
|
)
|
|
@@ -157,6 +157,8 @@ ALLOWLIST = [
|
|
|
157
157
|
r"agent-config", # refers to the package concept, not a specific project
|
|
158
158
|
r"shared.*package", # "shared package" concept
|
|
159
159
|
r"package repository", # "package repository" concept
|
|
160
|
+
r"scripts/mcp_server/", # MCP server module path (road-to-mcp-server.md Phase 1)
|
|
161
|
+
r"scripts\.mcp_server", # MCP server Python module entrypoint
|
|
160
162
|
]
|
|
161
163
|
|
|
162
164
|
# Directories to scan (only package files, not project-specific agents/)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Public-link checker for the agent-config public surface.
|
|
4
|
+
|
|
5
|
+
Scans the public-surface files (README.md, AGENTS.md, docs/architecture.md)
|
|
6
|
+
for markdown links into `docs/contracts/`, then validates each link against
|
|
7
|
+
the `stability:` frontmatter declared by the target file (per
|
|
8
|
+
`docs/contracts/STABILITY.md`).
|
|
9
|
+
|
|
10
|
+
Rules:
|
|
11
|
+
- target stability=stable → OK (no marker required).
|
|
12
|
+
- target stability=beta → OK; warns if surrounding text has no
|
|
13
|
+
visible "(beta)" marker.
|
|
14
|
+
- target stability=experimental → ERROR. Public surface MUST NOT link
|
|
15
|
+
to experimental contracts.
|
|
16
|
+
- target outside docs/contracts/ but referenced for contract-shaped
|
|
17
|
+
intent (links into agents/contexts/*.md from public files) → ERROR.
|
|
18
|
+
- target file missing → ERROR.
|
|
19
|
+
- target file under docs/contracts/ without `stability:` frontmatter
|
|
20
|
+
(except STABILITY.md itself) → ERROR.
|
|
21
|
+
|
|
22
|
+
Exit codes: 0 = clean, 1 = violations found, 3 = internal error.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
python3 scripts/check_public_links.py
|
|
26
|
+
python3 scripts/check_public_links.py --list # list contracts + levels
|
|
27
|
+
python3 scripts/check_public_links.py --json # machine-readable
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import argparse
|
|
33
|
+
import json
|
|
34
|
+
import re
|
|
35
|
+
import sys
|
|
36
|
+
from dataclasses import dataclass, asdict
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
40
|
+
PUBLIC_FILES = [Path("README.md"), Path("AGENTS.md"), Path("docs/architecture.md")]
|
|
41
|
+
CONTRACTS_DIR = Path("docs/contracts")
|
|
42
|
+
STABILITY_FILE = CONTRACTS_DIR / "STABILITY.md"
|
|
43
|
+
|
|
44
|
+
LINK_RE = re.compile(r"\[(?P<text>[^\]]+)\]\((?P<href>[^)\s]+)(?:\s+\"[^\"]*\")?\)")
|
|
45
|
+
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
|
|
46
|
+
STABILITY_RE = re.compile(r"^stability:\s*(\w+)\s*$", re.MULTILINE)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Violation:
|
|
51
|
+
file: str
|
|
52
|
+
line: int
|
|
53
|
+
href: str
|
|
54
|
+
reason: str
|
|
55
|
+
severity: str # "error" | "warning"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read_stability(path: Path) -> str | None:
|
|
59
|
+
if not path.exists():
|
|
60
|
+
return None
|
|
61
|
+
txt = path.read_text(encoding="utf-8")
|
|
62
|
+
m = FRONTMATTER_RE.match(txt)
|
|
63
|
+
if not m:
|
|
64
|
+
return None
|
|
65
|
+
sm = STABILITY_RE.search(m.group(1))
|
|
66
|
+
return sm.group(1) if sm else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def collect_contracts() -> dict[Path, str | None]:
|
|
70
|
+
out: dict[Path, str | None] = {}
|
|
71
|
+
for p in sorted((ROOT / CONTRACTS_DIR).glob("*.md")):
|
|
72
|
+
rel = p.relative_to(ROOT)
|
|
73
|
+
out[rel] = read_stability(p)
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def resolve(public_file: Path, href: str) -> Path | None:
|
|
78
|
+
href = href.split("#", 1)[0]
|
|
79
|
+
if not href or href.startswith(("http://", "https://", "mailto:", "tel:")):
|
|
80
|
+
return None
|
|
81
|
+
if href.startswith("/"):
|
|
82
|
+
return Path(href.lstrip("/"))
|
|
83
|
+
return (public_file.parent / href).resolve().relative_to(ROOT.resolve())
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def scan_file(public_file: Path, contracts: dict[Path, str | None]) -> list[Violation]:
|
|
87
|
+
abs_path = ROOT / public_file
|
|
88
|
+
if not abs_path.exists():
|
|
89
|
+
return []
|
|
90
|
+
violations: list[Violation] = []
|
|
91
|
+
for lineno, line in enumerate(abs_path.read_text(encoding="utf-8").splitlines(), 1):
|
|
92
|
+
for m in LINK_RE.finditer(line):
|
|
93
|
+
href = m.group("href")
|
|
94
|
+
text = m.group("text")
|
|
95
|
+
try:
|
|
96
|
+
target = resolve(public_file, href)
|
|
97
|
+
except ValueError:
|
|
98
|
+
continue
|
|
99
|
+
if target is None:
|
|
100
|
+
continue
|
|
101
|
+
if target.parts[:2] == ("agents", "contexts") and target.suffix == ".md":
|
|
102
|
+
violations.append(Violation(str(public_file), lineno, href,
|
|
103
|
+
"public surface MUST NOT link into agents/contexts/ — move target to docs/contracts/",
|
|
104
|
+
"error"))
|
|
105
|
+
continue
|
|
106
|
+
if target.parts[:2] != ("docs", "contracts") or target.suffix != ".md":
|
|
107
|
+
continue
|
|
108
|
+
if target == STABILITY_FILE:
|
|
109
|
+
continue
|
|
110
|
+
if target not in contracts:
|
|
111
|
+
violations.append(Violation(str(public_file), lineno, href,
|
|
112
|
+
f"target not found: {target}", "error"))
|
|
113
|
+
continue
|
|
114
|
+
level = contracts[target]
|
|
115
|
+
if level is None:
|
|
116
|
+
violations.append(Violation(str(public_file), lineno, href,
|
|
117
|
+
f"target missing 'stability:' frontmatter: {target}", "error"))
|
|
118
|
+
continue
|
|
119
|
+
if level == "experimental":
|
|
120
|
+
violations.append(Violation(str(public_file), lineno, href,
|
|
121
|
+
f"public surface MUST NOT link to experimental contract: {target}",
|
|
122
|
+
"error"))
|
|
123
|
+
continue
|
|
124
|
+
if level == "beta":
|
|
125
|
+
window = line.lower()
|
|
126
|
+
if "(beta)" not in window and "[beta]" not in window:
|
|
127
|
+
violations.append(Violation(str(public_file), lineno, href,
|
|
128
|
+
f"link to beta contract '{target}' lacks visible (beta) marker",
|
|
129
|
+
"warning"))
|
|
130
|
+
return violations
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main() -> int:
|
|
134
|
+
ap = argparse.ArgumentParser()
|
|
135
|
+
ap.add_argument("--list", action="store_true", help="list contracts + stability levels")
|
|
136
|
+
ap.add_argument("--json", action="store_true", help="machine-readable output")
|
|
137
|
+
ap.add_argument("--strict", action="store_true",
|
|
138
|
+
help="fail on warnings as well as errors (default: errors only)")
|
|
139
|
+
args = ap.parse_args()
|
|
140
|
+
|
|
141
|
+
contracts = collect_contracts()
|
|
142
|
+
if args.list:
|
|
143
|
+
for p, lvl in contracts.items():
|
|
144
|
+
print(f" {lvl or '(no frontmatter)':14} {p}")
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
missing_fm = [p for p, lvl in contracts.items() if lvl is None and p != STABILITY_FILE]
|
|
148
|
+
violations: list[Violation] = []
|
|
149
|
+
for p in missing_fm:
|
|
150
|
+
violations.append(Violation(str(p), 0, "(self)",
|
|
151
|
+
"missing 'stability:' frontmatter required by docs/contracts/STABILITY.md",
|
|
152
|
+
"error"))
|
|
153
|
+
for f in PUBLIC_FILES:
|
|
154
|
+
violations.extend(scan_file(f, contracts))
|
|
155
|
+
|
|
156
|
+
if args.json:
|
|
157
|
+
print(json.dumps([asdict(v) for v in violations], indent=2))
|
|
158
|
+
else:
|
|
159
|
+
errors = [v for v in violations if v.severity == "error"]
|
|
160
|
+
warnings = [v for v in violations if v.severity == "warning"]
|
|
161
|
+
for v in violations:
|
|
162
|
+
icon = "❌" if v.severity == "error" else "⚠️ "
|
|
163
|
+
loc = f"{v.file}:{v.line}" if v.line else v.file
|
|
164
|
+
print(f"{icon} {loc} {v.href}\n → {v.reason}")
|
|
165
|
+
if not violations:
|
|
166
|
+
print(f"✅ public-link check clean — {len(contracts)} contracts scanned, "
|
|
167
|
+
f"{len(PUBLIC_FILES)} public files clean")
|
|
168
|
+
else:
|
|
169
|
+
print(f"\nsummary: {len(errors)} error(s), {len(warnings)} warning(s)")
|
|
170
|
+
|
|
171
|
+
has_errors = any(v.severity == "error" for v in violations)
|
|
172
|
+
has_warnings = any(v.severity == "warning" for v in violations)
|
|
173
|
+
if has_errors:
|
|
174
|
+
return 1
|
|
175
|
+
if has_warnings and args.strict:
|
|
176
|
+
return 1
|
|
177
|
+
return 0
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
try:
|
|
182
|
+
sys.exit(main())
|
|
183
|
+
except Exception as e:
|
|
184
|
+
print(f"❌ internal error: {e}", file=sys.stderr)
|
|
185
|
+
sys.exit(3)
|
|
@@ -78,6 +78,7 @@ EXAMPLE_PATH_PATTERNS = [
|
|
|
78
78
|
re.compile(r"agents/overrides/"), # override examples
|
|
79
79
|
re.compile(r"commands/old-cmd"), # example placeholder
|
|
80
80
|
re.compile(r"agents/README"), # README reference (may not exist in package)
|
|
81
|
+
re.compile(r"agents/index[\w.-]*\.md"), # planned auto-generated artefact index (F5)
|
|
81
82
|
re.compile(r"agents/docs/"), # project-specific docs (not in package)
|
|
82
83
|
re.compile(r"agents/contexts/"), # project-specific contexts (not in package)
|
|
83
84
|
re.compile(r"agents/gates"), # project-specific policy docs
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Atomic-command linter for the command-collapse policy.
|
|
4
|
+
|
|
5
|
+
Reads the locked verb clusters from `docs/contracts/command-clusters.md`,
|
|
6
|
+
finds every command file under `.agent-src.uncompressed/commands/` that
|
|
7
|
+
was **added** since `--baseline` (default: `main`), and requires each
|
|
8
|
+
new file to declare either:
|
|
9
|
+
|
|
10
|
+
- `cluster: <locked-name>` (file is a cluster entry or sub-command), or
|
|
11
|
+
- `superseded_by: <slug>` (file is a deprecation shim).
|
|
12
|
+
|
|
13
|
+
Modifications to pre-existing files are NOT flagged — only additions.
|
|
14
|
+
This stops the atomic surface from growing without forcing every existing
|
|
15
|
+
command into a Phase 1 cluster (most aren't in Phase 1).
|
|
16
|
+
|
|
17
|
+
Exit codes: 0 = clean, 1 = violations found, 3 = internal error.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python3 scripts/lint_no_new_atomic_commands.py
|
|
21
|
+
python3 scripts/lint_no_new_atomic_commands.py --baseline origin/main
|
|
22
|
+
python3 scripts/lint_no_new_atomic_commands.py --all # ignore baseline
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import re
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
35
|
+
COMMANDS_DIR = Path(".agent-src.uncompressed/commands")
|
|
36
|
+
CLUSTER_CONTRACT = Path("docs/contracts/command-clusters.md")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Violation:
|
|
41
|
+
file: str
|
|
42
|
+
reason: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_locked_clusters() -> set[str]:
|
|
46
|
+
"""Parse the Phase 1 cluster table from the locked contract."""
|
|
47
|
+
text = (ROOT / CLUSTER_CONTRACT).read_text(encoding="utf-8")
|
|
48
|
+
# Locate the Phase 1 table; cluster names sit in backticks in column 1.
|
|
49
|
+
in_phase_1 = False
|
|
50
|
+
clusters: set[str] = set()
|
|
51
|
+
for line in text.splitlines():
|
|
52
|
+
if line.startswith("## Phase 1 clusters"):
|
|
53
|
+
in_phase_1 = True
|
|
54
|
+
continue
|
|
55
|
+
if in_phase_1 and line.startswith("## "):
|
|
56
|
+
break
|
|
57
|
+
if in_phase_1:
|
|
58
|
+
m = re.match(r"\|\s*`([a-z][a-z0-9-]*)`\s*\|", line)
|
|
59
|
+
if m:
|
|
60
|
+
clusters.add(m.group(1))
|
|
61
|
+
if not clusters:
|
|
62
|
+
print(
|
|
63
|
+
f"❌ Could not parse Phase 1 cluster table from {CLUSTER_CONTRACT}",
|
|
64
|
+
file=sys.stderr,
|
|
65
|
+
)
|
|
66
|
+
sys.exit(3)
|
|
67
|
+
return clusters
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def added_command_files(baseline: str) -> list[Path]:
|
|
71
|
+
"""Files under commands/ added (status A) since baseline."""
|
|
72
|
+
try:
|
|
73
|
+
result = subprocess.run(
|
|
74
|
+
["git", "diff", "--name-only", "--diff-filter=A",
|
|
75
|
+
f"{baseline}...HEAD", "--", str(COMMANDS_DIR)],
|
|
76
|
+
capture_output=True, text=True, cwd=ROOT, timeout=15,
|
|
77
|
+
)
|
|
78
|
+
except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
|
|
79
|
+
print(f"❌ git diff failed: {exc}", file=sys.stderr)
|
|
80
|
+
sys.exit(3)
|
|
81
|
+
if result.returncode != 0:
|
|
82
|
+
print(f"❌ git diff exit {result.returncode}: {result.stderr}",
|
|
83
|
+
file=sys.stderr)
|
|
84
|
+
sys.exit(3)
|
|
85
|
+
files = [Path(p) for p in result.stdout.splitlines()
|
|
86
|
+
if p.endswith(".md") and p != ""]
|
|
87
|
+
# Also include untracked (newly added, uncommitted) files.
|
|
88
|
+
try:
|
|
89
|
+
wt = subprocess.run(
|
|
90
|
+
["git", "status", "--porcelain", "--", str(COMMANDS_DIR)],
|
|
91
|
+
capture_output=True, text=True, cwd=ROOT, timeout=10,
|
|
92
|
+
)
|
|
93
|
+
for line in wt.stdout.splitlines():
|
|
94
|
+
if len(line) < 4:
|
|
95
|
+
continue
|
|
96
|
+
status = line[:2]
|
|
97
|
+
if status.strip() not in ("A", "??", "AM"):
|
|
98
|
+
continue
|
|
99
|
+
path = line[3:].strip().split(" -> ")[-1]
|
|
100
|
+
if path.endswith(".md"):
|
|
101
|
+
p = Path(path)
|
|
102
|
+
if p not in files:
|
|
103
|
+
files.append(p)
|
|
104
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
105
|
+
pass
|
|
106
|
+
return files
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def all_command_files() -> list[Path]:
|
|
110
|
+
return sorted((ROOT / COMMANDS_DIR).glob("*.md"))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parse_frontmatter(path: Path) -> dict[str, str]:
|
|
114
|
+
text = path.read_text(encoding="utf-8")
|
|
115
|
+
if not text.startswith("---"):
|
|
116
|
+
return {}
|
|
117
|
+
end = text.find("\n---", 3)
|
|
118
|
+
if end == -1:
|
|
119
|
+
return {}
|
|
120
|
+
fm: dict[str, str] = {}
|
|
121
|
+
for line in text[3:end].splitlines():
|
|
122
|
+
if ":" in line:
|
|
123
|
+
k, _, v = line.partition(":")
|
|
124
|
+
fm[k.strip()] = v.strip()
|
|
125
|
+
return fm
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def check_file(path: Path, clusters: set[str]) -> Violation | None:
|
|
129
|
+
abs_path = path if path.is_absolute() else ROOT / path
|
|
130
|
+
if not abs_path.exists():
|
|
131
|
+
return None # deleted file, nothing to check
|
|
132
|
+
fm = parse_frontmatter(abs_path)
|
|
133
|
+
if "superseded_by" in fm:
|
|
134
|
+
return None # shim — exempt
|
|
135
|
+
cluster = fm.get("cluster")
|
|
136
|
+
if not cluster:
|
|
137
|
+
return Violation(str(path),
|
|
138
|
+
"missing `cluster:` frontmatter "
|
|
139
|
+
f"(allowed: {sorted(clusters)})")
|
|
140
|
+
if cluster not in clusters:
|
|
141
|
+
return Violation(str(path),
|
|
142
|
+
f"`cluster: {cluster}` is not a locked cluster "
|
|
143
|
+
f"(allowed: {sorted(clusters)})")
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main() -> int:
|
|
148
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
149
|
+
ap.add_argument("--baseline", default="main",
|
|
150
|
+
help="git ref to diff against (default: main)")
|
|
151
|
+
ap.add_argument("--all", action="store_true",
|
|
152
|
+
help="check every command file, not just changed ones")
|
|
153
|
+
args = ap.parse_args()
|
|
154
|
+
|
|
155
|
+
clusters = load_locked_clusters()
|
|
156
|
+
targets = (all_command_files() if args.all
|
|
157
|
+
else added_command_files(args.baseline))
|
|
158
|
+
if not targets:
|
|
159
|
+
print(f"✅ No new commands added under {COMMANDS_DIR} "
|
|
160
|
+
f"(baseline: {args.baseline}).")
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
violations = [v for v in (check_file(p, clusters) for p in targets)
|
|
164
|
+
if v is not None]
|
|
165
|
+
if violations:
|
|
166
|
+
print(f"❌ {len(violations)} newly-added atomic command(s) violate "
|
|
167
|
+
f"the command-cluster policy:")
|
|
168
|
+
for v in violations:
|
|
169
|
+
print(f" • {v.file} — {v.reason}")
|
|
170
|
+
print(f"\nSee docs/contracts/command-clusters.md for the locked "
|
|
171
|
+
f"cluster names and frontmatter contract.")
|
|
172
|
+
return 1
|
|
173
|
+
print(f"✅ {len(targets)} newly-added command(s) all declare a valid "
|
|
174
|
+
f"`cluster:` (or `superseded_by:`).")
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
sys.exit(main())
|