@rungs/cli 0.3.1 → 0.4.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 (74) hide show
  1. package/README.md +6 -6
  2. package/dist/cli.js +2184 -478
  3. package/dist/cli.js.map +4 -4
  4. package/modules/README.md +25 -3
  5. package/modules/adr/files/{{path}}/README.md +1 -1
  6. package/modules/adr/gates/adr.toml +1 -1
  7. package/modules/adr/module.toml +1 -1
  8. package/modules/audit/fragments/AGENTS.md +2 -2
  9. package/modules/audit/module.toml +1 -1
  10. package/modules/audit/skills/assess/SKILL.md +1 -1
  11. package/modules/backlog/files/docs/{{root}}/BACKLOG.md +1 -1
  12. package/modules/backlog/files/docs/{{root}}/README.md +2 -2
  13. package/modules/backlog/files/docs/{{root}}/archive/README.md +1 -1
  14. package/modules/backlog/files/docs/{{root}}/items/README.md +1 -1
  15. package/modules/backlog/fragments/AGENTS.md +2 -2
  16. package/modules/backlog/module.toml +1 -1
  17. package/modules/backlog/skills/work-item/SKILL.md +1 -1
  18. package/modules/ci/files/{{workflow_path}} +3 -3
  19. package/modules/ci/module.toml +1 -1
  20. package/modules/concurrency/files/docs/concurrent-sessions.md +66 -18
  21. package/modules/concurrency/fragments/AGENTS.md +5 -4
  22. package/modules/concurrency/fragments/gitattributes +2 -2
  23. package/modules/concurrency/gates/concurrency.toml +3 -3
  24. package/modules/concurrency/module.toml +1 -1
  25. package/modules/doc-authority/files/{{registry_path}} +1 -1
  26. package/modules/doc-authority/module.toml +1 -1
  27. package/modules/findings/files/docs/{{backlog.root}}/FINDINGS.md +1 -1
  28. package/modules/findings/gates/findings.toml +5 -0
  29. package/modules/findings/module.toml +1 -1
  30. package/modules/findings/skills/record-finding/SKILL.md +1 -1
  31. package/modules/gates/files/.ai/gates.toml +1 -1
  32. package/modules/gates/fragments/AGENTS.md +6 -5
  33. package/modules/gates/module.toml +1 -1
  34. package/modules/instructions/files/.ai/rules/README.md +2 -2
  35. package/modules/instructions/files/.ai/rungs.mjs +52 -0
  36. package/modules/instructions/files/AGENTS.md +4 -2
  37. package/modules/instructions/files/CLAUDE.md +1 -1
  38. package/modules/instructions/fragments/AGENTS.md +2 -2
  39. package/modules/instructions/gates/core.toml +2 -2
  40. package/modules/instructions/module.toml +1 -1
  41. package/modules/release/files/{{changelog_dir}}/CONSUMED_THROUGH +1 -0
  42. package/modules/release/gates/release.toml +169 -17
  43. package/modules/release/module.toml +9 -5
  44. package/modules/release/skills/cut-release/SKILL.md +43 -15
  45. package/modules/session/files/{{archive}}/README.md +1 -1
  46. package/modules/session/files/{{path}} +2 -2
  47. package/modules/session/module.toml +1 -1
  48. package/modules/specs/files/{{path}}/README.md +2 -2
  49. package/modules/specs/module.toml +1 -1
  50. package/modules/workflows/module.toml +1 -1
  51. package/modules/workflows/rules/planning-tiers.md +1 -1
  52. package/package.json +3 -2
  53. package/src/add.ts +204 -48
  54. package/src/backlog.ts +354 -48
  55. package/src/check.ts +54 -33
  56. package/src/cli.ts +196 -69
  57. package/src/concurrency.ts +628 -42
  58. package/src/detect.ts +11 -3
  59. package/src/emitted-path.ts +274 -0
  60. package/src/engine-table.ts +66 -0
  61. package/src/engines.ts +18 -29
  62. package/src/engines2.ts +403 -20
  63. package/src/engines3.ts +111 -20
  64. package/src/explain.ts +3 -7
  65. package/src/help.ts +43 -0
  66. package/src/lifecycle.ts +86 -27
  67. package/src/manifest.ts +41 -5
  68. package/src/render.ts +106 -21
  69. package/src/selftest.ts +87 -10
  70. package/src/storage-key.ts +20 -0
  71. package/src/substitute.ts +47 -5
  72. package/src/text.ts +11 -0
  73. package/src/types.ts +16 -3
  74. package/src/version-source.ts +144 -0
package/modules/README.md CHANGED
@@ -84,9 +84,27 @@ whether its guard has ever actually fired.
84
84
  `consumed_by = "render"` — `instructions.harnesses` decides which harnesses exist and will never
85
85
  appear in a template. A dead-parameter lint that does not know the difference reports the second
