@rungs/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +196 -0
  3. package/modules/README.md +116 -0
  4. package/modules/adr/files/{{path}}/README.md +52 -0
  5. package/modules/adr/files/{{path}}/TEMPLATE.md +55 -0
  6. package/modules/adr/fragments/AGENTS.md +8 -0
  7. package/modules/adr/gates/adr.toml +65 -0
  8. package/modules/adr/module.toml +67 -0
  9. package/modules/audit/files/{{criteria_path}} +43 -0
  10. package/modules/audit/fragments/AGENTS.md +8 -0
  11. package/modules/audit/gates/audit.toml +48 -0
  12. package/modules/audit/module.toml +72 -0
  13. package/modules/audit/skills/assess/SKILL.md +62 -0
  14. package/modules/backlog/files/docs/{{root}}/BACKLOG.md +52 -0
  15. package/modules/backlog/files/docs/{{root}}/README.md +110 -0
  16. package/modules/backlog/files/docs/{{root}}/TEMPLATE.md +56 -0
  17. package/modules/backlog/files/docs/{{root}}/archive/README.md +13 -0
  18. package/modules/backlog/files/docs/{{root}}/items/README.md +13 -0
  19. package/modules/backlog/fragments/AGENTS.md +9 -0
  20. package/modules/backlog/gates/ids.toml +106 -0
  21. package/modules/backlog/module.toml +150 -0
  22. package/modules/backlog/rules/work-items.md +44 -0
  23. package/modules/backlog/skills/backlog-summary/SKILL.md +65 -0
  24. package/modules/backlog/skills/work-item/SKILL.md +96 -0
  25. package/modules/ci/files/{{workflow_path}} +43 -0
  26. package/modules/ci/gates/ci.toml +44 -0
  27. package/modules/ci/module.toml +74 -0
  28. package/modules/concurrency/files/docs/concurrent-sessions.md +87 -0
  29. package/modules/concurrency/fragments/AGENTS.md +10 -0
  30. package/modules/concurrency/fragments/gitattributes +15 -0
  31. package/modules/concurrency/gates/concurrency.toml +54 -0
  32. package/modules/concurrency/module.toml +113 -0
  33. package/modules/design-sync/fragments/AGENTS.md +8 -0
  34. package/modules/design-sync/gates/design.toml +70 -0
  35. package/modules/design-sync/module.toml +82 -0
  36. package/modules/design-sync/skills/design-align/SKILL.md +51 -0
  37. package/modules/design-sync/skills/design-pull/SKILL.md +50 -0
  38. package/modules/doc-authority/files/{{registry_path}} +59 -0
  39. package/modules/doc-authority/files/{{rules_path}} +51 -0
  40. package/modules/doc-authority/fragments/AGENTS.md +9 -0
  41. package/modules/doc-authority/gates/authority.toml +127 -0
  42. package/modules/doc-authority/module.toml +111 -0
  43. package/modules/findings/files/docs/{{backlog.root}}/FINDINGS.md +56 -0
  44. package/modules/findings/fragments/AGENTS.md +7 -0
  45. package/modules/findings/gates/findings.toml +68 -0
  46. package/modules/findings/module.toml +100 -0
  47. package/modules/findings/skills/record-finding/SKILL.md +63 -0
  48. package/modules/gates/files/.ai/gates.toml +49 -0
  49. package/modules/gates/fragments/AGENTS.md +8 -0
  50. package/modules/gates/fragments/gitignore +4 -0
  51. package/modules/gates/gates/structural.toml +127 -0
  52. package/modules/gates/module.toml +143 -0
  53. package/modules/gates/skills/harden-rule/SKILL.md +82 -0
  54. package/modules/instructions/files/.ai/rules/README.md +53 -0
  55. package/modules/instructions/files/AGENTS.md +104 -0
  56. package/modules/instructions/files/CLAUDE.md +11 -0
  57. package/modules/instructions/gates/core.toml +114 -0
  58. package/modules/instructions/module.toml +127 -0
  59. package/modules/release/fragments/AGENTS.md +8 -0
  60. package/modules/release/gates/release.toml +85 -0
  61. package/modules/release/module.toml +97 -0
  62. package/modules/release/skills/cut-release/SKILL.md +81 -0
  63. package/modules/session/files/{{archive}}/README.md +28 -0
  64. package/modules/session/files/{{path}} +56 -0
  65. package/modules/session/fragments/AGENTS.md +6 -0
  66. package/modules/session/gates/session.toml +63 -0
  67. package/modules/session/module.toml +72 -0
  68. package/modules/session/skills/close-session/SKILL.md +59 -0
  69. package/modules/skills/fragments/AGENTS.md +8 -0
  70. package/modules/skills/gates/skills.toml +94 -0
  71. package/modules/skills/module.toml +89 -0
  72. package/modules/skills/rules/skill-authoring.md +71 -0
  73. package/modules/specs/files/{{path}}/README.md +63 -0
  74. package/modules/specs/files/{{path}}/TEMPLATE.md +62 -0
  75. package/modules/specs/fragments/AGENTS.md +8 -0
  76. package/modules/specs/gates/specs.toml +119 -0
  77. package/modules/specs/module.toml +107 -0
  78. package/modules/workflows/fragments/AGENTS.md +8 -0
  79. package/modules/workflows/gates/workflows.toml +65 -0
  80. package/modules/workflows/module.toml +75 -0
  81. package/modules/workflows/rules/planning-tiers.md +46 -0
  82. package/modules/workflows/rules/reuse-decision.md +50 -0
  83. package/modules/workflows/skills/decompose/SKILL.md +75 -0
  84. package/package.json +42 -0
  85. package/src/add.ts +331 -0
  86. package/src/check.ts +216 -0
  87. package/src/cli.ts +411 -0
  88. package/src/detect.ts +246 -0
  89. package/src/engines.ts +260 -0
  90. package/src/engines2.ts +297 -0
  91. package/src/engines3.ts +194 -0
  92. package/src/glob.ts +101 -0
  93. package/src/lifecycle.ts +268 -0
  94. package/src/manifest.ts +108 -0
  95. package/src/render.ts +222 -0
  96. package/src/substitute.ts +78 -0
  97. package/src/types.ts +110 -0
