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,33 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const { Worker } = require('node:worker_threads');
5
+
6
+ const signals = require('./signals.cjs');
7
+
8
+ function expectOutput(expect, output) {
9
+ return new Promise((resolve, reject) => {
10
+ // Isolate the acceptance author's regexp so its backtracking has a 5-second limit.
11
+ const worker = new Worker(path.join(__dirname, 'expect-worker.cjs'),
12
+ { workerData: { expect, output } });
13
+ let finished = false;
14
+ const handlers = Object.entries(signals).map(([signal, exitCode]) =>
15
+ [signal, () => finish(null, Object.assign(new Error('Check interrupted.'), { exitCode }))]);
16
+ const finish = (result, error) => {
17
+ if (finished) return;
18
+ finished = true;
19
+ clearTimeout(timer);
20
+ for (const [signal, handler] of handlers) process.off(signal, handler);
21
+ worker.terminate().then(() => error ? reject(error) : resolve(result), reject);
22
+ };
23
+ const timer = setTimeout(() => finish('expect-timeout'), 5000);
24
+ for (const [signal, handler] of handlers) process.on(signal, handler);
25
+ worker.once('message', (matches) => finish(matches ? 'exit-0' : 'expect-unmet'));
26
+ worker.once('error', (error) => finish(null, error));
27
+ worker.once('exit', (code) => {
28
+ if (!finished) finish(null, new Error(`Expect worker exited without a result (${code}).`));
29
+ });
30
+ });
31
+ }
32
+
33
+ module.exports = { expectOutput };
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { createHash } = require('node:crypto');
6
+
7
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
8
+ const hash = (text) => createHash('sha256').update(text).digest('hex');
9
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
10
+ const strings = (value) => Array.isArray(value) && value.every((v) => typeof v === 'string');
11
+ const unique = (items) => [...new Set(items)];
12
+ const problem = (code, message, hint) => ({ code, message, hint });
13
+
14
+ function assertFile(ok, file, message) {
15
+ if (!ok) throw new Error(`${file}: ${message}`);
16
+ }
17
+
18
+ function read(root, file, optional = false) {
19
+ try { return fs.readFileSync(path.join(root, '.ultra', file), 'utf8'); }
20
+ catch (error) {
21
+ if (optional && error.code === 'ENOENT') return '';
22
+ throw new Error(`.ultra/${file}: ${error.message}`);
23
+ }
24
+ }
25
+
26
+ function readRecord(root, relative) {
27
+ const file = path.join(root, relative);
28
+ const result = { exists: true, bytes: null };
29
+ let fd;
30
+ try {
31
+ const entry = fs.lstatSync(file, { throwIfNoEntry: false });
32
+ result.exists = entry !== undefined;
33
+ if (!entry?.isFile()) return result;
34
+ const parent = fs.realpathSync(path.dirname(file));
35
+ const location = path.relative(fs.realpathSync(root), parent);
36
+ if (path.isAbsolute(location) || location === '..' || location.startsWith(`..${path.sep}`)) {
37
+ return result;
38
+ }
39
+ fd = fs.openSync(path.join(parent, path.basename(file)),
40
+ fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
41
+ if (fs.fstatSync(fd).isFile()) result.bytes = fs.readFileSync(fd);
42
+ } catch (error) {
43
+ if (!['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code)) {
44
+ throw new Error(`${relative}: ${error.message}`);
45
+ }
46
+ } finally {
47
+ if (fd !== undefined) fs.closeSync(fd);
48
+ }
49
+ return result;
50
+ }
51
+
52
+ function parse(text, file) {
53
+ try { return JSON.parse(text); }
54
+ catch (error) { throw new Error(`${file}: ${error.message}`); }
55
+ }
56
+
57
+ function markdown(text, visit) {
58
+ let fence = null;
59
+ const lines = text.replace(/\r\n/gu, '\n').split('\n');
60
+ lines.forEach((line, index) => {
61
+ const marker = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/u);
62
+ if (fence) {
63
+ visit(line, index + 1, false);
64
+ if (marker && marker[1][0] === fence.marker[0]
65
+ && marker[1].length >= fence.marker.length && !marker[2].trim()) fence = null;
66
+ } else if (marker && (marker[1][0] !== '`' || !marker[2].includes('`'))) {
67
+ fence = { marker: marker[1], line: index + 1 };
68
+ visit(line, index + 1, false);
69
+ } else visit(line, index + 1, true);
70
+ });
71
+ return fence?.line ?? null;
72
+ }
73
+
74
+ function requirements(text) {
75
+ const result = new Map();
76
+ const requirementText = new Map();
77
+ const shared = [];
78
+ let current = null;
79
+ const finish = () => {
80
+ if (!current) return;
81
+ const lines = current.lines.map((line) => line.trimEnd());
82
+ while (lines.length && lines.at(-1) === '') lines.pop();
83
+ while (lines.length && lines[0] === '') lines.shift();
84
+ const text = lines.join('\n');
85
+ result.set(current.id, hash(text));
86
+ requirementText.set(current.id, text);
87
+ current = null;
88
+ };
89
+ const unclosed = markdown(text, (line, number, outside) => {
90
+ const heading = outside && line.match(/^ {0,3}(#{1,2})(?:[ \t]+(.*)|[ \t]*)$/u);
91
+ const isRequirement = heading && heading[1] === '##' && heading[2]?.startsWith('REQ-');
92
+ if (outside && /^ {0,3}#+\s*REQ(?:[-_]|\s+\d)/iu.test(line) && !isRequirement) {
93
+ assertFile(false, `.ultra/spec.md:${number}`, `malformed requirement heading: ${line}`);
94
+ }
95
+ if (heading) {
96
+ finish();
97
+ const title = heading[2] || '';
98
+ if (heading[1] === '##' && title.startsWith('REQ-')) {
99
+ const id = title.split(/\s/u)[0];
100
+ const file = `.ultra/spec.md:${number}`;
101
+ assertFile(ID.test(id.slice(4)), file, `malformed requirement heading: ${line}`);
102
+ assertFile(!result.has(id), file, `duplicate requirement id ${id}`);
103
+ current = { id, lines: [] };
104
+ }
105
+ }
106
+ if (current) current.lines.push(line);
107
+ else if (line.trimEnd()) shared.push(line.trimEnd());
108
+ });
109
+ assertFile(!unclosed, `.ultra/spec.md:${unclosed}`, 'unclosed fence');
110
+ finish();
111
+ return { reqs: result, requirementText, shared: hash(shared.join('\n')) };
112
+ }
113
+
114
+ function validateCheck(item, valid) {
115
+ const invariant = Object.hasOwn(item, 'fix');
116
+ valid(!invariant || (typeof item.fix === 'string' && item.fix.trim().length > 0),
117
+ `${item.id}: fix must be a non-empty string`);
118
+ valid(invariant ? item.covers.length === 0 : item.covers.length > 0,
119
+ `${item.id}: invariants must cover nothing; other checks must cover a requirement`);
120
+ valid(item.run.trim().length > 0, `${item.id}: run must be non-empty`);
121
+ const timeout = item.timeout_sec === undefined ? 600 : item.timeout_sec;
122
+ valid(Number.isInteger(timeout) && timeout > 0 && timeout <= 86400,
123
+ `${item.id}: invalid timeout_sec`);
124
+ valid(typeof item.expect === 'string' && item.expect.length > 0,
125
+ `${item.id}: expect must be a non-empty regular expression string`);
126
+ let pattern;
127
+ try { pattern = new RegExp(item.expect, 'mu'); }
128
+ catch (error) { valid(false, `${item.id}: invalid expect: ${error.message}`); }
129
+ valid(!pattern.test(''), `${item.id}: expect must not match the empty string`);
130
+ }
131
+
132
+ function registry(root, file, kinds) {
133
+ const data = parse(read(root, file), `.ultra/${file}`);
134
+ const valid = (ok, message) => assertFile(ok, `.ultra/${file}`, message);
135
+ valid(object(data) && data.version === 1, 'expected an object with version 1');
136
+ if (file === 'plan.json' && Object.hasOwn(data, 'cleanup_every')) {
137
+ valid(Number.isInteger(data.cleanup_every) && data.cleanup_every > 0,
138
+ 'cleanup_every must be a positive integer');
139
+ }
140
+ for (const [kind, fields] of Object.entries(kinds)) {
141
+ valid(Array.isArray(data[kind]), `${kind} must be an array`);
142
+ const ids = new Set();
143
+ for (const item of data[kind]) {
144
+ valid(object(item) && typeof item.id === 'string' && ID.test(item.id),
145
+ `invalid ${kind} id`);
146
+ valid(!ids.has(item.id), `duplicate id ${item.id}`);
147
+ ids.add(item.id);
148
+ for (const [field, type] of Object.entries(fields)) {
149
+ valid(type === 'array' ? strings(item[field]) : typeof item[field] === type,
150
+ `${item.id}: invalid or missing ${field}`);
151
+ }
152
+ if (kind === 'checks') validateCheck(item, valid);
153
+ if (kind === 'nodes' && item.scope !== undefined) {
154
+ valid(strings(item.scope), `${item.id}: scope must be an array of strings`);
155
+ }
156
+ if (kind === 'nodes' && Object.hasOwn(item, 'kind')) {
157
+ valid(item.kind === 'cleanup', `${item.id}: kind must be cleanup`);
158
+ valid(item.covers.length === 0, `${item.id}: a cleanup node must cover nothing`);
159
+ }
160
+ if (kind === 'milestones' && item.journey !== undefined) {
161
+ valid(typeof item.journey === 'string', `${item.id}: journey must be a string`);
162
+ }
163
+ }
164
+ }
165
+ return data;
166
+ }
167
+
168
+ function rulingProblems(text) {
169
+ const problems = [];
170
+ // Each part needs a character that is neither space nor punctuation, so matching finds a
171
+ // split where all have content. No gap after a part: it would multiply backtracking.
172
+ const gap = '[\\s\\p{P}]*';
173
+ const part = `${gap}[^\\s\\p{P}].*?`;
174
+ const ruling = new RegExp(`^- ${gap}\\d{4}-\\d{2}-\\d{2}${gap}L[0-4]${gap}裁决${part}因为`
175
+ + `(?!${gap}如果判断错了${gap}代价)${part}(?:如果判断错了${gap})?代价(?:是|(?!是))${part}$`, 'u');
176
+ const hint = 'Use: - YYYY-MM-DD L0–L4 裁决:decision — 因为 reason '
177
+ + '— 如果判断错了,代价是 cost';
178
+ const unclosed = markdown(text, (line, number, outside) => {
179
+ if (outside && /^\s*[-*+] /u.test(line) && !ruling.test(line)) {
180
+ problems.push(problem('malformed-ruling', `.ultra/rulings.md:${number}: ${line}`, hint));
181
+ }
182
+ });
183
+ if (unclosed) problems.push(problem('malformed-ruling',
184
+ `.ultra/rulings.md:${unclosed}: unclosed fence`, 'Close the fence opened on this line.'));
185
+ return problems;
186
+ }
187
+
188
+ module.exports = { ID, hash, object, strings, unique, problem, read, readRecord, markdown,
189
+ requirements, registry, rulingProblems };
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { createHash } = require('node:crypto');
6
+ const { spawn, execFileSync } = require('node:child_process');
7
+ const INTERRUPTS = require('./signals.cjs');
8
+ let localVariables;
9
+ const boundRoots = new Set();
10
+ const mergeOptions = ['-c', 'maintenance.auto=false', '-c', 'gc.auto=0'];
11
+ const bindGit = (root) => { boundRoots.add(root); return () => boundRoots.delete(root); };
12
+
13
+ function gitEnvironment(root) {
14
+ localVariables ??= execFileSync('git', ['rev-parse', '--local-env-vars'],
15
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim().split('\n');
16
+ const env = { ...process.env };
17
+ for (const name of [...localVariables, 'GIT_LITERAL_PATHSPECS', 'GIT_GLOB_PATHSPECS',
18
+ 'GIT_NOGLOB_PATHSPECS', 'GIT_ICASE_PATHSPECS']) delete env[name];
19
+ if (boundRoots.has(root)) env.GIT_CEILING_DIRECTORIES = path.dirname(root);
20
+ return env;
21
+ }
22
+
23
+ function gitFailure(args, error) {
24
+ const reason = String(error.stderr || '').split('\n').find((line) => line.trim());
25
+ error.message = `Git failed: ${args.join(' ')}` + (reason ? `: ${reason}` : '');
26
+ if (INTERRUPTS[error.signal]) error.exitCode = INTERRUPTS[error.signal];
27
+ return error;
28
+ }
29
+
30
+ function git(root, args) {
31
+ if (boundRoots.has(root)) args = [...mergeOptions, ...args];
32
+ try {
33
+ return execFileSync('git', args, { cwd: root, env: gitEnvironment(root), maxBuffer: Infinity,
34
+ stdio: ['ignore', 'pipe', 'pipe'] });
35
+ } catch (error) {
36
+ throw gitFailure(args, error);
37
+ }
38
+ }
39
+
40
+ function gitAsync(root, args, options = {}) {
41
+ return new Promise((resolve, reject) => {
42
+ const child = spawn('git', args, { cwd: root, env: gitEnvironment(root),
43
+ stdio: ['ignore', 'pipe', 'pipe'], ...options });
44
+ const stdout = [];
45
+ const stderr = [];
46
+ child.stdout.on('data', (chunk) => stdout.push(chunk));
47
+ child.stderr.on('data', (chunk) => stderr.push(chunk));
48
+ child.on('error', (error) => reject(gitFailure(args, error)));
49
+ child.on('close', (code, signal) => {
50
+ if (code === 0) resolve(Buffer.concat(stdout));
51
+ else reject(gitFailure(args, Object.assign(new Error(),
52
+ { code, signal, stderr: Buffer.concat(stderr).toString('utf8') })));
53
+ });
54
+ });
55
+ }
56
+
57
+ const ownedFiles = new Set(['.ultra/spec.md', '.ultra/checks.json',
58
+ '.ultra/plan.json', '.ultra/rulings.md']);
59
+ const ownedDirs = ['.ultra/ledger', '.ultra/records', '.ultra/.runtime'];
60
+ const engineOwned = (name) => ownedFiles.has(name)
61
+ || ownedDirs.some((dir) => name === dir || name.startsWith(`${dir}/`));
62
+
63
+ function fingerprintEntries(digest, tree, exclude) {
64
+ // ls-tree separates mode/type/object metadata from the unquoted path with a tab.
65
+ const PATH_SEPARATOR = 0x09;
66
+ let start = 0;
67
+ for (let end = tree.indexOf(0); end !== -1; end = tree.indexOf(0, start)) {
68
+ const entry = tree.subarray(start, end);
69
+ const name = entry.subarray(entry.indexOf(PATH_SEPARATOR) + 1).toString('utf8');
70
+ if (!exclude(name)) digest.update(tree.subarray(start, end + 1));
71
+ start = end + 1;
72
+ }
73
+ }
74
+
75
+ function codeState(root, exclude = engineOwned) {
76
+ const digest = createHash('sha256');
77
+ fingerprintEntries(digest, git(root, ['ls-tree', '--full-tree', '-z', 'HEAD']),
78
+ (name) => name === '.ultra');
79
+ fingerprintEntries(digest, git(root, ['ls-tree', '--full-tree', '-z', 'HEAD', '--',
80
+ '.ultra/']), engineOwned);
81
+ const changes = git(root, ['--no-optional-locks', 'status', '--porcelain', '-z',
82
+ `--untracked-files=${exclude === engineOwned ? 'normal' : 'all'}`])
83
+ .toString('utf8').split('\0');
84
+ let dirty = false;
85
+ for (let i = 0; i < changes.length; i++) {
86
+ if (!changes[i]) continue;
87
+ if (!exclude(changes[i].slice(3).replace(/\/$/u, ''))) dirty = true;
88
+ if (/[RC]/u.test(changes[i].slice(0, 2)) && !exclude(changes[++i])) dirty = true;
89
+ }
90
+ return { code: digest.digest('hex'), dirty };
91
+ }
92
+
93
+ function projectRoot(requested) {
94
+ const root = path.resolve(requested);
95
+ if (!fs.existsSync(root)) {
96
+ throw new Error(`--root ${root} does not exist. Choose an existing project directory.`);
97
+ }
98
+ let top;
99
+ try { top = git(root, ['rev-parse', '--show-toplevel']).toString('utf8').replace(/\n$/u, ''); }
100
+ catch (error) {
101
+ if (error.exitCode) throw error;
102
+ throw new Error(`${root} is not inside a Git work tree. Run git init first.`);
103
+ }
104
+ try { git(top, ['rev-parse', '--verify', 'HEAD']); }
105
+ catch (error) {
106
+ if (error.exitCode) throw error;
107
+ throw new Error(`${top} has no commit. Commit the code first.`);
108
+ }
109
+ return top;
110
+ }
111
+
112
+ module.exports = { git, gitAsync, gitEnvironment, bindGit, mergeOptions, codeState, projectRoot };
@@ -0,0 +1,205 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { ID, object, strings, read } = require('./formats.cjs');
6
+
7
+ const hashValue = (value) => typeof value === 'string' && /^[a-f0-9]{64}$/u.test(value);
8
+ const hashes = (value) => object(value) && Object.values(value).every(hashValue);
9
+ const textFields = (record, fields) => fields.every((key) => typeof record[key] === 'string');
10
+ const checkSnapshots = (value) => object(value) && Object.values(value).every((check) =>
11
+ object(check) && hashValue(check.fp) && strings(check.covers));
12
+ const LEDGERS = ['evidence', 'plan-log', 'nodes', 'milestones', 'approvals'];
13
+ const approvalItem = (value) => typeof value === 'string' && value.split('/').length === 2
14
+ && value.split('/').every((part) => ID.test(part));
15
+
16
+ function validApproval(record) {
17
+ return strings(record.items) && record.items.length > 0
18
+ && record.items.every((item, i) => approvalItem(item) && (!i || record.items[i - 1] < item))
19
+ && object(record.owner) && typeof record.owner.words === 'string'
20
+ && record.owner.words.trim().length > 0 && typeof record.owner.proxy === 'boolean';
21
+ }
22
+
23
+ function validClosure(record) {
24
+ const fields = {
25
+ code: hashValue,
26
+ reqs: hashes,
27
+ journey: (v) => object(v) && textFields(v, ['check', 'run']),
28
+ review: (v) => object(v) && textFields(v, ['path']) && hashValue(v.sha256),
29
+ owner: (v) => object(v) && textFields(v, ['words']) && typeof v.proxy === 'boolean',
30
+ };
31
+ return typeof record.milestone === 'string'
32
+ && Object.entries(fields).every(([key, valid]) => !Object.hasOwn(record, key)
33
+ || valid(record[key]));
34
+ }
35
+
36
+ function recordTime(value) {
37
+ return typeof value === 'string'
38
+ && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
39
+ && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
40
+ }
41
+
42
+ function validRecord(record, name) {
43
+ if (!object(record) || record.v !== 1 || !recordTime(record.at)) return false;
44
+ if (['evidence', 'plan-log', 'nodes'].includes(name)
45
+ && Object.hasOwn(record, 'shared') && !hashValue(record.shared)) return false;
46
+ if (name === 'evidence') {
47
+ return textFields(record, ['check', 'run', 'reason', 'log'])
48
+ && typeof record.dirty === 'boolean' && hashValue(record.code)
49
+ && hashValue(record.check_fp) && hashes(record.reqs)
50
+ && ['pass', 'fail', 'unknown'].includes(record.outcome)
51
+ && (record.exit_code === null || Number.isInteger(record.exit_code))
52
+ && Number.isInteger(record.duration_ms) && record.duration_ms >= 0
53
+ && (record.error_line === null || typeof record.error_line === 'string');
54
+ }
55
+ if (name === 'plan-log') {
56
+ const diff = record.diff;
57
+ return typeof record.milestone === 'string' && hashes(record.requirements)
58
+ && strings(record.nodes) && checkSnapshots(record.checks)
59
+ && (!object(diff) || !Object.hasOwn(diff, 'shared') || diff.shared === true)
60
+ && ['baseline', 'change', 'removed'].includes(record.type)
61
+ && (record.type !== 'removed' || (!Object.keys(record.requirements).length
62
+ && !record.nodes.length && !Object.keys(record.checks).length))
63
+ && (record.type === 'baseline' || (object(diff)
64
+ && ['added', 'removed', 'modified', 'nodes_added', 'nodes_removed',
65
+ 'checks_added', 'checks_removed', 'checks_modified']
66
+ .every((key) => strings(diff[key]))));
67
+ }
68
+ if (name === 'nodes') return textFields(record, ['node']) && hashValue(record.code)
69
+ && hashes(record.reqs) && strings(record.runs);
70
+ if (name === 'approvals') return validApproval(record);
71
+ return name === 'milestones' && validClosure(record);
72
+ }
73
+
74
+ function ledger(root, name) {
75
+ const file = `ledger/${name}.jsonl`;
76
+ const text = read(root, file, true);
77
+ const lines = text.split('\n');
78
+ const records = [];
79
+ const warnings = [];
80
+ let repairEnd = null;
81
+ lines.forEach((line, index) => {
82
+ if (!line.trim()) return;
83
+ let record;
84
+ try { record = JSON.parse(line); } catch { /* validated below */ }
85
+ if (validRecord(record, name)) { records.push(record); return; }
86
+ const location = `.ultra/${file}:${index + 1}`;
87
+ if (index === lines.length - 1 && !text.endsWith('\n')) {
88
+ repairEnd = Buffer.byteLength(text.slice(0, text.lastIndexOf('\n') + 1));
89
+ warnings.push(`${location}: incomplete final ledger record ignored; repaired on next append`);
90
+ } else throw new Error(`${location}: malformed ledger record; remove or repair that line.`);
91
+ });
92
+ return { records, warnings, repairEnd, needsNewline: !!text && !text.endsWith('\n') };
93
+ }
94
+
95
+ function latest(records, field, id) {
96
+ let result;
97
+ for (const record of records) {
98
+ if (record[field] === id && (!result || record.at >= result.at)) result = record;
99
+ }
100
+ return result;
101
+ }
102
+
103
+ function alive(pid) {
104
+ try { process.kill(pid, 0); return true; }
105
+ catch (error) {
106
+ if (error.code === 'EPERM') return true;
107
+ if (error.code === 'ESRCH') return false;
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ function lockPid(root, name) {
113
+ try {
114
+ const pid = Number(fs.readFileSync(path.join(root, '.ultra/.runtime', `${name}.lock`), 'utf8'));
115
+ return Number.isInteger(pid) && pid > 0 && alive(pid) ? pid : null;
116
+ } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
117
+ }
118
+
119
+ function removeOwnedLock(file, pid) {
120
+ try {
121
+ if (Number(fs.readFileSync(file, 'utf8')) === pid) fs.unlinkSync(file);
122
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
123
+ }
124
+
125
+ function breakDeadLock(file, pid) {
126
+ const breaker = `${file}.break`;
127
+ let fd;
128
+ try { fd = fs.openSync(breaker, 'wx'); }
129
+ catch (error) {
130
+ if (error.code !== 'EEXIST') throw error;
131
+ try {
132
+ if (Date.now() - fs.statSync(breaker).mtimeMs > 10000) {
133
+ fs.unlinkSync(breaker);
134
+ return breakDeadLock(file, pid);
135
+ }
136
+ } catch (missing) { if (missing.code !== 'ENOENT') throw missing; }
137
+ return;
138
+ }
139
+ try {
140
+ // Only one breaker rechecks and removes a dead pid; other waiters cannot race it.
141
+ if (!alive(pid)) removeOwnedLock(file, pid);
142
+ } finally {
143
+ fs.closeSync(fd);
144
+ try { fs.unlinkSync(breaker); }
145
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
146
+ }
147
+ }
148
+
149
+ function acquireLock(root, name) {
150
+ const file = path.join(root, '.ultra', '.runtime', `${name}.lock`);
151
+ fs.mkdirSync(path.dirname(file), { recursive: true });
152
+ for (let attempt = 0; attempt < 2; attempt++) {
153
+ let fd;
154
+ try { fd = fs.openSync(file, 'wx'); }
155
+ catch (error) { if (error.code !== 'EEXIST') throw error; }
156
+ if (fd !== undefined) {
157
+ try { fs.writeFileSync(fd, String(process.pid)); }
158
+ finally { fs.closeSync(fd); }
159
+ return () => removeOwnedLock(file, process.pid);
160
+ }
161
+ try {
162
+ const pid = Number(fs.readFileSync(file, 'utf8'));
163
+ if (Number.isInteger(pid) && pid > 0 && !alive(pid)) breakDeadLock(file, pid);
164
+ else return null;
165
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
166
+ }
167
+ return null;
168
+ }
169
+
170
+ function withLock(root, action) {
171
+ const deadline = Date.now() + 30000;
172
+ while (true) {
173
+ const release = acquireLock(root, 'engine');
174
+ if (release) {
175
+ try { return action(); } finally { release(); }
176
+ }
177
+ if (Date.now() >= deadline) {
178
+ throw new Error('.ultra/.runtime/engine.lock: lock wait exceeded 30 s; check its holder.');
179
+ }
180
+ // Sleep without polling the filesystem in a busy loop while the holder works.
181
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
182
+ }
183
+ }
184
+
185
+ function append(root, name, record, locked = false) {
186
+ const write = () => {
187
+ const ledgers = new Map(LEDGERS.map((key) => [key, ledger(root, key)]));
188
+ const state = ledgers.get(name);
189
+ // Read and advance under the same lock so clock rollback cannot hide a new record.
190
+ const greatest = [...ledgers.values()].flatMap((s) => s.records)
191
+ .reduce((time, r) => Math.max(time, Date.parse(r.at)), -1);
192
+ record.at = new Date(Math.max(Date.now(), greatest + 1)).toISOString();
193
+ if (!validRecord(record, name)) {
194
+ throw new Error(`.ultra/ledger/${name}.jsonl: refusing to append a malformed ledger record.`);
195
+ }
196
+ const file = path.join(root, '.ultra', 'ledger', `${name}.jsonl`);
197
+ fs.mkdirSync(path.dirname(file), { recursive: true });
198
+ if (state.repairEnd !== null) fs.truncateSync(file, state.repairEnd);
199
+ const prefix = state.repairEnd === null && state.needsNewline ? '\n' : '';
200
+ fs.appendFileSync(file, `${prefix}${JSON.stringify(record)}\n`);
201
+ };
202
+ return locked ? write() : withLock(root, write);
203
+ }
204
+
205
+ module.exports = { ledger, latest, withLock, acquireLock, lockPid, append, approvalItem };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const { hash, read, requirements, registry } = require('./formats.cjs');
4
+ const { codeState, projectRoot } = require('./git-state.cjs');
5
+ const { ledger } = require('./ledger.cjs');
6
+
7
+ function load(requested) {
8
+ const root = projectRoot(requested);
9
+ const code = codeState(root);
10
+ const { reqs, requirementText, shared } = requirements(read(root, 'spec.md'));
11
+ const validated = registry(root, 'checks.json', { checks: { covers: 'array', run: 'string' } });
12
+ const checks = validated.checks.map((check) => {
13
+ const timeout = check.timeout_sec === undefined ? 600 : check.timeout_sec;
14
+ return { ...check, timeout, fp: hash(JSON.stringify([check.run, timeout, check.expect])) };
15
+ });
16
+ const { milestones, nodes, cleanup_every } = registry(root, 'plan.json', {
17
+ milestones: { title: 'string', requirements: 'array' },
18
+ nodes: { milestone: 'string', title: 'string', covers: 'array', depends: 'array' },
19
+ });
20
+ const history = ['evidence', 'plan-log', 'nodes', 'milestones', 'approvals']
21
+ .map((name) => ledger(root, name));
22
+ return { root, ...code, reqs, requirementText, shared, checks, milestones, nodes, cleanup_every,
23
+ rulings: read(root, 'rulings.md', true), warnings: history.flatMap((h) => h.warnings),
24
+ evidence: history[0].records, plans: history[1].records,
25
+ completions: history[2].records, closures: history[3].records, approvals: history[4].records };
26
+ }
27
+
28
+ module.exports = { load };