86
86
  kind as unused, and the obvious "fix" deletes it.
87
- 3. **`[provenance]` is required and validated.** Sources, patterns, and the incident. A module with
88
- no traceable source is one somebody invented, and `doctor` cannot ask its questions without the
89
- incident ([ADR-0005](../docs/decisions/ADR-0005-self-instrumentation.md)).
87
+ 3. **`[provenance]` is required and validated, and it declares which kind of module this is.** A
88
+ module with no traceable source is one somebody invented, which is what
89
+ [CLAUDE.md](../CLAUDE.md)'s evidence rule exists to prevent — so say which you are writing.
90
+ `kind = "extracted"` is the default and the shape all fifteen bundled modules use: `sources`,
91
+ `patterns` and `incident`, all required. `kind = "designed"` requires a first-person `rationale`
92
+ and **may not carry `sources` or `incident` at all**, because half a claim reads to a later
93
+ reader exactly like a whole one. `patterns` stays allowed for both: the catalogue entries are
94
+ themselves evidenced, and citing one claims nothing about this module's own history.
95
+ 3a. **The `designed` marker is printed, not merely stored.** `rungs modules` and `rungs add` both
96
+ name it wherever they name the module. This matters more than it looks: `[provenance]` is
97
+ validated at load and **read by no other code path**, so without a surface the distinction would
98
+ be a field nobody sees — which is rule 3's own failure mode wearing a new hat (F-037, F-038).
99
+ Note what this rule used to claim, and did not check: that `doctor` cannot ask its
100
+ [ADR-0005](../docs/decisions/ADR-0005-self-instrumentation.md) tier B question without the
101
+ incident. It can. That question quotes the **gate's `why`**, which `add` copies into
102
+ `.ai/gates.toml` — a different field, in a different file, that has always been the one doing
103
+ the work.
104
+ 3b. **`[conflicts]` is enforced by `add`, and the relation is symmetric.** Declaring
105
+ `conflicts = ["backlog"]` refuses the install and stops, the way a paradigm does, overridable
106
+ with `--confirm-conflict`. Only one side ever has to declare it — a module authored outside this
107
+ package can name a bundled one, and the bundled one will never name it back.
90
108
  4. **Skills stay spec-pure.** Six Agent Skills fields; Claude Code extensions are opted into in
91
109
  `module.toml`, per skill, with the portability cost stated there.
92
110
  5. **Every gate declares a self-test asserting both directions.** A gate whose rules are currently
@@ -131,6 +149,10 @@ whether its guard has ever actually fired.
131
149
  WI-001, where the same inference existed as a comment beside a `""` default, was implemented
132
150
  nowhere, and shipped a dangling `# AGENTS.md — ` into every scaffold. **A default that states
133
151
  its own derivation is checkable; a comment that states it is not.**
152
+ 9b-ii. **`rungs` is the other reserved namespace.** `{{rungs.version}}` is read from the executing
153
+ package's manifest and cannot be set by a module or consumer. It lets generated launchers pin the
154
+ artifact that emitted them without freezing that version as an install parameter: explicitly
155
+ running a newer CLI makes the managed launcher stale, and `upgrade --apply` advances it.
134
156
  9c. **A path parameter may contain separators**, so one parameter places a whole subtree —
135
157
  `files/{{path}}/README.md` with `path = "docs/decisions"`. A second "leaf" parameter is never
136
158
  needed, and adding one was caught and reverted during authoring.
@@ -9,7 +9,7 @@ it is superseded by a new record that names it.
9
9
  ## Index
10
10
 
11
11
  <!-- rungs:begin adr-index -->
12
- <!-- Generated by `rungs render`. -->
12
+ <!-- Generated by `node .ai/rungs.mjs render`. -->
13
13
 
14
14
  | ADR | Title | Status | Date |
15
15
  | --- | --- | --- | --- |
@@ -8,7 +8,7 @@ block = { file = "{{path}}/README.md", marker = "adr-index" }
8
8
  derives = "file-index"
9
9
  sources = ["{{path}}/{{id_prefix}}-*.md"]
10
10
  columns = ["id", "title", "status", "date"]
11
- command = "rungs render"
11
+ command = "node .ai/rungs.mjs render"
12
12
 
13
13
  # ── adr-required-fields ───────────────────────────────────────────────────────
14
14
 
@@ -1,6 +1,6 @@
1
1
  [module]
2
2
  name = "adr"
3
- version = "1.1.0"
3
+ version = "1.2.0"
4
4
  rung = 1
5
5
  summary = "Numbered, immutable decision records, with an admission rule that keeps the directory from becoming a second documentation tree."
6
6
 
@@ -1,8 +1,8 @@
1
- <!-- rungs:begin audit@1.0.0 -->
1
+ <!-- rungs:begin audit@1.1.0 -->
2
2
  ## Assessments
3
3
 
