@rungs/cli 0.1.3 → 0.3.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/README.md +61 -11
- package/dist/cli.js +1250 -129
- package/dist/cli.js.map +4 -4
- package/modules/README.md +13 -0
- package/modules/adr/gates/adr.toml +14 -2
- package/modules/adr/module.toml +19 -1
- package/modules/audit/module.toml +1 -0
- package/modules/backlog/gates/ids.toml +33 -0
- package/modules/backlog/module.toml +61 -1
- package/modules/ci/files/{{workflow_path}} +9 -1
- package/modules/ci/module.toml +2 -1
- package/modules/concurrency/files/docs/concurrent-sessions.md +11 -5
- package/modules/concurrency/module.toml +3 -1
- package/modules/design-sync/module.toml +2 -0
- package/modules/doc-authority/module.toml +4 -0
- package/modules/findings/module.toml +3 -0
- package/modules/gates/gates/structural.toml +61 -17
- package/modules/gates/module.toml +5 -0
- package/modules/instructions/module.toml +4 -0
- package/modules/release/gates/release.toml +72 -4
- package/modules/release/module.toml +16 -1
- package/modules/release/skills/cut-release/SKILL.md +8 -1
- package/modules/session/module.toml +4 -2
- package/modules/skills/module.toml +3 -0
- package/modules/specs/module.toml +4 -0
- package/modules/workflows/module.toml +2 -0
- package/package.json +1 -1
- package/src/add.ts +64 -2
- package/src/backlog.ts +197 -0
- package/src/check.ts +56 -6
- package/src/cli.ts +406 -27
- package/src/concurrency.ts +412 -0
- package/src/engines.ts +261 -13
- package/src/engines2.ts +89 -4
- package/src/engines3.ts +147 -0
- package/src/explain.ts +189 -0
- package/src/lifecycle.ts +90 -3
- package/src/manifest.ts +13 -1
- package/src/selftest.ts +237 -0
- package/src/types.ts +34 -0
package/modules/README.md
CHANGED
|
@@ -91,6 +91,19 @@ whether its guard has ever actually fired.
|
|
|
91
91
|
`module.toml`, per skill, with the portability cost stated there.
|
|
92
92
|
5. **Every gate declares a self-test asserting both directions.** A gate whose rules are currently
|
|
93
93
|
satisfied is indistinguishable from a gate that matches nothing.
|
|
94
|
+
5b. **Every gate declares its `applicability`**, next to its `engine`, and there is no default:
|
|
95
|
+
|
|
96
|
+
| Value | Means | Runs on a repo that is not ours? |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `repo-content` | Measures the repo's own content — a count, a length, whether a link resolves | **Yes** |
|
|
99
|
+
| `our-artifacts` | Checks something rungs wrote, which cannot exist unless we installed | No |
|
|
100
|
+
| `our-schema` | Reads their file against a shape we defined | No |
|
|
101
|
+
|
|
102
|
+
`doctor --explain` runs only `repo-content` against a repo that has its own equivalent of a
|
|
103
|
+
module. Omitting the field does not mean "safe": the gate is skipped **and named**, and
|
|
104
|
+
`rungs modules` refuses the manifest. This exists because the first version of `--explain`
|
|
105
|
+
decided it centrally by engine name, and produced 71 findings that were true about our
|
|
106
|
+
conventions and meaningless about the repos they landed on.
|
|
94
107
|
6. **`[detect]` must correctly classify all four source repos.** That is the Phase 6 acceptance
|
|
95
108
|
criterion, and it is why detection is biased toward false negatives.
|
|
96
109
|
7. **A managed-block marker uses the target file's comment syntax** — `<!-- rungs:begin x -->` in
|
|
@@ -23,6 +23,10 @@ enum = { status = ["proposed", "accepted", "rejected", "superseded"] }
|
|
|
23
23
|
# supersession leaves a reader on the stale record with no way to find the live one.
|
|
24
24
|
[frontmatter_schema.reciprocal]
|
|
25
25
|
pairs = [{ from = "superseded_by", to = "supersedes" }]
|
|
26
|
+
# A record cannot declare itself superseded without saying by what. Stated
|
|
27
|
+
# separately from `pairs` because it is a different failure: `pairs` catches a
|
|
28
|
+
# one-way link, this catches a missing one.
|
|
29
|
+
required_when = { superseded = "superseded_by" }
|
|
26
30
|
|
|
27
31
|
# Sections, not prose quality. The gate can check that `Alternatives considered` exists and is not
|
|
28
32
|
# empty; it cannot judge whether the alternatives were real, and one that tried would be the
|
|
@@ -40,12 +44,12 @@ done rather than a decision — which is what admission criterion 2 is guarding
|
|
|
40
44
|
# ── self-tests ────────────────────────────────────────────────────────────────
|
|
41
45
|
|
|
42
46
|
[[self_test]]
|
|
43
|
-
gate = "adr-
|
|
47
|
+
gate = "adr-sections-present"
|
|
44
48
|
expect = "fail"
|
|
45
49
|
fixture = { file = "{{path}}/{{id_prefix}}-0001-x.md", sections = ["Context", "Decision", "Consequences"] }
|
|
46
50
|
|
|
47
51
|
[[self_test]]
|
|
48
|
-
gate = "adr-
|
|
52
|
+
gate = "adr-sections-present"
|
|
49
53
|
expect = "pass"
|
|
50
54
|
fixture = { file = "{{path}}/{{id_prefix}}-0001-x.md", sections = ["Context", "Decision", "Consequences", "Alternatives considered", "Revisit triggers"] }
|
|
51
55
|
|
|
@@ -54,6 +58,14 @@ gate = "adr-required-fields"
|
|
|
54
58
|
expect = "fail"
|
|
55
59
|
fixture = { frontmatter = { id = "{{id_prefix}}-0001", title = "x", status = "superseded", date = "2026-01-01" } }
|
|
56
60
|
|
|
61
|
+
# The pass direction was missing entirely: both of this gate's fixtures described *sections* and
|
|
62
|
+
# were relabelled to `adr-sections-present` (F-018), which left it asserting only that something
|
|
63
|
+
# fails. A gate with no passing fixture cannot tell "correct" from "matches nothing".
|
|
64
|
+
[[self_test]]
|
|
65
|
+
gate = "adr-required-fields"
|
|
66
|
+
expect = "pass"
|
|
67
|
+
fixture = { frontmatter = { id = "{{id_prefix}}-0001", title = "x", status = "accepted", date = "2026-01-01" } }
|
|
68
|
+
|
|
57
69
|
[[self_test]]
|
|
58
70
|
gate = "adr-index-current"
|
|
59
71
|
expect = "fail"
|
package/modules/adr/module.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[module]
|
|
2
2
|
name = "adr"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.1.0"
|
|
4
4
|
rung = 1
|
|
5
5
|
summary = "Numbered, immutable decision records, with an admission rule that keeps the directory from becoming a second documentation tree."
|
|
6
6
|
|
|
@@ -27,14 +27,32 @@ default = "ADR"
|
|
|
27
27
|
id = "adr-index-current"
|
|
28
28
|
kind = "declared"
|
|
29
29
|
engine = "render-freshness"
|
|
30
|
+
applicability = "our-artifacts"
|
|
30
31
|
table = "gates/adr.toml"
|
|
31
32
|
tier = "fast"
|
|
32
33
|
why = "An index that has stopped listing every record is a routing table that silently drops decisions."
|
|
33
34
|
|
|
35
|
+
[[gates]]
|
|
36
|
+
id = "adr-sections-present"
|
|
37
|
+
kind = "declared"
|
|
38
|
+
engine = "sections"
|
|
39
|
+
applicability = "our-schema"
|
|
40
|
+
table = "gates/adr.toml"
|
|
41
|
+
tier = "fast"
|
|
42
|
+
why = """
|
|
43
|
+
The `[sections]` table required Context, Decision, Consequences, Alternatives considered and
|
|
44
|
+
Revisit triggers with non-empty bodies, and **no gate declared it** — a fully specified check that
|
|
45
|
+
nothing ran, found 2026-08-16 by executing the self-test fixtures (F-018). Two fixtures were even
|
|
46
|
+
labelled for `adr-required-fields`, which checks frontmatter and could never have satisfied them.
|
|
47
|
+
A record without alternatives is a description of what was done rather than a decision, which is
|
|
48
|
+
what the admission rule's second criterion guards against.
|
|
49
|
+
"""
|
|
50
|
+
|
|
34
51
|
[[gates]]
|
|
35
52
|
id = "adr-required-fields"
|
|
36
53
|
kind = "declared"
|
|
37
54
|
engine = "frontmatter-schema"
|
|
55
|
+
applicability = "our-schema"
|
|
38
56
|
table = "gates/adr.toml"
|
|
39
57
|
tier = "fast"
|
|
40
58
|
why = """
|
|
@@ -104,3 +104,36 @@ fixture = { items = ["{{id_prefix}}-001-a.md", "{{id_prefix}}-002-b.md"] }
|
|
|
104
104
|
gate = "backlog-ids"
|
|
105
105
|
expect = "fail"
|
|
106
106
|
fixture = { items = ["{{id_prefix}}-001-a.md"], marker = "{{id_prefix}}-001" }
|
|
107
|
+
|
|
108
|
+
# ── backlog-board-reconciled ──────────────────────────────────────────────────
|
|
109
|
+
#
|
|
110
|
+
# The board groups rows by status; each item declares its own. Nothing checked
|
|
111
|
+
# that the two agreed, and on 2026-08-16 fourteen rows disagreed — nine under
|
|
112
|
+
# `Proposed` and five under `Planned`, all naming files whose status was `done`,
|
|
113
|
+
# nine of them already moved into `archive/`.
|
|
114
|
+
#
|
|
115
|
+
# The heading map is data, not code, because a repo may rename a column or drop
|
|
116
|
+
# one. An undeclared heading is reported rather than skipped: a row that hides
|
|
117
|
+
# from the check is exactly the case this exists for.
|
|
118
|
+
|
|
119
|
+
[board_reconcile]
|
|
120
|
+
file = "docs/{{root}}/BACKLOG.md"
|
|
121
|
+
exempt_marker = "board-ok:" # ignored unless it states a reason
|
|
122
|
+
|
|
123
|
+
[board_reconcile.groups]
|
|
124
|
+
"In progress" = ["in_progress"]
|
|
125
|
+
"Review" = ["review"]
|
|
126
|
+
"Planned" = ["planned"]
|
|
127
|
+
"Accepted" = ["accepted"]
|
|
128
|
+
"Proposed" = ["proposed"]
|
|
129
|
+
"Deferred" = ["deferred"]
|
|
130
|
+
|
|
131
|
+
[[self_test]]
|
|
132
|
+
gate = "backlog-board-reconciled"
|
|
133
|
+
expect = "fail"
|
|
134
|
+
input = "## Proposed\n| [{{id_prefix}}-001](items/{{id_prefix}}-001-x.md) | done item filed as proposed | docs |"
|
|
135
|
+
|
|
136
|
+
[[self_test]]
|
|
137
|
+
gate = "backlog-board-reconciled"
|
|
138
|
+
expect = "pass"
|
|
139
|
+
input = "## Proposed\n| [{{id_prefix}}-001](items/{{id_prefix}}-001-x.md) | a genuinely proposed item | docs |"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[module]
|
|
2
2
|
name = "backlog"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.1.0"
|
|
4
4
|
rung = 1
|
|
5
5
|
summary = "Work items with stable ids, an eight-status lifecycle, a board, and the three gates that keep the bookkeeping honest."
|
|
6
6
|
|
|
@@ -42,6 +42,7 @@ default = "feature"
|
|
|
42
42
|
id = "backlog-ids"
|
|
43
43
|
kind = "declared"
|
|
44
44
|
engine = "id-integrity"
|
|
45
|
+
applicability = "our-schema"
|
|
45
46
|
table = "gates/ids.toml"
|
|
46
47
|
tier = "fast"
|
|
47
48
|
why = "An id claimed twice is two items the board shows as one."
|
|
@@ -50,6 +51,7 @@ why = "An id claimed twice is two items the board shows as one."
|
|
|
50
51
|
id = "backlog-stale-blocker"
|
|
51
52
|
kind = "declared"
|
|
52
53
|
engine = "id-integrity"
|
|
54
|
+
applicability = "our-schema"
|
|
53
55
|
table = "gates/ids.toml"
|
|
54
56
|
tier = "fast"
|
|
55
57
|
why = """
|
|
@@ -62,6 +64,7 @@ finished item as next owner, then found those routes quoted in a code comment as
|
|
|
62
64
|
id = "backlog-merged-status"
|
|
63
65
|
kind = "declared"
|
|
64
66
|
engine = "git-status-reconcile"
|
|
67
|
+
applicability = "our-schema"
|
|
65
68
|
table = "gates/ids.toml"
|
|
66
69
|
tier = "fast"
|
|
67
70
|
why = """
|
|
@@ -70,6 +73,22 @@ rift-forge: 37 items sat at `review` with their code already landed, and one sat
|
|
|
70
73
|
for eight days after its own merge.
|
|
71
74
|
"""
|
|
72
75
|
|
|
76
|
+
[[gates]]
|
|
77
|
+
id = "backlog-board-reconciled"
|
|
78
|
+
kind = "declared"
|
|
79
|
+
engine = "board-reconcile"
|
|
80
|
+
applicability = "our-artifacts"
|
|
81
|
+
table = "gates/ids.toml"
|
|
82
|
+
tier = "fast"
|
|
83
|
+
why = """
|
|
84
|
+
The board groups rows by status and each item declares its own; nothing checked that the two agreed.
|
|
85
|
+
Measured here 2026-08-16: fourteen rows disagreed — nine under `Proposed`, five under `Planned`, all
|
|
86
|
+
naming items whose files read `status: done`, and nine of them already moved into `archive/`, so the
|
|
87
|
+
board called work proposed while pointing at the directory for work that cannot change. An outside
|
|
88
|
+
reader checking whether that work was done would have been told no. `backlog-merged-status`
|
|
89
|
+
reconciles a branch against the same field; this is the layer above it.
|
|
90
|
+
"""
|
|
91
|
+
|
|
73
92
|
# ── skills ────────────────────────────────────────────────────────────────────
|
|
74
93
|
# SKILL.md files stay restricted to the six Agent Skills spec fields (ADR-0001).
|
|
75
94
|
# Claude Code extensions are opted into here, per skill, with the cost stated.
|
|
@@ -110,6 +129,47 @@ coexist — a milestone overlay sits on top of items. Three real options: adopt
|
|
|
110
129
|
unit and install only the missing gates, install items alongside, or skip.
|
|
111
130
|
"""
|
|
112
131
|
|
|
132
|
+
# The repo's work lives in an issue tracker. Without this, a team running
|
|
133
|
+
# everything through GitHub Issues matched no backlog signature at all, was
|
|
134
|
+
# reported `absent`, and `doctor` proposed installing a Markdown backlog beside
|
|
135
|
+
# the one they actually use — which is exactly the "why isn't this Jira?"
|
|
136
|
+
# objection, earned.
|
|
137
|
+
#
|
|
138
|
+
# **Detection only, and deliberately so.** An adapter that read or wrote an
|
|
139
|
+
# external tracker would need credentials, network and a client inside the
|
|
140
|
+
# consumer repo, which ADR-0002 rules out. ADR-0004 already answers the
|
|
141
|
+
# ownership worry the right way: adoption is a mapping, not a migration. So
|
|
142
|
+
# rungs' job here is to notice and stop, not to sync.
|
|
143
|
+
#
|
|
144
|
+
# Only configured issue *intake* counts. A `.github/` directory does not — 76
|
|
145
|
+
# local repos have one and none of them run their work in Issues (measured
|
|
146
|
+
# 2026-08-16). An `ISSUE_TEMPLATE` directory or a root issue template is
|
|
147
|
+
# somebody deliberately configuring how work arrives, which is the narrowest
|
|
148
|
+
# in-repo evidence that the tracker is the unit of work.
|
|
149
|
+
#
|
|
150
|
+
# Linear and Jira are **not** listed. They leave no reliable file-level trace —
|
|
151
|
+
# usually nothing but a URL in a PR template — and a signature guessed from a
|
|
152
|
+
# link is the confidently-wrong probe this repo refuses everywhere else.
|
|
153
|
+
# The note covers them because the trade-off does not depend on the vendor.
|
|
154
|
+
[[detect.paradigm]]
|
|
155
|
+
id = "external-tracker"
|
|
156
|
+
paths = [
|
|
157
|
+
".github/ISSUE_TEMPLATE/**",
|
|
158
|
+
".github/issue_template.md",
|
|
159
|
+
".github/ISSUE_TEMPLATE.md",
|
|
160
|
+
]
|
|
161
|
+
compare = "docs/research/synthesis.md#33-unit-of-work"
|
|
162
|
+
note = """
|
|
163
|
+
Work items here are Markdown files in the repo; your tracker is a system outside it. Neither is
|
|
164
|
+
better — they fail differently. A tracker gives you queries, notifications and non-engineers; files
|
|
165
|
+
give you review, history and offline agents, and they are the only form a gate can mechanically
|
|
166
|
+
check against a branch.
|
|
167
|
+
|
|
168
|
+
rungs will not sync, import or export. Three real options: keep the tracker as the unit of work and
|
|
169
|
+
install only the modules that do not duplicate it (`findings`, `adr`, `session`), install items
|
|
170
|
+
alongside for agent-facing work while humans stay in the tracker, or skip the module.
|
|
171
|
+
"""
|
|
172
|
+
|
|
113
173
|
# Proposes a parameter once presence is already established. Never concludes
|
|
114
174
|
# presence — hexguard-templates has 207 well-formed `FOUND-US-###` matches and
|
|
115
175
|
# no backlog; they are spec story ids.
|
|
@@ -29,8 +29,16 @@ jobs:
|
|
|
29
29
|
|
|
30
30
|
# Runs every gate in .ai/gates.toml — the same set, in the same order, as
|
|
31
31
|
# `rungs check` locally. A CI-only gate is one nobody can reproduce.
|
|
32
|
+
#
|
|
33
|
+
# Plain `check`, no flags. This said `check --tier full --reporter github`:
|
|
34
|
+
# `--tier` is not a flag the CLI accepts and `--reporter` does not exist at
|
|
35
|
+
# all, so the two values were parsed as a *path* and a *tier* — every run
|
|
36
|
+
# checked a directory named `full` and exited 1 with "no gates registered —
|
|
37
|
+
# is this a rungs repo?". Shipped in three of five profiles, so every repo
|
|
38
|
+
# that installed `ci` had a job that failed on every push and blamed the
|
|
39
|
+
# repo for it (F-030).
|
|
32
40
|
- name: Run gates
|
|
33
|
-
run: npx @rungs/cli check
|
|
41
|
+
run: npx @rungs/cli check
|
|
34
42
|
|
|
35
43
|
# Add a matrix job per package here rather than a workflow per package.
|
|
36
44
|
# A checklist step that creates a file creates N files:
|
package/modules/ci/module.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[module]
|
|
2
2
|
name = "ci"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.1.0"
|
|
4
4
|
rung = 1
|
|
5
5
|
summary = "One workflow that runs the gate registry, a matrix instead of a file per item, and a check that keeps it that way."
|
|
6
6
|
|
|
@@ -42,6 +42,7 @@ default = ".github/workflows/checks.yml"
|
|
|
42
42
|
id = "ci-workflow-proliferation"
|
|
43
43
|
kind = "declared"
|
|
44
44
|
engine = "file-population"
|
|
45
|
+
applicability = "repo-content"
|
|
45
46
|
table = "gates/ci.toml"
|
|
46
47
|
tier = "fast"
|
|
47
48
|
why = """
|
|
@@ -35,9 +35,15 @@ deliberate question, never as a ritual.
|
|
|
35
35
|
|
|
36
36
|
## A failure is attributed, never just counted
|
|
37
37
|
|
|
38
|
-
`
|
|
39
|
-
**inherited** (already red before you started — stated, never blocking)
|
|
40
|
-
blocks). Anything it cannot attribute also blocks: **we do not land on an
|
|
38
|
+
`land` re-runs each failing gate against the merge base in the throwaway worktree it already has,
|
|
39
|
+
and reports each failure as **inherited** (already red before you started — stated, never blocking)
|
|
40
|
+
or **INTRODUCED** (yours — blocks). Anything it cannot attribute also blocks: **we do not land on an
|
|
41
|
+
unknown.**
|
|
42
|
+
|
|
43
|
+
Attribution is **per finding, not per gate.** The first implementation compared gate ids, which
|
|
44
|
+
made an already-red gate a blind spot: a branch could add new broken links and land them as
|
|
45
|
+
inherited, because that gate was red either way. Measured on a scratch repo, and fixed the same
|
|
46
|
+
day — a gate you have not fixed does not excuse the new violations of it you bring.
|
|
41
47
|
|
|
42
48
|
This is not politeness about blame. *A gate that is red for reasons you did not cause and cannot
|
|
43
49
|
fix is a gate you learn to bypass, and a bypassed gate reports nothing.*
|
|
@@ -62,8 +68,8 @@ bug in whatever it is doing.
|
|
|
62
68
|
|
|
63
69
|
## Three things to know when a land refuses
|
|
64
70
|
|
|
65
|
-
- **Your worktree is
|
|
66
|
-
|
|
71
|
+
- **Your worktree is untouched.** `land` does all its work in a throwaway worktree of its own, so
|
|
72
|
+
whatever you had checked out is still checked out. A refusal costs you nothing to recover from.
|
|
67
73
|
- **Check the exit code of `land` itself.** Piping it through `tail` or `grep` reports *that*
|
|
68
74
|
command's status, so a refused land reads as success.
|
|
69
75
|
- **Reconcile generated artifacts by regenerating, never by merging text.** Take one side, re-run
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[module]
|
|
2
2
|
name = "concurrency"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
rung = 5
|
|
5
5
|
summary = "Many sessions on one integration branch: a verified base to cut from, attributed failures, a land protocol that cannot redden the branch, and merge drivers per conflict class."
|
|
6
6
|
|
|
@@ -45,6 +45,7 @@ default = "integ/"
|
|
|
45
45
|
id = "concurrency-no-integration-checkout"
|
|
46
46
|
kind = "declared"
|
|
47
47
|
engine = "git-state"
|
|
48
|
+
applicability = "our-schema"
|
|
48
49
|
table = "gates/concurrency.toml"
|
|
49
50
|
tier = "fast"
|
|
50
51
|
why = """
|
|
@@ -59,6 +60,7 @@ releases the branch mid-run. Two land processes were measured running at once.
|
|
|
59
60
|
id = "concurrency-generated-not-text-merged"
|
|
60
61
|
kind = "declared"
|
|
61
62
|
engine = "merge-driver-check"
|
|
63
|
+
applicability = "our-artifacts"
|
|
62
64
|
table = "gates/concurrency.toml"
|
|
63
65
|
tier = "fast"
|
|
64
66
|
why = """
|
|
@@ -31,6 +31,7 @@ default = "docs/design-requests"
|
|
|
31
31
|
id = "design-deltas-routed"
|
|
32
32
|
kind = "declared"
|
|
33
33
|
engine = "register-schema"
|
|
34
|
+
applicability = "our-schema"
|
|
34
35
|
table = "gates/design.toml"
|
|
35
36
|
tier = "fast"
|
|
36
37
|
why = """
|
|
@@ -44,6 +45,7 @@ they are aligned.
|
|
|
44
45
|
id = "design-mirror-not-edited"
|
|
45
46
|
kind = "declared"
|
|
46
47
|
engine = "render-freshness"
|
|
48
|
+
applicability = "our-artifacts"
|
|
47
49
|
table = "gates/design.toml"
|
|
48
50
|
tier = "fast"
|
|
49
51
|
why = "A hand-edit in the mirror is a local change that the next pull silently destroys, and until then it is a fact about the design system that the design system does not know."
|
|
@@ -38,6 +38,7 @@ default = true
|
|
|
38
38
|
id = "docauth-ownership-respected"
|
|
39
39
|
kind = "declared"
|
|
40
40
|
engine = "term-ownership"
|
|
41
|
+
applicability = "our-schema"
|
|
41
42
|
table = "gates/authority.toml"
|
|
42
43
|
tier = "fast"
|
|
43
44
|
why = """
|
|
@@ -50,6 +51,7 @@ because the failure it catches is a whole section restated in the wrong file, no
|
|
|
50
51
|
id = "docauth-scope-headers"
|
|
51
52
|
kind = "declared"
|
|
52
53
|
engine = "sections"
|
|
54
|
+
applicability = "our-schema"
|
|
53
55
|
table = "gates/authority.toml"
|
|
54
56
|
tier = "fast"
|
|
55
57
|
why = "A mandatory rule with a zero-cost check has no excuse to be manual, and this one was manual in the repo that called it non-negotiable."
|
|
@@ -58,6 +60,7 @@ why = "A mandatory rule with a zero-cost check has no excuse to be manual, an
|
|
|
58
60
|
id = "docauth-working-rules"
|
|
59
61
|
kind = "declared"
|
|
60
62
|
engine = "rule-propagation"
|
|
63
|
+
applicability = "our-schema"
|
|
61
64
|
table = "gates/authority.toml"
|
|
62
65
|
tier = "fast"
|
|
63
66
|
why = """
|
|
@@ -71,6 +74,7 @@ changes, and reads as verified precisely because it names a source.
|
|
|
71
74
|
id = "docauth-no-redirect-stubs"
|
|
72
75
|
kind = "declared"
|
|
73
76
|
engine = "file-population"
|
|
77
|
+
applicability = "repo-content"
|
|
74
78
|
table = "gates/authority.toml"
|
|
75
79
|
tier = "fast"
|
|
76
80
|
why = "A file that exists only to say 'go elsewhere' is a bug: it will be cited, and the citation will look authoritative."
|
|
@@ -30,6 +30,7 @@ default = "docs/{{backlog.root}}/FINDINGS.md"
|
|
|
30
30
|
id = "findings-ids"
|
|
31
31
|
kind = "declared"
|
|
32
32
|
engine = "id-integrity"
|
|
33
|
+
applicability = "our-schema"
|
|
33
34
|
table = "gates/findings.toml"
|
|
34
35
|
tier = "fast"
|
|
35
36
|
why = "Two findings sharing an id are one finding as far as every citation is concerned."
|
|
@@ -38,6 +39,7 @@ why = "Two findings sharing an id are one finding as far as every citation is
|
|
|
38
39
|
id = "findings-disposition-has-reason"
|
|
39
40
|
kind = "declared"
|
|
40
41
|
engine = "register-schema"
|
|
42
|
+
applicability = "our-schema"
|
|
41
43
|
table = "gates/findings.toml"
|
|
42
44
|
tier = "fast"
|
|
43
45
|
why = """
|
|
@@ -49,6 +51,7 @@ is the same observation the next session will record again. The reason is what t
|
|
|
49
51
|
id = "findings-self-declared-closure"
|
|
50
52
|
kind = "declared"
|
|
51
53
|
engine = "self-declared-closure"
|
|
54
|
+
applicability = "our-schema"
|
|
52
55
|
table = "gates/findings.toml"
|
|
53
56
|
tier = "fast"
|
|
54
57
|
why = "An open finding whose own detail says it is fixed contradicts the register; this narrow text check catches drift without pretending to prove code-level closure."
|
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
# ── gates-links-resolve · gates-paths-exist ───────────────────────────────────
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# Two entries, one per gate. They were a single table shared by both gate ids, which ran the
|
|
9
|
+
# identical markdown-link scan twice and reported every finding twice — 224 lines for 112 broken
|
|
10
|
+
# links on one repo (F-007). Splitting them also lets `gates-paths-exist` do the job its `why`
|
|
11
|
+
# actually describes: hexguard's instruction files name paths in code spans, not in markdown links.
|
|
12
|
+
[[link_integrity]]
|
|
13
|
+
id = "gates-links-resolve"
|
|
14
|
+
check = ["relative_markdown_links"]
|
|
9
15
|
scan = ["**/*.md", ".ai/**/*.toml"]
|
|
10
16
|
# `modules/**` is a rungs module source tree, not repo content: a fragment's
|
|
11
17
|
# links are relative to the file it merges *into*, not to where it is authored,
|
|
@@ -13,14 +19,23 @@ scan = ["**/*.md", ".ai/**/*.toml"]
|
|
|
13
19
|
# repo nothing — it has no `modules/` — and was found by running this against
|
|
14
20
|
# the rungs repo itself.
|
|
15
21
|
exclude = ["node_modules/**", "**/archive/**", ".git/**", "modules/*/fragments/**", "modules/*/files/**"]
|
|
16
|
-
check = ["relative_markdown_links", "backticked_paths"]
|
|
17
|
-
# Backticked paths are checked only when they look like paths — containing a `/` or a known
|
|
18
|
-
# extension — because prose is full of `identifiers` that are not files. Under-detection is the
|
|
19
|
-
# correct bias: a missed stale path is a nuisance, a false refusal on every code span is a gate
|
|
20
|
-
# people delete.
|
|
21
|
-
path_hint = ["/", ".md", ".ts", ".cs", ".mjs", ".toml", ".json", ".ps1"]
|
|
22
22
|
exempt = "path-ok:" # ignored unless it states a reason
|
|
23
23
|
|
|
24
|
+
[[link_integrity]]
|
|
25
|
+
id = "gates-paths-exist"
|
|
26
|
+
check = ["backticked_paths"]
|
|
27
|
+
# Instruction files, where a named path going stale is the measured incident. Deliberately not
|
|
28
|
+
# every markdown file: prose about a repo cites paths that were true when written, and a gate that
|
|
29
|
+
# refuses history is one people delete.
|
|
30
|
+
scan = ["AGENTS.md", "CLAUDE.md", ".ai/**/*.md", ".ai/**/*.toml", ".github/instructions/**/*.md"]
|
|
31
|
+
exclude = ["node_modules/**", "**/archive/**", ".git/**"]
|
|
32
|
+
# Checked only when it looks like a path — containing a `/` or a known extension — because prose is
|
|
33
|
+
# full of `identifiers` that are not files. Resolved from the repo root, which is what an
|
|
34
|
+
# instruction file's reader does. Under-detection is the correct bias: a missed stale path is a
|
|
35
|
+
# nuisance, a false refusal on every code span is a gate people delete.
|
|
36
|
+
path_hint = ["/", ".md", ".ts", ".cs", ".mjs", ".toml", ".json", ".ps1"]
|
|
37
|
+
exempt = "path-ok:"
|
|
38
|
+
|
|
24
39
|
[link_integrity.notes]
|
|
25
40
|
external = "not fetched — a gate that needs the network is a gate that fails on a plane"
|
|
26
41
|
|
|
@@ -84,11 +99,39 @@ gate = "gates-links-resolve"
|
|
|
84
99
|
expect = "pass"
|
|
85
100
|
input = "The module writes [its readme](${{path}}/README.md) once installed."
|
|
86
101
|
|
|
102
|
+
# WI-042. `path/file.ts:387` is a code reference, not a broken link — it is the form CLAUDE.md
|
|
103
|
+
# mandates. Resolved literally it reported 1,794 false findings on rift-forge, 46.6% of that repo's
|
|
104
|
+
# link findings. Both directions, because the strip must narrow what is reported and never widen it:
|
|
105
|
+
# an existing file with a line number passes, a missing one with a line number still fails.
|
|
106
|
+
[[self_test]]
|
|
107
|
+
gate = "gates-links-resolve"
|
|
108
|
+
expect = "pass"
|
|
109
|
+
input = "The store is built in [forge-store](./structural.toml:222)."
|
|
110
|
+
|
|
111
|
+
[[self_test]]
|
|
112
|
+
gate = "gates-links-resolve"
|
|
113
|
+
expect = "fail"
|
|
114
|
+
input = "The store is built in [forge-store](./no-such-file.toml:222)."
|
|
115
|
+
|
|
87
116
|
[[self_test]]
|
|
88
117
|
gate = "gates-paths-exist"
|
|
89
118
|
expect = "pass"
|
|
90
119
|
input = "The `Result<T>` type wraps it." # a code span that is not a path
|
|
91
120
|
|
|
121
|
+
# F-007. `backticked_paths` was declared in the table's `check` list and implemented nowhere, so
|
|
122
|
+
# this gate silently ran the markdown-link scan instead and double-reported it. Implemented now,
|
|
123
|
+
# and narrowed by measurement: the first run produced ten findings on this repo and all ten were
|
|
124
|
+
# wrong — slash commands, `WI-###` placeholders, illustrative directories, bare filenames.
|
|
125
|
+
[[self_test]]
|
|
126
|
+
gate = "gates-paths-exist"
|
|
127
|
+
expect = "fail"
|
|
128
|
+
input = "Claim an id from `docs/backlog/DOES-NOT-EXIST.md` first."
|
|
129
|
+
|
|
130
|
+
[[self_test]]
|
|
131
|
+
gate = "gates-paths-exist"
|
|
132
|
+
expect = "pass"
|
|
133
|
+
input = "Run `/work-item`, branch `feature/WI-###-slug`, and read `.cursor/rules/`."
|
|
134
|
+
|
|
92
135
|
[[self_test]]
|
|
93
136
|
gate = "gates-rules-declare-enforcement"
|
|
94
137
|
expect = "fail"
|
|
@@ -99,10 +142,20 @@ gate = "gates-rules-declare-enforcement"
|
|
|
99
142
|
expect = "pass"
|
|
100
143
|
fixture = { file = ".ai/rules/x.md", frontmatter = { description = "d", enforcement = "review-only" } }
|
|
101
144
|
|
|
145
|
+
# F-018. This gate's three fixtures all described `.claude/skills/**/SKILL.md`, and the skill
|
|
146
|
+
# frontmatter schema moved to the `skills` module — so they asserted a schema this table no longer
|
|
147
|
+
# contains and could never pass or fail for the stated reason. They were left behind by the move and
|
|
148
|
+
# nobody noticed, because no fixture had ever been executed. Replaced with the schema that is
|
|
149
|
+
# actually here.
|
|
102
150
|
[[self_test]]
|
|
103
151
|
gate = "gates-frontmatter-valid"
|
|
104
152
|
expect = "fail"
|
|
105
|
-
fixture = { file = ".
|
|
153
|
+
fixture = { file = ".ai/rules/x.md", frontmatter = { enforcement = "gated" } }
|
|
154
|
+
|
|
155
|
+
[[self_test]]
|
|
156
|
+
gate = "gates-frontmatter-valid"
|
|
157
|
+
expect = "pass"
|
|
158
|
+
fixture = { file = ".ai/rules/x.md", frontmatter = { description = "d", enforcement = "gated" } }
|
|
106
159
|
|
|
107
160
|
[[self_test]]
|
|
108
161
|
gate = "gates-self-tests-both-directions"
|
|
@@ -130,12 +183,3 @@ gate = "gates-paths-exist"
|
|
|
130
183
|
expect = "pass"
|
|
131
184
|
input = "The mapper lives in [`structural.toml`](./structural.toml)."
|
|
132
185
|
|
|
133
|
-
[[self_test]]
|
|
134
|
-
gate = "gates-frontmatter-valid"
|
|
135
|
-
expect = "pass"
|
|
136
|
-
fixture = { file = ".claude/skills/x/SKILL.md", frontmatter = { name = "x", description = "d" } }
|
|
137
|
-
|
|
138
|
-
[[self_test]]
|
|
139
|
-
gate = "gates-frontmatter-valid"
|
|
140
|
-
expect = "fail"
|
|
141
|
-
fixture = { file = ".claude/skills/x/SKILL.md", frontmatter = { description = "d" } }
|
|
@@ -39,6 +39,7 @@ default = 30000
|
|
|
39
39
|
id = "gates-links-resolve"
|
|
40
40
|
kind = "declared"
|
|
41
41
|
engine = "link-integrity"
|
|
42
|
+
applicability = "repo-content"
|
|
42
43
|
table = "gates/structural.toml"
|
|
43
44
|
tier = "fast"
|
|
44
45
|
why = "A relative link that stopped resolving is the cheapest possible signal that a document moved and its citers did not."
|
|
@@ -47,6 +48,7 @@ why = "A relative link that stopped resolving is the cheapest possible signal
|
|
|
47
48
|
id = "gates-paths-exist"
|
|
48
49
|
kind = "declared"
|
|
49
50
|
engine = "link-integrity"
|
|
51
|
+
applicability = "repo-content"
|
|
50
52
|
table = "gates/structural.toml"
|
|
51
53
|
tier = "fast"
|
|
52
54
|
why = "Instruction files name paths. hexguard's seven instruction files describe workspace registration and CI wiring across 105 packages, and nothing detected a moved path."
|
|
@@ -55,6 +57,7 @@ why = "Instruction files name paths. hexguard's seven instruction files descr
|
|
|
55
57
|
id = "gates-frontmatter-valid"
|
|
56
58
|
kind = "declared"
|
|
57
59
|
engine = "frontmatter-schema"
|
|
60
|
+
applicability = "our-schema"
|
|
58
61
|
table = "gates/structural.toml"
|
|
59
62
|
tier = "fast"
|
|
60
63
|
why = "A skill with malformed frontmatter does not fail; it silently never loads."
|
|
@@ -63,6 +66,7 @@ why = "A skill with malformed frontmatter does not fail; it silently never lo
|
|
|
63
66
|
id = "gates-rules-declare-enforcement"
|
|
64
67
|
kind = "declared"
|
|
65
68
|
engine = "frontmatter-schema"
|
|
69
|
+
applicability = "our-schema"
|
|
66
70
|
table = "gates/structural.toml"
|
|
67
71
|
tier = "fast"
|
|
68
72
|
why = """
|
|
@@ -75,6 +79,7 @@ not check, and all four decayed — including inside the file that states the ru
|
|
|
75
79
|
id = "gates-self-tests-both-directions"
|
|
76
80
|
kind = "declared"
|
|
77
81
|
engine = "gate-meta"
|
|
82
|
+
applicability = "our-artifacts"
|
|
78
83
|
table = "gates/structural.toml"
|
|
79
84
|
tier = "fast"
|
|
80
85
|
why = """
|
|
@@ -45,6 +45,7 @@ default = 200
|
|
|
45
45
|
id = "instructions-core-size"
|
|
46
46
|
kind = "declared"
|
|
47
47
|
engine = "file-budget"
|
|
48
|
+
applicability = "repo-content"
|
|
48
49
|
table = "gates/core.toml"
|
|
49
50
|
tier = "fast"
|
|
50
51
|
why = "rift-forge's entry document reached 1513 lines and every session read all of it, in the one repo that otherwise measured everything."
|
|
@@ -53,6 +54,7 @@ why = "rift-forge's entry document reached 1513 lines and every session read
|
|
|
53
54
|
id = "instructions-render-current"
|
|
54
55
|
kind = "declared"
|
|
55
56
|
engine = "render-freshness"
|
|
57
|
+
applicability = "our-artifacts"
|
|
56
58
|
table = "gates/core.toml"
|
|
57
59
|
tier = "fast"
|
|
58
60
|
why = "A rendered rule that no longer matches its source is a rule the agent is not reading. A green check means 'not yet regenerated', never 'current'."
|
|
@@ -61,6 +63,7 @@ why = "A rendered rule that no longer matches its source is a rule the agent
|
|
|
61
63
|
id = "instructions-repo-map-current"
|
|
62
64
|
kind = "declared"
|
|
63
65
|
engine = "render-freshness"
|
|
66
|
+
applicability = "our-artifacts"
|
|
64
67
|
table = "gates/core.toml"
|
|
65
68
|
tier = "fast"
|
|
66
69
|
why = "Every source repo hand-maintained a repo map and one of them grew five duplicated entries — in the file every session reads first."
|
|
@@ -72,6 +75,7 @@ why = "Every source repo hand-maintained a repo map and one of them grew five
|
|
|
72
75
|
id = "instructions-shell-backticks"
|
|
73
76
|
kind = "declared"
|
|
74
77
|
engine = "shell-safety"
|
|
78
|
+
applicability = "our-schema"
|
|
75
79
|
table = "gates/core.toml"
|
|
76
80
|
trigger = "pre-tool-use"
|
|
77
81
|
matcher = "Bash|PowerShell"
|