ultra-builder-pro-cli 0.12.1 → 0.13.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 (57) hide show
  1. package/.ultra-template/docs/research/README.md +21 -44
  2. package/.ultra-template/specs/discovery.md +10 -0
  3. package/CHANGELOG.md +53 -1
  4. package/README.md +25 -9
  5. package/adapters/_shared/handbook.cjs +2 -2
  6. package/adapters/tests/codex.test.cjs +8 -0
  7. package/commands/ultra-init.md +1 -1
  8. package/docs/AGENT-CONTEXT.md +4 -3
  9. package/docs/ARCHITECTURE.md +32 -16
  10. package/docs/PLAN.zh-CN.md +5 -4
  11. package/docs/ROADMAP.md +1 -1
  12. package/docs/STATE-DB-ACCESS-POLICY.md +3 -3
  13. package/docs/USER-HANDBOOK-CONTRACT.md +5 -4
  14. package/hooks/context_spine.py +37 -4
  15. package/mcp-server/lib/archive-journal.cjs +192 -0
  16. package/mcp-server/lib/archive-journal.test.cjs +35 -0
  17. package/mcp-server/lib/baseline-workflow.cjs +404 -38
  18. package/mcp-server/lib/baseline-workflow.test.cjs +252 -0
  19. package/mcp-server/lib/change-workflow.cjs +249 -75
  20. package/mcp-server/lib/change-workflow.test.cjs +235 -1
  21. package/mcp-server/lib/context-spine.test.cjs +5 -0
  22. package/mcp-server/lib/doctor.cjs +14 -1
  23. package/mcp-server/lib/doctor.test.cjs +42 -4
  24. package/mcp-server/lib/init-project.cjs +292 -9
  25. package/mcp-server/lib/spec-learning.test.cjs +1 -1
  26. package/mcp-server/lib/state-authority.cjs +9 -3
  27. package/mcp-server/lib/state-db.cjs +154 -32
  28. package/mcp-server/lib/state-db.test.cjs +136 -4
  29. package/mcp-server/lib/state-ops.cjs +68 -6
  30. package/mcp-server/lib/task-expander.cjs +7 -1
  31. package/mcp-server/lib/task-expander.test.cjs +71 -0
  32. package/mcp-server/server.cjs +14 -3
  33. package/mcp-server/tests/init-project.test.cjs +162 -1
  34. package/mcp-server/tests/migrate.test.cjs +33 -0
  35. package/mcp-server/tests/server-contract.test.cjs +384 -6
  36. package/mcp-server/tests/state-db.test.cjs +14 -2
  37. package/mcp-server/tests/state-ops.test.cjs +108 -0
  38. package/package.json +1 -1
  39. package/skills/ultra-change/SKILL.md +18 -10
  40. package/skills/ultra-deliver/SKILL.md +13 -9
  41. package/skills/ultra-dev/SKILL.md +6 -4
  42. package/skills/ultra-doctor/SKILL.md +22 -7
  43. package/skills/ultra-init/SKILL.md +104 -59
  44. package/skills/ultra-init/references/brownfield-adoption.md +65 -37
  45. package/skills/ultra-plan/SKILL.md +8 -5
  46. package/skills/ultra-research/SKILL.md +12 -5
  47. package/skills/ultra-status/SKILL.md +6 -4
  48. package/spec/cli-protocol.md +11 -1
  49. package/spec/mcp-tools.yaml +95 -9
  50. package/spec/schemas/state-db.sql +11 -0
  51. package/ultra-tools/cli.cjs +9 -5
  52. package/ultra-tools/commands/migrate.cjs +62 -36
  53. package/ultra-tools/commands/status.cjs +18 -9
  54. package/ultra-tools/commands/system.cjs +340 -6
  55. package/ultra-tools/commands/task.cjs +7 -1
  56. package/ultra-tools/status-command.test.cjs +29 -0
  57. package/ultra-tools/system-command.test.cjs +252 -1
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const INTENT_FILE = '.archive-intent.json';
7
+ const INTENT_VERSION = '1.0';
8
+
9
+ class ArchiveJournalError extends Error {
10
+ constructor(code, message) {
11
+ super(message);
12
+ this.name = 'ArchiveJournalError';
13
+ this.code = code;
14
+ }
15
+ }
16
+
17
+ function resolveInside(rootDir, relative, field) {
18
+ if (typeof relative !== 'string' || !relative.trim() || path.isAbsolute(relative)) {
19
+ throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', `${field} must be project-relative`);
20
+ }
21
+ const root = path.resolve(rootDir);
22
+ const resolved = path.resolve(root, relative);
23
+ if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
24
+ throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', `${field} escapes project root`);
25
+ }
26
+ return resolved;
27
+ }
28
+
29
+ function assertIntent(intent, rootDir) {
30
+ if (!intent || typeof intent !== 'object' || Array.isArray(intent)
31
+ || intent.schema_version !== INTENT_VERSION
32
+ || typeof intent.change_id !== 'string' || !intent.change_id
33
+ || !Array.isArray(intent.baseline_updates)
34
+ || typeof intent.summary !== 'string' || intent.summary.trim().length < 3) {
35
+ throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', 'archive intent shape is invalid');
36
+ }
37
+ const source = resolveInside(rootDir, intent.source, 'source');
38
+ const destination = resolveInside(rootDir, intent.destination, 'destination');
39
+ const activeRoot = path.resolve(rootDir, '.ultra', 'changes', 'active');
40
+ const archiveRoot = path.resolve(rootDir, '.ultra', 'changes', 'archive');
41
+ if (path.dirname(source) !== activeRoot || path.basename(source) !== intent.change_id) {
42
+ throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', 'archive intent source is not the active change root');
43
+ }
44
+ if (path.dirname(destination) !== archiveRoot
45
+ || !path.basename(destination).endsWith(`-${intent.change_id}`)) {
46
+ throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', 'archive intent destination is not the change archive root');
47
+ }
48
+ for (const update of intent.baseline_updates) resolveInside(rootDir, update, 'baseline update');
49
+ if (intent.previous_summary !== null && typeof intent.previous_summary !== 'string') {
50
+ throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', 'previous_summary must be string or null');
51
+ }
52
+ return { intent, source, destination };
53
+ }
54
+
55
+ function readIntentFile(file, rootDir) {
56
+ let intent;
57
+ try { intent = JSON.parse(fs.readFileSync(file, 'utf8')); }
58
+ catch (error) {
59
+ throw new ArchiveJournalError('ARCHIVE_INTENT_CORRUPT', `cannot read ${file}: ${error.message}`);
60
+ }
61
+ return assertIntent(intent, rootDir);
62
+ }
63
+
64
+ function writeJsonAtomic(file, value) {
65
+ const temp = `${file}.${process.pid}.tmp`;
66
+ fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
67
+ fs.renameSync(temp, file);
68
+ }
69
+
70
+ function renderSummary(change, summary, baselineUpdates, noBaselineChangeReason) {
71
+ return [
72
+ `# Archived change: ${change.title}`, '', summary.trim(), '',
73
+ '## Baseline reconciliation', '',
74
+ ...(baselineUpdates.length > 0
75
+ ? baselineUpdates.map((file) => `- ${file}`)
76
+ : [noBaselineChangeReason]), '',
77
+ ].join('\n');
78
+ }
79
+
80
+ function sameOperation(intent, { change, summary, baselineUpdates, noBaselineChangeReason }) {
81
+ return intent.change_id === change.id
82
+ && intent.source === change.artifact_root
83
+ && intent.summary === summary.trim()
84
+ && JSON.stringify(intent.baseline_updates) === JSON.stringify(baselineUpdates)
85
+ && (intent.no_baseline_change_reason || null) === (noBaselineChangeReason || null);
86
+ }
87
+
88
+ function prepareArchiveMove({
89
+ rootDir, change, summary, baselineUpdates, noBaselineChangeReason, now = new Date(),
90
+ }) {
91
+ const date = now.toISOString().slice(0, 10);
92
+ const source = resolveInside(rootDir, change.artifact_root, 'source');
93
+ const destinationRelative = path.join('.ultra', 'changes', 'archive', `${date}-${change.id}`);
94
+ const destination = resolveInside(rootDir, destinationRelative, 'destination');
95
+ const sourceIntentFile = path.join(source, INTENT_FILE);
96
+ const destinationIntentFile = path.join(destination, INTENT_FILE);
97
+
98
+ if (!fs.existsSync(source) && fs.existsSync(destinationIntentFile)) {
99
+ const resumed = readIntentFile(destinationIntentFile, rootDir);
100
+ if (!sameOperation(resumed.intent, {
101
+ change, summary, baselineUpdates, noBaselineChangeReason,
102
+ })) {
103
+ throw new ArchiveJournalError('ARCHIVE_INTENT_CONFLICT', 'existing archive intent differs from retry input');
104
+ }
105
+ return { ...resumed, resumed: true };
106
+ }
107
+ if (!fs.existsSync(source)) {
108
+ throw new ArchiveJournalError('ARCHIVE_SOURCE_MISSING', `active change root missing: ${change.artifact_root}`);
109
+ }
110
+ if (fs.existsSync(destination)) {
111
+ throw new ArchiveJournalError('ARCHIVE_EXISTS', `archive already exists: ${destination}`);
112
+ }
113
+
114
+ let prepared;
115
+ if (fs.existsSync(sourceIntentFile)) {
116
+ prepared = readIntentFile(sourceIntentFile, rootDir);
117
+ if (!sameOperation(prepared.intent, {
118
+ change, summary, baselineUpdates, noBaselineChangeReason,
119
+ })) {
120
+ throw new ArchiveJournalError('ARCHIVE_INTENT_CONFLICT', 'prepared archive intent differs from retry input');
121
+ }
122
+ } else {
123
+ const summaryPath = path.join(source, 'archive-summary.md');
124
+ const previousSummary = fs.existsSync(summaryPath) ? fs.readFileSync(summaryPath, 'utf8') : null;
125
+ const intent = {
126
+ schema_version: INTENT_VERSION,
127
+ change_id: change.id,
128
+ source: change.artifact_root,
129
+ destination: destinationRelative,
130
+ summary: summary.trim(),
131
+ baseline_updates: baselineUpdates,
132
+ no_baseline_change_reason: noBaselineChangeReason || null,
133
+ previous_summary: previousSummary,
134
+ created_at: now.toISOString(),
135
+ };
136
+ writeJsonAtomic(sourceIntentFile, intent);
137
+ prepared = assertIntent(intent, rootDir);
138
+ }
139
+
140
+ fs.writeFileSync(
141
+ path.join(source, 'archive-summary.md'),
142
+ renderSummary(change, summary, baselineUpdates, noBaselineChangeReason),
143
+ );
144
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
145
+ fs.renameSync(source, destination);
146
+ return { ...prepared, source, destination, resumed: false };
147
+ }
148
+
149
+ function listArchiveIntents(rootDir) {
150
+ const roots = [
151
+ path.join(rootDir, '.ultra', 'changes', 'active'),
152
+ path.join(rootDir, '.ultra', 'changes', 'archive'),
153
+ ];
154
+ const found = [];
155
+ for (const parent of roots) {
156
+ if (!fs.existsSync(parent)) continue;
157
+ for (const entry of fs.readdirSync(parent, { withFileTypes: true })) {
158
+ if (!entry.isDirectory()) continue;
159
+ const file = path.join(parent, entry.name, INTENT_FILE);
160
+ if (!fs.existsSync(file)) continue;
161
+ try { found.push({ ...readIntentFile(file, rootDir), file, error: null }); }
162
+ catch (error) { found.push({ file, error }); }
163
+ }
164
+ }
165
+ return found;
166
+ }
167
+
168
+ function completeArchiveIntent(rootDir, intent) {
169
+ const { destination } = assertIntent(intent, rootDir);
170
+ fs.rmSync(path.join(destination, INTENT_FILE), { force: true });
171
+ }
172
+
173
+ function rollbackArchiveIntent(rootDir, intent) {
174
+ const { source, destination } = assertIntent(intent, rootDir);
175
+ if (fs.existsSync(destination) && !fs.existsSync(source)) fs.renameSync(destination, source);
176
+ if (!fs.existsSync(source) || fs.existsSync(destination)) {
177
+ throw new ArchiveJournalError('ARCHIVE_ROLLBACK_FAILED', 'archive roots are not in a recoverable state');
178
+ }
179
+ const summaryPath = path.join(source, 'archive-summary.md');
180
+ if (intent.previous_summary === null) fs.rmSync(summaryPath, { force: true });
181
+ else fs.writeFileSync(summaryPath, intent.previous_summary);
182
+ fs.rmSync(path.join(source, INTENT_FILE), { force: true });
183
+ }
184
+
185
+ module.exports = {
186
+ ArchiveJournalError,
187
+ INTENT_FILE,
188
+ prepareArchiveMove,
189
+ listArchiveIntents,
190
+ completeArchiveIntent,
191
+ rollbackArchiveIntent,
192
+ };
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+
9
+ const journal = require('./archive-journal.cjs');
10
+
11
+ test('archive journal restores the exact active directory when finalization cannot run', () => {
12
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-archive-journal-'));
13
+ const source = path.join(rootDir, '.ultra', 'changes', 'active', 'change-a');
14
+ fs.mkdirSync(source, { recursive: true });
15
+ fs.writeFileSync(path.join(source, 'intent.md'), '# Change A\n');
16
+ try {
17
+ const prepared = journal.prepareArchiveMove({
18
+ rootDir,
19
+ change: { id: 'change-a', title: 'Change A', artifact_root: '.ultra/changes/active/change-a' },
20
+ summary: 'Archive Change A.', baselineUpdates: [],
21
+ noBaselineChangeReason: 'No baseline content changed.',
22
+ now: new Date('2026-07-18T00:00:00.000Z'),
23
+ });
24
+ assert.equal(fs.existsSync(prepared.source), false);
25
+ assert.equal(journal.listArchiveIntents(rootDir).length, 1);
26
+
27
+ journal.rollbackArchiveIntent(rootDir, prepared.intent);
28
+ assert.equal(fs.existsSync(source), true);
29
+ assert.equal(fs.existsSync(path.join(source, 'intent.md')), true);
30
+ assert.equal(fs.existsSync(path.join(source, 'archive-summary.md')), false);
31
+ assert.equal(journal.listArchiveIntents(rootDir).length, 0);
32
+ } finally {
33
+ fs.rmSync(rootDir, { recursive: true, force: true });
34
+ }
35
+ });