@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,127 @@
1
+ # Gate table for the `gates` module — the repo-agnostic structural set.
2
+ #
3
+ # Nothing here knows what a document *means*. Anything that does belongs to the module that owns
4
+ # that document (ADR-0003's declared/command boundary).
5
+
6
+ # ── gates-links-resolve · gates-paths-exist ───────────────────────────────────
7
+
8
+ [link_integrity]
9
+ scan = ["**/*.md", ".ai/**/*.toml"]
10
+ # `modules/**` is a rungs module source tree, not repo content: a fragment's
11
+ # links are relative to the file it merges *into*, not to where it is authored,
12
+ # so checking them where they sit reports every one as broken. Costs a consumer
13
+ # repo nothing — it has no `modules/` — and was found by running this against
14
+ # the rungs repo itself.
15
+ 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
+ exempt = "path-ok:" # ignored unless it states a reason
23
+
24
+ [link_integrity.notes]
25
+ external = "not fetched — a gate that needs the network is a gate that fails on a plane"
26
+
27
+ # ── gates-frontmatter-valid · gates-rules-declare-enforcement ─────────────────
28
+
29
+ [[frontmatter_schema]]
30
+ id = "rules"
31
+ scan = [".ai/rules/**/*.md"]
32
+ exclude = [".ai/rules/README.md"]
33
+ required = ["description", "enforcement"]
34
+ optional = ["paths"]
35
+ enum = { enforcement = ["gated", "review-only"] }
36
+ message = """
37
+ Every rule declares whether something enforces it. `review-only` is a legitimate answer and is
38
+ what most rules honestly are; the failure this catches is the silent third category, where a rule
39
+ reads as mandatory and nothing checks it.
40
+ """
41
+
42
+ # The skill frontmatter schema lived here and moved to the `skills` module, for the same reason
43
+ # `ids-unique` and `generated-current` did: knowing what a SKILL.md may contain is knowledge about
44
+ # skills, not a repo-agnostic structural fact. What `gates` contributes is the `frontmatter-schema`
45
+ # engine that both tables run on.
46
+
47
+ # ── gates-self-tests-both-directions ──────────────────────────────────────────
48
+ #
49
+ # The meta-gate.
50
+
51
+ [gate_meta]
52
+ require_self_tests = true
53
+ require_directions = ["pass", "fail"]
54
+ applies_to = "declared" # a command gate is the repo's own; we do not audit its tests
55
+ message = """
56
+ Declared gate `{id}` has no self-test expecting `{missing}`. A gate whose rules are all currently
57
+ satisfied is indistinguishable from a gate that matches nothing, and the fail case is the half
58
+ that proves it can still fire.
59
+ """
60
+
61
+ # ── self-tests ────────────────────────────────────────────────────────────────
62
+
63
+ [[self_test]]
64
+ gate = "gates-links-resolve"
65
+ expect = "fail"
66
+ input = "See [the plan](./does-not-exist.md)."
67
+
68
+ [[self_test]]
69
+ gate = "gates-links-resolve"
70
+ expect = "pass"
71
+ input = "See [this table](./structural.toml)."
72
+
73
+ [[self_test]]
74
+ gate = "gates-paths-exist"
75
+ expect = "pass"
76
+ input = "The `Result<T>` type wraps it." # a code span that is not a path
77
+
78
+ [[self_test]]
79
+ gate = "gates-rules-declare-enforcement"
80
+ expect = "fail"
81
+ fixture = { file = ".ai/rules/x.md", frontmatter = { description = "d", paths = ["**"] } }
82
+
83
+ [[self_test]]
84
+ gate = "gates-rules-declare-enforcement"
85
+ expect = "pass"
86
+ fixture = { file = ".ai/rules/x.md", frontmatter = { description = "d", enforcement = "review-only" } }
87
+
88
+ [[self_test]]
89
+ gate = "gates-frontmatter-valid"
90
+ expect = "fail"
91
+ fixture = { file = ".claude/skills/x/SKILL.md", frontmatter = { name = "x", description = "d", "argument-hint" = "y" } }
92
+
93
+ [[self_test]]
94
+ gate = "gates-self-tests-both-directions"
95
+ expect = "fail"
96
+ fixture = { gate = "example", self_tests = ["pass"] }
97
+
98
+ [[self_test]]
99
+ gate = "gates-self-tests-both-directions"
100
+ expect = "pass"
101
+ fixture = { gate = "example", self_tests = ["pass", "fail"] }
102
+
103
+ # `gates-paths-exist` and `gates-frontmatter-valid` share the tables above with
104
+ # their siblings, but the meta-gate keys self-tests by gate id — so a shared
105
+ # table still needs a pass and a fail per gate. That is correct: two gates
106
+ # running the same engine can still be scoped differently, and only their own
107
+ # fixtures prove each one fires.
108
+
109
+ [[self_test]]
110
+ gate = "gates-paths-exist"
111
+ expect = "fail"
112
+ input = "The mapper lives in [`src/gone.ts`](src/gone.ts)."
113
+
114
+ [[self_test]]
115
+ gate = "gates-paths-exist"
116
+ expect = "pass"
117
+ input = "The mapper lives in [`structural.toml`](./structural.toml)."
118
+
119
+ [[self_test]]
120
+ gate = "gates-frontmatter-valid"
121
+ expect = "pass"
122
+ fixture = { file = ".claude/skills/x/SKILL.md", frontmatter = { name = "x", description = "d" } }
123
+
124
+ [[self_test]]
125
+ gate = "gates-frontmatter-valid"
126
+ expect = "fail"
127
+ fixture = { file = ".claude/skills/x/SKILL.md", frontmatter = { description = "d" } }
@@ -0,0 +1,143 @@
1
+ [module]
2
+ name = "gates"
3
+ version = "1.0.0"
4
+ rung = 1
5
+ summary = "The gate runner, the registry every other module registers into, the run ledger, and the generic structural gate set."
6
+
7
+ [requires]
8
+ modules = ["instructions"]
9
+
10
+ [conflicts]
11
+ modules = []
12
+
13
+ # ── parameters ────────────────────────────────────────────────────────────────
14
+
15
+ [params.tiers]
16
+ description = "Ordered gate tiers. The first is the one meant to run constantly."
17
+ default = ["fast", "full"]
18
+
19
+ [params.ledger]
20
+ description = """
21
+ Record exit status and wall-clock per gate per run to a gitignored ledger (ADR-0005). Off means
22
+ `doctor` cannot answer which gates never fire, and declared durations stay hand-typed.
23
+ """
24
+ default = true
25
+
26
+ [params.fast_budget_ms]
27
+ description = """
28
+ Wall-clock the fast tier is expected to stay inside. Compared against the ledger's observed
29
+ values, not against typed per-gate numbers — that comparison is the reason the ledger exists.
30
+ """
31
+ default = 30000
32
+
33
+ # ── gates ─────────────────────────────────────────────────────────────────────
34
+ #
35
+ # Only genuinely repo-agnostic checks live here. Anything needing to know what a
36
+ # document means belongs to the module that owns that document.
37
+
38
+ [[gates]]
39
+ id = "gates-links-resolve"
40
+ kind = "declared"
41
+ engine = "link-integrity"
42
+ table = "gates/structural.toml"
43
+ tier = "fast"
44
+ why = "A relative link that stopped resolving is the cheapest possible signal that a document moved and its citers did not."
45
+
46
+ [[gates]]
47
+ id = "gates-paths-exist"
48
+ kind = "declared"
49
+ engine = "link-integrity"
50
+ table = "gates/structural.toml"
51
+ tier = "fast"
52
+ 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."
53
+
54
+ [[gates]]
55
+ id = "gates-frontmatter-valid"
56
+ kind = "declared"
57
+ engine = "frontmatter-schema"
58
+ table = "gates/structural.toml"
59
+ tier = "fast"
60
+ why = "A skill with malformed frontmatter does not fail; it silently never loads."
61
+
62
+ [[gates]]
63
+ id = "gates-rules-declare-enforcement"
64
+ kind = "declared"
65
+ engine = "frontmatter-schema"
66
+ table = "gates/structural.toml"
67
+ tier = "fast"
68
+ why = """
69
+ Every rule declares `gated` or `review-only`, and a rule claiming MANDATORY with no gate behind it
70
+ is reported. This is failure mode F1 made detectable: all four source repos wrote rules they could
71
+ not check, and all four decayed — including inside the file that states the rule.
72
+ """
73
+
74
+ [[gates]]
75
+ id = "gates-self-tests-both-directions"
76
+ kind = "declared"
77
+ engine = "gate-meta"
78
+ table = "gates/structural.toml"
79
+ tier = "fast"
80
+ why = """
81
+ The meta-gate. Every declared gate must carry at least one self-test expecting `fail` and one
82
+ expecting `pass`, because a gate whose rules are currently satisfied is indistinguishable from a
83
+ gate that matches nothing.
84
+ """
85
+
86
+ # ── skills ────────────────────────────────────────────────────────────────────
87
+
88
+ [skills.harden-rule]
89
+ # No extensions. Escalating a broken rule is exactly the kind of thing a model
90
+ # should be able to reach for on its own, mid-task, without being asked.
91
+
92
+ # ── detection (ADR-0004) ──────────────────────────────────────────────────────
93
+ #
94
+ # Deliberately narrow. A repo with a folder of validation scripts has gates; it
95
+ # does not have *this* registry, and conflating the two would let `add` believe
96
+ # it owns scripts it has never read.
97
+
98
+ [detect]
99
+ paths = [".ai/gates.toml"]
100
+ markers = ["rungs:begin gates"]
101
+
102
+ # Existing validators. Not a paradigm difference — a repo that has these has the
103
+ # thing this module wraps, and they adopt cleanly as `command` gates. `add`
104
+ # proposes registering them; it never rewrites one.
105
+ [[detect.adopt_as]]
106
+ kind = "command"
107
+ paths = [
108
+ ".github/scripts/check-*.mjs",
109
+ ".github/scripts/validate-*.mjs",
110
+ "scripts/validate-*.ps1",
111
+ "scripts/check-*.sh",
112
+ ]
113
+ note = """
114
+ Registering an existing validator gives it the runner, the ledger and attribution without changing
115
+ a line of it. This is the axiom-mesh case (8 PowerShell validators, no root package.json) and the
116
+ rift-forge case (82 registry entries, no ledger) in one mechanism.
117
+ """
118
+
119
+ # ── provenance (required, validated) ──────────────────────────────────────────
120
+
121
+ [provenance]
122
+ sources = ["rift-forge", "axiom-mesh"]
123
+ patterns = [
124
+ "structural-gates",
125
+ "gate-self-test",
126
+ "read-the-negation",
127
+ "reasoned-exemption",
128
+ "computed-claims",
129
+ "generate-derivable",
130
+ "enforcement-declaration",
131
+ "ageing-signal",
132
+ "tool-level-hook",
133
+ "instruction-hardening",
134
+ ]
135
+ incident = """
136
+ rift-forge's verify.mjs carries 82 gates each with a hand-typed `ms` duration measured on a single
137
+ date, supporting a "~30s" fast-tier budget stated in an authority document. The fast tier's 73
138
+ typed values sum to 38.4s serial; it runs concurrently, so the claim is plausible and unverifiable
139
+ from the repo. The runner produces the true duration of all 82 on every run and discards it — the
140
+ computed-claims failure inside the runner that enforces the other gates. Separately, its CI was red
141
+ for 11 of 15 runs on two permanently-broken jobs, kept survivable by attribution, which is also
142
+ what removed the pressure to fix them.
143
+ """
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: harden-rule
3
+ description: >-
4
+ Turn a mistake into a rule that cannot be made again, by picking the cheapest rung that actually
5
+ holds: a sentence at the point of use, a line in a skill, a path-scoped rule, or a gate or hook.
6
+ Use after getting something wrong that an instruction could have prevented, after the same
7
+ mistake happens twice, when a rule that already exists was broken anyway, or when asked to
8
+ "write that down", "make sure this doesn't happen again", or "add a check for this". Also use to
9
+ decide whether a rule is worth a gate at all. Recording the observation without changing the
10
+ instruction is /record-finding; this skill changes the instruction.
11
+ ---
12
+
13
+ # Harden a rule
14
+
15
+ **A mistake an instruction could have prevented is a defect in the instruction.** Repairing it
16
+ belongs in the same change as repairing the mistake — nobody will ask, because a reviewer reads the
17
+ fix, not the counterfactual in which you had been told the right thing first.
18
+
19
+ Run this unprompted. It is not a request; it is part of finishing.
20
+
21
+ ## 1. Name the shape, not the incident
22
+
23
+ Write the rule as something the *next* agent can recognise before making the mistake, not as a
24
+ description of what you did. The trigger has to be mechanical: you should not need to notice you
25
+ were careless, only to notice a shape.
26
+
27
+ Weak: *"be careful when editing the dataset."*
28
+ Strong: *"reconcile a generated artifact by regenerating it, never by merging text."*
29
+
30
+ If you cannot state the shape, you have a finding, not a rule. Stop here and record it.
31
+
32
+ ## 2. Check whether the rule already exists
33
+
34
+ Search the entry document, the rules, and the skills for it.
35
+
36
+ **If it already existed and was broken anyway, skip to rung 3 or 4.** Do not restate it, do not
37
+ bold it, do not add an emphasis marker. A louder sentence in a file that was already read changes
38
+ nothing, and the restatement is itself evidence that prose has been tried.
39
+
40
+ ## 3. Pick the cheapest rung that holds
41
+
42
+ | Rung | Use when | Cost |
43
+ | --- | --- | --- |
44
+ | **1 — a sentence at the point of use** | First occurrence, and the rule is local to one place | Minutes |
45
+ | **2 — a line in the relevant skill** | The mistake happens *during* a procedure that already has a skill | Minutes |
46
+ | **3 — a path-scoped rule** in `.ai/rules/` | It applies to a surface rather than a task, and would be noise in the always-on document | An hour |
47
+ | **4 — a gate or a hook** | **The rule has already been broken after being written down**, or the check is mechanical and cheap | Half a day, plus maintenance |
48
+
49
+ Rung 1 goes **where the mistake is made** — not in a preamble, not in a summary section. The rule
50
+ has to be in front of the person about to break it.
51
+
52
+ ## 4. If it is rung 4, build it properly
53
+
54
+ Add the gate to the owning module's table, or as a `command` gate in `.ai/gates.toml`. Then, before
55
+ you call it done:
56
+
57
+ - **Self-tests, both directions.** One expecting `fail`, one expecting `pass`. A gate whose rules
58
+ are all currently satisfied is indistinguishable from a gate that matches nothing.
59
+ - **Read the negation before the token.** If the gate refuses a phrase, check a preceding-context
60
+ window for a negation cue first — otherwise it refuses the sentence that documents the fix, and a
61
+ guard that refuses its own fix is one people disable.
62
+ - **An exemption must carry a reason.** `<marker>: <why>`, ignored when the reason is missing.
63
+ - **Recompute rather than compare.** If the gate checks a number, derive the number. A probe
64
+ encoding a guess is confidently wrong, which is worse than a typed value nobody trusts.
65
+ - **Pin what it does not cover**, in its own message. Green must never read as "verified".
66
+
67
+ ## 5. Write down where it is now true
68
+
69
+ A rule usually lives in more than one place: the authority document that explains *why*, the
70
+ always-on document that every session reads, and the skills that execute it.
71
+
72
+ **Fix the authority first, then the citers.** A citer corrected against a stale authority is a
73
+ second wrong statement, and the next reader cannot tell which one won.
74
+
75
+ **A citation is not propagation.** "See §3" ages into a false claim the moment §3 changes, and it
76
+ reads as verified precisely because it names a source.
77
+
78
+ ## 6. Say what you did
79
+
80
+ In the same change: what went wrong, which rung you chose, and why the cheaper rungs were not
81
+ enough. That last part is what stops the next person re-litigating it — and if the honest answer is
82
+ "prose had already failed twice", write that.
@@ -0,0 +1,53 @@
1
+ # Path-scoped rules
2
+
3
+ **These files are the source. The copies under `.claude/rules/`, `.github/instructions/` and
4
+ `.cursor/rules/` are generated — do not edit those.** Run `rungs render` after changing anything
5
+ here; `rungs check` refuses a stale rendering.
6
+
7
+ A rule here loads only when an agent touches a matching file, so it costs nothing until it is
8
+ relevant. That is what keeps [`../../AGENTS.md`](../../AGENTS.md) inside its line budget.
9
+
10
+ ## When something belongs here
11
+
12
+ | Content | Goes |
13
+ | --- | --- |
14
+ | A fact every session needs | `AGENTS.md` |
15
+ | A rule for one part of the tree | **here** |
16
+ | A multi-step procedure with a beginning and an end | a skill |
17
+
18
+ If you are adding a section to `AGENTS.md` that begins "when working on X…", it belongs here.
19
+
20
+ ## Format
21
+
22
+ ```markdown
23
+ ---
24
+ description: >-
25
+ What this covers and when it applies. One or two sentences — some harnesses use this to decide
26
+ whether to load the rule at all, so lead with the trigger.
27
+ paths:
28
+ - "src/api/**/*.ts"
29
+ - "tests/api/**"
30
+ enforcement: gated # gated | review-only
31
+ ---
32
+
33
+ # API rules
34
+
35
+ - Concrete, checkable statements. "Use 2-space indentation", not "format properly".
36
+ ```
37
+
38
+ - **`paths`** — globs. Omit for a rule that should load every session, but prefer `AGENTS.md` for
39
+ that; a rule with no paths is a rule with no reason to be here.
40
+ - **`enforcement`** — required. `gated` means a gate enforces it; `review-only` means nothing does.
41
+ There is no silent third category, because a silent third category is what every repo this
42
+ content came from actually had.
43
+
44
+ ## What does not survive rendering
45
+
46
+ Not every harness can express every field, and the render report names each loss:
47
+
48
+ | Field | Claude | Copilot | Cursor | AGENTS.md-only |
49
+ | --- | --- | --- | --- | --- |
50
+ | `paths` | ✅ | ✅ `applyTo` | ✅ `globs` | directory-level, or a routing line |
51
+ | `description` | dropped | ✅ | ✅ | — |
52
+
53
+ Read `.ai/render-report.md` after a render to see what your harness set actually received.
@@ -0,0 +1,104 @@
1
+ # AGENTS.md — {{project_name}}
2
+
3
+ Canonical agent policy for this repository. Read in full before making changes.
4
+
5
+ This is the **always-on** document: facts every session needs. Anything that applies to one part of
6
+ the tree belongs in [`.ai/rules/`](.ai/rules/README.md) instead, and anything that is a multi-step
7
+ procedure belongs in a skill. Both load only when relevant, and this file has a line budget that
8
+ `rungs check` enforces.
9
+
10
+ ## What this is
11
+
12
+ <!-- One paragraph: what the project does, for whom, and the one property that must not break. -->
13
+
14
+ ## Repo map
15
+
16
+ <!-- rungs:begin repo-map -->
17
+ <!-- Generated. Run `rungs render` after moving directories. -->
18
+ <!-- rungs:end repo-map -->
19
+
20
+ ## Validation matrix
21
+
22
+ **Run the narrowest validation that covers what you touched**, then the standard gates. Running
23
+ everything is slow enough to get skipped; running the wrong subset proves nothing.
24
+
25
+ | Change surface | Run |
26
+ | --- | --- |
27
+ | *(fill in per surface)* | |
28
+ | anything | `rungs check` |
29
+
30
+ ## Task loop
31
+
32
+ 1. **Start from the narrowest concrete anchor** — a file, a symbol, a failing test, a route.
33
+ 2. Read the scoped rule for that surface before editing broadly.
34
+ 3. Make the smallest change that proves or disproves the current hypothesis.
35
+ 4. Run the narrowest validation above.
36
+ 5. Update docs when public behaviour or developer workflow changed.
37
+
38
+ Choosing the anchor **before** reading instructions is the point: which instructions apply is then a
39
+ consequence of scope rather than a guess.
40
+
41
+ ## Non-negotiables
42
+
43
+ ### When you get something wrong, harden the instruction — without being asked
44
+
45
+ **A mistake an instruction could have prevented is a defect in the instruction**, and repairing it
46
+ is part of repairing the mistake. Do it in the same change, unprompted: a reviewer reads the fix,
47
+ not the counterfactual in which you had been told the right thing first, so the repair that does not
48
+ happen here does not happen at all.
49
+
50
+ Pick the cheapest rung that actually holds:
51
+
52
+ 1. **A sentence at the point of use** — where the mistake is made, not in a preamble.
53
+ 2. **A line in the relevant skill**, where an agent meets the rule during execution.
54
+ 3. **A path-scoped rule**, when it applies to a surface rather than a task.
55
+ 4. **A gate or a hook**, when the rule has already been broken *after* being written down.
56
+
57
+ **If the rule already existed and you broke it anyway, do not restate it — make it mechanical.** A
58
+ louder sentence in a file you have already read changes nothing. `/harden-rule` walks the ladder.
59
+
60
+ ### Editing files from the shell
61
+
62
+ **Never pipe a multi-line edit through `node -e "…"`, `python -c "…"`, or a shell-expanded
63
+ heredoc.** Write the script to a file and run it. Inside a double-quoted shell string backticks are
64
+ command substitution, so a `node -e` that writes documentation deletes the backticked words and
65
+ leaves grammatical text behind — and exits 0.
66
+
67
+ **Chain with `&&`, never `;`**, when a later step consumes an earlier one. A failed producer
68
+ followed by `;` yields an empty variable, and an empty variable written into a file is not a crash;
69
+ it is a wrong value that passes review.
70
+
71
+ ### Claims and numbers
72
+
73
+ - **A number a machine can compute is never typed by a human.** If it can be derived, generate it
74
+ and gate it.
75
+ - **A control that cannot fail loudly is not a control.** Filtering a command's output through
76
+ `| tail` or `| grep` reports *that* command's exit status, not the one you care about.
77
+ - **Check the artifact, not the bookkeeping about it.** A status field, a board row, or a triage is
78
+ a claim about the work; the branch, the test, and the file are the work.
79
+
80
+ <!--
81
+ Optional — uncomment if you want it. Not shipped active because it is a preference, not a universal:
82
+
83
+ ### Communication style
84
+
85
+ - Never tell me what I want to hear; prioritise truth over comfort.
86
+ - Contradict me when you disagree, and challenge assumptions.
87
+ - Be direct and concise. Skip validation and praise.
88
+ - If there is a better approach, recommend it even if I did not ask.
89
+ -->
90
+
91
+ ## Conventions
92
+
93
+ <!-- Positive and negative both. A prohibition should state the evidence that would reverse it,
94
+ e.g. "no dedicated X package until two real consumers prove a stable API". -->
95
+
96
+ - Commit messages: conventional prefixes (`feat:`, `fix:`, `docs:`, `chore:`…).
97
+
98
+ ## Routing
99
+
100
+ | If you need… | Go to |
101
+ | --- | --- |
102
+ | Rules for one part of the tree | [`.ai/rules/`](.ai/rules/README.md) |
103
+ | A multi-step procedure | the skills in this repo — invoke by name |
104
+ | Why a decision was made | *(add when `adr` is installed)* |
@@ -0,0 +1,11 @@
1
+ @AGENTS.md
2
+
3
+ <!--
4
+ Generated by `rungs`. Claude Code does not read AGENTS.md, so this file imports it — one source,
5
+ no second copy to drift (ADR-0001). The import is used rather than a symlink because a symlink
6
+ requires Administrator or Developer Mode on Windows.
7
+
8
+ Add Claude-specific content below this comment. It is yours; `rungs upgrade` will not touch it.
9
+ -->
10
+
11
+ ## Claude Code
@@ -0,0 +1,114 @@
1
+ # Gate table for the `instructions` module. Data only — no script enters the repo (ADR-0002).
2
+
3
+ # ── instructions-core-size ────────────────────────────────────────────────────
4
+ #
5
+ # Counts what is actually loaded: HTML comments are stripped by at least one harness before
6
+ # injection, so they do not count, and neither does frontmatter.
7
+
8
+ [file_budget]
9
+ file = "AGENTS.md"
10
+ max_lines = {{core_budget}}
11
+ ignore = ["html_comments", "frontmatter"]
12
+ warn_at = 0.8
13
+ message = """
14
+ AGENTS.md is over its {{core_budget}}-line budget. Move the most path-specific section into
15
+ .ai/rules/ — that is what the budget is for. Merged module fragments count toward it, so a fragment
16
+ that has grown past ~15 lines is usually the one to look at first.
17
+ """
18
+
19
+ # ── instructions-render-current ───────────────────────────────────────────────
20
+
21
+ [[render_freshness]]
22
+ id = "rules"
23
+ sources = [".ai/rules/**/*.md"]
24
+ exclude = [".ai/rules/README.md"] # the scaffold doc is not a rule
25
+ targets = [".claude/rules/**/*.md", ".github/instructions/**/*.instructions.md", ".cursor/rules/**/*.mdc"]
26
+ command = "rungs render"
27
+ # Targets are checked only for harnesses in the installed matrix; the rest are ignored rather
28
+ # than reported missing.
29
+
30
+ # ── instructions-repo-map-current ─────────────────────────────────────────────
31
+
32
+ [[render_freshness]]
33
+ id = "repo-map"
34
+ block = { file = "AGENTS.md", marker = "repo-map" }
35
+ derives = "directory-tree"
36
+ depth = 2
37
+ command = "rungs render"
38
+
39
+ # ── instructions-shell-backticks (hook: pre-tool-use) ─────────────────────────
40
+ #
41
+ # Refuses a command that would let the shell expand backticks inside a double-quoted string.
42
+ # It permits every form the rule prescribes, and asserts BOTH directions below, because a guard
43
+ # that also refuses its own fix is one people disable.
44
+
45
+ [shell_safety]
46
+ refuse = [
47
+ { pattern = "node\\s+-e\\s+\"[^\"]*`", why = "backtick inside a double-quoted -e string is command substitution" },
48
+ { pattern = "python\\s+-c\\s+\"[^\"]*`", why = "same trap, Python" },
49
+ { pattern = "<<\\s*EOF", why = "unquoted heredoc is shell-expanded; use <<'EOF'" },
50
+ ]
51
+ permit = [
52
+ "script file invocation",
53
+ "single-quoted -e string",
54
+ "<<'EOF'",
55
+ "escaped backtick",
56
+ ]
57
+ message = "Write the script to a file and run it. A file has no shell quoting layer, so backticks are just characters."
58
+
59
+ # ── self-tests ────────────────────────────────────────────────────────────────
60
+ #
61
+ # Both directions, always. A gate whose rules are currently satisfied is indistinguishable from a
62
+ # gate that matches nothing.
63
+
64
+ [[self_test]]
65
+ gate = "instructions-shell-backticks"
66
+ expect = "fail"
67
+ input = "node -e \"fs.writeFileSync('a.md', 'see `foo`')\""
68
+
69
+ [[self_test]]
70
+ gate = "instructions-shell-backticks"
71
+ expect = "pass"
72
+ input = "node scripts/edit.mjs"
73
+
74
+ [[self_test]]
75
+ gate = "instructions-shell-backticks"
76
+ expect = "pass"
77
+ input = "cat <<'EOF' > a.md"
78
+
79
+ [[self_test]]
80
+ gate = "instructions-shell-backticks"
81
+ expect = "fail"
82
+ input = "cat <<EOF > a.md"
83
+
84
+ [[self_test]]
85
+ gate = "instructions-core-size"
86
+ expect = "fail"
87
+ fixture = { file = "AGENTS.md", lines = 9999 }
88
+
89
+ [[self_test]]
90
+ gate = "instructions-core-size"
91
+ expect = "pass"
92
+ fixture = { file = "AGENTS.md", lines = 10 }
93
+
94
+ # ── render-freshness self-tests ───────────────────────────────────────────────
95
+
96
+ [[self_test]]
97
+ gate = "instructions-render-current"
98
+ expect = "fail"
99
+ fixture = { sources = [".ai/rules/backend.md"], targets = [] }
100
+
101
+ [[self_test]]
102
+ gate = "instructions-render-current"
103
+ expect = "pass"
104
+ fixture = { sources = [".ai/rules/backend.md"], targets = [".claude/rules/backend.md"] }
105
+
106
+ [[self_test]]
107
+ gate = "instructions-repo-map-current"
108
+ expect = "fail"
109
+ fixture = { file = "AGENTS.md", blocks = [] }
110
+
111
+ [[self_test]]
112
+ gate = "instructions-repo-map-current"
113
+ expect = "pass"
114
+ fixture = { file = "AGENTS.md", blocks = ["repo-map"] }