ultra-builder-pro-cli 0.21.1 → 0.22.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 (47) hide show
  1. package/.ultra-template/tasks/tasks.json +10 -3
  2. package/.ultra-template/templates/task-context.md +19 -0
  3. package/CHANGELOG.md +40 -0
  4. package/README.md +166 -22
  5. package/adapters/opencode.js +23 -12
  6. package/docs/AGENT-CONTEXT.md +22 -13
  7. package/docs/ARCHITECTURE.md +59 -24
  8. package/docs/ARTIFACT-AUTHORITY.md +70 -30
  9. package/docs/COMMIT-HASH-BACKFILL.md +33 -90
  10. package/docs/DECISIONS.md +9 -7
  11. package/docs/LEGACY-CLI-CROSSWALK.md +10 -2
  12. package/docs/PLUGIN-ISOLATION-CONTRACT.md +3 -2
  13. package/docs/ROADMAP.md +3 -3
  14. package/docs/RUNTIME-COMPAT-MATRIX.md +8 -6
  15. package/docs/STATE-DB-ACCESS-POLICY.md +35 -7
  16. package/docs/WORKFLOW-LIFECYCLE.md +40 -16
  17. package/hooks/README.md +5 -4
  18. package/hooks/active_task_context.py +41 -39
  19. package/mcp-server/lib/artifact-registry.cjs +7 -6
  20. package/mcp-server/lib/baseline-workflow.cjs +60 -24
  21. package/mcp-server/lib/context-paths.cjs +34 -3
  22. package/mcp-server/lib/doctor.cjs +28 -0
  23. package/mcp-server/lib/git-bootstrap.cjs +0 -1
  24. package/mcp-server/lib/init-project.cjs +52 -2
  25. package/mcp-server/lib/projector.cjs +43 -8
  26. package/mcp-server/lib/state-authority.cjs +19 -3
  27. package/mcp-server/lib/state-ops.cjs +6 -1
  28. package/mcp-server/lib/task-ledger.cjs +1342 -0
  29. package/mcp-server/server.cjs +145 -9
  30. package/package.json +1 -1
  31. package/skills/ultra-change/SKILL.md +10 -2
  32. package/skills/ultra-deliver/SKILL.md +6 -3
  33. package/skills/ultra-dev/SKILL.md +21 -6
  34. package/skills/ultra-doctor/SKILL.md +10 -1
  35. package/skills/ultra-init/SKILL.md +18 -9
  36. package/skills/ultra-plan/SKILL.md +11 -4
  37. package/skills/ultra-research/SKILL.md +12 -13
  38. package/skills/ultra-review/SKILL.md +5 -3
  39. package/skills/ultra-status/SKILL.md +11 -7
  40. package/skills/ultra-test/SKILL.md +3 -2
  41. package/spec/cli-protocol.md +3 -0
  42. package/spec/mcp-tools.yaml +127 -0
  43. package/spec/schemas/context-file.v4.5.schema.json +1 -1
  44. package/spec/schemas/state-db.sql +1 -1
  45. package/spec/schemas/task-ledger.v1.schema.json +217 -0
  46. package/ultra-tools/commands/migrate.cjs +5 -2
  47. package/.ultra-template/tasks/contexts/TEMPLATE.md +0 -19
@@ -18,6 +18,7 @@ const decisions = require('./decision-dialogue.cjs');
18
18
  const runtimePaths = require('./runtime-paths.cjs');
19
19
  const artifactRegistry = require('./artifact-registry.cjs');
20
20
  const planStore = require('./plan-store.cjs');
21
+ const taskLedger = require('./task-ledger.cjs');
21
22
  const closeJournal = require('../../orchestrator/session-close-journal.cjs');
22
23
 
23
24
  const SPEC_CONSUMER = 'spec-staleness';
@@ -84,6 +85,28 @@ function inspectSessionCloses(db, {
84
85
  };
85
86
  }
86
87
 