@@ -0,0 +1,150 @@
1
+ [module]
2
+ name = "backlog"
3
+ version = "1.0.0"
4
+ rung = 1
5
+ summary = "Work items with stable ids, an eight-status lifecycle, a board, and the three gates that keep the bookkeeping honest."
6
+
7
+ [requires]
8
+ modules = ["instructions"]
9
+
10
+ [conflicts]
11
+ modules = []
12
+
13
+ # ── parameters ────────────────────────────────────────────────────────────────
14
+ # Only things that substitute cleanly into prose and paths are parameters.
15
+ # The eight-status lifecycle is deliberately NOT one: a status set cannot be
16
+ # substituted into a table, a state diagram and a gate without a template
17
+ # language, and ADR-0003 chose not to have one. A repo that wants different
18
+ # statuses edits the file, and `add` records the divergence (ADR-0004 state 3).
19
+
20
+ [params.id_prefix]
21
+ description = "Prefix for work-item ids, e.g. WI-042"
22
+ default = "WI"
23
+ pattern = "^[A-Z]{1,6}$"
24
+
25
+ [params.root]
26
+ description = "Directory under docs/ that holds the backlog"
27
+ default = "backlog"
28
+
29
+ [params.integration_branch]
30
+ description = "Branch that item branches are cut from and merged back into"
31
+ default = "main"
32
+
33
+ [params.branch_prefix]
34
+ description = "Prefix for a feature item's branch"
35
+ default = "feature"
36
+
37
+ # ── gates ─────────────────────────────────────────────────────────────────────
38
+ # All three are `declared`: a CLI engine plus the table below. No script is
39
+ # written into the repo (ADR-0002).
40
+
41
+ [[gates]]
42
+ id = "backlog-ids"
43
+ kind = "declared"
44
+ engine = "id-integrity"
45
+ table = "gates/ids.toml"
46
+ tier = "fast"
47
+ why = "An id claimed twice is two items the board shows as one."
48
+
49
+ [[gates]]
50
+ id = "backlog-stale-blocker"
51
+ kind = "declared"
52
+ engine = "id-integrity"
53
+ table = "gates/ids.toml"
54
+ tier = "fast"
55
+ why = """
56
+ A document saying it is blocked on finished work reads as a live constraint, and the next session
57
+ plans around a wall that came down months ago. rift-forge measured 95% of 474 rows naming a
58
+ finished item as next owner, then found those routes quoted in a code comment as evidence.
59
+ """
60
+
61
+ [[gates]]
62
+ id = "backlog-merged-status"
63
+ kind = "declared"
64
+ engine = "git-status-reconcile"
65
+ table = "gates/ids.toml"
66
+ tier = "fast"
67
+ why = """
68
+ An item whose branch is merged cannot still be on its way to review. Measured 2026-08-13 in
69
+ rift-forge: 37 items sat at `review` with their code already landed, and one sat `in_progress`
70
+ for eight days after its own merge.
71
+ """
72
+
73
+ # ── skills ────────────────────────────────────────────────────────────────────
74
+ # SKILL.md files stay restricted to the six Agent Skills spec fields (ADR-0001).
75
+ # Claude Code extensions are opted into here, per skill, with the cost stated.
76
+
77
+ [skills.work-item]
78
+ extensions = { disable-model-invocation = true }
79
+ extension_note = """
80
+ `work-item` creates branches and merges. A model should not decide on its own that now is the
81
+ moment. Opting in means this skill no longer packages for claude.ai uploads or the Skills API,
82
+ which reject non-spec keys with a hard error; it remains portable to every skills-compatible CLI.
83
+ """
84
+
85
+ [skills.backlog-summary]
86
+ # No extensions. Read-only, and useful for a model to reach for unprompted.
87
+
88
+ # ── detection (ADR-0004) ──────────────────────────────────────────────────────
89
+ # Presence is decided by `paths` and `markers` only. Biased toward false
90
+ # negatives: when uncertain, fail to match.
91
+
92
+ [detect]
93
+ paths = [
94
+ "docs/*/items/**/*.md",
95
+ "docs/*/BACKLOG.md",
96
+ "docs/*/TEMPLATE.md",
97
+ "docs/.ai/backlog/**/*.md",
98
+ ]
99
+ markers = ["rungs:begin backlog"]
100
+
101
+ # A different system serving the same function. Never auto-adopted; `add` prints
102
+ # the comparison and stops (ADR-0004 state 5).
103
+ [[detect.paradigm]]
104
+ id = "milestones"
105
+ paths = ["docs/**/milestones/index.md", ".ai/context/session.md"]
106
+ compare = "docs/research/synthesis.md#33-unit-of-work"
107
+ note = """
108
+ Milestones (`M##-T#`) suit long sequenced programmes; work items suit continuous flow. They can
109
+ coexist — a milestone overlay sits on top of items. Three real options: adopt milestones as the
110
+ unit and install only the missing gates, install items alongside, or skip.
111
+ """
112
+
113
+ # Proposes a parameter once presence is already established. Never concludes
114
+ # presence — hexguard-templates has 207 well-formed `FOUND-US-###` matches and
115
+ # no backlog; they are spec story ids.
116
+ [[detect.infer]]
117
+ param = "id_prefix"
118
+ # The lookbehind is load-bearing and was added after a real false positive:
119
+ # `\b([A-Z]{1,6})-\d` matches `US-110` inside `FOUND-US-110`, because `-` to `U`
120
+ # is a word boundary. On hexguard-templates that proposed `US` — a spec story
121
+ # id — as the backlog prefix, which is precisely the failure ADR-0004 was
122
+ # written about, arriving through the inference path instead of the presence one.
123
+ pattern = "(?<![A-Za-z0-9-])([A-Z]{1,6})-\\d{1,4}\\b"
124
+ min = 20
125
+ scope = ["docs/**/*.md"]
126
+ exclude = ["docs/specs/**", "docs/decisions/**", "docs/adr/**"]
127
+ # Encodings, hashes and standards share the id shape. On hexguard the top
128
+ # proposal was `UTF` at 32 matches, from `UTF-8`.
129
+ exclude_values = ["UTF", "SHA", "ISO", "RFC", "MD", "AES", "RSA", "HTTP", "IPV", "X"]
130
+
131
+ # ── provenance (required, validated) ──────────────────────────────────────────
132
+
133
+ [provenance]
134
+ sources = ["rift-forge", "axiom-mesh"]
135
+ patterns = [
136
+ "work-item-lifecycle",
137
+ "item-template-required-fields",
138
+ "branch-per-item",
139
+ "planning-rides-trunk",
140
+ "scope-discipline",
141
+ "epics-and-subitems",
142
+ "bookkeeping-gates",
143
+ ]
144
+ incident = """
145
+ Two of the four source repos had no unit of work at all, and both show the same consequence:
146
+ hexguard produced 268 audit reports that could not become anything, because no object existed for
147
+ a finding to turn into; hexguard-templates cannot say what is in flight. rift-forge, which has the
148
+ object, needed two gates to keep its status fields honest — 37 items were found claiming `review`
149
+ with their code already on the integration branch.
150
+ """
@@ -0,0 +1,44 @@
1
+ ---
2
+ description: >-
3
+ How to read and change a work item — required sections, status honesty, and the scope rule.
4
+ Loads when an agent touches the backlog.
5
+ paths:
6
+ - "docs/{{root}}/**/*.md"
7
+ enforcement: gated
8
+ ---
9
+
10
+ # Working on a work item
11
+
12
+ **Authority:** [`docs/{{root}}/README.md`](../../docs/{{root}}/README.md). This rule is the
13
+ short form an agent meets while editing; the reasoning lives there.
14
+
15
+ ## Before changing an item
16
+
17
+ - **Re-derive the state; do not quote the board.** A status line, a board row or a triage is
18
+ bookkeeping *about* the work, not the work. Check the artifact the claim is about — the branch,
19
+ the tests, the file — before acting on what a field says.
20
+ - **The template's sections are required, not suggested.** `Out of scope` and
21
+ `Acceptance criteria` may be answered with an explicit "none, because…" but may not be left
22
+ blank or deleted. A blank one is an unfinished plan.
23
+
24
+ ## While executing
25
+
26
+ - **Never scope-creep.** Work discovered mid-flight becomes a **new item**; a mere observation
27
+ becomes a **finding**. An item that grew a second purpose should have been two.
28
+ - **Deviations from the plan go in `## Execution`, with the reason.** A plan that silently stopped
29
+ matching what was built is worse than no plan, because it reads as verified.
30
+
31
+ ## When it lands
32
+
33
+ - **Move the status in the same change that merges the branch.** `backlog-merged-status` refuses a
34
+ merged branch sitting at a pre-review status — measured elsewhere at 37 items, one of them eight
35
+ days stale.
36
+ - **Never write that something is blocked on finished work.** `backlog-stale-blocker` refuses it:
37
+ it reads as a live constraint and the next session plans around a wall that came down months ago.
38
+ Past tense ("was blocked on") is a record and is allowed.
39
+
40
+ ## Escape hatches
41
+
42
+ Both gates take an exemption **that must state a reason** — `branch-merged-ok: <why>` and
43
+ `owner-ok: <why>`. A marker with no reason is ignored: an escape hatch nobody has to justify is not
44
+ an escape hatch, it is an off switch.
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: backlog-summary
3
+ description: >-
4
+ Summarize the backlog: open work items grouped by category, the highest-value items with the
5
+ reasoning, stale or contradictory bookkeeping, and workflow-health observations with concrete
6
+ suggestions. Use whenever asked to "summarize the backlog", "what's open", "backlog status /
7
+ report / health", "rank the backlog", "what should we do next", or for any prioritization of
8
+ open work — even when the word "backlog" is not used but the question is about what to build
9
+ next. Read-only: it reports, it never changes an item. Executing one item is /work-item.
10
+ ---
11
+
12
+ # Summarize the backlog
13
+
14
+ Read-only. **This skill never edits an item, a status, or the board.**
15
+
16
+ ## 1. Read from the current tip, not a copy
17
+
18
+ Run against `{{integration_branch}}` as it is now. A worktree copy or a cached report goes stale,
19
+ and a summary built on one is confidently wrong about what is open.
20
+
21
+ ## 2. Gather
22
+
23
+ Read every file in `docs/{{root}}/items/`, plus `BACKLOG.md`. Do **not** trust the board's grouping
24
+ — derive status from the item files and note any disagreement, which is itself a finding worth
25
+ reporting.
26
+
27
+ ## 3. Report
28
+
29
+ ### Open work, by category
30
+
31
+ Group by the natural categories of this repo (its subsystems, surfaces, or domains — not by
32
+ status). For each: count, and the items with a one-line summary each.
33
+
34
+ ### Highest-value items, with the reasoning
35
+
36
+ Rank the top few, and **state why each ranks where it does** — value, cost, what it unblocks, what
37
+ it stops costing. A ranking without reasoning is an opinion wearing a number.
38
+
39
+ ### Bookkeeping that disagrees with reality
40
+
41
+ Report, do not fix:
42
+
43
+ - items whose `status` and merged branch disagree
44
+ - items `in_progress` with no branch, or with a branch that no longer exists
45
+ - documents claiming they wait on work that has finished
46
+ - epics whose `children` links are not reciprocated
47
+
48
+ ### Workflow health
49
+
50
+ Observations about the *process*, with a concrete suggestion each:
51
+
52
+ - items sitting in one status for a long time
53
+ - items whose acceptance criteria are blank while their status is past `planned`
54
+ - how much work arrives as items versus as unrecorded changes
55
+ - whether `items/` has outgrown readability and wants archiving
56
+
57
+ ## 4. What this cannot tell you
58
+
59
+ State these limits in the output rather than leaving the reader to infer them:
60
+
61
+ - it measures **what is recorded**, not what is happening — work done without an item is invisible
62
+ - a ranking is a judgement, and the reasoning is there so it can be argued with
63
+ - an empty result and an unread backlog produce the same summary, so say how many files were read
64
+
65
+ A report that cannot fail loudly is not a measurement.
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: work-item
3
+ description: >-
4
+ Execute a backlog work item ({{id_prefix}}-###) end to end through its lifecycle: claim or open
5
+ the item, branch, plan, implement, test, review, merge, and move the status. Use when asked to
6
+ "work on / tackle / start / do / ship {{id_prefix}}-###", to pick up the next item from the
7
+ backlog, or to work an epic's sub-items. Also use to open a new item from a request that turns
8
+ out to be real work rather than a quick fix. An out-of-scope observation is a finding, not an
9
+ item — that is /record-finding. Shipping a version is /cut-release. This skill is one item,
10
+ start to finish.
11
+ ---
12
+
13
+ # Execute a work item
14
+
15
+ **Authority:** [`docs/{{root}}/README.md`](../../../docs/{{root}}/README.md). Read it once per
16
+ session before the first item; this skill is the execution form, not the reasoning.
17
+
18
+ ## 0. Establish the item
19
+
20
+ - Given an id → open `docs/{{root}}/items/{{id_prefix}}-###-*.md`.
21
+ - Given a request with no id → decide first **whether this is an item at all**: a one-line fix with
22
+ no design choice is just a change; an observation you are not acting on now is a **finding**. If
23
+ it is an item, claim the next id from the `NEXT-ID` marker in `BACKLOG.md` and **bump the marker
24
+ on your own branch**, then copy `TEMPLATE.md`.
25
+ - Given "the next item" → read the board, but **re-derive before committing to it**. A board row is
26
+ bookkeeping about the work, not the work; scoping from a stale row is how work that is already
27
+ done gets started again.
28
+
29
+ ## 1. Check the status is real
30
+
31
+ Before doing anything, confirm the item's `status` and `branch` agree with git. If the branch is
32
+ merged and the status is pre-review, the work landed and the field lied — fix the field, say so in
33
+ `## Execution`, and stop. That is the whole task.
34
+
35
+ ## 2. Plan, if it is not planned
36
+
37
+ An `accepted` item needs its plan filled before code: **requirements · impacts · approach ·
38
+ acceptance criteria · out of scope**. None may be blank.
39
+
40
+ - Acceptance criteria must be **checkable**. "Works correctly" is not a criterion; "given X, the
41
+ endpoint returns 422 with a typed error" is.
42
+ - `Out of scope` is required — answer it or write `nothing deferred`. It is the section that stops
43
+ an item quietly growing a second purpose.
44
+ - A significant design choice gets an ADR. Link it; do not restate it here.
45
+
46
+ Status → `planned`.
47
+
48
+ ## 3. Branch and execute
49
+
50
+ ```bash
51
+ git switch -c {{branch_prefix}}/{{id_prefix}}-###-slug {{integration_branch}}
52
+ ```
53
+
54
+ Status → `in_progress`.
55
+
56
+ - **Follow the repo's engineering guides**, not this skill, for how the code should look.
57
+ - **Work discovered mid-flight does not join this item.** A new item, or a finding. Record it and
58
+ carry on — do not widen the branch.
59
+ - Deviations from the plan go in `## Execution` **with the reason**, as you make them. A plan that
60
+ silently stopped matching what was built reads as verified and is not.
61
+
62
+ ## 4. Test
63
+
64
+ Run the narrowest validation that covers what you touched, then the repo's standard gate set:
65
+
66
+ ```bash
67
+ rungs check
68
+ ```
69
+
70
+ Existing tests are not weakened to make a change pass. If a test is genuinely wrong, that is its
71
+ own item with its own reasoning.
72
+
73
+ ## 5. Review
74
+
75
+ Walk **each** acceptance criterion and write what you checked in `## Review`. A criterion you
76
+ cannot demonstrate is not met — leave it open and say so rather than marking the item done.
77
+
78
+ Status → `review`.
79
+
80
+ ## 6. Land
81
+
82
+ Merge into `{{integration_branch}}`, **move the status to `done` in the same change**, then delete
83
+ the branch. In that order: the merged-status gate can only see items whose branch still exists, so
84
+ deleting first costs you the check.
85
+
86
+ Update the board: remove the row from **In progress**, and archive the item if the repo archives on
87
+ completion.
88
+
89
+ ## When to stop and ask
90
+
91
+ - The plan cannot be written because the requirement is ambiguous in a way that changes the work.
92
+ - Execution reveals the approach is wrong — reopen the plan rather than improvising past it.
93
+ - Acceptance criteria conflict with something already shipped.
94
+
95
+ In each case, write what you found in the item first. The next session reads the item, not this
96
+ conversation.
@@ -0,0 +1,43 @@
1
+ # Gate run. Generated by `rungs`; the trigger block is regenerated from the `trigger` parameter.
2
+ name: checks
3
+
4
+ on:
5
+ # rungs:begin ci-triggers
6
+ push:
7
+ branches: ['**']
8
+ pull_request:
9
+ # rungs:end ci-triggers
10
+
11
+ concurrency:
12
+ # One run per ref. A superseded run tells you about a tree nobody has any more.
13
+ group: checks-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ gates:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ with:
22
+ # Gates that compare against the merge base need history. Cheaper than
23
+ # discovering mid-run that a gate cannot see what it is attributing against.
24
+ fetch-depth: 0
25
+
26
+ - uses: actions/setup-node@v4
27
+ with:
28
+ node-version: '22'
29
+
30
+ # Runs every gate in .ai/gates.toml — the same set, in the same order, as
31
+ # `rungs check` locally. A CI-only gate is one nobody can reproduce.
32
+ - name: Run gates
33
+ run: npx rungs check --tier full --reporter github
34
+
35
+ # Add a matrix job per package here rather than a workflow per package.
36
+ # A checklist step that creates a file creates N files:
37
+ #
38
+ # strategy:
39
+ # matrix:
40
+ # package: [a, b, c]
41
+ #
42
+ # `rungs check` reports the workflow count and fails past the threshold in
43
+ # .ai/gates.toml, for exactly that reason.
@@ -0,0 +1,44 @@
1
+ # Gate table for the `ci` module.
2
+
3
+ # ── ci-workflow-proliferation ─────────────────────────────────────────────────
4
+ #
5
+ # Counts near-identical workflow files. Similarity is structural — job names, step commands, and
6
+ # `uses:` refs — not textual, because near-identical files differ exactly in the package name.
7
+
8
+ [file_population]
9
+ scan = [".github/workflows/*.yml", ".github/workflows/*.yaml"]
10
+ similarity_on = ["job_ids", "step_uses", "step_run_shape"]
11
+ similarity_min = 0.85
12
+ warn_at = 4
13
+ fail_at = 8
14
+ exempt_marker = "proliferation-ok:" # ignored unless it states a reason
15
+ message = """
16
+ {count} workflows share ~{similarity} of their structure. This is the shape that reached 98 files
17
+ in one repo, where changing release policy became a 98-file edit. Replace them with one workflow
18
+ and a `strategy.matrix`, or state why not with `# proliferation-ok: <reason>`.
19
+ """
20
+
21
+ # The threshold is deliberately low. A repo with 8 genuinely different workflows can exempt them
22
+ # once; a repo generating one per package hits it at the point the pattern is still cheap to undo.
23
+
24
+ # ── self-tests ────────────────────────────────────────────────────────────────
25
+
26
+ [[self_test]]
27
+ gate = "ci-workflow-proliferation"
28
+ expect = "fail"
29
+ fixture = { workflows = 12, similarity = 0.97 }
30
+
31
+ [[self_test]]
32
+ gate = "ci-workflow-proliferation"
33
+ expect = "pass"
34
+ fixture = { workflows = 12, similarity = 0.30 }
35
+
36
+ [[self_test]]
37
+ gate = "ci-workflow-proliferation"
38
+ expect = "pass"
39
+ fixture = { workflows = 12, similarity = 0.97, exempt = "proliferation-ok: per-runtime, they differ in the container image" }
40
+
41
+ [[self_test]]
42
+ gate = "ci-workflow-proliferation"
43
+ expect = "fail"
44
+ fixture = { workflows = 12, similarity = 0.97, exempt = "proliferation-ok:" }
@@ -0,0 +1,74 @@
1
+ [module]
2
+ name = "ci"
3
+ version = "1.0.0"
4
+ rung = 1
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
+
7
+ [requires]
8
+ modules = ["gates"]
9
+
10
+ [conflicts]
11
+ modules = []
12
+
13
+ # Rung 1, but bundled in the `disciplined` profile rather than `tracked`: rung is
14
+ # how mature the practice is, a profile is which problem you have. A solo repo
15
+ # can be at rung 1 without needing CI at all.
16
+
17
+ [params.provider]
18
+ description = "CI provider. Only `github` is implemented."
19
+ default = "github"
20
+ allowed = ["github"]
21
+ consumed_by = "render"
22
+
23
+ [params.trigger]
24
+ description = """
25
+ When the gate run fires. `push` runs on every push to any branch. `land` runs only on the
26
+ integration branch and on merge refs, which is what a repo with billed minutes and many concurrent
27
+ sessions wants — rift-forge measured ~19 billed minutes per run, and one run per push across ~15
28
+ sessions is 400-1000 minutes a day.
29
+ """
30
+ default = "push"
31
+ allowed = ["push", "land"]
32
+ consumed_by = "render"
33
+ # Reaches file content through the `ci-triggers` managed block, not through a
34
+ # conditional. That is the general mechanism: a behavioural parameter changes
35
+ # what the renderer writes into a block, and substitution-only stays intact.
36
+
37
+ [params.workflow_path]
38
+ description = "Where the workflow file lands"
39
+ default = ".github/workflows/checks.yml"
40
+
41
+ [[gates]]
42
+ id = "ci-workflow-proliferation"
43
+ kind = "declared"
44
+ engine = "file-population"
45
+ table = "gates/ci.toml"
46
+ tier = "fast"
47
+ why = """
48
+ hexguard has 98 near-identical per-package release workflows because a phase checklist told the
49
+ agent to add one each time. Changing release policy there is a 98-file edit and the workflow
50
+ directory is unreadable. A checklist step that creates a file creates N files, and nobody notices
51
+ until N is large — so the count is checked.
52
+ """
53
+
54
+ [detect]
55
+ paths = [".github/workflows/*.yml", ".github/workflows/*.yaml", ".gitlab-ci.yml", "azure-pipelines.yml"]
56
+ markers = ["rungs:begin ci"]
57
+
58
+ [[detect.adopt_as]]
59
+ kind = "existing-workflows"
60
+ note = """
61
+ A repo with workflows has CI; it does not have this registry-driven one. Adopt means: leave every
62
+ existing workflow alone, add one that runs `rungs check`, and report the proliferation count so the
63
+ owner can see what they have. Never rewrite or delete a workflow — that is somebody's deploy.
64
+ """
65
+
66
+ [provenance]
67
+ sources = ["hexguard", "rift-forge"]
68
+ patterns = ["matrix-not-per-item-ci", "workflow-proliferation-check", "ci-at-land-time"]
69
+ incident = """
70
+ hexguard's .github/workflows/ holds 99 files: one CI workflow and 98 per-package release
71
+ workflows, near-identical, hand-maintained, emitted one at a time by Phase 3 of a mandatory
72
+ checklist. rift-forge went the other way and made CI cost-aware — item branches are deliberately
73
+ not triggers, because the repository is private and one run is ~19 billed minutes.
74
+ """
@@ -0,0 +1,87 @@
1
+ **Authoritative for:** the loop many sessions share: where to branch from, when to verify, how to land, and which conflicts are the tooling’s problem.
2
+ **Not authoritative for:** the backlog lifecycle those branches carry, and the gates the loop runs.
3
+
4
+ # Concurrent sessions
5
+
6
+ **Read the threshold first.** Below roughly five sessions working at once on one integration
7
+ branch, everything here costs more than it returns. It is a real tooling surface with its own
8
+ failure modes.
9
+
10
+ Every rule assumes one thing: **many sessions work at once, in separate worktrees, off one shared
11
+ branch, and they cannot see each other's work.**
12
+
13
+ ## The loop
14
+
15
+ | | |
16
+ | --- | --- |
17
+ | `rungs session start <branch>` | Cuts from `{{green_prefix}}<branch>` — the last merge that was actually verified — not the tip. Falls back to the tip and **says so** |
18
+ | `rungs check` | The fast tier. Run it constantly |
19
+ | `rungs preflight` | The integration branch moved: did it change files *you* changed? That, not the commit count, predicts a conflict |
20
+ | `rungs land <branch>` | merge → verify **the merged tree** → fast-forward → move the green ref |
21
+ | `rungs worktrees` | What is finished and prunable. **Reports only** — removing someone else's worktree is not a script's call |
22
+
23
+ ## Do not run the full tier before landing
24
+
25
+ `land` runs it on the merged tree itself. A separate full run beforehand proves nothing land will
26
+ not prove — and it is not merely wasted time: **the integration branch moves while it runs**, so
27
+ the pre-verify is what widens the window the merge then conflicts in.
28
+
29
+ Measured in one session: three of five land attempts refused, every one after a 4–12 minute
30
+ pre-verify, every one on the same generated artifacts. The attempt that landed did merge → resolve
31
+ → regenerate → land with nothing in between.
32
+
33
+ So: **fast tier constantly, full tier at the boundary.** Reach for it early only to answer a
34
+ deliberate question, never as a ritual.
35
+
36
+ ## A failure is attributed, never just counted
37
+
38
+ `check` re-runs each failing gate against the merge base in a throwaway worktree and reports it as
39
+ **inherited** (already red before you started — stated, never blocking) or **INTRODUCED** (yours —
40
+ blocks). Anything it cannot attribute also blocks: **we do not land on an unknown.**
41
+
42
+ This is not politeness about blame. *A gate that is red for reasons you did not cause and cannot
43
+ fix is a gate you learn to bypass, and a bypassed gate reports nothing.*
44
+
45
+ > **Attribution makes a red gate survivable, which also removes the pressure to fix it.** One repo
46
+ > ran 11 of its last 15 CI runs red on two permanently-broken jobs. The ledger's ageing signal
47
+ > exists for exactly this: fix them or delete them.
48
+
49
+ ## Land, then move the tip — in that order
50
+
51
+ `land` merges into a scratch `{{integ_prefix}}…` ref, verifies **that** tree, and only then
52
+ fast-forwards with a compare-and-swap. Two things follow:
53
+
54
+ - **The integration branch cannot go red from a merge nobody verified** — the ref update is
55
+ unreachable otherwise, and a refusal leaves it bit-for-bit unchanged with the merged tree parked
56
+ for you to fix.
57
+ - **Concurrent landing is refused, not silently merged.** A real lock names its holder and start
58
+ time and is taken over if the holder died.
59
+
60
+ **Nothing keeps the integration branch checked out.** If something needs it checked out, that is a
61
+ bug in whatever it is doing.
62
+
63
+ ## Three things to know when a land refuses
64
+
65
+ - **Your worktree is left detached.** Your branch and commits are safe; the files in front of you
66
+ are simply not yours. Switch back and carry on. Do not "restore" anything.
67
+ - **Check the exit code of `land` itself.** Piping it through `tail` or `grep` reports *that*
68
+ command's status, so a refused land reads as success.
69
+ - **Reconcile generated artifacts by regenerating, never by merging text.** Take one side, re-run
70
+ the producer, and re-pin what moved with the reason at the pin.
71
+
72
+ ## What conflicts, and which of it is the tooling's problem
73
+
74
+ | Class | Handling |
75
+ | --- | --- |
76
+ | **Id ledgers** — boards, registers, indexes | The driver takes the higher `NEXT-ID` and keeps both claim comments. **It resolves nothing else** — a row that moved section is handed back, because a union merge would keep both copies of it |
77
+ | **Generated artifacts** | The driver **always refuses** and prints the regenerate command |
78
+ | **Shared code** | **Not a tooling problem — a scheduling one.** Give a hot surface one owner at a time and batch same-surface work behind one session. `preflight` tells you when you are about to collide |
79
+
80
+ That third row is the honest one: it names the class this will not automate and prescribes
81
+ scheduling instead.
82
+
83
+ ## Worktree lifetime is item lifetime
84
+
85
+ Delete the branch when it lands. `rungs worktrees` measures whether that happened and flags
86
+ worktrees that are merged and clean — and, more usefully, ones that are merged **and dirty**, where
87
+ uncommitted work is sitting in a branch that has already landed.
@@ -0,0 +1,10 @@
1
+ <!-- rungs:begin concurrency@1.0.0 -->
2
+ ## Concurrent sessions
3
+
4
+ Many sessions share `{{integration_branch}}` and cannot see each other — see
5
+ [`docs/concurrent-sessions.md`](docs/concurrent-sessions.md). Cut with `rungs session start`
6
+ (from the last **verified** merge, not the tip), run the fast tier constantly, and land with
7
+ `rungs land` — **never `git merge` by hand**. Do not run the full tier before landing: it widens
8
+ the window the merge then conflicts in. Reconcile generated artifacts by **regenerating**, never
9
+ by merging text.
10
+ <!-- rungs:end concurrency -->
@@ -0,0 +1,15 @@
1
+ # rungs:begin concurrency@1.0.0
2
+ # Merge drivers per conflict class. INERT until `rungs setup git` installs them — the driver names
3
+ # below mean nothing to git on a fresh clone, and merges quietly fall back to the default. For a
4
+ # generated artifact that default is a clean text merge of a file that is a fixed point of nothing.
5
+
6
+ # Id ledgers: take the higher NEXT-ID counter, keep both claim comments, resolve nothing else.
7
+ docs/**/BACKLOG.md merge=rungs-ledger
8
+ docs/**/FINDINGS.md merge=rungs-ledger
9
+
10
+ # Generated artifacts: always refuse, and print the regenerate command.
11
+ .claude/rules/** merge=rungs-generated
12
+ .github/instructions/** merge=rungs-generated
13
+ .cursor/rules/** merge=rungs-generated
14
+ .ai/render-report.md merge=rungs-generated
15
+ # rungs:end concurrency