@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,59 @@
1
+ ---
2
+ name: close-session
3
+ description: >-
4
+ Close out a working session: write the dated archive note, reset the live handoff document to a
5
+ cold-readable resume point, and record what became an active constraint. Use when asked to
6
+ "close the session", "wrap up", "archive this session", "hand off", "write the handoff", or at
7
+ the end of a long stretch of work before context is lost. Recording an out-of-scope observation
8
+ is /record-finding; finishing a tracked item is /work-item.
9
+ ---
10
+
11
+ # Close a session
12
+
13
+ The test for everything below: **could a session starting cold tomorrow, with no memory of this
14
+ conversation, pick up from the document alone?** If not, it is not closed.
15
+
16
+ ## 1. Write the archive note
17
+
18
+ `{{archive}}/YYYY-MM-DD_session-NN_<what-closed>-and-<what-is-next>.md`. The filename carries both
19
+ halves so the directory is scannable without opening anything.
20
+
21
+ Contents:
22
+
23
+ - **Delivered** — what actually landed, with ids.
24
+ - **Not delivered** — what was planned and did not happen, **and why**. This is the half people
25
+ skip, and it is the half that stops the next session re-planning the same thing.
26
+ - **Decisions taken**, with reasons. Flag any that should become an active constraint.
27
+ - **What turned out to be wrong** during the session, and what replaced it.
28
+
29
+ Write what was believed at close. **Never revise an archived note later** — its value is that it
30
+ records what was known then.
31
+
32
+ ## 2. Rewrite the live document
33
+
34
+ `{{path}}` is **rewritten, not appended to.** Delete what is no longer true rather than layering
35
+ qualifications on it.
36
+
37
+ - **Resume from** — one concrete next action. A path, a command, an id. Not "continue the
38
+ refactor"; something that can be executed.
39
+ - **Active constraints** — move any decision here that a fresh session would otherwise relitigate,
40
+ each with why it is closed. Be specific: *"the storage boundary is fixed by ADR-0007, do not
41
+ re-scope it"* beats *"architecture is settled"*. **This section is what pays for the document.**
42
+ - **Working assumptions** — what is believed but unverified. Being wrong here is expected; the
43
+ value is telling the next session which claims to distrust first.
44
+ - **Open questions** — mark which block progress. "None blocking" is worth stating.
45
+
46
+ ## 3. Check it against the archive
47
+
48
+ The live document should now say nothing the archive note repeats. If it does, one of them is
49
+ wrong later: **the live document is present tense, the archive is past tense**, and a fact in both
50
+ will drift in one.
51
+
52
+ ## 4. What not to do
53
+
54
+ - **Do not close a session to avoid finishing something.** If work is half-done, say so in
55
+ `In progress` with what remains — a handoff that hides a loose end is worse than no handoff.
56
+ - **Do not summarise the conversation.** The archive records outcomes and decisions, not a
57
+ narrative. Nobody reads a transcript.
58
+ - **Do not promote an assumption to a constraint** because it went unchallenged. A constraint is
59
+ something decided; an assumption is something believed.
@@ -0,0 +1,8 @@
1
+ <!-- rungs:begin skills@1.0.0 -->
2
+ ## Skills
3
+
4
+ Multi-step procedures live in [`{{dir}}/`]({{dir}}/) and load only when used. Authoring rules are
5
+ in [`.ai/rules/`](.ai/rules/README.md): six spec frontmatter fields and no more, a description that
6
+ lists the phrases people actually say, and **every skill names its neighbours** — the failure past a
7
+ dozen skills is the wrong one firing and running to completion.
8
+ <!-- rungs:end skills -->
@@ -0,0 +1,94 @@
1
+ # Gate table for the `skills` module.
2
+ #
3
+ # Moved here from `gates/structural.toml`: what a SKILL.md may contain is knowledge about skills,
4
+ # not a repo-agnostic structural fact. `gates` contributes the frontmatter-schema engine.
5
+
6
+ # ── skills-spec-pure ──────────────────────────────────────────────────────────
7
+
8
+ [[frontmatter_schema]]
9
+ id = "skills"
10
+ scan = ["{{dir}}/**/SKILL.md"]
11
+ required = ["name", "description"]
12
+ allowed = ["name", "description", "license", "compatibility", "metadata", "allowed-tools"]
13
+ # Extensions are legal only when the owning module opted in. The gate reads that manifest rather
14
+ # than a repo-local allowlist, so the portability cost stays attached to the decision.
15
+ extensions_allowed_from = "module.toml:skills.<name>.extensions"
16
+ message = """
17
+ `{file}` uses non-spec frontmatter key `{key}`. claude.ai uploads and the Skills API reject unknown
18
+ keys with a hard error rather than ignoring them. If this is deliberate, opt in from the owning
19
+ module's manifest, where the portability cost is recorded next to the choice.
20
+ """
21
+
22
+ # ── skills-description-routes ─────────────────────────────────────────────────
23
+ #
24
+ # Structure, not quality. A gate cannot judge whether a description is *good*; it can tell that it
25
+ # is a single clause with no trigger vocabulary, which is the shape that never fires.
26
+
27
+ [[frontmatter_schema.field_shape]]
28
+ field = "description"
29
+ min_words = 20
30
+ requires_any = ["Use when", "Use for", "Use whenever", "Trigger"]
31
+ message = """
32
+ `{file}`'s description does not say when to use it. At startup an agent loads only the name and
33
+ description, so this is the entire routing surface — lead with the use case and list the phrases
34
+ people actually say.
35
+ """
36
+
37
+ # ── skills-names-neighbours ───────────────────────────────────────────────────
38
+ #
39
+ # Only fires past a threshold: in a repo with three skills there is nothing to disambiguate, and
40
+ # demanding cross-references there is the checklist-as-noise failure.
41
+
42
+ [cross_reference]
43
+ scan = ["{{dir}}/**/SKILL.md"]
44
+ field = "description"
45
+ min_skills = 6
46
+ requires_mention_of_peer = true
47
+ exempt_marker = "no-neighbours-ok:" # ignored unless it states a reason
48
+ message = """
49
+ `{file}` names no neighbouring skill. With {count} skills in this repo, the failure mode is no
50
+ longer "no skill fires" but "a plausible-but-wrong skill fires and runs to completion". Name the
51
+ adjacent skills and the boundary, in one clause each.
52
+ """
53
+
54
+ # ── self-tests ────────────────────────────────────────────────────────────────
55
+
56
+ [[self_test]]
57
+ gate = "skills-spec-pure"
58
+ expect = "fail"
59
+ fixture = { frontmatter = { name = "x", description = "d", "argument-hint" = "y" }, opted_in = [] }
60
+
61
+ [[self_test]]
62
+ gate = "skills-spec-pure"
63
+ expect = "pass"
64
+ fixture = { frontmatter = { name = "x", description = "d", "disable-model-invocation" = true }, opted_in = ["disable-model-invocation"] }
65
+
66
+ [[self_test]]
67
+ gate = "skills-spec-pure"
68
+ expect = "fail"
69
+ fixture = { frontmatter = { name = "x", description = "d", "disable-model-invocation" = true }, opted_in = [] }
70
+
71
+ [[self_test]]
72
+ gate = "skills-description-routes"
73
+ expect = "fail"
74
+ fixture = { description = "Executes the release process." }
75
+
76
+ [[self_test]]
77
+ gate = "skills-description-routes"
78
+ expect = "pass"
79
+ fixture = { description = "Cut, tag and ship a release from the active candidate. Use when asked to cut the release, tag a version, hotfix production, or roll back a deploy." }
80
+
81
+ [[self_test]]
82
+ gate = "skills-names-neighbours"
83
+ expect = "pass"
84
+ fixture = { skill_count = 3, mentions_peer = false }
85
+
86
+ [[self_test]]
87
+ gate = "skills-names-neighbours"
88
+ expect = "fail"
89
+ fixture = { skill_count = 9, mentions_peer = false }
90
+
91
+ [[self_test]]
92
+ gate = "skills-names-neighbours"
93
+ expect = "pass"
94
+ fixture = { skill_count = 9, mentions_peer = false, exempt = "no-neighbours-ok: the only skill touching deployment" }
@@ -0,0 +1,89 @@
1
+ [module]
2
+ name = "skills"
3
+ version = "1.0.0"
4
+ rung = 2
5
+ summary = "Skill-authoring discipline and the gates that hold it: spec-pure frontmatter, descriptions that route, neighbours named, and output that lands somewhere."
6
+
7
+ [requires]
8
+ modules = ["instructions"]
9
+
10
+ [conflicts]
11
+ modules = []
12
+
13
+ # This is not "the ability to have skills" — every module ships skills through
14
+ # its own skills/ directory. This module ships the discipline for writing good
15
+ # ones, and it is where the SKILL.md frontmatter schema lives, because knowing
16
+ # what a skill may contain is knowledge about skills rather than a repo-agnostic
17
+ # structural fact.
18
+
19
+ [params.dir]
20
+ description = """
21
+ Where skills live. Resolved from the harness set (ADR-0001): `.claude/skills/` reaches Claude Code,
22
+ Copilot and Cursor today; `.agents/skills/` is the vendor-neutral direction and is not read by
23
+ Claude Code.
24
+ """
25
+ default = ".claude/skills"
26
+ allowed = [".claude/skills", ".agents/skills"]
27
+ consumed_by = "render"
28
+
29
+ [[gates]]
30
+ id = "skills-spec-pure"
31
+ kind = "declared"
32
+ engine = "frontmatter-schema"
33
+ table = "gates/skills.toml"
34
+ tier = "fast"
35
+ why = """
36
+ The Agent Skills spec allows six frontmatter fields. Anything else is a harness extension, and
37
+ claude.ai uploads and the Skills API reject unknown keys with a hard error rather than ignoring
38
+ them. An extension must be opted into in the owning module's manifest, where its portability cost
39
+ is stated at the point of the choice.
40
+ """
41
+
42
+ [[gates]]
43
+ id = "skills-description-routes"
44
+ kind = "declared"
45
+ engine = "frontmatter-schema"
46
+ table = "gates/skills.toml"
47
+ tier = "fast"
48
+ why = """
49
+ A description is the entire routing surface — at startup an agent loads only the name and
50
+ description, so a vague one is the single most common reason a skill never fires. It has to name
51
+ what the skill does and the phrases people actually say.
52
+ """
53
+
54
+ [[gates]]
55
+ id = "skills-names-neighbours"
56
+ kind = "declared"
57
+ engine = "cross-reference"
58
+ table = "gates/skills.toml"
59
+ tier = "fast"
60
+ why = """
61
+ Past about a dozen skills with overlapping vocabulary the failure stops being "no skill fires" and
62
+ becomes "a plausible-but-wrong skill fires and runs to completion". Every skill naming its
63
+ neighbours makes each one a correction surface.
64
+ """
65
+
66
+ [detect]
67
+ paths = [".claude/skills/**/SKILL.md", ".agents/skills/**/SKILL.md", ".github/prompts/*.prompt.md", ".ai/prompts/*.md"]
68
+ markers = ["rungs:begin skills"]
69
+
70
+ [[detect.adopt_as]]
71
+ kind = "prompt-library"
72
+ paths = [".github/prompts/*.prompt.md", ".ai/prompts/*.md"]
73
+ note = """
74
+ A markdown prompt library is a skill set without an invocation surface. Adopting means proposing a
75
+ conversion — frontmatter added, one directory per skill — and it is worth doing: the format is now
76
+ an open standard, so the portability objection that justified staying with plain prompts is gone.
77
+ `add` proposes; it never rewrites a prompt in place.
78
+ """
79
+
80
+ [provenance]
81
+ sources = ["rift-forge", "axiom-mesh", "hexguard"]
82
+ patterns = ["invocable-procedure", "skill-neighbours", "prompt-writes-artifact", "operating-skills", "prompt-index-routing"]
83
+ incident = """
84
+ axiom-mesh wrote 21 prompt playbooks, several with no equivalent anywhere else, and gave them no
85
+ invocation surface at all — they must be found, opened and pasted. hexguard-templates shipped the
86
+ best decision procedure in the corpus behind a placeholder table. rift-forge, at 13 skills, had to
87
+ invent the neighbour-naming convention because descriptions alone had stopped disambiguating
88
+ /curate-mechanic from /curate-champion from /patch-ingest.
89
+ """
@@ -0,0 +1,71 @@
1
+ ---
2
+ description: >-
3
+ How to write a skill that fires when it should, does not fire when it should not, and leaves
4
+ something behind. Loads when creating or editing any SKILL.md.
5
+ paths:
6
+ - "{{dir}}/**/SKILL.md"
7
+ enforcement: gated
8
+ ---
9
+
10
+ # Writing a skill
11
+
12
+ ## What earns a skill
13
+
14
+ A **multi-step procedure with a beginning and an end**. Not a fact — that is the entry document.
15
+ Not a rule about a surface — that is a path-scoped rule. A skill's body loads only when it is used,
16
+ so long reference material costs nothing until needed; a fact that belongs in every session costs
17
+ the full context every session and belongs elsewhere.
18
+
19
+ ## Frontmatter: six fields, and that is the list
20
+
21
+ `name` · `description` · `license` · `compatibility` · `metadata` · `allowed-tools`.
22
+
23
+ Anything else is a harness extension. claude.ai uploads and the Skills API **reject** unknown keys
24
+ with a hard error rather than ignoring them, so an extension is opted into in the owning module's
25
+ manifest, where the cost is stated at the point of the choice — not added here quietly.
26
+
27
+ The two that matter:
28
+
29
+ - **`disable-model-invocation: true`** for anything with side effects — releases, deploys, sends,
30
+ merges. You do not want a model deciding that now is the moment to ship.
31
+ - **`user-invocable: false`** for background knowledge that is not an action anyone would type.
32
+
33
+ ## The description is the whole routing surface
34
+
35
+ At startup an agent loads **only the name and description**. A vague description is the single most
36
+ common reason a skill never fires.
37
+
38
+ Write it in the third person. Lead with the use case, then **list the phrases people actually
39
+ say** — the exact words, not a paraphrase of the capability.
40
+
41
+ > *Execute a backlog work item end to end… Use when asked to "work on / tackle / start / ship
42
+ > WI-###", to pick up the next item, or to work an epic's sub-items.*
43
+
44
+ ## Name your neighbours
45
+
46
+ **Every skill names the adjacent skills and the boundary between them**, in the description, in one
47
+ clause each.
48
+
49
+ The failure this fixes is not "no skill fires". It is **a plausible-but-wrong skill firing and
50
+ running to completion** — which is worse, because it produces confident output against the wrong
51
+ procedure. Naming neighbours makes every skill a correction surface:
52
+
53
+ > *One mechanic across many subjects is /curate-mechanic; a whole new dataset is /patch-ingest;
54
+ > this skill is one subject, whole kit.*
55
+
56
+ ## Land something durable
57
+
58
+ A skill that only produces conversation evaporates. **Say in the body where the output goes** — a
59
+ register row, a document at a known path, a status field. This is what makes a skill set compound
60
+ instead of being re-derived every session.
61
+
62
+ ## Write standing instructions, not one-time steps
63
+
64
+ The rendered body enters the conversation once and stays for the session; it is not re-read on
65
+ later turns. Guidance that should apply throughout a task must read as a standing rule, not as
66
+ step 4.
67
+
68
+ ## Say what the skill cannot do
69
+
70
+ Near the end, briefly. The limits are what stop it being used for the adjacent problem it almost
71
+ fits — which is the same failure as the wrong skill firing, arriving one step later.
@@ -0,0 +1,63 @@
1
+ **Authoritative for:** spec conventions: ids, per-story status, the mandatory scope section, and how to propose new cross-cutting scope.
2
+ **Not authoritative for:** the behaviour of any surface, which each spec owns, and what is currently being built, which is the backlog.
3
+
4
+ # Specs
5
+
6
+ What each surface is supposed to do. **Specs describe behaviour; they do not track work** — if you
7
+ need to know what is happening right now, that is the backlog, not here.
8
+
9
+ ## Index
10
+
11
+ <!-- rungs:begin specs-index -->
12
+ <!-- Generated by `rungs render`. -->
13
+
14
+ | Spec | Scope | Status | File |
15
+ | --- | --- | --- | --- |
16
+ | — | | | |
17
+
18
+ <!-- rungs:end specs-index -->
19
+
20
+ ## Conventions
21
+
22
+ - **Frontmatter on every spec:** `id`, `type: spec`, `scope`, `status`, `created`, `updated`.
23
+ - **Stable ids** per feature (`{{feature_format}}`) and per story (`{{story_format}}`). Cite these
24
+ from commits, pull requests and work items so work traces back to the spec that justified it.
25
+ - **Status is per feature and story, not per file:** ✅ implemented · 🟡 partial · ⬜ planned.
26
+ Fine-grained integration detail belongs in the linked plan or work item; the spec only has to be
27
+ accurate at the feature level.
28
+ - **The Scope section is mandatory**, and its out-of-scope half is the point: it is what stops an
29
+ agent silently expanding a surface while implementing an unrelated story.
30
+ - **One file per surface.** Split into `spec.md` + `stories/` only past ~{{split_lines}} lines —
31
+ do not pre-split. Structure created ahead of content is structure nobody fills.
32
+
33
+ ## Demo is not done
34
+
35
+ An endpoint, screen or flow that exists to **showcase** a capability in isolation is 🟡 with an
36
+ explicit note. **Never ✅.**
37
+
38
+ A spec that overclaims integration is worse than no spec, because it is read as verified — by the
39
+ next contributor, by a plan that cites it, and by an agent deciding a story is already handled.
40
+ `rungs check` refuses a ✅ story that does not name what closed it.
41
+
42
+ ## Proposing new cross-cutting scope
43
+
44
+ Written down so a fresh contributor does not have to reverse-engineer it from examples:
45
+
46
+ 1. **Find the surface that owns it.** If two surfaces both plausibly own it, that is the thing to
47
+ resolve first — not by picking one, but by naming the boundary.
48
+ 2. **Claim the next feature id** under that surface's prefix.
49
+ 3. **State the mechanism once**, in the owning spec, and have other specs reference it. A
50
+ mechanism described in two specs will diverge, and the second copy always reads as authoritative
51
+ to whoever finds it first.
52
+ 4. **Name a reference consumer.** A cross-cutting feature with no consumer is a design, not a
53
+ requirement.
54
+ 5. **Fill Non-goals and Open questions.** An empty Open questions section on a new proposal is
55
+ almost always untrue.
56
+
57
+ ## Status legend
58
+
59
+ | | |
60
+ | --- | --- |
61
+ | ✅ | Implemented and integrated. Names the commit, pull request or work item that closed it |
62
+ | 🟡 | Partial. **Carries a note saying what is missing** — a bare 🟡 is a guess |
63
+ | ⬜ | Planned. Specified, not built |
@@ -0,0 +1,62 @@
1
+ ---
2
+ id: <PREFIX>
3
+ type: spec
4
+ scope: <what surface this covers>
5
+ status: draft # draft | stable | superseded
6
+ created: YYYY-MM-DD
7
+ updated: YYYY-MM-DD
8
+ ---
9
+
10
+ # <Surface> — specification
11
+
12
+ ## Purpose
13
+
14
+ What this surface is for, in two or three sentences. The property that must not break.
15
+
16
+ ## Scope
17
+
18
+ **In scope**
19
+
20
+ -
21
+
22
+ **Out of scope**
23
+
24
+ - **Required — answer, do not delete.** What this surface explicitly does *not* cover, and where
25
+ that lives instead if it lives anywhere.
26
+ >
27
+ > This half is why the section is mandatory. Without it, "the spec did not say I could not" is a
28
+ > defensible reading, and surfaces grow sideways during unrelated work.
29
+
30
+ ## Reference implementation
31
+
32
+ <!-- The directory that demonstrates the correct pattern. Cheaper than prose and it cannot drift
33
+ from the code, because it *is* the code. -->
34
+
35
+ ## Features
36
+
37
+ ### <PREFIX>-F01 — <name>
38
+
39
+ **Status:** ⬜
40
+
41
+ <What it does. The mechanism, stated once — other specs reference this rather than restating it.>
42
+
43
+ #### Stories
44
+
45
+ | Id | Story | Status | Closed by |
46
+ | --- | --- | --- | --- |
47
+ | `<PREFIX>-US-001` | As a …, I can … | ⬜ | |
48
+
49
+ **Acceptance criteria**
50
+
51
+ - Checkable statements. "Given X, the endpoint returns 422 with a typed error", not "handles
52
+ errors properly".
53
+
54
+ ## Non-goals
55
+
56
+ Things a reader might reasonably expect here and will not find, **with the reason**. A non-goal
57
+ without a reason reads as an oversight and gets proposed again.
58
+
59
+ ## Open questions
60
+
61
+ Unresolved, with who or what would resolve them. An empty section on a new spec is almost always
62
+ untrue — say "none known" deliberately if you mean it.
@@ -0,0 +1,8 @@
1
+ <!-- rungs:begin specs@1.0.0 -->
2
+ ## Specs
3
+
4
+ Behaviour is specified in [`{{path}}/`]({{path}}/README.md) with stable feature and story ids —
5
+ cite them from commits and work items. Specs say what is *in scope*; they do not track what is
6
+ happening. **Demo is not done**: a showcase is 🟡 with a note, never ✅, and a ✅ story must name
7
+ what closed it.
8
+ <!-- rungs:end specs -->
@@ -0,0 +1,119 @@
1
+ # Gate table for the `specs` module.
2
+
3
+ # ── specs-status-evidence ─────────────────────────────────────────────────────
4
+ #
5
+ # The "demo is not done" gate. A story claiming completion must name what closed it; a partial
6
+ # story must say what is missing. Presence, never quality — a gate cannot judge whether the named
7
+ # commit actually implemented the story, and one that tried would be confidently wrong.
8
+
9
+ [register_schema]
10
+ scan = ["{{path}}/**/*.md"]
11
+ table_columns = ["Id", "Story", "Status", "Closed by"]
12
+ enum = { Status = ["✅", "🟡", "⬜"] }
13
+
14
+ [[register_schema.conditional]]
15
+ when = { Status = "✅" }
16
+ non_empty = ["Closed by"]
17
+ message = """
18
+ Story {id} is marked ✅ with nothing in `Closed by`. A spec that overclaims integration is read as
19
+ verified — by the next contributor, by a plan citing it, and by an agent deciding the story is
20
+ already handled. Name the commit, pull request or work item, or mark it 🟡 with what is missing.
21
+ """
22
+
23
+ [[register_schema.conditional]]
24
+ when = { Status = "🟡" }
25
+ requires_note = true
26
+ message = "Story {id} is 🟡 with no note. A bare partial is a guess; say what is missing."
27
+
28
+ # ── specs-scope-section ───────────────────────────────────────────────────────
29
+
30
+ [sections]
31
+ scan = ["{{path}}/**/*.md"]
32
+ exclude = ["{{path}}/README.md", "{{path}}/TEMPLATE.md"]
33
+ required = ["Purpose", "Scope"]
34
+ non_empty = true
35
+ # The out-of-scope half specifically, not just the heading.
36
+ required_subsections = { Scope = ["Out of scope"] }
37
+ message = """
38
+ {file} has no non-empty `Out of scope`. Without it, "the spec did not say I could not" is a
39
+ defensible reading, and the surface grows sideways during unrelated work.
40
+ """
41
+
42
+ # ── specs-ids-unique ──────────────────────────────────────────────────────────
43
+
44
+ [kinds.feature]
45
+ format = "[A-Z]{2,8}-F\\d{2}"
46
+ sources = ["{{path}}/**/*.md"]
47
+
48
+ [kinds.story]
49
+ format = "[A-Z]{2,8}-US-\\d{1,4}"
50
+ sources = ["{{path}}/**/*.md"]
51
+
52
+ [citations]
53
+ roots = ["docs", ".ai", "AGENTS.md"]
54
+ extensions = [".md"]
55
+
56
+ # ── specs-split-threshold ─────────────────────────────────────────────────────
57
+
58
+ [file_budget]
59
+ scan = ["{{path}}/**/*.md"]
60
+ exclude = ["{{path}}/README.md"]
61
+ max_lines = {{split_lines}}
62
+ warn_at = 0.85
63
+ message = """
64
+ {file} is past {{split_lines}} lines. Split it into spec.md + stories/ — a spec nobody reads to
65
+ the end is one whose later sections collect unverified claims. Do not pre-split anything else.
66
+ """
67
+
68
+ # ── self-tests ────────────────────────────────────────────────────────────────
69
+
70
+ [[self_test]]
71
+ gate = "specs-status-evidence"
72
+ expect = "fail"
73
+ fixture = { row = { Id = "X-US-001", Status = "✅", "Closed by" = "" } }
74
+
75
+ [[self_test]]
76
+ gate = "specs-status-evidence"
77
+ expect = "pass"
78
+ fixture = { row = { Id = "X-US-001", Status = "✅", "Closed by" = "WI-042" } }
79
+
80
+ [[self_test]]
81
+ gate = "specs-status-evidence"
82
+ expect = "fail"
83
+ fixture = { row = { Id = "X-US-002", Status = "🟡" }, note = "" }
84
+
85
+ [[self_test]]
86
+ gate = "specs-scope-section"
87
+ expect = "fail"
88
+ fixture = { sections = ["Purpose", "Scope"], subsections = { Scope = ["In scope"] } }
89
+
90
+ [[self_test]]
91
+ gate = "specs-scope-section"
92
+ expect = "pass"
93
+ fixture = { sections = ["Purpose", "Scope"], subsections = { Scope = ["In scope", "Out of scope"] } }
94
+
95
+ [[self_test]]
96
+ gate = "specs-split-threshold"
97
+ expect = "fail"
98
+ fixture = { file = "{{path}}/a/spec.md", lines = 900 }
99
+
100
+ [[self_test]]
101
+ gate = "specs-split-threshold"
102
+ expect = "pass"
103
+ fixture = { file = "{{path}}/a/spec.md", lines = 400 }
104
+
105
+ # ── specs-ids-unique self-tests ───────────────────────────────────────────────
106
+ #
107
+ # Story ids are cited from commits, plans and work items, so a duplicate makes
108
+ # every one of those citations ambiguous — and the citation still resolves,
109
+ # which is why this is checked rather than left to be noticed.
110
+
111
+ [[self_test]]
112
+ gate = "specs-ids-unique"
113
+ expect = "fail"
114
+ fixture = { stories = ["FOUND-US-001", "FOUND-US-001"] }
115
+
116
+ [[self_test]]
117
+ gate = "specs-ids-unique"
118
+ expect = "pass"
119
+ fixture = { stories = ["FOUND-US-001", "AUTH-US-001"] }