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.
- package/CHANGELOG.md +113 -3
- package/README.md +67 -478
- package/README.zh-CN.md +49 -422
- package/adapters/_shared/frontmatter.cjs +1 -18
- package/adapters/_shared/plugin-core.cjs +17 -83
- package/adapters/_shared/runtime-assets.cjs +6 -120
- package/adapters/claude.js +8 -24
- package/adapters/codex.js +16 -38
- package/bin/install.js +35 -57
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +13 -101
- package/docs/examples/drive/claude-reviewer.sh +53 -0
- package/docs/examples/drive/claude-writer.sh +43 -0
- package/engine/FORMATS.md +1051 -0
- package/engine/checks.cjs +139 -0
- package/engine/cli.cjs +148 -0
- package/engine/completion.cjs +48 -0
- package/engine/context.cjs +94 -0
- package/engine/drive.cjs +220 -0
- package/engine/expect-worker.cjs +5 -0
- package/engine/expect.cjs +33 -0
- package/engine/formats.cjs +189 -0
- package/engine/git-state.cjs +112 -0
- package/engine/ledger.cjs +205 -0
- package/engine/loading.cjs +28 -0
- package/engine/merge.cjs +273 -0
- package/engine/milestone.cjs +89 -0
- package/engine/plan.cjs +232 -0
- package/engine/project.cjs +5 -0
- package/engine/signals.cjs +3 -0
- package/engine/states.cjs +175 -0
- package/hooks/session-start.cjs +54 -0
- package/package.json +17 -52
- package/skills/ultra/SKILL.md +395 -0
- package/.ultra-template/changes/abandoned/.gitkeep +0 -0
- package/.ultra-template/changes/active/.gitkeep +0 -1
- package/.ultra-template/changes/archive/.gitkeep +0 -1
- package/.ultra-template/contexts/TEMPLATE.md +0 -135
- package/.ultra-template/decisions/.gitkeep +0 -1
- package/.ultra-template/evidence/.gitkeep +0 -1
- package/.ultra-template/north-star.md +0 -52
- package/.ultra-template/project-brief.md +0 -28
- package/.ultra-template/research/.gitkeep +0 -1
- package/.ultra-template/specs/architecture.md +0 -81
- package/.ultra-template/specs/discovery.md +0 -74
- package/.ultra-template/specs/product.md +0 -80
- package/.ultra-template/specs/research-distillate.md +0 -57
- package/.ultra-template/tasks.json +0 -4
- package/.ultra-template/test-report.json +0 -44
- package/adapters/_shared/host-profile.cjs +0 -5
- package/adapters/grok.js +0 -242
- package/adapters/kimi.js +0 -232
- package/adapters/opencode.js +0 -408
- package/adapters/zcode.js +0 -375
- package/bin/delegate-worker.cjs +0 -5
- package/bin/delegate.cjs +0 -5
- package/docs/ARCHITECTURE.md +0 -274
- package/docs/ARTIFACT-AUTHORITY.md +0 -305
- package/docs/DECISIONS.md +0 -229
- package/docs/PHILOSOPHY.md +0 -261
- package/docs/RUNTIME-COMPAT-MATRIX.md +0 -315
- package/docs/SKILL-AUTHORING.md +0 -113
- package/docs/WORKFLOW-LIFECYCLE.md +0 -350
- package/docs/evals/adversarial-review-2026-08-14.md +0 -84
- package/docs/evals/ultra-5-0-journey-results.md +0 -62
- package/docs/evals/ultra-5-0-live-acp-conformance.md +0 -93
- package/docs/evals/ultra-5-0-route-journeys.md +0 -145
- package/docs/evals/zcode-automation-2026-08-14.md +0 -103
- package/hooks/README.md +0 -52
- package/hooks/_common.py +0 -1779
- package/hooks/adapters/codex.py +0 -295
- package/hooks/adapters/grok.py +0 -134
- package/hooks/adapters/kimi.py +0 -156
- package/hooks/adapters/zcode.py +0 -227
- package/hooks/block_dangerous_commands.py +0 -201
- package/hooks/compact_context.py +0 -224
- package/hooks/mid_workflow_recall.py +0 -47
- package/hooks/post_edit_guard.py +0 -215
- package/hooks/session_context.py +0 -741
- package/skills/ultra-change/SKILL.md +0 -165
- package/skills/ultra-change/references/change-contract.md +0 -199
- package/skills/ultra-change/references/execution-grant.md +0 -109
- package/skills/ultra-change/references/primary-transfer.md +0 -273
- package/skills/ultra-change/scripts/validate_primary_transfer.cjs +0 -1928
- package/skills/ultra-delegate/SKILL.md +0 -195
- package/skills/ultra-delegate/patches/zcode-acp-0.1.0-zcode-0.16.5.patch +0 -184
- package/skills/ultra-delegate/references/delegation-contract.md +0 -138
- package/skills/ultra-delegate/scripts/acp-conformance.cjs +0 -287
- package/skills/ultra-delegate/scripts/acp-driver.cjs +0 -762
- package/skills/ultra-delegate/scripts/agent-execution-driver.cjs +0 -320
- package/skills/ultra-delegate/scripts/cli-driver.cjs +0 -61
- package/skills/ultra-delegate/scripts/delegate-profiles/kimi-read-only.md +0 -15
- package/skills/ultra-delegate/scripts/delegate-profiles/kimi-write.md +0 -17
- package/skills/ultra-delegate/scripts/delegate-worker.cjs +0 -328
- package/skills/ultra-delegate/scripts/delegate.cjs +0 -690
- package/skills/ultra-delegate/scripts/delegate_wait.py +0 -86
- package/skills/ultra-delegate/scripts/host-profile.cjs +0 -267
- package/skills/ultra-deliver/SKILL.md +0 -194
- package/skills/ultra-deliver/references/baseline-reconciliation.md +0 -41
- package/skills/ultra-dev/SKILL.md +0 -265
- package/skills/ultra-dev/references/debugging.md +0 -22
- package/skills/ultra-domain-modeling/SKILL.md +0 -79
- package/skills/ultra-grilling/SKILL.md +0 -121
- package/skills/ultra-grilling/references/reframing.md +0 -76
- package/skills/ultra-init/SKILL.md +0 -143
- package/skills/ultra-init/references/brownfield-adoption.md +0 -32
- package/skills/ultra-init/scripts/init_project.cjs +0 -1031
- package/skills/ultra-plan/SKILL.md +0 -160
- package/skills/ultra-plan/references/task-evidence-v2.md +0 -300
- package/skills/ultra-plan/scripts/validate_task_evidence.cjs +0 -859
- package/skills/ultra-research/SKILL.md +0 -167
- package/skills/ultra-research/references/00-problem-validation.md +0 -36
- package/skills/ultra-research/references/01-opportunity-discovery.md +0 -33
- package/skills/ultra-research/references/02-market-assessment.md +0 -31
- package/skills/ultra-research/references/03-alternatives.md +0 -31
- package/skills/ultra-research/references/04-product-strategy.md +0 -41
- package/skills/ultra-research/references/05-assumptions-validation.md +0 -31
- package/skills/ultra-research/references/10-user-personas.md +0 -30
- package/skills/ultra-research/references/11-user-scenarios.md +0 -28
- package/skills/ultra-research/references/20-user-stories.md +0 -31
- package/skills/ultra-research/references/21-features-scope.md +0 -38
- package/skills/ultra-research/references/22-success-metrics.md +0 -33
- package/skills/ultra-research/references/30-architecture-context.md +0 -30
- package/skills/ultra-research/references/31-solution-strategy.md +0 -28
- package/skills/ultra-research/references/32-building-blocks.md +0 -28
- package/skills/ultra-research/references/40-deployment.md +0 -29
- package/skills/ultra-research/references/41-quality-risks.md +0 -28
- package/skills/ultra-research/references/99-synthesis.md +0 -59
- package/skills/ultra-research/references/north-star-v2.md +0 -168
- package/skills/ultra-research/references/wayfinding.md +0 -50
- package/skills/ultra-research/scripts/validate_north_star.cjs +0 -1174
- package/skills/ultra-review/SKILL.md +0 -275
- package/skills/ultra-review/references/adversarial-evaluation.md +0 -31
- package/skills/ultra-review/references/code.md +0 -17
- package/skills/ultra-review/references/comments.md +0 -12
- package/skills/ultra-review/references/design.md +0 -12
- package/skills/ultra-review/references/errors.md +0 -13
- package/skills/ultra-review/references/review-modes.md +0 -29
- package/skills/ultra-review/references/spec.md +0 -18
- package/skills/ultra-review/references/tests.md +0 -12
- package/skills/ultra-review/references/unified-schema.md +0 -284
- package/skills/ultra-review/references/worker-packet.md +0 -183
- package/skills/ultra-review/scripts/review_wait.py +0 -2943
- package/skills/ultra-status/SKILL.md +0 -209
- package/skills/ultra-status/references/route-contract.md +0 -100
- package/skills/ultra-tdd/SKILL.md +0 -91
- package/skills/ultra-tdd/references/templates/README.md +0 -20
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +0 -79
- package/skills/ultra-tdd/references/templates/persistence-real.ts +0 -78
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +0 -78
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +0 -73
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +0 -66
- package/skills/ultra-tdd/references/test-execution.md +0 -23
- package/skills/ultra-test/SKILL.md +0 -244
- package/skills/ultra-test/references/export-syntax.md +0 -18
- package/skills/ultra-test/scripts/validate_review_transport.cjs +0 -549
- package/skills/ultra-test/scripts/worktree_digest.cjs +0 -576
- package/skills/ultra-think/SKILL.md +0 -56
- package/skills/ultra-think/references/autonomy-boundary.md +0 -48
- 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.
|