4
4
  **`/assess`** checks one {{subject}} against [`{{criteria_path}}`]({{criteria_path}}) and writes
5
5
  each failure as a **row in the findings register**. Never a document per subject — that shape
6
- reached 268 files in one repo with no way to say which findings were open, and `rungs check`
6
+ reached 268 files in one repo with no way to say which findings were open, and `node .ai/rungs.mjs check`
7
7
  refuses it re-forming.
8
8
  <!-- rungs:end audit -->
@@ -1,6 +1,6 @@
1
1
  [module]
2
2
  name = "audit"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  rung = 2
5
5
  summary = "A repeatable assessment whose output is rows in the findings register, never a document per subject."
6
6
 
@@ -39,7 +39,7 @@ Each failure is one row: severity from the criteria table, what specifically fai
39
39
  (a path, a symbol, a command), when to act, how to fix.
40
40
 
41
41
  **Do not produce an audit document.** One repo in the corpus generated 268 of them and still cannot
42
- say which findings are open — the prompt was fine, the output form was the defect. `rungs check`
42
+ say which findings are open — the prompt was fine, the output form was the defect. `node .ai/rungs.mjs check`
43
43
  refuses the document tree from re-forming.
44
44
 
45
45
  ## 4. Report the run itself
@@ -7,7 +7,7 @@ The board. One row per live work item, grouped by status. Items live in
7
7
  [`items/`](items/); finished work moves to [`archive/`](archive/).
8
8
 
9
9
  <!-- NEXT-ID: {{id_prefix}}-001 -->
10
- <!-- Claim from this marker and bump it on your own branch. `rungs check` refuses a duplicate. -->
10
+ <!-- Claim from this marker and bump it on your own branch. `node .ai/rungs.mjs check` refuses a duplicate. -->
11
11
 
12
12
  ## In progress
13
13
 
@@ -91,14 +91,14 @@ An epic (`type: epic`) has no branch of its own. Membership is a **two-way link*
91
91
  ## 8. Archiving
92
92
 
93
93
  `items/` should hold only work that can still change. Move finished items to `archive/` with
94
- `rungs backlog archive` — it recomputes every link repo-wide, so archived ids still resolve and
94
+ `node .ai/rungs.mjs backlog archive` — it recomputes every link repo-wide, so archived ids still resolve and
95
95
  stay permanently spent.
96
96
 
97
97
  **Never edit an archived item.** If archived work turns out to be wrong, that is a *new* item.
98
98
 
99
99
  ## 9. What keeps this true
100
100
 
101
- Three gates, run by `rungs check`:
101
+ Three gates, run by `node .ai/rungs.mjs check`:
102
102
 
103
103
  | Gate | Refuses |
104
104
  | --- | --- |
@@ -6,7 +6,7 @@
6
6
  Finished work items, moved here so [`../items/`](../items/README.md) holds only work that can still
7
7
  change.
8
8
 
9
- Move them with `rungs backlog archive`, never by hand — it recomputes every link repo-wide, and
9
+ Move them with `node .ai/rungs.mjs backlog archive`, never by hand — it recomputes every link repo-wide, and
10
10
  archived ids stay indexed so citations to them keep resolving and the ids stay permanently spent.
11
11
 
12
12
  **Never edit an archived item.** If archived work turns out to be wrong, that is a new item. The
@@ -7,7 +7,7 @@ One file per work item: `{{id_prefix}}-###-slug.md`, from
7
7
  [`../TEMPLATE.md`](../TEMPLATE.md).
8
8
 
9
9
  This directory holds **only work that can still change.** Finished items move to
10
- [`../archive/`](../archive/) with `rungs backlog archive`, which recomputes every link repo-wide so
10
+ [`../archive/`](../archive/) with `node .ai/rungs.mjs backlog archive`, which recomputes every link repo-wide so
11
11
  archived ids still resolve and stay permanently spent.
12
12
 
13
13
  Never edit an archived item. If archived work turns out to be wrong, that is a new item.
@@ -1,9 +1,9 @@
1
- <!-- rungs:begin backlog@1.0.0 -->
1
+ <!-- rungs:begin backlog@1.2.0 -->
2
2
  ## Work tracking
3
3
 
4
4
  Non-trivial work is a **work item** (`{{id_prefix}}-###`) under
5
5
  [`docs/{{root}}/`](docs/{{root}}/README.md) — that file is the methodology and is mandatory reading
6
6
  before your first item. Branch `{{branch_prefix}}/{{id_prefix}}-###-slug` off
7
7
  `{{integration_branch}}`; claim ids from `NEXT-ID` on your own branch; never scope-creep an item.
8
- Status must agree with git, and `rungs check` enforces it. Execute one with **`/work-item`**.
8
+ Status must agree with git, and `node .ai/rungs.mjs check` enforces it. Execute one with **`/work-item`**.
9
9
  <!-- rungs:end backlog -->
