ultra-builder-pro-cli 5.0.0 → 6.0.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 (159) hide show
  1. package/CHANGELOG.md +113 -3
  2. package/README.md +67 -478
  3. package/README.zh-CN.md +49 -422
  4. package/adapters/_shared/frontmatter.cjs +1 -18
  5. package/adapters/_shared/plugin-core.cjs +17 -83
  6. package/adapters/_shared/runtime-assets.cjs +6 -120
  7. package/adapters/claude.js +8 -24
  8. package/adapters/codex.js +16 -38
  9. package/bin/install.js +35 -57
  10. package/docs/PLUGIN-ISOLATION-CONTRACT.md +13 -101
  11. package/docs/examples/drive/claude-reviewer.sh +53 -0
  12. package/docs/examples/drive/claude-writer.sh +43 -0
  13. package/engine/FORMATS.md +1051 -0
  14. package/engine/checks.cjs +139 -0
  15. package/engine/cli.cjs +148 -0
  16. package/engine/completion.cjs +48 -0
  17. package/engine/context.cjs +94 -0
  18. package/engine/drive.cjs +220 -0
  19. package/engine/expect-worker.cjs +5 -0
  20. package/engine/expect.cjs +33 -0
  21. package/engine/formats.cjs +189 -0
  22. package/engine/git-state.cjs +112 -0
  23. package/engine/ledger.cjs +205 -0
  24. package/engine/loading.cjs +28 -0
  25. package/engine/merge.cjs +273 -0
  26. package/engine/milestone.cjs +89 -0
  27. package/engine/plan.cjs +232 -0
  28. package/engine/project.cjs +5 -0
  29. package/engine/signals.cjs +3 -0
  30. package/engine/states.cjs +175 -0
  31. package/hooks/session-start.cjs +54 -0
  32. package/package.json +17 -52
  33. package/skills/ultra/SKILL.md +395 -0
  34. package/.ultra-template/changes/abandoned/.gitkeep +0 -0
  35. package/.ultra-template/changes/active/.gitkeep +0 -1
  36. package/.ultra-template/changes/archive/.gitkeep +0 -1
  37. package/.ultra-template/contexts/TEMPLATE.md +0 -135
  38. package/.ultra-template/decisions/.gitkeep +0 -1
  39. package/.ultra-template/evidence/.gitkeep +0 -1
  40. package/.ultra-template/north-star.md +0 -52
  41. package/.ultra-template/project-brief.md +0 -28
  42. package/.ultra-template/research/.gitkeep +0 -1
  43. package/.ultra-template/specs/architecture.md +0 -81
  44. package/.ultra-template/specs/discovery.md +0 -74
  45. package/.ultra-template/specs/product.md +0 -80
  46. package/.ultra-template/specs/research-distillate.md +0 -57
  47. package/.ultra-template/tasks.json +0 -4
  48. package/.ultra-template/test-report.json +0 -44
  49. package/adapters/_shared/host-profile.cjs +0 -5
  50. package/adapters/grok.js +0 -242
  51. package/adapters/kimi.js +0 -232
  52. package/adapters/opencode.js +0 -408
  53. package/adapters/zcode.js +0 -375
  54. package/bin/delegate-worker.cjs +0 -5
  55. package/bin/delegate.cjs +0 -5
  56. package/docs/ARCHITECTURE.md +0 -274
  57. package/docs/ARTIFACT-AUTHORITY.md +0 -305
  58. package/docs/DECISIONS.md +0 -229
  59. package/docs/PHILOSOPHY.md +0 -261
  60. package/docs/RUNTIME-COMPAT-MATRIX.md +0 -315
  61. package/docs/SKILL-AUTHORING.md +0 -113
  62. package/docs/WORKFLOW-LIFECYCLE.md +0 -350
  63. package/docs/evals/adversarial-review-2026-08-14.md +0 -84
  64. package/docs/evals/ultra-5-0-journey-results.md +0 -62
  65. package/docs/evals/ultra-5-0-live-acp-conformance.md +0 -93
  66. package/docs/evals/ultra-5-0-route-journeys.md +0 -145
  67. package/docs/evals/zcode-automation-2026-08-14.md +0 -103
  68. package/hooks/README.md +0 -52
  69. package/hooks/_common.py +0 -1779
  70. package/hooks/adapters/codex.py +0 -295
  71. package/hooks/adapters/grok.py +0 -134
  72. package/hooks/adapters/kimi.py +0 -156
  73. package/hooks/adapters/zcode.py +0 -227
  74. package/hooks/block_dangerous_commands.py +0 -201
  75. package/hooks/compact_context.py +0 -224
  76. package/hooks/mid_workflow_recall.py +0 -47
  77. package/hooks/post_edit_guard.py +0 -215
  78. package/hooks/session_context.py +0 -741
  79. package/skills/ultra-change/SKILL.md +0 -165
  80. package/skills/ultra-change/references/change-contract.md +0 -199
  81. package/skills/ultra-change/references/execution-grant.md +0 -109
  82. package/skills/ultra-change/references/primary-transfer.md +0 -273
  83. package/skills/ultra-change/scripts/validate_primary_transfer.cjs +0 -1928
  84. package/skills/ultra-delegate/SKILL.md +0 -195
  85. package/skills/ultra-delegate/patches/zcode-acp-0.1.0-zcode-0.16.5.patch +0 -184
  86. package/skills/ultra-delegate/references/delegation-contract.md +0 -138
  87. package/skills/ultra-delegate/scripts/acp-conformance.cjs +0 -287
  88. package/skills/ultra-delegate/scripts/acp-driver.cjs +0 -762
  89. package/skills/ultra-delegate/scripts/agent-execution-driver.cjs +0 -320
  90. package/skills/ultra-delegate/scripts/cli-driver.cjs +0 -61
  91. package/skills/ultra-delegate/scripts/delegate-profiles/kimi-read-only.md +0 -15
  92. package/skills/ultra-delegate/scripts/delegate-profiles/kimi-write.md +0 -17
  93. package/skills/ultra-delegate/scripts/delegate-worker.cjs +0 -328
  94. package/skills/ultra-delegate/scripts/delegate.cjs +0 -690
  95. package/skills/ultra-delegate/scripts/delegate_wait.py +0 -86
  96. package/skills/ultra-delegate/scripts/host-profile.cjs +0 -267
  97. package/skills/ultra-deliver/SKILL.md +0 -194
  98. package/skills/ultra-deliver/references/baseline-reconciliation.md +0 -41
  99. package/skills/ultra-dev/SKILL.md +0 -265
  100. package/skills/ultra-dev/references/debugging.md +0 -22
  101. package/skills/ultra-domain-modeling/SKILL.md +0 -79
  102. package/skills/ultra-grilling/SKILL.md +0 -121
  103. package/skills/ultra-grilling/references/reframing.md +0 -76
  104. package/skills/ultra-init/SKILL.md +0 -143
  105. package/skills/ultra-init/references/brownfield-adoption.md +0 -32
  106. package/skills/ultra-init/scripts/init_project.cjs +0 -1031
  107. package/skills/ultra-plan/SKILL.md +0 -160
  108. package/skills/ultra-plan/references/task-evidence-v2.md +0 -300
  109. package/skills/ultra-plan/scripts/validate_task_evidence.cjs +0 -859
  110. package/skills/ultra-research/SKILL.md +0 -167
  111. package/skills/ultra-research/references/00-problem-validation.md +0 -36
  112. package/skills/ultra-research/references/01-opportunity-discovery.md +0 -33
  113. package/skills/ultra-research/references/02-market-assessment.md +0 -31
  114. package/skills/ultra-research/references/03-alternatives.md +0 -31
  115. package/skills/ultra-research/references/04-product-strategy.md +0 -41
  116. package/skills/ultra-research/references/05-assumptions-validation.md +0 -31
  117. package/skills/ultra-research/references/10-user-personas.md +0 -30
  118. package/skills/ultra-research/references/11-user-scenarios.md +0 -28
  119. package/skills/ultra-research/references/20-user-stories.md +0 -31
  120. package/skills/ultra-research/references/21-features-scope.md +0 -38
  121. package/skills/ultra-research/references/22-success-metrics.md +0 -33
  122. package/skills/ultra-research/references/30-architecture-context.md +0 -30
  123. package/skills/ultra-research/references/31-solution-strategy.md +0 -28
  124. package/skills/ultra-research/references/32-building-blocks.md +0 -28
  125. package/skills/ultra-research/references/40-deployment.md +0 -29
  126. package/skills/ultra-research/references/41-quality-risks.md +0 -28
  127. package/skills/ultra-research/references/99-synthesis.md +0 -59
  128. package/skills/ultra-research/references/north-star-v2.md +0 -168
  129. package/skills/ultra-research/references/wayfinding.md +0 -50
  130. package/skills/ultra-research/scripts/validate_north_star.cjs +0 -1174
  131. package/skills/ultra-review/SKILL.md +0 -275
  132. package/skills/ultra-review/references/adversarial-evaluation.md +0 -31
  133. package/skills/ultra-review/references/code.md +0 -17
  134. package/skills/ultra-review/references/comments.md +0 -12
  135. package/skills/ultra-review/references/design.md +0 -12
  136. package/skills/ultra-review/references/errors.md +0 -13
  137. package/skills/ultra-review/references/review-modes.md +0 -29
  138. package/skills/ultra-review/references/spec.md +0 -18
  139. package/skills/ultra-review/references/tests.md +0 -12
  140. package/skills/ultra-review/references/unified-schema.md +0 -284
  141. package/skills/ultra-review/references/worker-packet.md +0 -183
  142. package/skills/ultra-review/scripts/review_wait.py +0 -2943
  143. package/skills/ultra-status/SKILL.md +0 -209
  144. package/skills/ultra-status/references/route-contract.md +0 -100
  145. package/skills/ultra-tdd/SKILL.md +0 -91
  146. package/skills/ultra-tdd/references/templates/README.md +0 -20
  147. package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +0 -79
  148. package/skills/ultra-tdd/references/templates/persistence-real.ts +0 -78
  149. package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +0 -78
  150. package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +0 -73
  151. package/skills/ultra-tdd/references/templates/vertical-slice.ts +0 -66
  152. package/skills/ultra-tdd/references/test-execution.md +0 -23
  153. package/skills/ultra-test/SKILL.md +0 -244
  154. package/skills/ultra-test/references/export-syntax.md +0 -18
  155. package/skills/ultra-test/scripts/validate_review_transport.cjs +0 -549
  156. package/skills/ultra-test/scripts/worktree_digest.cjs +0 -576
  157. package/skills/ultra-think/SKILL.md +0 -56
  158. package/skills/ultra-think/references/autonomy-boundary.md +0 -48
  159. package/skills/ultra-think/references/interaction-boundary.md +0 -42
