@rungs/cli 0.3.0 → 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.
- package/README.md +6 -6
- package/dist/cli.js +2194 -488
- package/dist/cli.js.map +4 -4
- package/modules/README.md +25 -3
- package/modules/adr/files/{{path}}/README.md +1 -1
- package/modules/adr/gates/adr.toml +1 -1
- package/modules/adr/module.toml +1 -1
- package/modules/audit/fragments/AGENTS.md +2 -2
- package/modules/audit/module.toml +1 -1
- package/modules/audit/skills/assess/SKILL.md +1 -1
- package/modules/backlog/files/docs/{{root}}/BACKLOG.md +1 -1
- package/modules/backlog/files/docs/{{root}}/README.md +2 -2
- package/modules/backlog/files/docs/{{root}}/archive/README.md +1 -1
- package/modules/backlog/files/docs/{{root}}/items/README.md +1 -1
- package/modules/backlog/fragments/AGENTS.md +2 -2
- package/modules/backlog/module.toml +1 -1
- package/modules/backlog/skills/work-item/SKILL.md +1 -1
- package/modules/ci/files/{{workflow_path}} +3 -3
- package/modules/ci/module.toml +1 -1
- package/modules/concurrency/files/docs/concurrent-sessions.md +66 -18
- package/modules/concurrency/fragments/AGENTS.md +5 -4
- package/modules/concurrency/fragments/gitattributes +2 -2
- package/modules/concurrency/gates/concurrency.toml +3 -3
- package/modules/concurrency/module.toml +1 -1
- package/modules/doc-authority/files/{{registry_path}} +1 -1
- package/modules/doc-authority/module.toml +1 -1
- package/modules/findings/files/docs/{{backlog.root}}/FINDINGS.md +1 -1
- package/modules/findings/gates/findings.toml +5 -0
- package/modules/findings/module.toml +1 -1
- package/modules/findings/skills/record-finding/SKILL.md +1 -1
- package/modules/gates/files/.ai/gates.toml +1 -1
- package/modules/gates/fragments/AGENTS.md +6 -5
- package/modules/gates/module.toml +1 -1
- package/modules/instructions/files/.ai/rules/README.md +2 -2
- package/modules/instructions/files/.ai/rungs.mjs +52 -0
- package/modules/instructions/files/AGENTS.md +4 -2
- package/modules/instructions/files/CLAUDE.md +1 -1
- package/modules/instructions/fragments/AGENTS.md +2 -2
- package/modules/instructions/gates/core.toml +2 -2
- package/modules/instructions/module.toml +1 -1
- package/modules/release/files/{{changelog_dir}}/CONSUMED_THROUGH +1 -0
- package/modules/release/gates/release.toml +169 -17
- package/modules/release/module.toml +9 -5
- package/modules/release/skills/cut-release/SKILL.md +43 -15
- package/modules/session/files/{{archive}}/README.md +1 -1
- package/modules/session/files/{{path}} +2 -2
- package/modules/session/module.toml +1 -1
- package/modules/specs/files/{{path}}/README.md +2 -2
- package/modules/specs/module.toml +1 -1
- package/modules/workflows/module.toml +1 -1
- package/modules/workflows/rules/planning-tiers.md +1 -1
- package/package.json +3 -2
- package/src/add.ts +204 -48
- package/src/backlog.ts +354 -48
- package/src/check.ts +54 -33
- package/src/cli.ts +196 -69
- package/src/concurrency.ts +628 -42
- package/src/detect.ts +11 -3
- package/src/emitted-path.ts +274 -0
- package/src/engine-table.ts +66 -0
- package/src/engines.ts +40 -32
- package/src/engines2.ts +424 -29
- package/src/engines3.ts +115 -23
- package/src/explain.ts +3 -7
- package/src/help.ts +43 -0
- package/src/lifecycle.ts +95 -31
- package/src/manifest.ts +41 -5
- package/src/render.ts +106 -21
- package/src/selftest.ts +87 -10
- package/src/storage-key.ts +20 -0
- package/src/substitute.ts +47 -5
- package/src/text.ts +11 -0
- package/src/types.ts +16 -3
- 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
|
|
88
|
-
no traceable source is one somebody invented,
|
|
89
|
-
|
|
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.
|
|
@@ -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
|
|
package/modules/adr/module.toml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<!-- rungs:begin audit@1.
|
|
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 -->
|
|
@@ -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.
|
|
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 -->
|
|
@@ -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:
|
|
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.
|
package/modules/ci/module.toml
CHANGED
|
@@ -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** →
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
| --- | --- | --- |
|
|
@@ -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"
|
|
@@ -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.
|
|
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
|
|
5
|
-
tier at a boundary. **Never weaken a gate to make a change pass**; if a gate is
|
|
6
|
-
own work. Every rule you add declares `gated` or `review-only` — there is no third
|
|
7
|
-
rule that already existed? Do not restate it, make it mechanical:
|
|
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,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
|
|
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
|
+
<!-- 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
|
#
|