@@ -1,6 +1,6 @@
1
1
  [module]
2
2
  name = "backlog"
3
- version = "1.1.0"
3
+ version = "1.2.0"
4
4
  rung = 1
5
5
  summary = "Work items with stable ids, an eight-status lifecycle, a board, and the three gates that keep the bookkeeping honest."
6
6
 
@@ -64,7 +64,7 @@ Status → `in_progress`.
64
64
  Run the narrowest validation that covers what you touched, then the repo's standard gate set:
65
65
 
66
66
  ```bash
67
- rungs check
67
+ node .ai/rungs.mjs check
68
68
  ```
69
69
 
70
70
  Existing tests are not weakened to make a change pass. If a test is genuinely wrong, that is its
@@ -28,7 +28,7 @@ jobs:
28
28
  node-version: '22'
29
29
 
30
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.
31
+ # `node .ai/rungs.mjs check` locally. A CI-only gate is one nobody can reproduce.
32
32
  #
33
33
  # Plain `check`, no flags. This said `check --tier full --reporter github`:
34
34
  # `--tier` is not a flag the CLI accepts and `--reporter` does not exist at
@@ -38,7 +38,7 @@ jobs:
38
38
  # that installed `ci` had a job that failed on every push and blamed the
39
39
  # repo for it (F-030).
40
40
  - name: Run gates
41
- run: npx @rungs/cli check
41
+ run: node .ai/rungs.mjs check
42
42
 
43
43
  # Add a matrix job per package here rather than a workflow per package.
44
44
  # A checklist step that creates a file creates N files:
@@ -47,5 +47,5 @@ jobs:
47
47
  # matrix:
48
48
  # package: [a, b, c]
49
49
  #
50
- # `rungs check` reports the workflow count and fails past the threshold in
50
+ # `node .ai/rungs.mjs check` reports the workflow count and fails past the threshold in
51
51
  # .ai/gates.toml, for exactly that reason.
@@ -1,6 +1,6 @@
1
1
  [module]
2
2
  name = "ci"
3
- version = "1.1.0"
3
+ version = "1.2.0"
4
4
  rung = 1
5
5
  summary = "One workflow that runs the gate registry, a matrix instead of a file per item, and a check that keeps it that way."
6
6
 
@@ -14,11 +14,11 @@ branch, and they cannot see each other's work.**
14
14
 
15
15
  | | |
