ultra-builder-pro-cli 0.20.0 → 0.21.1

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 (102) hide show
  1. package/.ultra-template/docs/research/README.md +26 -5
  2. package/.ultra-template/specs/discovery.md +1 -1
  3. package/.ultra-template/tasks/tasks.json +1 -1
  4. package/CHANGELOG.md +66 -0
  5. package/README.md +54 -24
  6. package/adapters/_shared/codex-assets.cjs +69 -11
  7. package/adapters/_shared/interaction-contract.cjs +36 -3
  8. package/adapters/_shared/runtime-assets.cjs +41 -1
  9. package/adapters/claude.js +7 -5
  10. package/adapters/codex.js +1 -1
  11. package/adapters/kimi.js +2 -8
  12. package/adapters/opencode.js +30 -28
  13. package/bin/orchestrator.js +64 -24
  14. package/docs/AGENT-CONTEXT.md +74 -43
  15. package/docs/ARCHITECTURE.md +63 -31
  16. package/docs/ARTIFACT-AUTHORITY.md +234 -0
  17. package/docs/DECISIONS.md +10 -4
  18. package/docs/LEGACY-CLI-CROSSWALK.md +6 -4
  19. package/docs/PLUGIN-ISOLATION-CONTRACT.md +4 -3
  20. package/docs/ROADMAP.md +19 -5
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +24 -13
  22. package/docs/STATE-DB-ACCESS-POLICY.md +11 -9
  23. package/docs/WORKFLOW-LIFECYCLE.md +70 -39
  24. package/hooks/README.md +6 -5
  25. package/hooks/active_task_context.py +97 -12
  26. package/hooks/context_spine.py +16 -4
  27. package/hooks/health_check.py +5 -4
  28. package/hooks/pre_stop_check.py +5 -2
  29. package/hooks/runtime_paths.py +396 -0
  30. package/hooks/subagent_tracker.py +5 -2
  31. package/hooks/workflow_checkpoint.py +16 -3
  32. package/hooks/workflow_context.py +11 -3
  33. package/hooks/workflow_resume.py +7 -4
  34. package/mcp-server/breadcrumb.cjs +20 -2
  35. package/mcp-server/lib/archive-journal.cjs +1026 -111
  36. package/mcp-server/lib/archive-mutation-worker.py +365 -0
  37. package/mcp-server/lib/artifact-registry.cjs +1126 -0
  38. package/mcp-server/lib/baseline-workflow.cjs +9 -4
  39. package/mcp-server/lib/change-packet.cjs +818 -0
  40. package/mcp-server/lib/change-workflow.cjs +932 -176
  41. package/mcp-server/lib/context-paths.cjs +111 -0
  42. package/mcp-server/lib/context-spine.cjs +511 -45
  43. package/mcp-server/lib/decision-dialogue.cjs +345 -3
  44. package/mcp-server/lib/delivery-transaction.cjs +471 -0
  45. package/mcp-server/lib/doctor-backup-worker.cjs +328 -0
  46. package/mcp-server/lib/doctor.cjs +373 -11
  47. package/mcp-server/lib/git-bootstrap.cjs +459 -47
  48. package/mcp-server/lib/init-project.cjs +190 -95
  49. package/mcp-server/lib/plan-publication-worker.cjs +830 -0
  50. package/mcp-server/lib/plan-store.cjs +700 -5
  51. package/mcp-server/lib/project-breadcrumb.cjs +23 -4
  52. package/mcp-server/lib/projector.cjs +174 -40
  53. package/mcp-server/lib/runtime-paths.cjs +1687 -0
  54. package/mcp-server/lib/safe-project-file.cjs +385 -0
  55. package/mcp-server/lib/spec-learning.cjs +28 -12
  56. package/mcp-server/lib/state-db.cjs +263 -9
  57. package/mcp-server/lib/state-ops.cjs +248 -49
  58. package/mcp-server/lib/telemetry.cjs +12 -3
  59. package/mcp-server/lib/workflow-state.cjs +610 -58
  60. package/mcp-server/server.cjs +278 -33
  61. package/orchestrator/daemon.cjs +2 -10
  62. package/orchestrator/parallel-orchestrator.cjs +314 -50
  63. package/orchestrator/planner/plan-builder.cjs +1 -1
  64. package/orchestrator/recovery.cjs +125 -7
  65. package/orchestrator/session-close-journal-worker.cjs +5 -0
  66. package/orchestrator/session-close-journal.cjs +1014 -0
  67. package/orchestrator/session-runner.cjs +679 -69
  68. package/orchestrator/worktree-manager.cjs +186 -35
  69. package/package.json +2 -3
  70. package/skills/cc-collab/SKILL.md +4 -1
  71. package/skills/codex-collab/SKILL.md +5 -2
  72. package/skills/ultra-change/SKILL.md +26 -8
  73. package/skills/ultra-deliver/SKILL.md +23 -11
  74. package/skills/ultra-dev/SKILL.md +33 -6
  75. package/skills/ultra-doctor/SKILL.md +6 -2
  76. package/skills/ultra-init/SKILL.md +4 -2
  77. package/skills/ultra-plan/SKILL.md +49 -16
  78. package/skills/ultra-research/SKILL.md +30 -27
  79. package/skills/ultra-review/SKILL.md +9 -5
  80. package/skills/ultra-status/SKILL.md +9 -4
  81. package/skills/ultra-test/SKILL.md +10 -5
  82. package/skills/ultra-think/SKILL.md +6 -2
  83. package/skills/ultra-think/references/decision-dialogue.md +17 -6
  84. package/skills/ultra-verify/SKILL.md +4 -1
  85. package/spec/cli-protocol.md +18 -7
  86. package/spec/mcp-tools.yaml +502 -44
  87. package/spec/schemas/change-delta.v1.schema.json +153 -0
  88. package/spec/schemas/documentation-reconciliation.v1.schema.json +106 -0
  89. package/spec/schemas/mcp-tools.schema.json +1 -1
  90. package/spec/schemas/state-db.sql +50 -13
  91. package/spec/schemas/tasks.v4.5.schema.json +1 -1
  92. package/ultra-tools/cli.cjs +3 -2
  93. package/ultra-tools/commands/db.cjs +76 -25
  94. package/ultra-tools/commands/migrate.cjs +995 -80
  95. package/ultra-tools/commands/session.cjs +34 -8
  96. package/ultra-tools/commands/status.cjs +32 -6
  97. package/ultra-tools/commands/system.cjs +129 -22
  98. package/commands/learn.md +0 -10
  99. package/orchestrator/skill-miner.cjs +0 -133
  100. package/output-styles/coding-vibes.md +0 -67
  101. package/output-styles/structural-thinking.md +0 -61
  102. package/skills/learn/SKILL.md +0 -58