88
+ function inspectTeamTaskLedger(db, rootDir) {
89
+ try {
90
+ const health = taskLedger.inspectTaskLedger(db, { rootDir });
91
+ const taskCount = db.prepare('SELECT COUNT(*) AS count FROM tasks').get().count;
92
+ let status = 'warning';
93
+ if (health.status === 'current') status = 'pass';
94
+ return {
95
+ ...health,
96
+ condition: health.status,
97
+ status,
98
+ task_count: taskCount,
99
+ };
100
+ } catch (error) {
101
+ return {
102
+ status: 'fail',
103
+ condition: 'invalid',
104
+ code: error.code || 'TASK_LEDGER_INVALID',
105
+ message: error.message,
106
+ };
107
+ }
108
+ }
109
+
87
110
  function inspectSystem(db, { rootDir = process.cwd() } = {}) {
88
111
  const tables = new Set(tableNames(db));
89
112
  const missing = REQUIRED_TABLES.filter((name) => !tables.has(name));
@@ -137,6 +160,9 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
137
160
  const planPublications = missing.length === 0
138
161
  ? planStore.inspectPlanPublications(db, { rootDir })
139
162
  : { status: 'fail', pending: 0, transactions: [], issues: [] };
163
+ const taskLedgerHealth = missing.length === 0
164
+ ? inspectTeamTaskLedger(db, rootDir)
165
+ : { status: 'fail', condition: 'state_unavailable', code: 'STATE_DB_UNAVAILABLE' };
140
166
  const baselineCheckStatus = baseline.status === 'pass'
141
167
  ? 'pass'
142
168
  : (baseline.baseline?.status === 'ready' ? 'fail' : 'warning');
@@ -148,6 +174,7 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
148
174
  || decisionHealth.status === 'fail'
149
175
  || artifactHealth.status === 'fail'
150
176
  || planPublications.status === 'fail'
177
+ || taskLedgerHealth.status === 'fail'
151
178
  || baselineCheckStatus === 'fail';
152
179
  return {
153
180
  status: degraded ? 'degraded' : 'healthy',
@@ -167,6 +194,7 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
167
194
  },
168
195
  change_artifacts: { status: activeMissing === 0 ? 'pass' : 'fail', missing: activeMissing },
169
196
  artifacts: artifactHealth,
197
+ task_ledger: taskLedgerHealth,
170
198
  plan_publications: planPublications,
171
199
  archive_recovery: {
172
200
  status: archiveIntents.length === 0 ? 'pass' : 'fail',
@@ -62,7 +62,6 @@ const ULTRA_GIT_ARTIFACT_CLASSES = Object.freeze({
62
62
  '.ultra/tasks/tasks.json',
63
63
  '.ultra/tasks/plan.md',
64
64
  '.ultra/tasks/plan.json',
65
- '.ultra/tasks/contexts/example.md',
66
65
  ]),
67
66
  research: Object.freeze([
68
67
  '.ultra/docs/research/README.md',
@@ -17,6 +17,7 @@ const Database = require('better-sqlite3');
17
17
 
18
18
  const { EXPECTED_VERSION, initStateDb } = require('./state-db.cjs');
19
19
  const { assertStateAuthority, inspectProjection } = require('./state-authority.cjs');
20
+ const taskLedger = require('./task-ledger.cjs');
20
21
  const baselines = require('./baseline-workflow.cjs');
21
22
  const gitBootstrap = require('./git-bootstrap.cjs');
22
23
  const runtimePaths = require('./runtime-paths.cjs');
@@ -417,6 +418,8 @@ function createResumeSnapshot(ultraDir, stateDbPath) {
417
418
  const projectionTargets = [
418
419
  path.join('tasks', 'tasks.json'),
419
420
  path.join('tasks', 'contexts'),
421
+ path.join('.runtime', 'projections'),
422
+ path.join('.runtime', 'backups', 'task-ledger'),
420
423
  ];
421
424
  let db;
422
425
  try {
@@ -621,6 +624,7 @@ function resumeProject({
621
624
  let copiedFiles = [];
622
625
  let baselineResult;
623
626
  let workflowResult;
627
+ let teamCheckpoint;
624
628
  let resumeSnapshot;
625
629
  const completeResume = () => {
626
630
  try {
@@ -631,7 +635,11 @@ function resumeProject({
631
635
  }
632
636
  initialized = initStateDb(stateDbPath);
633
637
  const { db } = initialized;
634
- if (projection && projection.taskCount > 0) assertStateAuthority(db, absTarget);
638
+ if (projection && (
639
+ projection.taskCount > 0 || projection.kind === taskLedger.LEDGER_KIND
640
+ )) {
641
+ teamCheckpoint = assertStateAuthority(db, absTarget, { importTeamLedger: true });
642
+ }
635
643
  copiedFiles = copyMissingTemplate(source, ultraDir);
636
644
  validateTasksTemplate(ultraDir);
637
645
  repositoryProfile.git = gitSetup.result;
@@ -653,6 +661,26 @@ function resumeProject({
653
661
  }, { rootDir: absTarget });
654
662
  }
655
663
  workflowResult = ensureInitializationWorkflows(db, baselineResult, { rootDir: absTarget });
664
+ if (projection?.kind === 'legacy-task-projection') {
665
+ const publication = taskLedger.publishTaskLedger(db, {
666
+ rootDir: absTarget,
667
+ reason: 'legacy_projection_upgraded',
668
+ });
669
+ teamCheckpoint = {
670
+ generation: publication.ledger.generation,
671
+ state_digest: publication.ledger.state_digest,
672
+ imported: 0,
673
+ deleted: 0,
674
+ imported_changes: 0,
675
+ imported_baseline: false,
676
+ requires_plan_revalidation: false,
677
+ requires_baseline_revalidation: false,
678
+ already_current: true,
679
+ published: publication.changed,
680
+ migrated_legacy_projection: Boolean(publication.migrated_legacy_projection),
681
+ legacy_backup_path: publication.legacy_backup_path || null,
682
+ };
683
+ }
656
684
  ops.appendEvent(db, {
657
685
  type: 'project_resumed',
658
686
  payload: {
@@ -687,7 +715,12 @@ function resumeProject({
687
715
  }
688
716
  }
689
717
  const rollbackErrors = [...stateRollbackErrors, ...gitRollbackErrors];
690
- if (error.code && ['LEGACY_STATE_MIGRATION_REQUIRED', 'STATE_AUTHORITY_CONFLICT', 'STATE_PROJECTION_INVALID'].includes(error.code)) {
718
+ if (error instanceof taskLedger.TaskLedgerError
719
+ || (error.code && [
720
+ 'LEGACY_STATE_MIGRATION_REQUIRED',
721
+ 'STATE_AUTHORITY_CONFLICT',
722
+ 'STATE_PROJECTION_INVALID',
723
+ ].includes(error.code))) {
691
724
  throw new InitProjectError(
692
725
  error.code,
693
726
  error.message,
@@ -751,6 +784,22 @@ function resumeProject({
751
784
  git: gitSetup.result,
752
785
  workflow: workflowResult,
753
786
  };
787
+ if (teamCheckpoint) {
788
+ result.team_checkpoint = {
789
+ generation: teamCheckpoint.generation,
790
+ state_digest: teamCheckpoint.state_digest,
791
+ imported_tasks: teamCheckpoint.imported,
792
+ deleted_tasks: teamCheckpoint.deleted,
793
+ imported_changes: teamCheckpoint.imported_changes,
794
+ imported_baseline: teamCheckpoint.imported_baseline,
795
+ requires_plan_revalidation: teamCheckpoint.requires_plan_revalidation,
796
+ requires_baseline_revalidation: teamCheckpoint.requires_baseline_revalidation,
797
+ already_current: teamCheckpoint.already_current,
798
+ published: Boolean(teamCheckpoint.published),
799
+ migrated_legacy_projection: Boolean(teamCheckpoint.migrated_legacy_projection),
800
+ legacy_backup_path: teamCheckpoint.legacy_backup_path || undefined,
801
+ };
802
+ }
754
803
  if (initialized.backup_path) result.migration_backup_path = initialized.backup_path;
755
804
  if (runtimeLayout.backupPath) {
756
805
  result.runtime_migration_backup_path = runtimeLayout.backupPath;
@@ -815,6 +864,7 @@ function initProject({
815
864
  );
816
865
  }
817
866
  if (projection && projection.taskCount > 0
867
+ && projection.kind !== taskLedger.LEDGER_KIND
818
868
  && !fs.existsSync(locatedStatePath)) {
819
869
  throw projectionMigrationError(projection);
820
870
  }
@@ -3,8 +3,8 @@
3
3
  // Projector: regenerates the read-only file views from .ultra/.runtime/state.db.
4
4
  //
5
5
  // state.db owns lifecycle and artifact bindings (D32); registered digest-bound
6
- // files own semantic/evidence bodies. tasks.json and contexts/task-*.md are
7
- // complete projections — humans may read them, never write them. The projector
6
+ // files own semantic/evidence bodies. Live tasks.json and contexts/task-*.md are
7
+ // checkout-local projections — humans may read them, never write them. The projector
8
8
  // is trigger-based: state-ops calls projectAll() after each successful
9
9
  // write transaction. Projection output passes the v4.5 schemas under
10
10
  // spec/schemas/ — see mcp-server/tests/projector.test.cjs for the
@@ -57,8 +57,8 @@ function rowToProjection(row) {
57
57
 
58
58
  function defaultPaths(rootDir) {
59
59
  return {
60
- tasksJson: path.join(rootDir, '.ultra', 'tasks', 'tasks.json'),
61
- contextsDir: path.join(rootDir, '.ultra', 'tasks', 'contexts'),
60
+ tasksJson: path.join(rootDir, '.ultra', '.runtime', 'projections', 'tasks.json'),
61
+ contextsDir: path.join(rootDir, '.ultra', '.runtime', 'projections', 'contexts'),
62
62
  };
63
63
  }
64
64
 
@@ -266,7 +266,27 @@ function projectContext(db, taskId, { contextsDir } = {}, opts = {}) {
266
266
  }
267
267
  }
268
268
  const target = contextPaths.resolveContextPath(rootDir, projection.context_file, { taskId });
269
- const promoted = promoteAuthoredContext(db, taskId, target, rootDir);
269
+ const legacyValue = String(projection.context_file || '')
270
+ .replaceAll('\\', '/')
271
+ .replace(/^\.\//, '');
272
+ const legacyDefault = `${contextPaths.LEGACY_CONTEXT_ROOT_RELATIVE}/${
273
+ path.posix.basename(target.relative)
274
+ }`;
275
+ const legacyTarget = contextPaths.resolveLegacyContextPath(
276
+ rootDir,
277
+ legacyValue.startsWith(`${contextPaths.LEGACY_CONTEXT_ROOT_RELATIVE}/`)
278
+ ? legacyValue
279
+ : legacyDefault,
280
+ { taskId },
281
+ );
282
+ const legacyPromoted = promoteAuthoredContext(db, taskId, legacyTarget, rootDir);
283
+ if (fs.existsSync(legacyTarget.file)
284
+ && contextPaths.isGeneratedContextContents(fs.readFileSync(legacyTarget.file, 'utf8'))) {
285
+ fs.rmSync(legacyTarget.file, { force: true });
286
+ } else if (legacyPromoted) {
287
+ fs.rmSync(legacyTarget.file, { force: true });
288
+ }
289
+ const promoted = promoteAuthoredContext(db, taskId, target, rootDir) || legacyPromoted;
270
290
  const next = buildContextDoc(projection);
271
291
  writeAtomic(target.file, next);
272
292
  return { path: target.file, task_id: taskId, promoted };
@@ -284,8 +304,7 @@ function listContextFiles(directory) {
284
304
  return output;
285
305
  }
286
306
 
287
- function pruneGeneratedGhosts(db, rootDir, expected) {
288
- const contextsDir = defaultPaths(rootDir).contextsDir;
307
+ function pruneGeneratedGhostsIn(db, rootDir, contextsDir, expected) {
289
308
  const pruned = [];
290
309
  for (const file of listContextFiles(contextsDir)) {
291
310
  const relative = path.relative(rootDir, file).split(path.sep).join('/');
@@ -306,7 +325,23 @@ function pruneGeneratedGhosts(db, rootDir, expected) {
306
325
  throw error;
307
326
  }
308
327
  }
309
- return pruned.sort();
328
+ return pruned;
329
+ }
330
+
331
+ function pruneGeneratedGhosts(db, rootDir, expected) {
332
+ const runtime = pruneGeneratedGhostsIn(
333
+ db,
334
+ rootDir,
335
+ defaultPaths(rootDir).contextsDir,
336
+ expected,
337
+ );
338
+ const legacy = pruneGeneratedGhostsIn(
339
+ db,
340
+ rootDir,
341
+ path.join(rootDir, ...contextPaths.LEGACY_CONTEXT_ROOT_RELATIVE.split('/')),
342
+ new Set(),
343
+ );
344
+ return [...runtime, ...legacy].sort();
310
345
  }
311
346
 
312
347
  function projectAll(db, { rootDir = '.', tasksJson, contextsDir } = {}) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const taskLedger = require('./task-ledger.cjs');
5
6
 
6
7
  class StateAuthorityError extends Error {
7
8
  constructor(code, message, details) {
@@ -32,24 +33,39 @@ function inspectProjection(rootDir) {
32
33
  { projection_path: projectionPath },
33
34
  );
34
35
  }
36
+ if (projection.kind === taskLedger.LEDGER_KIND) {
37
+ const ledger = taskLedger.validateLedger(projection, projectionPath);
38
+ return {
39
+ path: projectionPath,
40
+ version: ledger.schema_version,
41
+ kind: ledger.kind,
42
+ taskCount: ledger.tasks.length,
43
+ };
44
+ }
35
45
  return {
36
46
  path: projectionPath,
37
47
  version: String(projection.version || projection.schema_version || 'unknown'),
48
+ kind: 'legacy-task-projection',
38
49
  taskCount: projection.tasks.length,
39
50
  };
40
51
  }
41
52
 
42
- function assertStateAuthority(db, rootDir) {
53
+ function assertStateAuthority(db, rootDir, { importTeamLedger = false } = {}) {
43
54
  const authoritativeCount = db.prepare('SELECT COUNT(*) AS count FROM tasks').get().count;
44
- if (authoritativeCount > 0) return;
45
55
  const projection = inspectProjection(rootDir);
46
- if (!projection || projection.taskCount === 0) return;
56
+ if (projection?.kind === taskLedger.LEDGER_KIND) {
57
+ return importTeamLedger
58
+ ? taskLedger.syncTaskLedger(db, { rootDir })
59
+ : taskLedger.inspectTaskLedger(db, { rootDir });
60
+ }
61
+ if (authoritativeCount > 0 || !projection || projection.taskCount === 0) return null;
47
62
 
48
63
  const details = {
49
64
  authoritative_task_count: authoritativeCount,
50
65
  legacy_task_count: projection.taskCount,
51
66
  projection_path: projection.path,
52
67
  projection_version: projection.version,
68
+ projection_kind: projection.kind,
53
69
  };
54
70
  const migrationTargets = {
55
71
  '4.4': '4.5',
@@ -518,7 +518,12 @@ function patchTask(db, id, patch = {}) {
518
518
  }
519
519
  if (nextStatus !== null) {
520
520
  const allowed = STATUS_TRANSITIONS[current.status] || new Set();
521
- if (!allowed.has(nextStatus) && nextStatus !== current.status) {
521
+ const recoveringUncommittedCompletion = current.status === 'completed'
522
+ && nextStatus === 'blocked'
523
+ && !current.completion_commit
524
+ && Boolean(current.session_id);
525
+ if (!allowed.has(nextStatus) && nextStatus !== current.status
526
+ && !recoveringUncommittedCompletion) {
522
527
  throw new StateOpsError(
523
528
  'ILLEGAL_STATUS_TRANSITION',
524
529
  `cannot transition task ${id} from ${current.status} to ${nextStatus}`,