16
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 |
17
+ | `node .ai/rungs.mjs 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
+ | `node .ai/rungs.mjs check` | The fast tier. Run it constantly |
19
+ | `node .ai/rungs.mjs preflight` | The integration branch moved: did it change files *you* changed? That, not the commit count, predicts a conflict |
20
+ | `node .ai/rungs.mjs land <branch>` | merge → verify **the merged tree** → atomically advance integration and green refs |
21
+ | `node .ai/rungs.mjs worktrees` | What is finished and prunable. **Reports only** — removing someone else's worktree is not a script's call |
22
22
 
23
23
  ## Do not run the full tier before landing
24
24
 
@@ -35,10 +35,17 @@ deliberate question, never as a ritual.
35
35
 
36
36
  ## A failure is attributed, never just counted
37
37
 
38
- `land` re-runs each failing gate against the merge base in the throwaway worktree it already has,
39
- and reports each failure as **inherited** (already red before you started stated, never blocking)
40
- or **INTRODUCED** (yours blocks). Anything it cannot attribute also blocks: **we do not land on an
41
- unknown.**
38
+ `land` re-runs each failing gate against the merge base in the throwaway worktree it already has.
39
+ A finding is **inherited** only when one independent control reproduces it too: the invoking
40
+ worktree must be clean, detached and at the exact integration commit. That checkout can retain
41
+ ignored dependencies a fresh worktree does not have. If the exact control passes while the scratch
42
+ fails, the scratch runtime is **UNVERIFIED**, not evidence that the branch inherited a red gate.
43
+ An attached, dirty or different-commit invoking worktree cannot be the control, so a scratch
44
+ failure then blocks and the merge is parked for recovery.
45
+
46
+ With that control established, each failure is reported as **inherited** (already red before you
47
+ started — stated, never blocking) or **INTRODUCED** (yours — blocks). Anything it cannot attribute
48
+ also blocks: **we do not land on an unknown.**
42
49
 
43
50
  Attribution is **per finding, not per gate.** The first implementation compared gate ids, which
44
51
  made an already-red gate a blind spot: a branch could add new broken links and land them as
@@ -54,22 +61,63 @@ fix is a gate you learn to bypass, and a bypassed gate reports nothing.*
54
61
 
55
62
  ## Land, then move the tip — in that order
56
63
 
57
- `land` merges into a scratch `{{integ_prefix}}…` ref, verifies **that** tree, and only then
58
- fast-forwards with a compare-and-swap. Two things follow:
64
+ `land` merges in a detached scratch worktree, verifies **that** tree, and only then advances the
65
+ integration and green refs together in one expected-old transaction. Both writes target the exact,
66
+ direct stored refs without following symbolic refs. Two things follow:
59
67
 
60
68
  - **The integration branch cannot go red from a merge nobody verified** — the ref update is
61
- unreachable otherwise, and a refusal leaves it bit-for-bit unchanged with the merged tree parked
62
- for you to fix.
69
+ unreachable otherwise, and a refusal does not partially advance integration or its verified
70
+ marker. The merged tree is kept for you to fix.
63
71
  - **Concurrent landing is refused, not silently merged.** A real lock names its holder and start
64
- time and is taken over if the holder died.
65
-
66
- **Nothing keeps the integration branch checked out.** If something needs it checked out, that is a
67
- bug in whatever it is doing.
72
+ time and is taken over if the holder died. Git also compares both managed refs with the values
73
+ captured before verification; a competing change wins without being overwritten.
74
+
75
+ **Nothing keeps the integration or green ref checked out.** `land` refuses before gates if a
76
+ worktree holds either one, then checks exact ref identity and holders again after arbitrary gate
77
+ code and immediately before the transaction. Case-aliased and symbolic configured refs are
78
+ rejected because their spelling can otherwise make checkout discovery and ref mutation disagree.
79
+ A worktree branch spelling that collides by the same Windows/APFS key counts as a holder even when
80
+ its branch ref is dangling, so Rungs will not create a differently spelled managed or recovery ref
81
+ behind that worktree's stale index and files.
82
+
83
+ Rungs queries the repository's ref backend before checking raw stored refs. Git versions that
84
+ predate that query are files-only and may either reject the option or echo it while exiting zero;
85
+ both responses use the files scanner. An actual backend other than `files` or `reftable` is refused
86
+ until Rungs can enumerate it safely.
87
+
88
+ There is one deliberately narrow raw-Git boundary. After the last identity check but before Git
89
+ takes a lock, another process can replace a validated direct ref with a same-object-ID symbolic ref,
90
+ or create a dangling symbolic ref at the recovery name Rungs is about to create. Git's public ref
91
+ transaction compares object IDs (or absence), not direct-versus-symbolic type. `--no-deref`
92
+ protects the symbolic target. Supported Git versions differ on whether create-only refuses a
93
+ dangling symbolic name or replaces the name itself, so Rungs cannot rely on one portable ref-type
94
+ precondition. Do not mutate managed or recovery refs outside the Rungs land protocol; closing these
95
+ micro-windows requires a cooperative repository-wide transaction protocol, not another
96
+ before/after check.
97
+
98
+ ## Recovery refs are retained
99
+
100
+ When a verified merge cannot advance, `land` reports the exact branch that preserves it. The
101
+ preferred name is `{{integ_prefix}}<branch>`. If that branch is checked out or already preserves
102
+ different work, Rungs creates a collision-free name containing the full merge identity instead.
103
+ Creation is compare-and-swap and never follows a symbolic ref.
104
+
105
+ Within that cooperative protocol, Rungs never overwrites or deletes an observed recovery branch,
106
+ including after a later successful land. Cleanup is an explicit operator decision: inspect the
107
+ reported ref and every worktree holding it, then delete it only when its work is no longer needed.
108
+ Repeating the same refused land reuses an unheld recovery ref that already points at the identical
109
+ merge instead of accumulating duplicates.
68
110
 
69
111
  ## Three things to know when a land refuses
70
112
 
71
113
  - **Your worktree is untouched.** `land` does all its work in a throwaway worktree of its own, so
72
114
  whatever you had checked out is still checked out. A refusal costs you nothing to recover from.
115
+ - **Use the recovery ref that the refusal reports.** Its name can carry a merge-identity suffix when
116
+ the preferred `{{integ_prefix}}…` name is held or contains other work. Do not assume the preferred
117
+ name was changed, and remove recovery refs only after inspecting them.
118
+ - **Run from the detached integration checkout when inherited red is possible.** All-green merges
119
+ need no baseline control. A failing scratch can be attributed as inherited only when the invoking
120
+ worktree is clean, detached and still at the integration commit with its gate runtime available.
73
121
  - **Check the exit code of `land` itself.** Piping it through `tail` or `grep` reports *that*
74
122
  command's status, so a refused land reads as success.
75
123
  - **Reconcile generated artifacts by regenerating, never by merging text.** Take one side, re-run
@@ -88,6 +136,6 @@ scheduling instead.
88
136
 
89
137
  ## Worktree lifetime is item lifetime
90
138
 
91
- Delete the branch when it lands. `rungs worktrees` measures whether that happened and flags
139
+ Delete the branch when it lands. `node .ai/rungs.mjs worktrees` measures whether that happened and flags
92
140
  worktrees that are merged and clean — and, more usefully, ones that are merged **and dirty**, where
93
141
  uncommitted work is sitting in a branch that has already landed.
@@ -1,10 +1,11 @@
1
- <!-- rungs:begin concurrency@1.0.0 -->
1
+ <!-- rungs:begin concurrency@1.4.0 -->
2
2
  ## Concurrent sessions
3
3
 
4
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`
5
+ [`docs/concurrent-sessions.md`](docs/concurrent-sessions.md). Cut with `node .ai/rungs.mjs session start`
6
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
7
+ `node .ai/rungs.mjs land` — **never `git merge` by hand**. Do not run the full tier before landing: it widens
8
8
  the window the merge then conflicts in. Reconcile generated artifacts by **regenerating**, never
