@rungs/cli 0.1.3 → 0.2.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 +45 -8
- package/dist/cli.js +883 -127
- 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/module.toml +1 -0
- package/modules/concurrency/module.toml +2 -0
- 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 +43 -0
- 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 +182 -0
- package/src/check.ts +47 -4
- package/src/cli.ts +297 -27
- package/src/engines.ts +261 -13
- package/src/engines2.ts +69 -3
- 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 +221 -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.
|
package/modules/ci/module.toml
CHANGED
|
@@ -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"
|
|
@@ -47,6 +47,25 @@ uncovered = [
|
|
|
47
47
|
]
|
|
48
48
|
note = "A green check means these files agree, not that the version is correct everywhere."
|
|
49
49
|
|
|
50
|
+
# ── release-fragment-current ──────────────────────────────────────────────────
|
|
51
|
+
#
|
|
52
|
+
# The other half of the fragment discipline. `release-changelog-fragment` catches a release-worthy
|
|
53
|
+
# change that added no fragment; this catches a fragment that was consumed and never deleted.
|
|
54
|
+
#
|
|
55
|
+
# Only filenames that parse as a version are judged. A repo whose fragments are named per-change
|
|
56
|
+
# (`42.feature.md`) states no version, so there is nothing to compare and nothing is claimed — a
|
|
57
|
+
# gate that refused a naming convention it was not asked about is one people turn off.
|
|
58
|
+
|
|
59
|
+
[[changelog_freshness]]
|
|
60
|
+
id = "release-fragment-current"
|
|
61
|
+
fragments = ["{{changelog_dir}}/*.md"]
|
|
62
|
+
version = { file = "package.json", path = "version" }
|
|
63
|
+
message = """
|
|
64
|
+
This fragment names a version below the one being prepared, so it belongs to a release that already
|
|
65
|
+
shipped. Delete it — fragments are consumed, not archived, and one left behind is republished as
|
|
66
|
+
though it were new work.
|
|
67
|
+
"""
|
|
68
|
+
|
|
50
69
|
# ── self-tests ────────────────────────────────────────────────────────────────
|
|
51
70
|
|
|
52
71
|
[[self_test]]
|
|
@@ -74,6 +93,30 @@ gate = "release-changelog-fragment"
|
|
|
74
93
|
expect = "fail"
|
|
75
94
|
fixture = { changed = ["src/a.ts"], fragments = [], exempt = "changelog-ok:" }
|
|
76
95
|
|
|
96
|
+
[[self_test]]
|
|
97
|
+
gate = "release-fragment-current"
|
|
98
|
+
expect = "fail"
|
|
99
|
+
fixture = { dir = "changelog.d", version = "0.2.0", fragments = ["0.1.1.md"] }
|
|
100
|
+
|
|
101
|
+
[[self_test]]
|
|
102
|
+
gate = "release-fragment-current"
|
|
103
|
+
expect = "pass"
|
|
104
|
+
fixture = { dir = "changelog.d", version = "0.2.0", fragments = ["0.2.0.md"] }
|
|
105
|
+
|
|
106
|
+
# A fragment for a version *above* the one being prepared is not stale — it is a branch that
|
|
107
|
+
# already decided it is a minor while the package has not been bumped yet.
|
|
108
|
+
[[self_test]]
|
|
109
|
+
gate = "release-fragment-current"
|
|
110
|
+
expect = "pass"
|
|
111
|
+
fixture = { dir = "changelog.d", version = "0.2.0", fragments = ["0.3.0.md"] }
|
|
112
|
+
|
|
113
|
+
# Per-change fragment naming states no version, so there is nothing to compare and nothing is
|
|
114
|
+
# claimed. The gate must stay silent rather than refuse a convention it was not asked about.
|
|
115
|
+
[[self_test]]
|
|
116
|
+
gate = "release-fragment-current"
|
|
117
|
+
expect = "pass"
|
|
118
|
+
fixture = { dir = "changelog.d", version = "0.2.0", fragments = ["42.feature.md"] }
|
|
119
|
+
|
|
77
120
|
[[self_test]]
|
|
78
121
|
gate = "release-version-consistent"
|
|
79
122
|
expect = "fail"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[module]
|
|
2
2
|
name = "release"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.1.0"
|
|
4
4
|
rung = 3
|
|
5
5
|
summary = "Candidate, stable and deploy branch lines; one procedure for cutting a release, hotfixing and rolling back; changelog fragments instead of a shared file."
|
|
6
6
|
|
|
@@ -46,6 +46,7 @@ consumed_by = "render"
|
|
|
46
46
|
id = "release-changelog-fragment"
|
|
47
47
|
kind = "declared"
|
|
48
48
|
engine = "file-population"
|
|
49
|
+
applicability = "repo-content"
|
|
49
50
|
table = "gates/release.toml"
|
|
50
51
|
tier = "fast"
|
|
51
52
|
why = """
|
|
@@ -54,10 +55,24 @@ cut time when nobody remembers what it did. Cheaper to require the fragment on t
|
|
|
54
55
|
the change.
|
|
55
56
|
"""
|
|
56
57
|
|
|
58
|
+
[[gates]]
|
|
59
|
+
id = "release-fragment-current"
|
|
60
|
+
kind = "declared"
|
|
61
|
+
engine = "changelog-freshness"
|
|
62
|
+
applicability = "repo-content"
|
|
63
|
+
table = "gates/release.toml"
|
|
64
|
+
tier = "fast"
|
|
65
|
+
why = """
|
|
66
|
+
Fragments are consumed at release time, not archived — one left behind appears in the next release
|
|
67
|
+
too, where it reads as unreleased work. `cut-release` §3 has said so in prose since it was written,
|
|
68
|
+
and prose did not hold it: this repo's own `0.1.1` fragment survived two releases (F-022).
|
|
69
|
+
"""
|
|
70
|
+
|
|
57
71
|
[[gates]]
|
|
58
72
|
id = "release-version-consistent"
|
|
59
73
|
kind = "declared"
|
|
60
74
|
engine = "computed-claim"
|
|
75
|
+
applicability = "our-schema"
|
|
61
76
|
table = "gates/release.toml"
|
|
62
77
|
tier = "fast"
|
|
63
78
|
why = "A version appears in several files and drifts in exactly one of them. It is derivable, so it is computed rather than typed."
|
|
@@ -22,9 +22,16 @@ mismatch is usually a fragment somebody skipped, not a versioning disagreement.
|
|
|
22
22
|
## 2. Gate
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
rungs check
|
|
25
|
+
rungs check
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
Run **every** registered gate. This said `rungs check --tier full`, which selects only gates
|
|
29
|
+
labelled `full` — on a repo whose gates are all `fast`, that is zero of them, and the run reports
|
|
30
|
+
no gates rather than a pass. A release step that gates on nothing while looking green is worse than
|
|
31
|
+
one that is skipped, because nobody goes back to check it.
|
|
32
|
+
|
|
33
|
+
Narrow to a tier (`rungs check --fast`) only when you know the tier holds what you meant to run.
|
|
34
|
+
|
|
28
35
|
**Do not proceed on a red gate**, and do not weaken one to get through. A release is exactly the
|
|
29
36
|
moment the temptation is highest and the cost of yielding is highest.
|
|
30
37
|
|