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.
- package/.ultra-template/tasks/tasks.json +10 -3
- package/.ultra-template/templates/task-context.md +19 -0
- package/CHANGELOG.md +40 -0
- package/README.md +166 -22
- package/adapters/opencode.js +23 -12
- package/docs/AGENT-CONTEXT.md +22 -13
- package/docs/ARCHITECTURE.md +59 -24
- package/docs/ARTIFACT-AUTHORITY.md +70 -30
- package/docs/COMMIT-HASH-BACKFILL.md +33 -90
- package/docs/DECISIONS.md +9 -7
- package/docs/LEGACY-CLI-CROSSWALK.md +10 -2
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +3 -2
- package/docs/ROADMAP.md +3 -3
- package/docs/RUNTIME-COMPAT-MATRIX.md +8 -6
- package/docs/STATE-DB-ACCESS-POLICY.md +35 -7
- package/docs/WORKFLOW-LIFECYCLE.md +40 -16
- package/hooks/README.md +5 -4
- package/hooks/active_task_context.py +41 -39
- package/mcp-server/lib/artifact-registry.cjs +7 -6
- package/mcp-server/lib/baseline-workflow.cjs +60 -24
- package/mcp-server/lib/context-paths.cjs +34 -3
- package/mcp-server/lib/doctor.cjs +28 -0
- package/mcp-server/lib/git-bootstrap.cjs +0 -1
- package/mcp-server/lib/init-project.cjs +52 -2
- package/mcp-server/lib/projector.cjs +43 -8
- package/mcp-server/lib/state-authority.cjs +19 -3
- package/mcp-server/lib/state-ops.cjs +6 -1
- package/mcp-server/lib/task-ledger.cjs +1342 -0
- package/mcp-server/server.cjs +145 -9
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +10 -2
- package/skills/ultra-deliver/SKILL.md +6 -3
- package/skills/ultra-dev/SKILL.md +21 -6
- package/skills/ultra-doctor/SKILL.md +10 -1
- package/skills/ultra-init/SKILL.md +18 -9
- package/skills/ultra-plan/SKILL.md +11 -4
- package/skills/ultra-research/SKILL.md +12 -13
- package/skills/ultra-review/SKILL.md +5 -3
- package/skills/ultra-status/SKILL.md +11 -7
- package/skills/ultra-test/SKILL.md +3 -2
- package/spec/cli-protocol.md +3 -0
- package/spec/mcp-tools.yaml +127 -0
- package/spec/schemas/context-file.v4.5.schema.json +1 -1
- package/spec/schemas/state-db.sql +1 -1
- package/spec/schemas/task-ledger.v1.schema.json +217 -0
- package/ultra-tools/commands/migrate.cjs +5 -2
- 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',
|
|
@@ -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 &&
|
|
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
|
|
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
|
-
//
|
|
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', '
|
|
61
|
-
contextsDir: path.join(rootDir, '.ultra', '
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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}`,
|