9
- by merging text.
9
+ by merging text. Keep the integration and green refs out of every worktree while landing. A refused
10
+ land reports the exact recovery ref it preserved; Rungs never deletes that ref for you.
10
11
  <!-- rungs:end concurrency -->
@@ -1,5 +1,5 @@
1
- # rungs:begin concurrency@1.0.0
2
- # Merge drivers per conflict class. INERT until `rungs setup git` installs them — the driver names
1
+ # rungs:begin concurrency@1.4.0
2
+ # Merge drivers per conflict class. INERT until `node .ai/rungs.mjs setup git` installs them — the driver names
3
3
  # below mean nothing to git on a fresh clone, and merges quietly fall back to the default. For a
4
4
  # generated artifact that default is a clean text merge of a file that is a fixed point of nothing.
5
5
 
@@ -6,7 +6,7 @@
6
6
  refuse_checked_out = ["{{integration_branch}}"]
7
7
  scope = "all-worktrees"
8
8
  message = """
9
- `{{integration_branch}}` is checked out in {worktree}. Nothing should hold it: `rungs land` runs
9
+ `{{integration_branch}}` is checked out in {worktree}. Nothing should hold it: `node .ai/rungs.mjs land` runs
10
10
  from wherever you are, verifies on a scratch ref, and advances the branch with a compare-and-swap.
11
11
  Holding it checked out blocks every other session and does not prevent concurrent landing anyway —
12
12
  switching to the scratch ref releases it mid-run, and two land processes have been measured running
@@ -23,12 +23,12 @@ at once under exactly that design.
23
23
  attributes_file = ".gitattributes"
24
24
  required_drivers = ["rungs-ledger", "rungs-generated"]
25
25
  verify_installed = true
26
- install_command = "rungs setup git"
26
+ install_command = "node .ai/rungs.mjs setup git"
27
27
  message = """
28
28
  The merge drivers named in .gitattributes are not installed in this clone, so they are inert and
29
29
  merges are falling back to git's default. For a generated artifact that default is a clean text
30
30
  merge of a file that is a fixed point of nothing — it surfaces later as a mismatch with an