@@ -9,9 +9,10 @@
9
9
  const path = require('node:path');
10
10
  const { initStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
11
11
  const ops = require('../../mcp-server/lib/state-ops.cjs');
12
+ const runtimePaths = require('../../mcp-server/lib/runtime-paths.cjs');
12
13
  const sessionRunner = require('../../orchestrator/session-runner.cjs');
13
14
 
14
- const DEFAULT_DB_PATH = path.join('.ultra', 'state.db');
15
+ const DEFAULT_DB_PATH = runtimePaths.STATE_DB_RELATIVE_PATH;
15
16
 
16
17
  const USAGE = `ultra-tools session <verb> [flags]
17
18
 
@@ -21,11 +22,11 @@ VERBS:
21
22
  reap [--grace-seconds <n>] orphan sweeper (Phase 4.5.6)
22
23
  get --sid <sid>
23
24
  heartbeat --sid <sid>
24
- close --sid <sid> --status completed|crashed|orphan [--remove-worktree]
25
+ close --sid <sid> --status completed|crashed [--remove-worktree]
25
26
  subscribe --since-id <n> [--sid <sid>] [--limit <n>]
26
27
 
27
28
  GLOBAL FLAGS:
28
- --db <path> path to state.db (default: .ultra/state.db)
29
+ --db <path> path to state.db (default: .ultra/.runtime/state.db)
29
30
  --repo-root <path> repository root for verified worktree cleanup (default: cwd)
30
31
  -h, --help show this message
31
32
  `;
@@ -56,10 +57,31 @@ function parseFlags(args) {
56
57
  }
57
58
 
58
59
  function withDb(flags, fn) {
59
- const dbPath = path.resolve(flags.db || DEFAULT_DB_PATH);
60
- const { db } = initStateDb(dbPath);
61
- try { return fn(db); }
62
- finally { try { closeStateDb(db); } catch (_) { /* ignore */ } }
60
+ let db;
61
+ try {
62
+ const rootDir = path.resolve(flags.repo_root || process.cwd());
63
+ const env = flags.db
64
+ ? { UBP_DB_PATH: path.resolve(flags.db) }
65
+ : process.env;
66
+ const dbPath = runtimePaths.resolveConfiguredStateDb(rootDir, {
67
+ env,
68
+ migrateLegacy: true,
69
+ }).stateDbPath;
70
+ ({ db } = initStateDb(dbPath));
71
+ return fn(db);
72
+ } catch (error) {
73
+ emit({
74
+ ok: false,
75
+ error: {
76
+ code: error.code || 'STATE_DB_ERROR',
77
+ message: error.message,
78
+ retriable: false,
79
+ },
80
+ });
81
+ return 2;
82
+ } finally {
83
+ try { closeStateDb(db); } catch (_) { /* ignore */ }
84
+ }
63
85
  }
64
86
 
65
87
  function cmdList(flags) {
@@ -84,7 +106,11 @@ function cmdAdmission(flags) {
84
106
  if (!flags.task_id) { emit({ ok: false, error: { code: 'VALIDATION_ERROR', message: 'missing --task-id' } }); return 1; }
85
107
  return withDb(flags, (db) => {
86
108
  try {
87
- const v = ops.admissionCheck(db, flags.task_id);
109
+ const v = sessionRunner.admissionCheck(
110
+ db,
111
+ path.resolve(flags.repo_root || process.cwd()),
112
+ flags.task_id,
113
+ );
88
114
  emit({ ok: true, data: v });
89
115
  return 0;
90
116
  } catch (err) {
@@ -13,11 +13,13 @@ const ops = require('../../mcp-server/lib/state-ops.cjs');
13
13
  const baselines = require('../../mcp-server/lib/baseline-workflow.cjs');
14
14
  const workflowState = require('../../mcp-server/lib/workflow-state.cjs');
15
15
  const decisionDialogue = require('../../mcp-server/lib/decision-dialogue.cjs');
16
+ const artifactRegistry = require('../../mcp-server/lib/artifact-registry.cjs');
16
17
  const { readBreadcrumb } = require('../../mcp-server/lib/context-spine.cjs');
18
+ const runtimePaths = require('../../mcp-server/lib/runtime-paths.cjs');
17
19
 
18
20
  const USAGE = `ultra-tools status [flags]
19
21
 
20
- Reads .ultra/state.db and prints baseline, workflow, task, session, valid transitions, and
22
+ Reads .ultra/.runtime/state.db and prints baseline, workflow, task, session, valid transitions, and
21
23
  cost-observability status.
22
24
 
23
25
  FLAGS:
@@ -175,6 +177,16 @@ function buildStatusPanel(db, { since = null, limit = 3, rootDir = process.cwd()
175
177
  awaiting_blocking: 0, checkpoint_ready: 0,
176
178
  deferred_blocking: 0, stale_artifacts: [], current: null, current_thread_id: null,
177
179
  };
180
+ const artifactTables = [
181
+ 'artifacts', 'artifact_edges', 'baselines', 'workflow_steps',
182
+ 'decision_threads', 'context_snapshots',
183
+ ];
184
+ const artifacts = artifactTables.every((table) => tables.has(table))
185
+ ? artifactRegistry.inspectArtifactHealth(db, { rootDir })
186
+ : {
187
+ status: 'unavailable', registered: 0, managed: 0, unmanaged: 0,
188
+ issues: [], counts: {},
189
+ };
178
190
  let transitions;
179
191
  let breadcrumb = null;
180
192
  try {
@@ -249,6 +261,7 @@ function buildStatusPanel(db, { since = null, limit = 3, rootDir = process.cwd()
249
261
  baseline,
250
262
  workflows,
251
263
  decisions,
264
+ artifacts,
252
265
  current_change: currentChange,
253
266
  current_task: currentTask,
254
267
  evidence,
@@ -301,6 +314,11 @@ function renderHuman(panel) {
301
314
  } else if (panel.decisions.checkpoint_ready > 0) {
302
315
  lines.push(`Decision checkpoint: ${panel.decisions.current_thread_id} awaits confirmation`);
303
316
  }
317
+ lines.push(
318
+ `Artifacts: registered=${panel.artifacts.registered} managed=${panel.artifacts.managed}`
319
+ + ` unmanaged=${panel.artifacts.unmanaged} health=${panel.artifacts.status}`
320
+ + (panel.artifacts.issues.length > 0 ? ` issues=${panel.artifacts.issues.length}` : ''),
321
+ );
304
322
  lines.push(
305
323
  `Changes: active=${panel.changes.active} blocked=${panel.changes.blocked} ready=${panel.changes.ready} archived=${panel.changes.archived}`,
306
324
  );
@@ -355,14 +373,13 @@ function renderHuman(panel) {
355
373
  return lines.join('\n') + '\n';
356
374
  }
357
375
 
358
- function resolveDbPath() {
359
- if (process.env.UBP_DB_PATH) return path.resolve(process.env.UBP_DB_PATH);
360
- return path.resolve('.ultra', 'state.db');
376
+ function resolveDbPath(rootDir = process.cwd(), env = process.env) {
377
+ return runtimePaths.locateStateDb(rootDir, { env });
361
378
  }
362
379
 
363
380
  function resolveRootDir(dbPath) {
364
381
  if (process.env.UBP_ROOT_DIR) return path.resolve(process.env.UBP_ROOT_DIR);
365
- return path.dirname(path.dirname(dbPath));
382
+ return runtimePaths.projectRootFromStateDbPath(dbPath);
366
383
  }
367
384
 
368
385
  function dispatch(args) {
@@ -372,7 +389,15 @@ function dispatch(args) {
372
389
  process.stdout.write(USAGE);
373
390
  return 0;
374
391
  }
375
- const dbPath = resolveDbPath();
392
+ const discoveryRoot = process.env.UBP_ROOT_DIR
393
+ ? path.resolve(process.env.UBP_ROOT_DIR)
394
+ : process.cwd();
395
+ let dbPath;
396
+ try {
397
+ dbPath = resolveDbPath(discoveryRoot, process.env);
398
+ } catch (error) {
399
+ return fail(error.code || 'STATE_DB_PATH_INVALID', error.message);
400
+ }
376
401
  if (!fs.existsSync(dbPath)) {
377
402
  return fail('STATE_DB_MISSING', `state.db not found at ${dbPath}`);
378
403
  }
@@ -403,5 +428,6 @@ module.exports = {
403
428
  renderHuman,
404
429
  parseSince,
405
430
  parseFlags,
431
+ resolveDbPath,
406
432
  resolveRootDir,
407
433
  };
@@ -7,6 +7,7 @@ const Database = require('better-sqlite3');
7
7
  const { initStateDb, openStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
8
8
  const { initProject } = require('../../mcp-server/lib/init-project.cjs');
9
9
  const doctor = require('../../mcp-server/lib/doctor.cjs');
10
+ const runtimePaths = require('../../mcp-server/lib/runtime-paths.cjs');
10
11
 
11
12
  const RESTORE_CONFIRMATION = 'REPLACE_CORRUPT_ULTRA_STATE';
12
13
  const REBASELINE_CONFIRMATION = 'REBASELINE_CORRUPT_ULTRA_STATE';
@@ -14,7 +15,7 @@ const USAGE = `ultra-tools system <verb> [options]
14
15
 
15
16
  VERBS:
16
17
  doctor [--repair]
17
- restore --backup <.ultra/backups/...db> --confirm ${RESTORE_CONFIRMATION}
18
+ restore --backup <.ultra/.runtime/backups/...db> --confirm ${RESTORE_CONFIRMATION}
18
19
  rebaseline --project-name <name> [--scope <path>] --confirm ${REBASELINE_CONFIRMATION}
19
20
  `;
20
21
 
@@ -96,9 +97,15 @@ function inspectSqliteSnapshot(file) {
96
97
  }
97
98
 
98
99
  function ensureCorruptState(rootDir) {
99
- const statePath = path.join(rootDir, '.ultra', 'state.db');
100
+ const statePath = runtimePaths.ensureRuntimeState(rootDir).stateDbPath;
100
101
  const state = inspectSqliteSnapshot(statePath);
101
- if (!state.exists) throw new SystemCommandError('STATE_DB_MISSING', `.ultra/state.db not found under ${rootDir}`, 2);
102
+ if (!state.exists) {
103
+ throw new SystemCommandError(
104
+ 'STATE_DB_MISSING',
105
+ `.ultra/.runtime/state.db not found under ${rootDir}`,
106
+ 2,
107
+ );
108
+ }
102
109
  if (!state.corrupt) {
103
110
  throw new SystemCommandError(
104
111
  'STATE_DB_NOT_CORRUPT',
@@ -109,44 +116,84 @@ function ensureCorruptState(rootDir) {
109
116
  return statePath;
110
117
  }
111
118
 
119
+ function resolveThroughExistingAncestor(candidate) {
120
+ const missing = [];
121
+ let current = path.resolve(candidate);
122
+ while (!fs.existsSync(current)) {
123
+ const parent = path.dirname(current);
124
+ if (parent === current) return path.resolve(candidate);
125
+ missing.unshift(path.basename(current));
126
+ current = parent;
127
+ }
128
+ return path.join(fs.realpathSync(current), ...missing);
129
+ }
130
+
112
131
  function managedBackup(rootDir, candidate) {
113
132
  if (!candidate) throw new SystemCommandError('VALIDATION_ERROR', '--backup is required');
114
- const backupRoot = path.join(rootDir, '.ultra', 'backups');
115
- if (!fs.existsSync(backupRoot)) {
116
- throw new SystemCommandError('BACKUP_NOT_FOUND', `managed backup directory does not exist: ${backupRoot}`);
133
+ const runtimeBackupRoot = runtimePaths.pathsFor(rootDir).backupsDir;
134
+ const legacyBackupRoot = path.join(rootDir, '.ultra', 'backups');
135
+ const backupRoots = [runtimeBackupRoot, legacyBackupRoot].filter((entry) => fs.existsSync(entry));
136
+ if (backupRoots.length === 0) {
137
+ throw new SystemCommandError(
138
+ 'BACKUP_NOT_FOUND',
139
+ `managed backup directory does not exist: ${runtimeBackupRoot}`,
140
+ );
141
+ }
142
+ let requested = resolveThroughExistingAncestor(
143
+ path.isAbsolute(candidate) ? candidate : path.resolve(rootDir, candidate),
144
+ );
145
+ const legacyPrefix = `${resolveThroughExistingAncestor(legacyBackupRoot)}${path.sep}`;
146
+ if (!fs.existsSync(requested) && requested.startsWith(legacyPrefix)) {
147
+ requested = path.join(runtimeBackupRoot, path.relative(legacyBackupRoot, requested));
117
148
  }
118
- const resolvedRoot = fs.realpathSync(backupRoot);
119
- const requested = path.isAbsolute(candidate) ? candidate : path.resolve(rootDir, candidate);
120
149
  let resolved;
121
150
  try { resolved = fs.realpathSync(requested); }
122
151
  catch {
123
152
  throw new SystemCommandError('BACKUP_NOT_FOUND', `backup does not exist: ${requested}`);
124
153
  }
125
- if (!resolved.startsWith(`${resolvedRoot}${path.sep}`)) {
154
+ const managed = backupRoots.some((entry) => {
155
+ const resolvedRoot = fs.realpathSync(entry);
156
+ return resolved.startsWith(`${resolvedRoot}${path.sep}`);
157
+ });
158
+ if (!managed) {
126
159
  throw new SystemCommandError(
127
160
  'BACKUP_OUTSIDE_MANAGED_ROOT',
128
- `backup must be inside ${backupRoot}`,
161
+ `backup must be inside ${runtimeBackupRoot}`,
129
162
  );
130
163
  }
131
164
  if (!fs.statSync(resolved).isFile()) {
132
165
  throw new SystemCommandError('BACKUP_NOT_FOUND', `backup is not a file: ${resolved}`);
133
166
  }
134
- const inspection = inspectSqlite(resolved);
167
+ for (const suffix of ['', '-wal', '-shm']) {
168
+ const member = `${resolved}${suffix}`;
169
+ if (!fs.existsSync(member)) continue;
170
+ const stat = fs.lstatSync(member);
171
+ if (!stat.isFile() || stat.isSymbolicLink()) {
172
+ throw new SystemCommandError(
173
+ 'BACKUP_INVALID',
174
+ `backup snapshot member must be a regular file: ${member}`,
175
+ );
176
+ }
177
+ }
178
+ const inspection = inspectSqliteSnapshot(resolved);
135
179
  if (!inspection.valid) {
136
- throw new SystemCommandError('BACKUP_INVALID', `backup is not a verified Ultra SQLite database: ${resolved}`);
180
+ throw new SystemCommandError(
181
+ 'BACKUP_INVALID',
182
+ `backup is not a verified Ultra SQLite snapshot: ${resolved}`,
183
+ );
137
184
  }
138
185
  return resolved;
139
186
  }
140
187
 
141
188
  function createRecoveryDir(rootDir, prefix) {
142
- const dir = path.join(rootDir, '.ultra', 'backups', `${prefix}-${stamp()}`);
189
+ const dir = path.join(runtimePaths.pathsFor(rootDir).backupsDir, `${prefix}-${stamp()}`);
143
190
  fs.mkdirSync(path.dirname(dir), { recursive: true });
144
191
  fs.mkdirSync(dir, { recursive: false });
145
192
  return dir;
146
193
  }
147
194
 
148
195
  function moveStateFiles(statePath, recoveryDir, moved = []) {
149
- for (const suffix of ['', '-wal', '-shm']) {
196
+ for (const suffix of ['-wal', '-shm', '']) {
150
197
  const source = `${statePath}${suffix}`;
151
198
  if (!fs.existsSync(source)) continue;
152
199
  const target = path.join(recoveryDir, `state.db${suffix}`);
@@ -175,6 +222,52 @@ function restoreMovedFiles(moved) {
175
222
  return errors;
176
223
  }
177
224
 
225
+ function copySnapshot(source, target) {
226
+ const copied = [];
227
+ try {
228
+ for (const suffix of ['', '-wal', '-shm']) {
229
+ const member = `${source}${suffix}`;
230
+ if (!fs.existsSync(member)) continue;
231
+ const staged = `${target}${suffix}`;
232
+ fs.copyFileSync(member, staged, fs.constants.COPYFILE_EXCL);
233
+ copied.push(staged);
234
+ }
235
+ const inspection = inspectSqlite(target);
236
+ if (!inspection.valid) {
237
+ throw new SystemCommandError(
238
+ 'BACKUP_INVALID',
239
+ `staged backup is not a verified Ultra SQLite snapshot: ${source}`,
240
+ );
241
+ }
242
+ return copied;
243
+ } catch (error) {
244
+ for (const file of copied) {
245
+ try { fs.rmSync(file, { force: true }); } catch { /* best effort */ }
246
+ }
247
+ throw error;
248
+ }
249
+ }
250
+
251
+ function installSnapshot(staging, activeState) {
252
+ const installed = [];
253
+ try {
254
+ // The main DB is the authority completion marker, so publish it last.
255
+ for (const suffix of ['-wal', '-shm', '']) {
256
+ const source = `${staging}${suffix}`;
257
+ if (!fs.existsSync(source)) continue;
258
+ const target = `${activeState}${suffix}`;
259
+ fs.renameSync(source, target);
260
+ installed.push(target);
261
+ }
262
+ return installed;
263
+ } catch (error) {
264
+ for (const target of installed.reverse()) {
265
+ try { fs.rmSync(target, { force: true }); } catch { /* rollback continues */ }
266
+ }
267
+ throw error;
268
+ }
269
+ }
270
+
178
271
  async function dispatchRestore(args) {
179
272
  let options;
180
273
  try { options = parseOptions(args, new Set(['backup', 'confirm'])); }
@@ -194,14 +287,15 @@ async function dispatchRestore(args) {
194
287
  let moved = [];
195
288
  let activeState;
196
289
  let staging;
290
+ let installed = [];
197
291
  try {
198
292
  activeState = ensureCorruptState(rootDir);
199
293
  const sourceBackup = managedBackup(rootDir, options.backup);
200
294
  const recoveryDir = createRecoveryDir(rootDir, 'restore');
201
- staging = path.join(rootDir, '.ultra', `.state.restore-${stamp()}.db`);
202
- fs.copyFileSync(sourceBackup, staging, fs.constants.COPYFILE_EXCL);
295
+ staging = path.join(runtimePaths.pathsFor(rootDir).runtimeDir, `.state.restore-${stamp()}.db`);
296
+ copySnapshot(sourceBackup, staging);
203
297
  moveStateFiles(activeState, recoveryDir, moved);
204
- fs.renameSync(staging, activeState);
298
+ installed = installSnapshot(staging, activeState);
205
299
  const initialized = initStateDb(activeState);
206
300
  closeStateDb(initialized.db);
207
301
  emit({
@@ -219,7 +313,12 @@ async function dispatchRestore(args) {
219
313
  return 0;
220
314
  } catch (error) {
221
315
  if (staging) {
222
- try { fs.rmSync(staging, { force: true }); } catch { /* rollback continues */ }
316
+ for (const suffix of ['', '-wal', '-shm']) {
317
+ try { fs.rmSync(`${staging}${suffix}`, { force: true }); } catch { /* rollback continues */ }
318
+ }
319
+ }
320
+ for (const target of installed.reverse()) {
321
+ try { fs.rmSync(target, { force: true }); } catch { /* rollback continues */ }
223
322
  }
224
323
  const rollbackErrors = activeState ? restoreMovedFiles(moved) : [];
225
324
  emit({
@@ -291,7 +390,7 @@ async function dispatchRebaseline(args) {
291
390
  } catch (error) {
292
391
  const rollbackErrors = [];
293
392
  if (moved.length > 0) {
294
- const generatedState = activeState || path.join(rootDir, '.ultra', 'state.db');
393
+ const generatedState = activeState || runtimePaths.pathsFor(rootDir).stateDbPath;
295
394
  for (const suffix of ['', '-wal', '-shm']) {
296
395
  try { fs.rmSync(`${generatedState}${suffix}`, { force: true }); }
297
396
  catch (rollbackError) { rollbackErrors.push(`generated state cleanup failed: ${rollbackError.message}`); }
@@ -330,14 +429,22 @@ async function dispatchDoctor(args) {
330
429
  return 1;
331
430
  }
332
431
  const rootDir = process.cwd();
333
- const dbPath = path.join(rootDir, '.ultra', 'state.db');
432
+ const repair = args.includes('--repair');
433
+ const dbPath = repair
434
+ ? runtimePaths.ensureRuntimeState(rootDir).stateDbPath
435
+ : runtimePaths.locateStateDb(rootDir);
334
436
  if (!fs.existsSync(dbPath)) {
335
- emit({ ok: false, error: { code: 'STATE_DB_MISSING', message: `.ultra/state.db not found under ${rootDir}` } });
437
+ emit({
438
+ ok: false,
439
+ error: {
440
+ code: 'STATE_DB_MISSING',
441
+ message: `.ultra/.runtime/state.db not found under ${rootDir}`,
442
+ },
443
+ });
336
444
  return 2;
337
445
  }
338
446
  let db;
339
447
  try {
340
- const repair = args.includes('--repair');
341
448
  const initialized = repair ? initStateDb(dbPath) : null;
342
449
  db = initialized ? initialized.db : openStateDb(dbPath);
343
450
  const data = await doctor.runDoctor(db, { rootDir, repair: args.includes('--repair') });
package/commands/learn.md DELETED
@@ -1,10 +0,0 @@
1
- ---
2
- description: Capture one verified reusable workflow as a portable user skill after explicit approval
3
- argument-hint: "[skill-name]"
4
- workflow-ref: "@skills/learn/SKILL.md"
5
- ---
6
-
7
- # Capture a reusable skill
8
-
9
- Read and follow `@skills/learn/SKILL.md` with `$ARGUMENTS` as optional naming guidance.
10
- Treat the referenced skill as the only workflow definition.
@@ -1,133 +0,0 @@
1
- 'use strict';
2
-
3
- // Phase 7.3 — Heuristic skill miner.
4
- //
5
- // Scans a completed session's event stream for "solved something
6
- // non-trivial" signals. Each signal produces a skills/learned/
7
- // <ts>_<sid>_unverified.md draft the operator can promote to a proper
8
- // skill by removing the _unverified suffix and filling in reuse notes.
9
- //
10
- // No LLM call. The heuristic is deliberately coarse so the feature ships
11
- // zero-API-cost; signal quality is explicitly marked "unverified" in the
12
- // frontmatter.
13
-
14
- const fs = require('node:fs');
15
- const path = require('node:path');
16
-
17
- const ops = require('../mcp-server/lib/state-ops.cjs');
18
-
19
- const LEARNED_DIR = 'learned';
20
-
21
- // Event type → draft kind + description template.
22
- const SIGNALS = Object.freeze({
23
- task_completed: {
24
- kind: 'task-completion',
25
- describe: (task) => `Shipped task ${task.id} (${task.title}). Heuristic draft — fill reuse notes before removing _unverified.`,
26
- },
27
- task_circuit_broken: {
28
- kind: 'debug-pattern',
29
- describe: (task, e) => `Task ${task.id} (${task.title}) tripped the breaker after ${e.payload && e.payload.threshold} failures. Inspect the failure stream for a recurring debug pattern.`,
30
- },
31
- session_crashed: {
32
- kind: 'recovery-pattern',
33
- describe: (task) => `Session on task ${task.id} (${task.title}) crashed. Record the recovery path once investigated.`,
34
- },
35
- });
36
-
37
- function sanitizeForFilename(s) {
38
- return String(s).replace(/[^\w.-]/g, '_').slice(0, 80);
39
- }
40
-
41
- function renderDraft({ task, session, signalType, kind, description, events }) {
42
- const ts = new Date().toISOString();
43
- const evIds = events.map((e) => e.id).slice(0, 20);
44
- const lines = [
45
- '---',
46
- `name: ${sanitizeForFilename(task.id + '_' + session.sid)}_unverified`,
47
- `description: "Heuristic draft from session ${session.sid}, task ${task.id}"`,
48
- `kind: ${kind}`,
49
- `source_session: ${session.sid}`,
50
- `source_task: ${task.id}`,
51
- `signal: ${signalType}`,
52
- `ts: ${ts}`,
53
- 'unverified: true',
54
- '---',
55
- '',
56
- '## Problem',
57
- '',
58
- `${task.title}${task.trace_to ? ` (trace: ${task.trace_to})` : ''}`,
59
- '',
60
- '## Signal',
61
- '',
62
- description,
63
- '',
64
- '## Evidence',
65
- '',
66
- `- events: [${evIds.join(', ')}]`,
67
- `- runtime: ${session.runtime}`,
68
- `- final task status: ${task.status}`,
69
- '',
70
- '## Suggested reuse',
71
- '',
72
- '[human: fill in before removing the `_unverified` suffix]',
73
- '',
74
- ];
75
- return lines.join('\n');
76
- }
77
-
78
- function existingDraftsForSession(learnedDir, sid) {
79
- if (!fs.existsSync(learnedDir)) return [];
80
- return fs.readdirSync(learnedDir).filter((f) => f.includes(sid) && f.endsWith('_unverified.md'));
81
- }
82
-
83
- function mineSession(db, { sid, skillsRoot } = {}) {
84
- if (!sid || !skillsRoot) return { drafts: [] };
85
- const session = ops.readSession(db, sid);
86
- if (!session) return { drafts: [] };
87
-
88
- const learnedDir = path.join(skillsRoot, LEARNED_DIR);
89
- fs.mkdirSync(learnedDir, { recursive: true });
90
-
91
- // Idempotency: skip if a draft already exists for this sid.
92
- if (existingDraftsForSession(learnedDir, sid).length > 0) {
93
- return { drafts: [] };
94
- }
95
-
96
- const task = ops.readTask(db, session.task_id);
97
- if (!task) return { drafts: [] };
98
-
99
- const { events } = ops.subscribeEventsSince(db, {
100
- since_id: 0,
101
- task_id: session.task_id,
102
- limit: 500,
103
- });
104
- const sessionEvents = events.filter((e) => e.session_id === sid);
105
-
106
- // Pick the first matching signal (most specific one per session — we don't
107
- // want one session producing three drafts).
108
- for (const e of sessionEvents) {
109
- const sig = SIGNALS[e.type];
110
- if (!sig) continue;
111
- const content = renderDraft({
112
- task,
113
- session,
114
- signalType: e.type,
115
- kind: sig.kind,
116
- description: sig.describe(task, e),
117
- events: sessionEvents,
118
- });
119
- const filename = `${Date.now()}_${sanitizeForFilename(sid)}_unverified.md`;
120
- const target = path.join(learnedDir, filename);
121
- fs.writeFileSync(target, content);
122
- return { drafts: [target] };
123
- }
124
-
125
- return { drafts: [] };
126
- }
127
-
128
- module.exports = {
129
- mineSession,
130
- SIGNALS,
131
- // exposed for tests
132
- _internal: { renderDraft, existingDraftsForSession, sanitizeForFilename },
133
- };
@@ -1,67 +0,0 @@
1
- ---
2
- name: Coding Vibes
3
- description: Energetic, casual coding buddy with modern dev vibes - adapts to user's language while keeping the energy
4
- ---
5
-
6
- # Coding Vibes Style 🚀
7
-
8
- You're a high-energy coding buddy who talks like a modern developer friend. Your vibe is smart, fast-moving, and always hyped to help crush problems!
9
-
10
- ## Core Rule: Language Adaptation
11
- **CRITICAL: Always match the user's language and cultural context while maintaining the energetic dev vibe.**
12
-
13
- ## Communication Style
14
-
15
- **Tone & Energy:**
16
- - Be casual, direct, and playful ✅
17
- - Use modern slang appropriate to user's language/culture
18
- - Include emojis for emotional context (🔥, 💀, 👍, 🙏, 🚀)
19
- - Stay energetic and enthusiastic about problem-solving
20
-
21
- **Writing Pattern:**
22
- - Keep sentences short and punchy
23
- - Use frequent line breaks for chat-like flow
24
- - Avoid corporate speak or robotic language
25
- - Sound like you're messaging a local teammate
26
-
27
- **Developer Mindset:**
28
- - Jump straight into solutions
29
- - Share quick wins and insights
30
- - Call out smooth solutions
31
- - Be real when things get tricky
32
-
33
- ## Language-Specific Examples
34
-
35
- **English:**
36
- - Starting: "Let's gooo!" "I got you!" "This one's clean!"
37
- - Success: "That's fire!" "Clean!" "Absolutely goated!"
38
- - Thinking: "Lemme think..." "This is spicy..."
39
-
40
- **中文:**
41
- - Starting: "来咯!" "搞起!" "这个简单!"
42
- - Success: "666!" "yyds!" "搞定了!"
43
- - Thinking: "让我想想..." "这里tricky..."
44
-
45
- **日本語:**
46
- - Starting: "やるぞ!" "いいね!" "簡単だ!"
47
- - Success: "すげー!" "完璧!" "やったー!"
48
- - Thinking: "ちょっと待って..." "難しいな..."
49
-
50
- **Español:**
51
- - Starting: "¡Vamos!" "¡Perfecto!" "¡Fácil!"
52
- - Success: "¡Genial!" "¡Increíble!" "¡Listo!"
53
- - Thinking: "A ver..." "Está complicado..."
54
-
55
- ## Response Structure
56
-
57
- **Quick Start:**
58
- - Lead with energy in user's language
59
- - Get to the point fast
60
- - Match cultural enthusiasm patterns
61
-
62
- **Problem Solving:**
63
- - Use natural flow words in target language
64
- - Celebrate wins appropriately for culture
65
- - Be honest about complexity using local expressions
66
-
67
- Remember: You're the local coding friend who makes development fun and approachable in any language! 🔥