@@ -0,0 +1,1051 @@
1
+ # UBP v6 engine: truth files and CLI contract
2
+
3
+ The engine is deterministic code. It reads the project's truth files, runs registered
4
+ checks, records evidence, and computes progress. The model writes intent, specification
5
+ and plan; it never writes progress, evidence or completion records by hand.
6
+
7
+ Plan of record: `thinking/back-to-essentials-20261030/RESULT.md` (§3.1, §3.3–3.5, §3.9, §4,
8
+ §5 P1–P4).
9
+
10
+ ## Layout
11
+
12
+ All paths are relative to the project root: the top level of the Git work tree, which
13
+ contains `.ultra/`.
14
+
15
+ | Path | Layer | Written by |
16
+ |---|---|---|
17
+ | `.ultra/spec.md` | L1 behavior specification | planner (model); Owner confirms |
18
+ | `.ultra/checks.json` | acceptance check registry | acceptance author (model) |
19
+ | `.ultra/plan.json` | L3 milestones and task graph | planner (model) |
20
+ | `.ultra/rulings.md` | ruling log | any role, append only |
21
+ | `.ultra/ledger/evidence.jsonl` | check runs | engine only |
22
+ | `.ultra/ledger/plan-log.jsonl` | plan snapshots and changes | engine only |
23
+ | `.ultra/ledger/nodes.jsonl` | node completions | engine only |
24
+ | `.ultra/ledger/milestones.jsonl` | milestone closures | engine only (`milestone close`) |
25
+ | `.ultra/ledger/approvals.jsonl` | the Owner's approvals of reductions | engine only (`approve`) |
26
+ | `.ultra/records/milestones/<milestone>-review.md` | milestone review and its verdict | reviewer (never the author) |
27
+ | `.ultra/records/handoffs/<node>.md` | what a finished node hands to its dependents | the node's writer |
28
+ | `.ultra/records/…` | other records, such as the interview and the Owner-touch log | the method |
29
+ | `.ultra/.runtime/checks/<run>.log` | full check output | engine; disposable |
30
+ | `.ultra/.runtime/engine.lock` | writer lock | engine; disposable |
31
+ | `.ultra/.runtime/engine.lock.break` | serializes breaking a dead writer's lock | engine; disposable |
32
+ | `.ultra/.runtime/merge/<id>/` | the merge gate's temporary work tree | engine; disposable |
33
+ | `.ultra/.runtime/merge.lock` | held by the one running merge | engine; disposable |
34
+ | `.ultra/.runtime/merge.lock.break` | serializes breaking a dead merge's lock | engine; disposable |
35
+ | `.ultra/.runtime/drive.lock` | held by the one running drive | engine; disposable |
36
+ | `.ultra/.runtime/drive.lock.break` | serializes breaking a dead drive's lock | engine; disposable |
37
+
38
+ Missing ledger files mean "no records". Missing `rulings.md` means "no rulings".
39
+ `spec.md`, `checks.json` and `plan.json` are required. `.ultra/.runtime/` is never committed.
40
+
41
+ The engine-owned paths are `.ultra/spec.md`, `.ultra/checks.json`, `.ultra/plan.json`,
42
+ `.ultra/rulings.md`, `.ultra/ledger/`, `.ultra/records/` and `.ultra/.runtime/`. Everything
43
+ else, including other files under `.ultra/` such as check scripts, is code (see "Git
44
+ state"). A check must not read files under `.ultra/records/`: editing a record never
45
+ invalidates evidence, so a check that read one could stay green after the record changed.
46
+
47
+ Reading a record file (a milestone review or a handoff): the engine reads it only when its
48
+ parent directory exists and resolves, through symbolic links, inside the root (both paths
49
+ compared after resolving), and the file, opened without following a symbolic link in its
50
+ last component and without blocking, is a regular file. A symbolic link, a FIFO, a device
51
+ or a directory at that path is never read. Git stores symbolic links, so a cloned project
52
+ can carry one; this rule keeps a record from pulling a file from outside the project into
53
+ the engine's output, and from making a command wait.
54
+
55
+ The ledgers are committed project history, like the truth files. When two branches append
56
+ to the same ledger, Git's union merge keeps both sides' lines; projects add this line to
57
+ `.gitattributes`:
58
+
59
+ ```
60
+ .ultra/ledger/*.jsonl merge=union
61
+ ```
62
+
63
+ Line order in a ledger therefore carries no meaning except as a tie-breaker (see "Ledger
64
+ records").
65
+
66
+ ## Markdown rules
67
+
68
+ `spec.md`, `rulings.md` and milestone reviews are read line by line:
69
+
70
+ - A fence opens on a line indented by at most three spaces that starts with at least three
71
+ backticks or at least three tildes. For a backtick fence, the rest of the line must not
72
+ contain a backtick; otherwise the line is ordinary text (inline code such as
73
+ ```` ```npm test``` must print ````). A fence closes on a line indented by at most three
74
+ spaces that holds at least as many of the opening character and otherwise only
75
+ whitespace. Lines inside a fence are ordinary text.
76
+ - A fence still open at the end of the file is an error: `spec.md` is malformed, and in
77
+ `rulings.md` it is a `malformed-ruling` problem. Either message names the opening line.
78
+ In a milestone review it is not an error; the lines after the opening are inside it.
79
+ - Headings are ATX headings outside fences: a line indented by at most three spaces that
80
+ starts with `#` or `##` followed by a space, a tab or the end of the line. Setext
81
+ underlines (`---`, `===`) are ordinary text.
82
+ - Messages about a line use the form `<file>:<line>: <problem>`, with physical 1-based
83
+ line numbers.
84
+
85
+ ## `.ultra/spec.md` — requirements
86
+
87
+ A requirement starts at a level-2 heading `## REQ-<id> <title>`. `<id>` matches
88
+ `[A-Za-z0-9][A-Za-z0-9._-]*`. The block runs until the next level-1 or level-2 heading, or
89
+ the end of the file. Other level-2 headings (for example `## Notes`) are not requirements
90
+ and end the previous block.
91
+
92
+ Requirement text: the block's lines (heading included) after CRLF→LF, with trailing
93
+ whitespace removed from every line and leading/trailing empty lines dropped, joined with
94
+ `\n`. The requirement hash is the SHA-256 hex of that text in UTF-8. Any other edit to
95
+ the block changes the hash.
96
+
97
+ Malformed: a level-2 heading starting with `REQ-` whose id does not match the pattern; a
98
+ duplicate id; a fence still open at the end of the file; a near-miss heading, that is a
99
+ line outside fences, indented by at most three spaces, made of one or more `#`, then any
100
+ whitespace (Unicode whitespace included) or none, then `REQ` in any letter case followed
101
+ by `-`, by `_`, or by whitespace and a digit, which is not a requirement heading (for
102
+ example `### REQ-3`, `##REQ-4`, `## REQ-5` with an ideographic space, `## Req-7`,
103
+ `## REQ 8`, `## req_9`), so these mistypings cannot drop a requirement from the count.
104
+ Headings such as `## Requirements` or `## Req mapping` are ordinary sections.
105
+
106
+ Shared text: every line of the spec outside the requirement blocks, that is the lines
107
+ before the first requirement and every section under a level-1 or level-2 heading that is
108
+ not a requirement (for example `## Direction` or `## Interface`), headings included. Its
109
+ hash is the SHA-256 hex, in UTF-8, of those lines after CRLF→LF, each with trailing
110
+ whitespace removed, empty lines dropped, joined with `\n`; with no such line it is the hash
111
+ of the empty string. So blank lines and trailing spaces never change it, and any other
112
+ edit does.
113
+
114
+ The shared text holds what every requirement relies on (direction, interface
115
+ conventions). The engine cannot tell which requirements a shared line concerns, so a
116
+ change to it invalidates as if every requirement had changed (see "States"). A convention
117
+ that concerns one requirement belongs in that requirement's block, where changing it
118
+ invalidates only that requirement.
119
+
120
+ ## `.ultra/checks.json` — check registry
121
+
122
+ ```json
123
+ {
124
+ "version": 1,
125
+ "checks": [
126
+ { "id": "C-add", "covers": ["REQ-001"], "run": "node --test test/add.test.js",
127
+ "timeout_sec": 120, "expect": "pass [1-9]" }
128
+ ]
129
+ }
130
+ ```
131
+
132
+ - `id`: unique, `[A-Za-z0-9][A-Za-z0-9._-]*`.
133
+ - `covers`: array of requirement ids, non-empty except for an invariant. This is the only
134
+ link between checks and requirements.
135
+ - `fix`: optional non-empty string. A check with `fix` is an invariant (RESULT §3.7): a
136
+ rule the code must always keep, such as a size limit or a forbidden import, with the
137
+ way to repair a violation written in `fix`. An invariant covers no requirement
138
+ (`covers: []`) and belongs to no milestone.
139
+ - `run`: non-empty shell command (see "Running a check").
140
+ - `timeout_sec`: optional positive integer, at most 86400; default 600.
141
+ - `expect`: required, non-empty regular expression source (JavaScript, flags `mu`) that
142
+ does not match the empty string (`^`, `x*` and `(?:)` are malformed). A zero exit counts
143
+ as a pass only when the output tail matches it. It is the acceptance author's statement
144
+ of what proves that the check really ran, for example `pass [1-9]` for `node --test`,
145
+ so a test filter that matched nothing never passes.
146
+
147
+ Check fingerprint: SHA-256 hex of `JSON.stringify([run, effectiveTimeoutSec, expect])`.
148
+ `covers` is not part of the fingerprint; requirement links are tracked per requirement.
149
+
150
+ Malformed: invalid JSON, wrong types, missing fields, duplicate id, empty or invalid
151
+ `expect`, an `expect` that matches the empty string, an empty `covers` without `fix`, a
152
+ `fix` that is not a non-empty string, a `fix` on a check that covers a requirement.
153
+
154
+ ## `.ultra/plan.json` — milestones and nodes
155
+
156
+ ```json
157
+ {
158
+ "version": 1,
159
+ "milestones": [
160
+ { "id": "M1", "title": "Core list", "requirements": ["REQ-001", "REQ-002"],
161
+ "journey": "C-add" }
162
+ ],
163
+ "nodes": [
164
+ { "id": "N-01", "milestone": "M1", "title": "Add items", "covers": ["REQ-001"],
165
+ "depends": [], "scope": ["src/items/"] }
166
+ ]
167
+ }
168
+ ```
169
+
170
+ - Milestone and node ids: unique within their kind, `[A-Za-z0-9][A-Za-z0-9._-]*`.
171
+ - Array order is plan order. `requirements`, `covers` and `depends` are arrays of
172
+ strings; `scope` is an optional array of path prefixes, required by `ubp drive` (see
173
+ "Drive (unattended run)").
174
+ - `kind`: optional; the only value is `"cleanup"`. A cleanup node (RESULT §3.7) covers no
175
+ requirement (`covers: []`): the cleaner scans the code against the invariants and the
176
+ project's principles and turns what it finds into small nodes or fixes inside its
177
+ scope. Every other node covers at least one requirement (else `empty-node`).
178
+ - `cleanup_every`: optional top-level positive integer K. When present, the plan gate
179
+ requires every milestone to end, in plan order, with a cleanup node, and no run of
180
+ more than K consecutive other nodes within a milestone (`cleanup-missing`).
181
+ - `journey`: optional check id of the milestone's end-to-end journey, the check that walks
182
+ the user's path through the milestone's behavior. `milestone close` requires one; the
183
+ plan gate requires that it names a registered check covering at least one of the
184
+ milestone's requirements. It is not part of the milestone's snapshot. Ceiling: switching
185
+ the journey to another check that already covers the milestone is not a recorded plan
186
+ change.
187
+
188
+ Malformed: invalid JSON, wrong types (including a `journey` that is not a string),
189
+ missing fields, duplicate ids, a `kind` other than `"cleanup"`, a cleanup node that covers
190
+ a requirement, a `cleanup_every` that is not a positive integer.
191
+
192
+ ## `.ultra/rulings.md` — ruling log
193
+
194
+ Every line outside fences whose first non-space characters are a list marker (`-`, `*` or
195
+ `+` followed by a space) is a ruling entry; other lines are free text. The canonical form
196
+ is:
197
+
198
+ ```
199
+ - 2026-10-31 L1 裁决:<decision> — 因为 <reason> — 如果判断错了,代价是 <cost>
200
+ ```
201
+
202
+ An entry is unindented and has, in this order, a date `YYYY-MM-DD`, a layer `L0`–`L4`,
203
+ `裁决` and the decision, `因为` and the reason, and `代价` (optionally preceded by
204
+ `如果判断错了` and followed by `是`) and the cost. The reading is tolerant: between and
205
+ around the parts any run of spaces and punctuation (dashes `—`, `–`, `-`, colons, commas,
206
+ full- or half-width), or none, is accepted. The decision, the reason and the cost must each
207
+ hold a character that is neither a space nor punctuation; the line passes when some split
208
+ gives all three such content (so a reason may start with `代价`), and a reason that is only
209
+ `如果判断错了` is empty.
210
+
211
+ ## Git state
212
+
213
+ - Code fingerprint: SHA-256 hex of two outputs concatenated, each record keeping its
214
+ terminating NUL: `git ls-tree --full-tree -z HEAD` without the record whose path is
215
+ `.ultra`, then `git ls-tree --full-tree -z HEAD -- .ultra/` (the entries directly
216
+ inside `.ultra/`) without the records of engine-owned paths. Tree ids commit to their
217
+ contents, so the fingerprint covers every committed file except the engine-owned ones;
218
+ it does not depend on Git's path-quoting settings. Edits to engine-owned files never
219
+ invalidate evidence; any committed change elsewhere, including a check script kept under
220
+ `.ultra/`, does, which forces full regression (the ratchet).
221
+ - Dirty: `git --no-optional-locks status --porcelain -z --untracked-files=normal`
222
+ reports a path that is neither an engine-owned path nor inside one (for a rename or
223
+ copy, either path). An untracked `.ultra/` directory as a whole is therefore dirty;
224
+ the engine's own untracked ledgers and runtime files never are.
225
+ - Environment: every Git command the engine runs, and every check it runs, gets the
226
+ environment without the variables `git rev-parse --local-env-vars` lists (`GIT_DIR`,
227
+ `GIT_WORK_TREE`, `GIT_INDEX_FILE` and the like), so `--root`, or the merge gate's
228
+ temporary work tree, alone decides the repository. A Git hook exports those variables.
229
+ The list includes `GIT_CONFIG_PARAMETERS` and `GIT_CONFIG_COUNT`, so configuration passed
230
+ through the environment (`git -c`, CI settings) does not reach the engine's Git commands.
231
+ The engine also removes `GIT_LITERAL_PATHSPECS`, `GIT_GLOB_PATHSPECS`,
232
+ `GIT_NOGLOB_PATHSPECS` and `GIT_ICASE_PATHSPECS`, so its pathspecs (the merge gate's
233
+ `':(icase).ultra'`) mean the same in every environment.
234
+ - The engine never limits the size of Git's output: a top-level directory with tens of
235
+ thousands of entries, or as many modified files, must work.
236
+ - Usage errors (exit 2), each with its own message and hint: `--root` does not exist; the
237
+ directory is not inside a Git work tree (hint: `git init`); the repository has no commit
238
+ (hint: commit the code first).
239
+ - A Git command the engine expects to succeed that fails for another reason (identity,
240
+ a hook, signing, a broken repository) exits 2 with the message
241
+ `Git failed: <arguments>: <first non-empty line of Git's standard error>` (only the
242
+ arguments when Git wrote nothing), so the reason reaches the user.
243
+
244
+ ## Running a check
245
+
246
+ - The command runs as `/bin/sh -c <run>` in the project root, inherits the engine's
247
+ environment, and gets its own process group.
248
+ - When the shell exits, the engine kills that process group (a group that is already gone
249
+ is fine), so background children never outlive the check. At the timeout it kills the
250
+ group the same way. A `kill` that fails with `ESRCH` or `EPERM` (macOS answers `EPERM`
251
+ for a group whose processes have all exited) is not an error.
252
+ - Standard input is empty. Standard output and standard error both go straight to the log
253
+ file, through one open file so their interleaving is kept. The engine reads no pipe, so
254
+ a descendant that left the group cannot keep the engine waiting, and the engine's peak
255
+ memory does not grow with the size of the output. Ceiling: such a descendant may keep
256
+ appending to the log after the engine has read it.
257
+ - After the shell exits or is killed, the engine reads the last 4 MiB of the log (the
258
+ output tail); `expect` and `error_line` are evaluated on it.
259
+ - `expect` is evaluated with a limit of 5 seconds, so a pathological pattern cannot hang
260
+ the engine; reaching the limit gives outcome `unknown` with reason `expect-timeout`.
261
+ - Evidence is dirty when the tree is dirty before or after the run, or when the code
262
+ fingerprint after the run differs from the one before it.
263
+ - When the engine receives SIGINT, SIGTERM or SIGHUP during a run, it kills the running
264
+ check's process group, records nothing for that run, and exits 130, 143 or 129
265
+ respectively. Ceiling: an engine killed with SIGKILL cannot clean up, and its check's
266
+ process group keeps running until it ends by itself.
267
+
268
+ ## Ledger records (JSON Lines, append only)
269
+
270
+ Reading: every non-empty line must be a valid record of its file. A line that is not
271
+ makes every command exit 2 with `<file>:<line>: malformed ledger record` and a hint to
272
+ remove or repair that line; the engine never rewrites earlier lines. One exception: when a
273
+ file does not end with a newline and its last line is not a valid record, that line is an
274
+ incomplete write. It is ignored, reported in `warnings`, and removed (the file is truncated
275
+ after its last newline) before the engine's next append to that file.
276
+
277
+ `at` is ISO-8601 UTC with milliseconds (`YYYY-MM-DDTHH:MM:SS.mmmZ`), so string order is
278
+ time order; a record whose `at` has another form is malformed. When the engine appends a
279
+ record, its `at` is the current time or, when that is not later than the greatest `at`
280
+ in any of the five ledgers, 1 ms after it; the engine holds the lock while it reads the
281
+ ledgers and appends. A clock that runs fast or steps back therefore cannot make a later
282
+ record look older than an earlier one, in the same file or across files (the plan-change
283
+ count compares plan-log times with closure times).
284
+
285
+ Latest record: the record with the greatest `at`; ties go to the later line.
286
+
287
+ Evidence (`evidence.jsonl`), one per executed check:
288
+
289
+ ```json
290
+ {"v":1,"run":"<unique id>","check":"C-add","at":"<ISO time>","outcome":"pass|fail|unknown",
291
+ "reason":"<see below>","exit_code":0,"duration_ms":812,"code":"<code fingerprint>",
292
+ "dirty":false,"check_fp":"<check fingerprint>","reqs":{"REQ-001":"<requirement hash>"},
293
+ "shared":"<shared text hash>","log":".ultra/.runtime/checks/<run>.log","error_line":null}
294
+ ```
295
+
296
+ | Observation | outcome | reason |
297
+ |---|---|---|
298
+ | exit 0, output tail matches `expect` | pass | `exit-0` |
299
+ | exit 0, output tail does not match `expect` | unknown | `expect-unmet` |
300
+ | exit 0, `expect` reached its time limit | unknown | `expect-timeout` |
301
+ | exit 126 or 127 | unknown | `not-runnable` |
302
+ | other non-zero exit | fail | `exit-<n>` |
303
+ | killed by the timeout | unknown | `timeout` |
304
+ | killed by another signal | unknown | `signal-<NAME>`, for example `signal-SIGTERM` |
305
+ | the shell could not be started | unknown | `spawn-error` |
306
+
307
+ `reqs` holds the current hashes of the covered requirements that exist in the spec;
308
+ `shared` holds the hash of the spec's shared text when the check ran.
309
+ `duration_ms` is measured with a monotonic clock, so a wall clock that steps back during a
310
+ run never makes it negative and never loses the run.
311
+ `error_line` is null on pass; otherwise the first line of the output tail that matches
312
+ `/^\s*(?:✖|not ok\b|FAIL(?:ED)?\b)|\b(?:[A-Z][A-Za-z]*)?Error\b/u`, else the last non-empty
313
+ line, cut to 200 characters.
314
+
315
+ Plan log (`plan-log.jsonl`), written by a passing gate:
316
+
317
+ ```json
318
+ {"v":1,"type":"baseline|change|removed","milestone":"M1","at":"<ISO time>",
319
+ "shared":"<shared text hash>","requirements":{"REQ-001":"<hash>"},"nodes":["N-01"],
320
+ "checks":{"C-add":{"fp":"<check fingerprint>","covers":["REQ-001"]}},
321
+ "diff":{"added":[],"removed":[],"modified":[],"nodes_added":[],"nodes_removed":[],
322
+ "checks_added":[],"checks_removed":[],"checks_modified":[]}}
323
+ ```
324
+
325
+ A milestone's snapshot is the shared text hash; its requirement ids mapped to current
326
+ hashes; its node ids, sorted; and every check that covers at least one of its requirements,
327
+ mapped to the check's fingerprint and the sorted ids of the milestone's requirements it
328
+ covers. So removing a check, narrowing what it covers or editing it is a plan change of
329
+ every milestone it touches: it is recorded with its diff, and until then the gate is
330
+ `stale`. `diff` is present on `change` and `removed`; every id list in it is sorted in
331
+ code-unit order (JavaScript's default sort). `diff` also holds `"shared": true` when the
332
+ snapshot's shared hash differs from the previous record's, or the previous record has none;
333
+ the key is absent otherwise. What the gate records is defined under "Plan gate".
334
+
335
+ Node completion (`nodes.jsonl`):
336
+ `{"v":1,"node":"N-01","at":"…","code":"…","reqs":{"REQ-001":"…"},"shared":"…","runs":["…"]}`.
337
+
338
+ `shared` in evidence, plan-log and node completion records is optional for reading: records
339
+ written before P3 lack it and stay valid. When present it must be a SHA-256 hex, and a
340
+ plan-log `diff.shared`, when present, must be `true`. Every record the engine writes now
341
+ carries `shared`.
342
+
343
+ Upgrading a project whose records predate P3: its evidence, nodes and plan-log records have
344
+ no `shared`, so every check and every done node turns `stale` (`shared-changed`) and the
345
+ gate is `stale`. Run `ubp gate` once (it records a `change` for every milestone, with an
346
+ otherwise empty diff and `shared: true`, which raises `本里程碑计划变更` until the next
347
+ closure), then `ubp check`, then `ubp node done` for each node that was done, in dependency
348
+ order. A milestone review should read those records as the upgrade, not as a change of
349
+ intent.
350
+
351
+ Approval (`approvals.jsonl`), written by `approve`:
352
+
353
+ ```json
354
+ {"v":1,"items":["M2/C-delete-page","M2/REQ-delete-page"],"at":"<ISO time>",
355
+ "owner":{"words":"<the Owner's words, verbatim>","proxy":false}}
356
+ ```
357
+
358
+ `items` is non-empty, sorted in code-unit order, without duplicates; each item is
359
+ `<milestone id>/<requirement or check id>`, both parts matching the id pattern of
360
+ "`.ultra/spec.md`". `owner.words` is a non-empty string and `owner.proxy` a boolean.
361
+
362
+ Milestone closure (`milestones.jsonl`), written by `milestone close`:
363
+
364
+ ```json
365
+ {"v":1,"milestone":"M1","at":"<ISO time>","code":"<code fingerprint>",
366
+ "reqs":{"REQ-001":"<requirement hash>"},"journey":{"check":"C-add","run":"<run id>"},
367
+ "review":{"path":".ultra/records/milestones/M1-review.md","sha256":"<file hash>"},
368
+ "owner":{"words":"<the Owner's words, verbatim>","proxy":false}}
369
+ ```
370
+
371
+ `reqs` holds the current hashes of the milestone's requirements; `journey.run` is the
372
+ journey check's `last_run`; `review.sha256` is the SHA-256 hex of the review file's bytes,
373
+ so a later edit to the review can be detected. A record holding only `v`, `milestone` and
374
+ `at` is still valid (a closure recorded before P2); every other field, when present, must
375
+ have the type shown. Ceiling: the engine cannot tell a hand-written closure from its own.
376
+
377
+ Writing: the engine checks every record against these rules before it appends it. A record
378
+ that would not be valid (for example an `at` past year 9999, reached through the time
379
+ floor) is not written, and the command exits 2, so an append never leaves a ledger that
380
+ every later command rejects.
381
+
382
+ ## States
383
+
384
+ A check's latest record decides its state; an older record never stands in for a newer
385
+ one. A check's `last_run` is the `run` of its latest record, or null.
386
+
387
+ Check state, and requirement-check pair state, evaluated in this order:
388
+
389
+ 1. No record → `unknown` with reason `not-run`. Latest record dirty → `unknown` with
390
+ reason `dirty-run` (a failure observed on a dirty tree must not leave an older pass
391
+ standing).
392
+ 2. Stale reasons are collected, in this order: `check-changed` (fingerprint differs),
393
+ `code-changed` (code fingerprint differs), `shared-changed` (the shared text hash differs
394
+ from the record's `shared`, or the record has none), `req-changed:<id>` (a covered
395
+ requirement's hash differs from, or is missing in, the record's `reqs`). For a pair, only
396
+ the pair's own requirement can contribute `req-changed`. Any stale reason → `stale`.
397
+ 3. Otherwise the outcome: pass → `green` with no reasons; fail → `red` and unknown →
398
+ `unknown`, each with the record's `reason` as its only reason.
399
+
400
+ Invariant state: the check state of the invariant. An invariant is not green when its
401
+ state is anything but `green` (`unknown`, `stale` and `red` alike).
402
+
403
+ Requirement state: no covering check → `uncovered`. Otherwise over its pairs: any `red`
404
+ → `red`; else any `unknown` → `unknown`; else any `stale` → `stale`; else `green`.
405
+ Only `green` counts as done; `unknown` and `uncovered` never do.
406
+
407
+ Node state:
408
+
409
+ - `done`: a completion record exists, its `shared` equals the current shared text hash,
410
+ and every covered requirement's current hash equals the hash in the node's latest
411
+ record.
412
+ - `stale`: a completion record exists but its `shared` differs or is missing (reason
413
+ `shared-changed`, first), or some covered requirement's hash differs or is missing
414
+ (reasons `req-changed:<id>`).
415
+ - `ready`: no completion record and every dependency is `done`. A cleanup node's
416
+ dependencies are its `depends` and every node before it in its milestone (plan order),
417
+ as `node done` requires; here and in "actionable" below.
418
+ - `blocked`: otherwise.
419
+
420
+ A node is actionable when it is `ready`, or `stale` with every dependency `done`.
421
+
422
+ Invalidation precision: a node turns stale only when the text of a requirement it covers,
423
+ or the shared text, changes, so only the nodes behind a changed requirement are worked
424
+ again. A check's evidence is tied to the whole code fingerprint, so any commit to code
425
+ makes every check stale: the engine cannot know which code a check exercises, and a
426
+ narrower rule could keep a broken check green. After any code commit, `ubp check` reruns
427
+ every check, which is the ratchet: a requirement of the current or a closed milestone that
428
+ was green must be green again. Ceiling: a project whose full check run is slow pays it
429
+ after every node; the upgrade path is per-check input prefixes, valid only where a check's
430
+ inputs can be stated and enforced.
431
+
432
+ Closed milestones: those with a closure record. Current milestone: the first milestone in
433
+ plan order that is not closed; if all are closed, the last milestone; with no milestones,
434
+ none.
435
+
436
+ Next item, in this order:
437
+
438
+ 1. the first actionable node, in plan order, among the nodes of the closed milestones and
439
+ of the current milestone;
440
+ 2. `检查` when some requirement of those milestones, or some invariant, is not green;
441
+ 3. `全部完成` when the plan has at least one milestone and every milestone is closed;
442
+ 4. `里程碑门` when there is a current milestone and every one of its nodes is done;
443
+ 5. otherwise `无(受阻)`.
444
+
445
+ Closed not green: the number of requirements of closed milestones other than the current
446
+ milestone whose state is not green.
447
+
448
+ Plan changes of the current milestone: the number of `change` and `removed` records, of
449
+ any milestone, whose `at` is later than the greatest `at` in `milestones.jsonl` (all
450
+ such records when there is no closure). Renaming or shrinking the current milestone, or
451
+ changing a closed one, therefore never resets the count.
452
+
453
+ Gate state: `fail` when the plan gate reports any problem; otherwise `stale` when a gate
454
+ run would record something; otherwise `pass`.
455
+
456
+ ## Progress line
457
+
458
+ ```
459
+ <M> · <green>/<total> 绿[ · <n> 红][ · <n> 过期][ · <n> 未知][ · <n> 未覆盖] · 下一项 <next> · 本里程碑计划变更 <k> 次[ · 不变量 <n> 未绿][ · 已关闭里程碑 <n> 项未绿][ · 工作区未提交][ · 计划门未通过| · 计划门待重跑][ · drive 运行中]
460
+ ```
461
+
462
+ Counts cover the current milestone's requirements that exist in the spec; the five state
463
+ counts always sum to the total. A bracketed count appears only when it is non-zero;
464
+ `工作区未提交` appears when the tree is dirty now; the gate segment appears when the gate
465
+ state is not `pass`; `drive 运行中` appears while a live process holds
466
+ `.ultra/.runtime/drive.lock` (see "Drive (unattended run)"). With no current milestone, `<M>` is `无`.
467
+
468
+ ## Plan gate
469
+
470
+ Problems are objects `{ "code", "message", "hint" }`; the hint says how to fix it.
471
+
472
+ | code | condition |
473
+ |---|---|
474
+ | `unknown-requirement` | a milestone, node or check names a requirement absent from the spec |
475
+ | `unplanned-requirement` | a spec requirement is in no milestone |
476
+ | `requirement-in-multiple-milestones` | a requirement is in more than one milestone |
477
+ | `empty-plan` | `plan.json` has no milestones |
478
+ | `empty-milestone` | a milestone has no requirements or no nodes |
479
+ | `empty-node` | a node covers no requirement |
480
+ | `uncovered-requirement` | a planned requirement has no covering check |
481
+ | `requirement-without-node` | a planned requirement is covered by no node of its milestone |
482
+ | `unknown-milestone` | a node names a milestone that does not exist |
483
+ | `node-outside-milestone` | a node covers a requirement of another milestone |
484
+ | `unknown-dependency` | a node depends on a node that does not exist |
485
+ | `dependency-on-later-milestone` | a node depends on a node of a later milestone |
486
+ | `dependency-cycle` | the dependency graph has a cycle |
487
+ | `unknown-journey` | a milestone's `journey` names a check absent from `checks.json` |
488
+ | `journey-outside-milestone` | a milestone's `journey` check covers none of its requirements |
489
+ | `cleanup-missing` | with `cleanup_every` K: a milestone does not end with a cleanup node, or holds more than K consecutive other nodes; one problem per milestone |
490
+ | `reduction-unapproved` | the plan drops a requirement or a check from a milestone, or narrows what a check covers, without a recorded approval (below) |
491
+ | `malformed-ruling` | a ruling entry in `rulings.md` does not have the ruling form, or a fence there is never closed |
492
+
493
+ `reduction-unapproved` problems, one per unapproved item, follow every other plan problem,
494
+ and `malformed-ruling` comes last.
495
+
496
+ Reductions. For each milestone id in the plan log whose latest record is not `removed`,
497
+ in order of first appearance in the log, compare that record's snapshot with the
498
+ milestone's current snapshot (empty when the milestone is no longer in `plan.json`). Its
499
+ reduction items are, requirements first, then checks, each group sorted by id:
500
+
501
+ - `<milestone>/<requirement>` for every requirement in the record that the current
502
+ snapshot lacks (deleted from the spec, or moved to another milestone);
503
+ - `<milestone>/<check>` for every check in the record that the current snapshot lacks, and
504
+ for every check in both whose current `covers` lacks a requirement that the record's
505
+ `covers` held and that is still in the current snapshot.
506
+
507
+ An item is approved when some record in `approvals.jsonl` lists it and has an `at` later
508
+ than both the milestone's latest plan-log record and its closure, if any: an approval
509
+ answers the plan as it stood, so a later recorded change of that milestone, or its closure,
510
+ leaves an unused approval behind. One item covers any reduction of what it names: approving
511
+ `M1/C-12` to narrow it also allows deleting it. Each unapproved item is one problem; its
512
+ message names the item and its hint names `ubp approve <item> --owner "<words>"`. Once the
513
+ gate passes, it records the change, and the item is no longer in the latest snapshot, so
514
+ an approval is used once. Only the Owner approves a reduction (RESULT §3.2, §3.7): the
515
+ ratchet keeps every check of a requirement until they do.
516
+
517
+ Renaming a milestone removes everything it held from the old id (see "What a passing gate
518
+ records"), so a rename needs the same approval for each of the old milestone's items.
519
+
520
+ Ceiling: the engine sees removals, not meaning. These pass the gate as modifications:
521
+ rewording a requirement so that it asks for less; weakening the assertions inside a check's
522
+ test file; changing a check's `run` or `expect` in `checks.json` so it proves less; deleting
523
+ a convention from the shared text. The method has the planner classify such an edit as a
524
+ reduction and record the Owner's approval with `ubp approve` before making it; the
525
+ milestone review reads the plan-log diffs. Approval records are checked for their shape
526
+ only, so a hand-written line counts like one written by `ubp approve`. An invariant belongs to
527
+ no milestone, so removing one is no plan change: it needs no approval and leaves no
528
+ plan-log record; the method records it as a ruling.
529
+
530
+ What a passing gate records, in this order:
531
+
532
+ 1. For each milestone in `plan.json`, in plan order: `baseline` when it has no plan-log
533
+ record; `change` when its latest record is of type `removed` or holds a different
534
+ snapshot, with `diff` against that record.
535
+ 2. For each milestone id in the plan log whose latest record is not `removed` and that is
536
+ no longer in `plan.json`, in order of first appearance in the log: `removed`, with
537
+ empty `requirements`, `nodes` and `checks` and a `diff` whose `removed`,
538
+ `nodes_removed` and `checks_removed` list everything that record held.
539
+
540
+ A renamed milestone therefore appears as `removed` plus `baseline`, and no requirement can
541
+ leave the plan without a record that names it.
542
+
543
+ ## Milestone gate
544
+
545
+ `milestone close <id> --owner <words> --review <path> [--proxy]` records that the Owner let
546
+ milestone `<id>` pass after seeing its demo, its journey result, its plan changes and the
547
+ rulings (RESULT §3.9).
548
+
549
+ - `--owner`: the Owner's words, verbatim; required, and not empty after trimming.
550
+ - `--proxy`: the words were given by the assistant on the Owner's behalf. The closure
551
+ records `proxy: true`, so it reads as not validated by the real Owner.
552
+ - `--review`: the milestone review, a path relative to the project root. An absolute path,
553
+ one that leaves the root, and one that names the root itself (`.`) are usage errors. The
554
+ reviewer is never the author of the milestone's code; the recommended place is
555
+ `.ultra/records/milestones/<id>-review.md`. The closure records the path relative to the
556
+ root without `.` or `..` segments, with `/` separators (`./.ultra/records/x.md` is
557
+ recorded as `.ultra/records/x.md`).
558
+ - `--owner` and `--review` are each given once; repeating either is a usage error. An
559
+ option value is the next argument; a value that starts with `--` is read as a missing
560
+ value (ceiling: Owner words cannot start with `--`).
561
+
562
+ Holding the lock, the command reloads the project and collects every problem, in this
563
+ order:
564
+
565
+ | code | condition |
566
+ |---|---|
567
+ | `plan-gate` | the plan gate state is not `pass` |
568
+ | `dirty-tree` | the tree is dirty |
569
+ | `not-current` | `<id>` is already closed, or a milestone before it in plan order is not closed |
570
+ | `node-not-done` | a node of the milestone is not done; one problem per node |
571
+ | `requirement-not-green` | a requirement of this milestone or of a closed milestone is not green; one problem per requirement |
572
+ | `no-journey` | the milestone names no `journey` |
573
+ | `journey-not-green` | the milestone's `journey` names a registered check whose check state is not `green` |
574
+ | `invariant-not-green` | an invariant is not green; one problem per invariant, its hint the invariant's `fix` when its state is `red`, otherwise ``Run `ubp check <id>`.`` (a stale or unknown invariant has not been run on this code, so its `fix` would mislead) |
575
+ | `review-missing` | the review cannot be read under "Reading a record file" (see "Layout"): nothing is there, or it is a symbolic link, a FIFO, a directory or another file that is not regular, or its parent directory does not exist or resolves outside the root |
576
+ | `review-not-accepted` | the review is a regular file, but outside fences (see "Markdown rules") it does not hold exactly one line starting with `Verdict:`, or that line, without trailing whitespace, is not `Verdict: accept` (lines end at LF; a CR before it is trailing whitespace; a UTF-8 byte order mark at the start of the file is ignored) |
577
+
578
+ The bytes that were checked are the bytes that are hashed. A FIFO at the review path is
579
+ `review-missing` at once, never a wait while the lock is held. With no problem the command appends
580
+ the closure record.
581
+
582
+ The journey must be green as a check, not only through its pairs: a journey that also
583
+ covers a requirement of a later milestone turns `stale` when that requirement is edited,
584
+ while the pair for this milestone's requirement stays green, so `requirement-not-green`
585
+ alone would not catch it.
586
+
587
+ Ceiling: records never dirty the tree, so the engine does not require the review to be
588
+ committed. The method commits it with the closure record; `review.sha256` lets anyone
589
+ compare the committed file with what the gate accepted.
590
+
591
+ Text output is `milestone <id>: pass` or `milestone <id>: fail` followed by one
592
+ `<code>: <message> — <hint>` line per problem.
593
+
594
+ ## Node brief
595
+
596
+ `context <id> [--budget <bytes>]` prints the brief a writer needs for node `<id>`, compiled
597
+ from the truth files and records (RESULT §3.5), and nothing else. The same files give the
598
+ same bytes. Layout, with the sections in this order:
599
+
600
+ ```text
601
+ # Node <id>: <title>
602
+
603
+ Milestone <milestone id>: <milestone title> · node state <state>
604
+
605
+ ## Requirements
606
+
607
+ <requirement text>
608
+
609
+ ## Checks that must turn green (read-only)
610
+
611
+ - <check id> · <state> · `<run>`
612
+
613
+ ## Scope
614
+
615
+ - <prefix>
616
+
617
+ ## Handoffs from dependencies
618
+
619
+ ### <dependency id>
620
+
621
+ <handoff>
622
+
623
+ ## Rules
624
+
625
+ <rules>
626
+ ```
627
+
628
+ - Milestone line: when the node's milestone is not in `plan.json`, it reads
629
+ `Milestone <milestone id>: not in .ultra/plan.json · node state <state>` (the plan gate
630
+ reports it).
631
+ - Requirements: each covered requirement's text, as defined for its hash, in the node's
632
+ `covers` order, separated by one empty line; with none (a cleanup node), the single line
633
+ `None.` A covered id absent from the spec appears
634
+ as the line `<id>: not in .ultra/spec.md` (the plan gate reports it).
635
+ - Checks: every check covering at least one of the node's requirements, in registry order,
636
+ with its current state; for a cleanup node, every invariant instead, each line ending
637
+ ` · fix: <fix>`; with none, the single line `None.`
638
+ - Scope: one line per prefix; with no `scope` or an empty one, the single line
639
+ `Not declared: change only what the requirements need.`
640
+ - Handoffs: one `### <dependency id>` subsection per direct dependency, in `depends` order.
641
+ For a dependency that names a node of the plan, it holds the text of
642
+ `.ultra/records/handoffs/<dependency>.md` (CRLF→LF, whitespace-only lines at the start
643
+ and whitespace at the end of the text removed); the line `No handoff written.` when
644
+ nothing is at that path and no directory on the path is a file, or when its text is
645
+ empty; otherwise, when it cannot be read under "Reading a record file" (see "Layout"),
646
+ the line `Not read: not a regular file inside the project.` For a dependency that names
647
+ no node, it holds the line `Not a node in .ultra/plan.json.` and no file is read, so a crafted
648
+ `depends` entry can never pull a file from outside the records into a brief. A node
649
+ without dependencies has the single line `None.` instead.
650
+ - Rules: the fixed text below, with `<id>` replaced by the node id and `<checks>` by the
651
+ ids of the checks listed above, in the same order, separated by one space (`--stale`
652
+ when there are none).
653
+ - Blocks are separated by exactly one empty line, as in the layout: each heading from its
654
+ body, each section from the next heading, and each handoff subsection from the next.
655
+ - The brief ends with exactly one newline; text mode writes it to stdout unchanged.
656
+
657
+ ```text
658
+ - Do not edit .ultra/spec.md, .ultra/checks.json, .ultra/plan.json, .ultra/ledger/ or the files the checks above run. If a check or a requirement looks wrong, stop and escalate.
659
+ - Change only files inside the scope, and commit your work: evidence from an uncommitted tree never counts.
660
+ - Run `ubp check <checks>` until every check above is green, then `ubp check` (every stale check: invariants and finished requirements must stay green), then `ubp node done <id>`.
661
+ - Escalate instead of deciding: a change of direction (L0) and any deletion or narrowing of a requirement go to the Owner.
662
+ - Record every other decision you made without asking as one line in .ultra/rulings.md: `- YYYY-MM-DD Lx 裁决:<决定> — 因为 <理由> — 如果判断错了,代价是 <代价>`.
663
+ - Before `ubp node done <id>`, write .ultra/records/handoffs/<id>.md: the interfaces you created, what dependent nodes must know, and what is left unfinished.
664
+ - If three attempts leave a check red, stop and report the check, its error line and what you tried.
665
+ ```
666
+
667
+ Budget: the brief's size in UTF-8 bytes must not exceed the budget, 32768 by default or
668
+ `--budget`, a positive integer. When it does, handoffs are replaced one at a time, starting
669
+ from the last dependency, by the line
670
+ `Omitted to fit the budget: read .ultra/records/handoffs/<dependency>.md`, until the brief
671
+ fits. Only a handoff read from a file whose text is longer in UTF-8 bytes than its
672
+ replacement line is replaced; the others are skipped. So every omission makes the brief
673
+ smaller and points at a file that exists. Requirements, checks and rules are never cut:
674
+ when the brief still does not fit, `context` writes nothing to stdout and exits 1 with the
675
+ problem `brief-over-budget`, whose message names the size and the budget and whose hint is
676
+ to split the node; text mode prints the problem on stderr.
677
+
678
+ ## Retro
679
+
680
+ `retro` lists the failure classes since the latest closure (RESULT §3.7: a mechanical error
681
+ seen twice becomes a check). It reads only.
682
+
683
+ - Window: evidence records whose `at` is later than the greatest `at` in
684
+ `milestones.jsonl`; all records when there is none. `since` is that `at`, or null.
685
+ - Failures: records in the window with `dirty: false` and outcome `fail` or `unknown`, except
686
+ the red-before-green runs of test-first work: a record whose reason is not `timeout`,
687
+ `expect-timeout`, `not-runnable` or `spawn-error` counts only when an earlier line of
688
+ `evidence.jsonl` (inside the window or before it) holds a `pass` with `dirty: false` for
689
+ the same check. So a check failing before it ever passed is not a failure class; a
690
+ regression after a pass is, and a check that cannot run or hangs always is.
691
+ - Class: the record's `error_line` (or its `reason` when `error_line` is null or empty) with every
692
+ run of ASCII digits replaced by `#`, every run of whitespace by one space, and the ends
693
+ trimmed.
694
+ - `classes`: one entry `{ class, count, checks, runs }` per class: `count` failing records,
695
+ `checks` their sorted unique check ids, `runs` their run ids in record order (latest
696
+ record order, ties by line). Sorted by `count` descending, then by `class` in code-unit
697
+ order.
698
+ - Text mode prints one line per class, `<count> × <class> · <checks joined by ", ">`, then
699
+ `重复两次及以上:<n> 类`, where n counts classes with `count` at least 2.
700
+
701
+ The method turns each class seen twice into an invariant or a ruling.
702
+
703
+ ## Merge gate
704
+
705
+ `merge <branch> --expect-head <commit>` brings a branch in only when the merged tree keeps
706
+ the finished work green (RESULT §3.6: verify the expected head, run the full regression, bisect on
707
+ red). `<branch>` is anything `git rev-parse --verify <x>^{commit}` resolves; `<commit>` is a
708
+ full commit id (40 or 64 hex digits) that resolves. Anything else is a usage error: a name
709
+ such as `HEAD` or the current branch would always match.
710
+
711
+ Problems, collected in this order; the first of `merge-busy`, `head-moved`, `dirty-tree`,
712
+ `plan-gate` and `nothing-to-merge` that applies stops the command before any merge is tried,
713
+ and `merge-conflict`, `truth-changed` or `merged-gate` (the first that applies, in that
714
+ order) stops it before any check runs:
715
+
716
+ | code | condition |
717
+ |---|---|
718
+ | `merge-busy` | another `ubp merge` holds `.ultra/.runtime/merge.lock`; refused at once, never a wait, and nothing under `.ultra/.runtime/merge/` is touched |
719
+ | `head-moved` | HEAD is not the `--expect-head` commit, or it moved before the fast-forward |
720
+ | `dirty-tree` | the tree is dirty, or a file under `.ultra/` outside `.ultra/.runtime/` differs from HEAD or is untracked and not ignored (`git ls-files --others --exclude-standard`); the message names each such `.ultra/` path, and reads `Uncommitted code` when only code is dirty, never ending with an empty list (hint: commit `.ultra/` first; the merge brings the branch's ledger lines in) |
721
+ | `plan-gate` | the plan gate state is not `pass` |
722
+ | `nothing-to-merge` | the branch's commit is already an ancestor of HEAD |
723
+ | `merge-conflict` | merging the branch into HEAD conflicts; the message names the conflicting paths. When one of them is under `.ultra/ledger/`, the hint says to add `.ultra/ledger/*.jsonl merge=union` to `.gitattributes` on the main line |
724
+ | `truth-changed` | the merge commit differs from HEAD at a path that is `.ultra` or lies under `.ultra/`, other than an added or modified regular file (Git mode `100644` or `100755`) at `.ultra/ledger/evidence.jsonl`, `.ultra/ledger/nodes.jsonl`, `.ultra/rulings.md` or a path under `.ultra/records/`. Compared with `git diff-tree -r --no-renames HEAD <merge commit> -- ':(icase).ultra'`, which also lists a case variant of the directory, and paths compared exactly as Git prints them, so a case variant such as `.ULTRA/ledger/milestones.jsonl` or `.ultra/ledger/Milestones.jsonl` is not allowed, on any file system (a case-insensitive one would check it out into `.ultra/`); a symbolic link, a submodule, a type change or a deletion is refused even at an allowed path. One problem per path, in Git's order, the message naming the path. Everything else under `.ultra/` (spec, checks, plan, plan log, closures, approvals) is the planner's or the controller's, made on the main line |
725
+ | `merged-gate` | the merged tree has plan problems, computed as `ubp gate` computes them (for example a malformed ruling the branch added); the message lists their codes |
726
+ | `check-not-green` | after the merge, a blocking check (below) did not pass; one problem per check, in registry order |
727
+ | `fast-forward-failed` | the main tree could not fast-forward to the merge commit while HEAD stayed put; the message carries Git's first error line |
728
+
729
+ Every check runs in the temporary tree. A run passes when its outcome is `pass` and its
730
+ evidence is clean. The merge keeps the finished work green (RESULT §3.6), not the unfinished
731
+ work of other writers: a check that does not pass blocks when it is
732
+
733
+ 1. an invariant;
734
+ 2. a check covering a requirement of a closed milestone (closures from the main tree's
735
+ ledger);
736
+ 3. a check covering a requirement that a finished node covers: a node with a completion
737
+ record in the merged tree's `.ultra/ledger/nodes.jsonl`, whether it finished on the main
738
+ line or the branch brings its completion in; or
739
+ 4. any other check covering a requirement of the current milestone (decided in the main
740
+ tree), when it passes on HEAD alone (see "Bisect"): the branch broke what already worked.
741
+
742
+ A check that does not pass and is none of these does not block. For a check of the current
743
+ milestone (it did not pass on HEAD alone either) the merge adds the warning
744
+ `<check> is not green after the merge and was not green before it; no finished work needs it`,
745
+ in registry order: this is unfinished work, for example another writer's node, as at a
746
+ milestone gate before its last node. Checks of later milestones run too but never block and
747
+ add no warning. Ceiling: a requirement added to a closed milestone after its closure is not
748
+ built yet, so its red check blocks, under rule 2, every merge except the one of the branch
749
+ that builds it; build it first.
750
+
751
+ Procedure: the engine first takes `.ultra/.runtime/merge.lock` (created exclusively, holding
752
+ the merge's pid in decimal; a lock whose pid is not alive is broken through
753
+ `.ultra/.runtime/merge.lock.break` exactly as `engine.lock` is in "Concurrency") and holds it
754
+ until its temporary work tree is removed. Holding it, the engine deletes the leftover
755
+ entries under `.ultra/.runtime/merge/` and drops the registrations of work trees inside that
756
+ directory; it never prunes or removes any other work tree. It adds a detached temporary work tree at HEAD under
757
+ `.ultra/.runtime/merge/<id>/`, merges the branch there with `git merge --no-ff --no-edit`
758
+ (message `Merge <branch> (ubp merge gate)`, the repository's Git identity, configuration and
759
+ hooks), and runs every
760
+ check there. Every Git command of the merge runs with `-c maintenance.auto=false -c
761
+ gc.auto=0`, so the gate never starts Git's automatic maintenance, which would expire reflog
762
+ entries of the whole repository. Their evidence goes to the temporary tree and is discarded. With no problem,
763
+ the main tree's current branch fast-forwards to that merge commit (`git merge --ff-only`). On
764
+ refusal the main tree, HEAD and the branch are unchanged.
765
+
766
+ Every Git command the engine runs in the temporary tree, including those of the checks'
767
+ bookkeeping, is bound to that tree's own Git directory, so it can never reach the main
768
+ repository: when the temporary tree or its `.git` file has gone, the command fails, the merge
769
+ exits 2, and the main tree (tracked, untracked and ignored files), its index, HEAD, its reflog
770
+ and the branch are unchanged.
771
+
772
+ The temporary work tree is removed whatever the outcome: `git worktree remove --force`, and
773
+ when that fails, making its directories writable, deleting it and dropping its registration
774
+ (only its own). When it still cannot be removed, the result stands: `merge --json` lists the failure
775
+ in `warnings`, and text mode prints `warning: <message>` after the result.
776
+
777
+ An interrupt (SIGINT or SIGTERM) before the fast-forward stops the merge: the main tree,
778
+ HEAD and the branch are unchanged, the temporary work tree is removed, and the command exits
779
+ 130, 143 or 129. This holds whether the signal reaches the engine alone or its whole process group
780
+ (a terminal's Ctrl-C), and whether it arrives during a check, during a Git command or between
781
+ them: a Git command killed by the signal never turns the exit into 2. Once the fast-forward
782
+ has started, an interrupt no longer stops it: the fast-forward runs outside the engine's
783
+ process group, completes, and the command exits 0.
784
+
785
+ Bisect, for each blocking check, in registry order: when it blocks only under rule 3 and every
786
+ finished node covering its requirements finished on the branch (its completion record is not
787
+ in HEAD's `nodes.jsonl`), the result is `the branch marks <node> done` (the first such node in
788
+ plan order), with no search. Otherwise the engine first tries HEAD alone; when the check does not
789
+ pass there either, the result is `not green at HEAD before the merge`. Otherwise the branch's
790
+ first-parent commits after the merge base, oldest first, are searched in halves for the
791
+ first commit whose merge into HEAD makes that check not pass; a commit whose merge conflicts
792
+ counts as not passing. The result is `first bad commit <full sha> <subject>`. Ceiling: the
793
+ search assumes that once a commit breaks the check, later ones keep it broken.
794
+
795
+ Each `check-not-green` message reads `<check> is not green: <error line of its run, or its
796
+ reason when there is none, or `dirty-run` for a dirty pass>; <bisect result>`.
797
+
798
+ `merge --json` returns `{ ok, merged, problems, warnings }`; `merged` is the merge commit, or
799
+ null on refusal; `warnings` is an array of strings: the non-blocking checks above, then
800
+ any work tree the engine could not remove. Text mode prints `merge <branch>: pass
801
+ <first 7 hex of the merge commit>`, or `merge <branch>: refused` and each problem with its
802
+ hint.
803
+
804
+ Ceilings: checks run in a fresh work tree, so a project whose checks need generated files that
805
+ Git does not track cannot use the gate yet (upgrade path: a setup command in `checks.json`).
806
+ The temporary tree lies inside the main tree (`.ultra/.runtime/merge/<id>/`), so a module
807
+ resolver that walks up parent directories, as Node's does, finds the main tree's installed
808
+ dependencies: a dependency the branch adds but the main tree has not installed is not seen,
809
+ and one the main tree has installed hides a missing one; install on the main line before
810
+ merging such a branch. The forbidden paths are checked on the merged tree, so the engine
811
+ trusts the evidence and node lines a branch brings in. A forged completion only makes more
812
+ checks block. A forged evidence line can keep a red check green on the main line (a branch
813
+ that changes no code leaves the fingerprint unchanged), so after each merge the method runs
814
+ `ubp check --all`: its runs are appended after every branch record and supersede it; the merge holds no `engine.lock` while checks run, only
815
+ `merge.lock`.
816
+
817
+ ## Drive (unattended run)
818
+
819
+ `ubp drive --writer <command> [--reviewer <command>] [--attempts <n>] [--max-writers <n>]
820
+ [--timeout-sec <s>]` runs the method's controller loop without a person until the next
821
+ Owner touch. It is the same loop as "Every session starts the same way" and "Node loop" in
822
+ the method, with the judgment steps replaced by stops. `<command>` is one shell command
823
+ line, run with `/bin/sh -c` in the project root; it is not parsed by the engine.
824
+ `--attempts` (default 3) bounds the writer attempts per node, `--max-writers` (default 10)
825
+ the writer attempts of the whole run, and `--timeout-sec` (default 3600, at most 86400)
826
+ each writer or reviewer run.
827
+
828
+ The tree is *clean* for the drive when the code tree is clean (see "Git state"), no path
829
+ under `.ultra/` outside `.ultra/.runtime/` and `.ultra/ledger/` is untracked or differs from
830
+ HEAD, and no index entry is marked assume-unchanged or skip-worktree (`git ls-files -v`
831
+ shows a lowercase tag or `S`), since such an entry hides a work-tree change from Git. The
832
+ *Git settings* are the contents of the repository's `config`, `config.worktree` and
833
+ `info/exclude` (located with `git rev-parse --git-path`; a missing file is a content of its
834
+ own). Ceilings: Git configuration outside the repository (global, system, a file it includes,
835
+ or the default excludes file `$XDG_CONFIG_HOME/git/ignore`) is not compared; a file Git
836
+ ignores is not seen, so a fix that exists only in an ignored file (a `.gitignore` in the tree,
837
+ committed or not, is enough, with no bad intent) passes the checks in the work tree while HEAD
838
+ lacks it, and the drive can stop `milestone-gate` on a broken HEAD: the method runs the
839
+ milestone's checks in a clean checkout of HEAD before the demo; a sparse checkout marks paths
840
+ skip-worktree, so it is never clean for the drive. The *truth bytes* are the contents, in the work tree and at HEAD, of
841
+ `.ultra/spec.md`, `.ultra/checks.json`, `.ultra/plan.json`,
842
+ `.ultra/ledger/milestones.jsonl`, `.ultra/ledger/approvals.jsonl` and
843
+ `.ultra/ledger/plan-log.jsonl`, in that order (a missing file is a content of its own).
844
+
845
+ Each round reads the state (as `status`) and acts on `next` (an actionable node comes before
846
+ `检查`, see "States"):
847
+
848
+ | State | Action |
849
+ |---|---|
850
+ | the tree is not clean | stop `dirty-tree` |
851
+ | gate `stale` | run the plan gate (as `gate`); go on when it passes, else stop `plan-gate` |
852
+ | gate `fail` | stop `plan-gate` |
853
+ | `next` is a node id | in this order: a node without a non-empty `scope` stops `scope-missing`; a brief (as `context`, default budget) over budget stops `brief-over-budget` with its problem; a run that already started `--max-writers` writers stops `writer-budget`; when any check is `stale` or `unknown`, run the stale checks (as `check`), which gives the ratchet its baseline; then write the brief to `.ultra/.runtime/briefs/<node>.md` and run one writer attempt (below) |
854
+ | `next` is `检查` | run the stale checks (as `check`); when `next` is still `检查` afterwards, stop `checks-not-green` |
855
+ | `next` is `里程碑门` | run every check (as `check --all`); when `next` is still `里程碑门`, one reviewer run if `--reviewer` is given (below), then stop `milestone-gate`; otherwise go on with the next round |
856
+ | `next` is `全部完成` | stop `all-done` |
857
+ | `next` is `无(受阻)` | stop `blocked` |
858
+
859
+ A writer or reviewer run gets the engine's environment without the Git repository-location
860
+ variables (as for checks), plus `UBP_ROOT` (absolute project root), `UBP_DRIVE=1`, `PATH`
861
+ with `<root>/.ultra/.runtime/drive/<run>/bin` first, where an executable `ubp` runs this
862
+ engine (`exec '<node>' '<engine>/cli.cjs' "$@"`, written again before every run), and `UBP`, the same command as a
863
+ shell-quoted line (for example `'/usr/bin/node' '/x/engine/cli.cjs'`; use it as
864
+ `eval "$UBP check"`; prefer it: a login shell may put another `ubp` before the drive's
865
+ on `PATH`). A writer also gets `UBP_NODE` (node id), `UBP_BRIEF` (absolute path
866
+ of the brief) and `UBP_ATTEMPT` (1-based); a reviewer gets `UBP_MILESTONE` (milestone id)
867
+ and `UBP_REVIEW` (below). While `UBP_DRIVE` is set, `approve`, `milestone close`, `merge`
868
+ and `drive` exit 2 with `Not allowed inside ubp drive.` and change nothing.
869
+
870
+ The run's stdout and stderr go to `.ultra/.runtime/drive/<run>/<node>-<attempt>.log`
871
+ (`review.log` for the reviewer). At `--timeout-sec`, and when the drive is interrupted, the
872
+ drive sends SIGTERM to the run's whole process group and SIGKILL to the group 5 seconds
873
+ later, so an inner `ubp` can stop its own checks first; when the run's shell exits by
874
+ itself and its group still has members, the drive does the same to them (SIGTERM, then
875
+ SIGKILL 5 seconds later). Ceiling: a process the run moved to another process group or
876
+ session (a check an inner `ubp` started is one; that `ubp` stops it on SIGTERM) is not
877
+ reached. The step's `exit` is the
878
+ shell's exit code, `timeout`, or `signal-<NAME>`.
879
+
880
+ After a writer attempt exits, whatever its exit code, the drive verifies, in this order,
881
+ and the first failing step decides:
882
+
883
+ 1. each truth file's work-tree content is the one before the attempt, and its content at
884
+ HEAD is either the one at HEAD before the attempt or the work-tree content before it
885
+ (`truth-touched`, one per file, in the order above): a writer never plans, closes or
886
+ approves, though it may commit what the engine or the Owner had already written, such
887
+ as the plan-log lines of a gate the drive ran;
888
+ 2. the HEAD before the attempt is an ancestor of the HEAD after it (`history-rewritten`);
889
+ 3. the tree is clean and the Git settings are the ones before the attempt
890
+ (`writer-left-dirty`);
891
+ 4. every path changed between the two HEADs, except the truth files (step 1 decides them),
892
+ `.ultra/ledger/evidence.jsonl` and `.ultra/ledger/nodes.jsonl`, is named by no check
893
+ (`check-touched`) and lies in the node's scope, is `.ultra/rulings.md` or lies under
894
+ `.ultra/records/handoffs/`
895
+ (`out-of-scope`), one problem per path, in path order; then every node other than this
896
+ one that got a completion record during the attempt is `out-of-scope` (subject the node
897
+ id), in plan order. A check names a path when the path equals a word of its `run`
898
+ (split on whitespace, with surrounding quotes and a leading `./` removed). A scope entry
899
+ `p` holds the path `p` and every path under `p/` (`p` without a trailing `/`);
900
+ 5. the plan gate state is `pass`, read without running the gate, so the drive never
901
+ records a plan change for a writer (`plan-gate`, with the gate's problems);
902
+ 6. the drive runs, as `check <ids>` in registry order, the checks covering the node's
903
+ requirements, every invariant, every check with an evidence record added during the
904
+ attempt (a record that is not one of the records before it, compared whole, each earlier
905
+ record matching at most one later record, so a copy of an old record with any field
906
+ changed counts as added) and every stale or unknown check; a requirement
907
+ of the current or a closed milestone that was green before the attempt (after the
908
+ baseline run) and is not green now is `ratchet-broken` (one per requirement, in
909
+ requirement order);
910
+ 7. the node is `done` (`node-not-done`), and every check covering its requirements and
911
+ every invariant is green (`node-not-green`, one per check, in registry order), which
912
+ is what `node done` requires, so a completion record the writer wrote by hand cannot
913
+ pass a red invariant. Ceiling: for a cleanup node, `node done` also requires the
914
+ requirements of closed milestones, and those of its milestone covered by done nodes, to
915
+ be green; step 7 does not, so a hand-written cleanup completion over such a red
916
+ requirement is accepted, and the run stops later at `checks-not-green`.
917
+
918
+ Every failure stops the drive at once with its problems (the engine never reverts: a person
919
+ decides), except `node-not-done`, which starts the next attempt with a freshly compiled
920
+ brief while the node has attempts left; after the last one the drive stops
921
+ `attempts-exhausted`. Ceiling: a check file inside a node's scope that no check's `run`
922
+ names (a helper the test imports) is not protected; keep check files outside every scope.
923
+
924
+ A reviewer run happens once, at the milestone gate, when `--reviewer` is given. `UBP_REVIEW`
925
+ is the absolute path of `.ultra/records/milestones/<milestone>-review.md`, which must not
926
+ exist before the run (otherwise stop `review-exists`). Afterwards the HEAD, the truth bytes,
927
+ the Git settings and `next` (`里程碑门`) must be unchanged and the tree clean apart from the review file
928
+ (else stop `reviewer-changed-tree`), and the file must be a valid review: its first line
929
+ starts with `Reviewer: ` and exactly one line outside fences is `Verdict: accept` or
930
+ `Verdict: revise` (else stop `review-invalid`). The drive then stops `milestone-gate` with
931
+ the verdict. It never closes a milestone, approves anything or writes
932
+ `.ultra/records/owner-touches.md`: those are the Owner's.
933
+
934
+ `<run>` is a fresh UUID. The drive first prints `drive: run <run>` (on stderr with
935
+ `--json`). In text mode it then prints each step as it ends, `drive: <action> <subject>:
936
+ <result>`, with ` (exit <exit>)` appended for writer and reviewer steps; at the stop, the
937
+ stopping problems as `check` prints them (`<code>: <message> — <hint>`, one per line) and
938
+ last `drive: stop <reason>`. With `--json` it prints at the end
939
+ `{ run, stop, verdict, steps: [{ action, subject, attempt, result, exit, problems }],
940
+ problems }`, where `action` is `gate`, `check`, `context`, `writer` or `reviewer` (nothing
941
+ else is a step), `exit` is null for steps without a shell, `verdict` is `accept`, `revise`
942
+ or null, and `problems` are those of the stop. A `check` step's subject is `stale` or `all`
943
+ and its result is `pass` when every check it ran passed, else `fail`; the checks run to
944
+ verify a writer belong to its `writer` step. Exit 0 when the stop is `all-done`, or
945
+ `milestone-gate` without a `revise` verdict; 1 for every other stop; 2 for usage errors and
946
+ engine failures; 130, 143 or 129 when interrupted by SIGINT, SIGTERM or SIGHUP (the running
947
+ writer or reviewer is stopped first; text mode has already printed the steps so far).
948
+ Ceiling: with `--json`, an interrupted or failed drive prints no result; its run id on
949
+ stderr locates the logs.
950
+
951
+ Before its first round the drive takes `.ultra/.runtime/drive.lock`, created and broken as
952
+ `engine.lock` is in "Concurrency" (with `drive.lock.break`), and it removes the lock when its
953
+ process exits, after the result is printed, on every path, an interrupt included; a drive
954
+ killed before it can clean up leaves a lock whose dead pid the next drive breaks. While a live process holds the lock, `drive`
955
+ starts no writer or reviewer and stops `drive-running` with one problem `drive-running`
956
+ (hint: wait for the running drive to exit; `ubp status` shows it). The drive holds no engine
957
+ lock across rounds: each step takes and releases the engine lock as the corresponding
958
+ command does, so a writer can run `ubp` inside its attempt. `UBP_DRIVE` keeps a writer or
959
+ reviewer from starting a drive. Ceiling: a writer briefed by hand on the same tree while a
960
+ drive runs is not prevented; `ubp status` shows the running drive so the method can wait. A
961
+ left-over lock whose pid another process has reused, or an empty one (a drive killed between
962
+ creating and writing it), blocks every drive until it is removed; the method checks the
963
+ pid's command with `ps`.
964
+
965
+ ## Concurrency
966
+
967
+ `gate`, `node done`, `milestone close` and `approve` hold `.ultra/.runtime/engine.lock` while they
968
+ read the ledgers and append to them; every other ledger append holds it while it reads the
969
+ ledgers' greatest `at` and appends. The lock file is created exclusively and contains the
970
+ holder's pid in decimal. A command waits up to 30 seconds for the lock and exits 2 if it
971
+ cannot get it.
972
+
973
+ A pid is alive when `kill(pid, 0)` succeeds or fails with `EPERM`. A lock whose pid is not
974
+ alive may be broken, but only by a command that first creates
975
+ `.ultra/.runtime/engine.lock.break` exclusively; holding it, the command re-reads the
976
+ lock, removes it only if it still names the same dead pid, and then removes the break
977
+ file. So two waiters can never both break a lock, and a live holder's lock is never
978
+ removed. A break file older than 10 seconds belongs to a breaker that died and may be
979
+ removed by anyone (ceiling: a breaker stalled for longer than that between its re-read
980
+ and its removal could still race).
981
+
982
+ The holder removes the lock when done if it still names the holder's pid; a lock that is
983
+ already gone is not an error and never changes the command's result.
984
+
985
+ ## CLI
986
+
987
+ `node engine/cli.cjs <command> [options] [--root <dir>] [--json]`, also reachable as
988
+ `ubp <command>` through `bin/install.js`. The command comes first (`ubp status --json`);
989
+ options may follow in any order. `--root` defaults to the current directory.
990
+
991
+ | Command | Effect | Exit |
992
+ |---|---|---|
993
+ | `status` | prints the progress line, or with `--json` the full state | 0; 2 malformed/usage |
994
+ | `gate` | runs the plan gate; records plan-log entries when it passes | 0 pass; 1 problems; 2 |
995
+ | `check [--stale\|--all\|--req <id>\|<check-id>…]` | runs checks in registry order, records evidence, prints `<check>: <outcome> (<reason>)` per check (with `, dirty: not counted` inside the parentheses for a dirty run; for an invariant whose run did not pass, a next line ` fix: <fix>`) and then the progress line; default `--stale` (checks whose state is `stale` or `unknown`) | 0 when every check in scope is green after the runs, 1 otherwise; 2. The scope is the selected checks, and for the default `--stale` every check in the registry, so `ubp check` never exits 0 while a check is red |
996
+ | `node done <id>` | records completion when the plan gate state is `pass`, the tree is clean, every dependency is done, every covered requirement is green and every invariant is green; for a cleanup node, also every node before it in its milestone (plan order) is done (`dependency-not-done`, one per node) and every requirement of a closed milestone and every requirement of its own milestone covered by a done node is green | 0 recorded; 1 refused with problems; 2 |
997
+ | `retro` | prints the failure classes since the latest closure (see "Retro"); reads only | 0; 2 |
998
+ | `merge <branch> --expect-head <commit>` | the merge gate (see "Merge gate") | 0 merged; 1 refused with problems; 2 |
999
+ | `drive --writer <command> [--reviewer <command>] [--attempts <n>] [--max-writers <n>] [--timeout-sec <s>]` | the unattended controller loop (see "Drive (unattended run)") | 0 at `all-done`, or at `milestone-gate` without a `revise` verdict; 1 any other stop; 2 usage or engine failure; 130/143/129 interrupted |
1000
+ | `milestone close <id> --owner <words> --review <path> [--proxy]` | records the closure when the milestone gate finds no problem (see "Milestone gate") | 0 recorded; 1 refused with problems; 2 |
1001
+ | `context <id> [--budget <bytes>]` | prints the node brief (see "Node brief") | 0; 1 over budget; 2 |
1002
+ | `approve <milestone>/<id>… --owner <words> [--proxy]` | records the Owner's approval of the listed reduction items | 0 recorded; 2 |
1003
+
1004
+ `status --json` returns
1005
+ `{ milestone, line, counts: {total, green, red, stale, unknown, uncovered}, next,
1006
+ plan_changes, closed_not_green, gate, problems, warnings, dirty, drive, requirements: [{id,
1007
+ milestone, state, reasons, checks: [{id, state, reasons}]}], checks: [{id, state, reasons,
1008
+ last_run}], nodes: [{id, milestone, state, reasons}], invariants: [{id, state, reasons,
1009
+ fix}] }`; invariants appear in `checks` too, in registry order. `drive` is the pid of the
1010
+ live process holding `.ultra/.runtime/drive.lock`, else null; reading it never takes,
1011
+ breaks or removes the lock.
1012
+
1013
+ `gate --json` returns `{ ok, problems, recorded: [{milestone, type}] }`.
1014
+ `check --json` returns `{ dirty, runs: [{check, run, outcome, reason, dirty}] }`; the top-level
1015
+ `dirty` says whether the tree is dirty after the runs (as in `status`), and each run's
1016
+ `dirty` is its evidence record's `dirty`.
1017
+ `node done --json` returns `{ ok, node, problems }`; each refusal is a problem
1018
+ `{ code, message, hint }` with code `plan-gate`, `dirty-tree`, `dependency-not-done`,
1019
+ `requirement-not-green` (one per requirement) or `invariant-not-green` (one per invariant,
1020
+ its hint as under "Milestone gate"), in that order.
1021
+ `retro --json` returns `{ since, classes }` (see "Retro"); `merge --json` returns
1022
+ `{ ok, merged, problems, warnings }` (see "Merge gate").
1023
+ `milestone close --json` returns `{ ok, milestone, problems }`, with the codes listed under
1024
+ "Milestone gate". `context --json` returns `{ ok, node, bytes, omitted, brief, problems }`:
1025
+ `omitted` lists, in omission order, the dependencies whose handoffs were omitted, and on
1026
+ refusal `brief` is null and `bytes` is the size reached after every handoff that could be
1027
+ omitted was omitted.
1028
+ `approve --json` returns `{ ok: true, items }` with the recorded, sorted items; in text mode
1029
+ it prints `approved: <items joined by ", ">`. Each item must name a milestone whose latest
1030
+ plan-log record is not `removed` and a requirement or check in that record's snapshot, so
1031
+ an approval can be given before the edit it allows; any other item, no item, or `--owner`
1032
+ missing, repeated or blank after trimming is a usage error (exit 2) and records nothing.
1033
+ The engine cannot tell the Owner's words from words written for them; `--proxy` marks an
1034
+ answer the assistant gave for the Owner.
1035
+
1036
+ Options belong to their command: `--owner` and `--proxy` only to `milestone close` and
1037
+ `approve`, `--review` only to `milestone close`, `--expect-head` only to `merge` (given
1038
+ once), `--budget` only to `context`, and the check
1039
+ selectors only to `check`; anywhere else they are usage errors. Giving `--owner`, `--review`
1040
+ or `--budget` twice is a usage error.
1041
+
1042
+ Exit 1 only ever means that a check in scope is not green after `check`, the plan gate
1043
+ found problems, `node done`, `milestone close` or `merge` refused, a node brief does not fit
1044
+ its budget, or `drive` stopped anywhere but `all-done` or an unrevised `milestone-gate`.
1045
+ Malformed truth files and ledgers, usage errors (an unknown command, node id,
1046
+ milestone id, check id or `--req` requirement id, a missing, invalid or repeated option
1047
+ value) and every other engine failure exit 2; an interrupted engine exits 130, 143 or 129
1048
+ (SIGINT, SIGTERM or SIGHUP).
1049
+ `status` exits 0 even when the plan gate has problems; it reports them in `gate` and
1050
+ `problems`. Malformed files are named on stderr. Text-mode commands print warnings on
1051
+ stderr.