31
- uninformative diff. Run `rungs setup git` once per clone.
31
+ uninformative diff. Run `node .ai/rungs.mjs setup git` once per clone.
32
32
  """
33
33
 
34
34
  # ── self-tests ────────────────────────────────────────────────────────────────
@@ -1,6 +1,6 @@
1
1
  [module]
2
2
  name = "concurrency"
3
- version = "1.2.0"
3
+ version = "1.4.0"
4
4
  rung = 5
5
5
  summary = "Many sessions on one integration branch: a verified base to cut from, attributed failures, a land protocol that cannot redden the branch, and merge drivers per conflict class."
6
6
 
@@ -9,7 +9,7 @@ before any content is written.** If the answer is missing, add the row before wr
9
9
  ## The registry
10
10
 
11
11
  The third column is what makes this enforceable rather than aspirational: it converts "one source
12
- of truth" from a principle into a lookup, and it is what `rungs check` reads.
12
+ of truth" from a principle into a lookup, and it is what `node .ai/rungs.mjs check` reads.
13
13
 
14
14
  | Topic | Owner | Must NOT appear in |
15
15
  | --- | --- | --- |
@@ -1,6 +1,6 @@
1
1
  [module]
2
2
  name = "doc-authority"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  rung = 4
5
5
  summary = "One owner per topic, declared in a registry and checked — plus a propagation gate for rules that live in more surfaces than their authority."
6
6
 
@@ -46,7 +46,7 @@ reason**:
46
46
  | **fixed** | It was resolved directly | What changed, and where |
47
47
  | **dismissed** | It is not a problem, or not one worth solving | *Why not* — this is the one people skip, and it is the one that stops the same observation being recorded again next month |
48
48
 
49
- `rungs check` refuses a closed finding with no reason.
49
+ `node .ai/rungs.mjs check` refuses a closed finding with no reason.
50
50
 
51
51
  ## What this register does not do
52
52
 
@@ -73,6 +73,11 @@ gate = "findings-disposition-has-reason"
73
73
  expect = "pass"
74
74
  fixture = { table = "Open", row = { Id = "{{id_prefix}}-002", Sev = "high", Pri = "now", What = "x", Evidence = "none — hunch" } }
75
75
 
76
+ [[self_test]]
77
+ gate = "findings-disposition-has-reason"
78
+ expect = "pass"
79
+ fixture = { table = "Open", row = { Id = "—", Sev = "", Pri = "", What = "*nothing open*", Evidence = "" } }
80
+
76
81
  [[self_test]]
77
82
  gate = "findings-self-declared-closure"
78
83
  expect = "fail"
@@ -1,6 +1,6 @@
1
1
  [module]
2
2
  name = "findings"
3
- version = "1.1.0"
3
+ version = "1.2.0"
4
4
  rung = 1
5
5
  summary = "A register for what is noticed before it is decided, with a triage path into work items."
6
6
 
@@ -54,7 +54,7 @@ Every finding leaves **Open** by exactly one disposition, each with a written re
54
54
 
55
55
  **Dismissal is the one that matters.** It is the disposition people skip, and its reason is what
56
56
  stops the same observation being recorded again next month by someone who cannot tell it was
57
- already considered. `rungs check` refuses a closed finding with no reason.
57
+ already considered. `node .ai/rungs.mjs check` refuses a closed finding with no reason.
58
58
 
59
59
  ## Before triaging, re-check
60
60
 
@@ -1,6 +1,6 @@
1
1
  # Gate registry.
2
2
  #
3
- # Every gate this repo runs is declared here, and `rungs check` runs them. Modules add their own
3
+ # Every gate this repo runs is declared here, and `node .ai/rungs.mjs check` runs them. Modules add their own
4
4
  # entries on install — those live inside marked blocks and are regenerated; anything outside a
5
5
  # block is yours and is never touched.
6
6
 
@@ -1,8 +1,9 @@
1
- <!-- rungs:begin gates@1.0.0 -->
1
+ <!-- rungs:begin gates@1.1.0 -->
2
2
  ## Gates
3
3
 
4
- `rungs check` runs everything in [`.ai/gates.toml`](.ai/gates.toml) — fast tier constantly, full
5
- tier at a boundary. **Never weaken a gate to make a change pass**; if a gate is wrong that is its
6
- own work. Every rule you add declares `gated` or `review-only` — there is no third option. Broke a
7
- rule that already existed? Do not restate it, make it mechanical: **`/harden-rule`**.
4
+ `node .ai/rungs.mjs check` runs everything in [`.ai/gates.toml`](.ai/gates.toml) — fast tier
5
+ constantly, full tier at a boundary. **Never weaken a gate to make a change pass**; if a gate is
6
+ wrong that is its own work. Every rule you add declares `gated` or `review-only` — there is no third
7
+ option. Broke a rule that already existed? Do not restate it, make it mechanical:
8
+ **`/harden-rule`**.
8
9
  <!-- rungs:end gates -->
@@ -1,6 +1,6 @@
1
1
  [module]
2
2
  name = "gates"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  rung = 1
5
5
  summary = "The gate runner, the registry every other module registers into, the run ledger, and the generic structural gate set."
6
6
 
@@ -1,8 +1,8 @@
1
1
  # Path-scoped rules
2
2
 
3
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.
4
+ `.cursor/rules/` are generated — do not edit those.** Run `node .ai/rungs.mjs render` after changing
5
+ anything here; `node .ai/rungs.mjs check` refuses a stale rendering.
6
6
 
7
7
  A rule here loads only when an agent touches a matching file, so it costs nothing until it is
8
8
  relevant. That is what keeps [`../../AGENTS.md`](../../AGENTS.md) inside its line budget.
@@ -0,0 +1,52 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+
5
+ const pinnedPackageSpec = '@rungs/cli@{{rungs.version}}';
6
+ const pinnedVersion = pinnedPackageSpec.slice('@rungs/cli@'.length);
7
+ const requested = process.argv.slice(2);
8
+
9
+ // The launcher cannot discover an upgrade while running its current pin. An
10
+ // upgrade therefore names the next exact version explicitly; the newer CLI
11
+ // rewrites this managed file to make that version the normal pin. Tags, ranges,
12
+ // URLs and paths are refused.
13
+ const exactVersion = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?(?:\+[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?$/;
14
+ let selectedVersion = pinnedVersion;
15
+ if (requested[0] === 'upgrade' && requested[1] === '--to') {
16
+ if (!exactVersion.test(requested[2] ?? '')) {
17
+ console.error('rungs launcher: upgrade --to requires an exact version such as 1.2.3 or 1.2.3-beta.1');
18
+ process.exit(1);
19
+ }
20
+ selectedVersion = requested[2];
21
+ requested.splice(1, 2);
22
+ } else if (requested.includes('--to')) {
23
+ console.error('rungs launcher: --to is only valid immediately after upgrade');
24
+ process.exit(1);
25
+ }
26
+
27
+ const packageSpec = selectedVersion === pinnedVersion ? pinnedPackageSpec : `@rungs/cli@${selectedVersion}`;
28
+ const npmArgs = ['exec', '--yes', `--package=${packageSpec}`, '--', 'rungs', ...requested];
29
+
30
+ // npm is a JavaScript program. Calling its entry point through this Node process
31
+ // avoids a shell on every platform, so an argument cannot become shell syntax.
32
+ // npm sets npm_execpath when it launched the parent; standard Node installers
33
+ // also place npm beside node. POSIX can safely fall back to its executable.
34
+ const adjacentNpm = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
35
+ const inheritedNpm = process.env.npm_execpath;
36
+ const npmCli = inheritedNpm && existsSync(inheritedNpm) ? inheritedNpm : existsSync(adjacentNpm) ? adjacentNpm : undefined;
37
+
38
+ if (!npmCli && process.platform === 'win32') {
39
+ console.error('rungs launcher: could not locate npm-cli.js beside Node; install npm or invoke this command from npm');
40
+ process.exitCode = 1;
41
+ } else {
42
+ const child = npmCli
43
+ ? spawnSync(process.execPath, [npmCli, ...npmArgs], { stdio: 'inherit', windowsHide: true })
44
+ : spawnSync('npm', npmArgs, { stdio: 'inherit' });
45
+
46
+ if (child.error) {
47
+ console.error(`rungs launcher: ${child.error.message}`);
48
+ process.exitCode = 1;
49
+ } else {
50
+ process.exitCode = child.status ?? 1;
51
+ }
52
+ }
@@ -5,7 +5,9 @@ Canonical agent policy for this repository. Read in full before making changes.
5
5
  This is the **always-on** document: facts every session needs. Anything that applies to one part of
6
6
  the tree belongs in [`.ai/rules/`](.ai/rules/README.md) instead, and anything that is a multi-step
7
7
  procedure belongs in a skill. Both load only when relevant, and this file has a line budget that
8
- `rungs check` enforces.
8
+ `node .ai/rungs.mjs check` enforces. Use `node .ai/rungs.mjs <command>` for every Rungs command;
9
+ the committed launcher pins the exact CLI version this repository has reviewed. To upgrade, choose
10
+ the next immutable version explicitly: `node .ai/rungs.mjs upgrade --to <exact-version> --apply`.
9
11
 
10
12
  ## What this is
11
13
 
@@ -19,7 +21,7 @@ everything is slow enough to get skipped; running the wrong subset proves nothin
19
21
  | Change surface | Run |
20
22
  | --- | --- |
21
23
  | *(fill in per surface)* | |
22
- | anything | `rungs check` |
24
+ | anything | `node .ai/rungs.mjs check` |
23
25
 
24
26
  ## Task loop
25
27
 
@@ -5,7 +5,7 @@
5
5
  no second copy to drift (ADR-0001). The import is used rather than a symlink because a symlink
6
6
  requires Administrator or Developer Mode on Windows.
7
7
 
8
- Add Claude-specific content below this comment. It is yours; `rungs upgrade` will not touch it.
8
+ Add Claude-specific content below this comment. It is yours; `node .ai/rungs.mjs upgrade` will not touch it.
9
9
  -->
10
10
 
11
11
  ## Claude Code
@@ -1,8 +1,8 @@
1
- <!-- rungs:begin instructions@1.1.0 -->
1
+ <!-- rungs:begin instructions@1.2.0 -->
2
2
  ## Repo map
3
3
 
4
4
  <!-- rungs:begin repo-map -->
5
- <!-- Generated. Run `rungs render` after moving directories. -->
5
+ <!-- Generated. Run `node .ai/rungs.mjs render` after moving directories. -->
6
6
  <!-- rungs:end repo-map -->
7
7
  ## Execution boundary
8
8
 
@@ -23,7 +23,7 @@ id = "rules"
23
23
  sources = [".ai/rules/**/*.md"]
24
24
  exclude = [".ai/rules/README.md"] # the scaffold doc is not a rule
25
25
  targets = [".claude/rules/**/*.md", ".github/instructions/**/*.instructions.md", ".cursor/rules/**/*.mdc"]
26
- command = "rungs render"
26
+ command = "node .ai/rungs.mjs render"
27
27
  # Targets are checked only for harnesses in the installed matrix; the rest are ignored rather
28
28
  # than reported missing.
29
29
 
@@ -34,7 +34,7 @@ id = "repo-map"
34
34
  block = { file = "AGENTS.md", marker = "repo-map" }
35
35
  derives = "directory-tree"
36
36
  depth = 2
37
- command = "rungs render"
37
+ command = "node .ai/rungs.mjs render"
38
38
 
39
39
  # ── instructions-shell-backticks (hook: pre-tool-use) ─────────────────────────
40
40
  #