ultra-builder-pro-cli 0.14.0 → 0.16.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/docs/research/README.md +11 -7
- package/.ultra-template/specs/architecture.md +36 -36
- package/.ultra-template/specs/discovery.md +46 -33
- package/.ultra-template/specs/product.md +38 -29
- package/.ultra-template/specs/research-distillate.md +48 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
- package/CHANGELOG.md +77 -1
- package/README.md +46 -24
- package/adapters/_shared/codex-assets.cjs +6 -2
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/handbook.cjs +2 -1
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +3 -2
- package/agents/review-coordinator.md +9 -6
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +1 -1
- package/commands/ultra-think.md +2 -2
- package/docs/AGENT-CONTEXT.md +43 -19
- package/docs/ARCHITECTURE.md +54 -13
- package/docs/PLAN.zh-CN.md +4 -2
- package/docs/ROADMAP.md +10 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +33 -29
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
- package/docs/WORKFLOW-LIFECYCLE.md +342 -0
- package/hooks/README.md +6 -1
- package/hooks/health_check.py +12 -3
- package/hooks/subagent_tracker.py +52 -77
- package/mcp-server/hook-event.cjs +34 -0
- package/mcp-server/lib/archive-journal.cjs +17 -5
- package/mcp-server/lib/baseline-workflow.cjs +158 -18
- package/mcp-server/lib/change-workflow.cjs +598 -85
- package/mcp-server/lib/context-spine.cjs +305 -25
- package/mcp-server/lib/decision-dialogue.cjs +713 -0
- package/mcp-server/lib/doctor.cjs +42 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +15 -0
- package/mcp-server/lib/projector.cjs +57 -3
- package/mcp-server/lib/spec-learning.cjs +79 -2
- package/mcp-server/lib/state-db.cjs +168 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/workflow-state.cjs +2305 -0
- package/mcp-server/server.cjs +215 -15
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +13 -3
- package/skills/ultra-change/SKILL.md +94 -80
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +76 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +62 -59
- package/skills/ultra-doctor/SKILL.md +61 -82
- package/skills/ultra-init/SKILL.md +101 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +99 -85
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +102 -60
- package/skills/ultra-research/references/00-problem-validation.md +36 -0
- package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
- package/skills/ultra-research/references/02-market-assessment.md +30 -0
- package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
- package/skills/ultra-research/references/04-product-strategy.md +32 -0
- package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
- package/skills/ultra-research/references/10-user-personas.md +29 -0
- package/skills/ultra-research/references/11-user-scenarios.md +28 -0
- package/skills/ultra-research/references/20-user-stories.md +30 -0
- package/skills/ultra-research/references/21-features-scope.md +29 -0
- package/skills/ultra-research/references/22-success-metrics.md +28 -0
- package/skills/ultra-research/references/30-architecture-context.md +30 -0
- package/skills/ultra-research/references/31-solution-strategy.md +28 -0
- package/skills/ultra-research/references/32-building-blocks.md +28 -0
- package/skills/ultra-research/references/40-deployment.md +29 -0
- package/skills/ultra-research/references/41-quality-risks.md +28 -0
- package/skills/ultra-research/references/99-synthesis.md +35 -0
- package/skills/ultra-review/SKILL.md +63 -73
- package/skills/ultra-review/references/review-modes.md +27 -0
- package/skills/ultra-review/references/unified-schema.md +25 -0
- package/skills/ultra-review/scripts/review_wait.py +40 -3
- package/skills/ultra-status/SKILL.md +41 -41
- package/skills/ultra-test/SKILL.md +54 -55
- package/skills/ultra-think/SKILL.md +26 -12
- package/skills/ultra-think/references/decision-dialogue.md +130 -0
- package/skills/ultra-verify/SKILL.md +1 -1
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/spec/cli-protocol.md +17 -3
- package/spec/mcp-tools.yaml +683 -64
- package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
- package/spec/schemas/delivery-report.v1.schema.json +111 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +140 -1
- package/spec/schemas/tasks.v4.5.schema.json +8 -1
- package/spec/schemas/test-report.v1.schema.json +194 -0
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/migrate.cjs +3 -3
- package/ultra-tools/commands/status.cjs +228 -12
- package/.ultra-template/delivery-report.json +0 -9
- package/.ultra-template/test-report.json +0 -8
- package/adapters/_shared/tests/file-ops.test.cjs +0 -54
- package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
- package/adapters/_shared/tests/handbook.test.cjs +0 -101
- package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
- package/adapters/_shared/tests/provenance.test.cjs +0 -133
- package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
- package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
- package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
- package/adapters/_shared/tests/validate.test.cjs +0 -30
- package/adapters/tests/claude.test.cjs +0 -138
- package/adapters/tests/codex-hook.test.cjs +0 -142
- package/adapters/tests/codex.test.cjs +0 -430
- package/adapters/tests/kimi-hook.test.cjs +0 -149
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -357
- package/mcp-server/lib/archive-journal.test.cjs +0 -35
- package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
- package/mcp-server/lib/change-workflow.test.cjs +0 -971
- package/mcp-server/lib/context-spine.test.cjs +0 -93
- package/mcp-server/lib/doctor.test.cjs +0 -148
- package/mcp-server/lib/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -82
- package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
- package/mcp-server/lib/runtime-state.test.cjs +0 -92
- package/mcp-server/lib/spec-learning.test.cjs +0 -67
- package/mcp-server/lib/state-db.test.cjs +0 -314
- package/mcp-server/lib/task-expander.test.cjs +0 -169
- package/mcp-server/lib/topo.test.cjs +0 -158
- package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
- package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
- package/mcp-server/tests/concurrency-update.test.cjs +0 -96
- package/mcp-server/tests/concurrency.test.cjs +0 -112
- package/mcp-server/tests/db-ops.test.cjs +0 -121
- package/mcp-server/tests/init-project.test.cjs +0 -393
- package/mcp-server/tests/migrate.test.cjs +0 -232
- package/mcp-server/tests/projector.test.cjs +0 -162
- package/mcp-server/tests/server-contract.test.cjs +0 -1467
- package/mcp-server/tests/staleness.test.cjs +0 -190
- package/mcp-server/tests/state-db.test.cjs +0 -106
- package/mcp-server/tests/state-ops.test.cjs +0 -449
- package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
- package/mcp-server/tests/telemetry.test.cjs +0 -219
- package/orchestrator/planner/plan-builder.test.cjs +0 -165
- package/orchestrator/tests/auto-merge.test.cjs +0 -216
- package/orchestrator/tests/daemon.test.cjs +0 -311
- package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
- package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
- package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
- package/orchestrator/tests/recovery.test.cjs +0 -190
- package/orchestrator/tests/session-runner.test.cjs +0 -314
- package/orchestrator/tests/skill-miner.test.cjs +0 -147
- package/orchestrator/tests/worktree-manager.test.cjs +0 -148
- package/skills/ultra-research/references/architecture.md +0 -38
- package/skills/ultra-research/references/problem-product.md +0 -39
- package/skills/ultra-research/references/quality-delivery.md +0 -34
- package/skills/ultra-research/references/synthesis.md +0 -38
- package/skills/ultra-research/references/users-scope.md +0 -40
- package/ultra-tools/legacy-memory.test.cjs +0 -80
- package/ultra-tools/status-command.test.cjs +0 -181
- package/ultra-tools/system-command.test.cjs +0 -309
|
@@ -1,67 +0,0 @@
|
|
|
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
|
-
const { execFileSync } = require('node:child_process');
|
|
9
|
-
|
|
10
|
-
const { initStateDb, closeStateDb } = require('./state-db.cjs');
|
|
11
|
-
const changes = require('./change-workflow.cjs');
|
|
12
|
-
const learning = require('./spec-learning.cjs');
|
|
13
|
-
|
|
14
|
-
function fixture() {
|
|
15
|
-
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-learning-'));
|
|
16
|
-
execFileSync('git', ['init', '-q'], { cwd: rootDir });
|
|
17
|
-
execFileSync('git', ['config', 'user.email', 'test@ubp.dev'], { cwd: rootDir });
|
|
18
|
-
execFileSync('git', ['config', 'user.name', 'ubp-test'], { cwd: rootDir });
|
|
19
|
-
fs.writeFileSync(path.join(rootDir, 'README.md'), '# Fixture\n');
|
|
20
|
-
execFileSync('git', ['add', 'README.md'], { cwd: rootDir });
|
|
21
|
-
execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: rootDir });
|
|
22
|
-
const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
|
|
23
|
-
db.prepare(
|
|
24
|
-
`INSERT INTO baselines
|
|
25
|
-
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
26
|
-
VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'accepted fixture', ?)`,
|
|
27
|
-
).run(new Date().toISOString());
|
|
28
|
-
changes.createChange(db, {
|
|
29
|
-
id: 'learning-change', title: 'Learning change', kind: 'quick',
|
|
30
|
-
intent: 'Exercise guarded specification-learning transitions.',
|
|
31
|
-
}, { rootDir });
|
|
32
|
-
return { rootDir, db };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
test('spec learning cannot skip approval or mutate after apply', () => {
|
|
36
|
-
const fx = fixture();
|
|
37
|
-
try {
|
|
38
|
-
learning.proposeSpecLearning(fx.db, {
|
|
39
|
-
id: 'candidate-1', change_id: 'learning-change', target_ref: 'README.md',
|
|
40
|
-
summary: 'A stable public contract was discovered.',
|
|
41
|
-
}, { rootDir: fx.rootDir });
|
|
42
|
-
assert.throws(
|
|
43
|
-
() => learning.resolveSpecLearning(fx.db, {
|
|
44
|
-
change_id: 'learning-change', candidate_id: 'candidate-1', decision: 'apply',
|
|
45
|
-
resolution: 'Applied.',
|
|
46
|
-
}, { rootDir: fx.rootDir }),
|
|
47
|
-
(error) => error.code === 'ILLEGAL_LEARNING_TRANSITION',
|
|
48
|
-
);
|
|
49
|
-
learning.resolveSpecLearning(fx.db, {
|
|
50
|
-
change_id: 'learning-change', candidate_id: 'candidate-1', decision: 'approve',
|
|
51
|
-
}, { rootDir: fx.rootDir });
|
|
52
|
-
learning.resolveSpecLearning(fx.db, {
|
|
53
|
-
change_id: 'learning-change', candidate_id: 'candidate-1', decision: 'apply',
|
|
54
|
-
resolution: 'Applied to README.md.',
|
|
55
|
-
}, { rootDir: fx.rootDir });
|
|
56
|
-
assert.throws(
|
|
57
|
-
() => learning.resolveSpecLearning(fx.db, {
|
|
58
|
-
change_id: 'learning-change', candidate_id: 'candidate-1', decision: 'reject',
|
|
59
|
-
resolution: 'Too late.',
|
|
60
|
-
}, { rootDir: fx.rootDir }),
|
|
61
|
-
(error) => error.code === 'ILLEGAL_LEARNING_TRANSITION',
|
|
62
|
-
);
|
|
63
|
-
} finally {
|
|
64
|
-
closeStateDb(fx.db);
|
|
65
|
-
fs.rmSync(fx.rootDir, { recursive: true, force: true });
|
|
66
|
-
}
|
|
67
|
-
});
|
|
@@ -1,314 +0,0 @@
|
|
|
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 {
|
|
10
|
-
EXPECTED_VERSION,
|
|
11
|
-
REQUIRED_TABLES,
|
|
12
|
-
SCHEMA_FILE,
|
|
13
|
-
initStateDb,
|
|
14
|
-
closeStateDb,
|
|
15
|
-
openStateDb,
|
|
16
|
-
tableNames,
|
|
17
|
-
} = require('./state-db.cjs');
|
|
18
|
-
const stateOps = require('./state-ops.cjs');
|
|
19
|
-
|
|
20
|
-
function tmpDbPath(prefix = 'ubp-state') {
|
|
21
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
|
|
22
|
-
return { dir, file: path.join(dir, 'state.db') };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
test('initStateDb creates workflow tables without an Ultra memory store', () => {
|
|
26
|
-
const { dir, file } = tmpDbPath();
|
|
27
|
-
try {
|
|
28
|
-
const init = initStateDb(file);
|
|
29
|
-
assert.equal(init.created, true);
|
|
30
|
-
assert.equal(init.schema_version, EXPECTED_VERSION);
|
|
31
|
-
for (const t of REQUIRED_TABLES) {
|
|
32
|
-
assert.ok(init.tables.includes(t), `missing table ${t}`);
|
|
33
|
-
}
|
|
34
|
-
assert.ok(!init.tables.includes('memory_entries'));
|
|
35
|
-
assert.ok(!init.tables.includes('memory_fts'));
|
|
36
|
-
closeStateDb(init.db);
|
|
37
|
-
} finally {
|
|
38
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('initStateDb applies WAL + busy_timeout + foreign_keys pragmas', () => {
|
|
43
|
-
const { dir, file } = tmpDbPath();
|
|
44
|
-
try {
|
|
45
|
-
const { db } = initStateDb(file);
|
|
46
|
-
assert.equal(db.pragma('journal_mode', { simple: true }), 'wal');
|
|
47
|
-
assert.equal(db.pragma('busy_timeout', { simple: true }), 5000);
|
|
48
|
-
assert.equal(db.pragma('foreign_keys', { simple: true }), 1);
|
|
49
|
-
closeStateDb(db);
|
|
50
|
-
} finally {
|
|
51
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('initStateDb is idempotent — second call does not duplicate seed rows', () => {
|
|
56
|
-
const { dir, file } = tmpDbPath();
|
|
57
|
-
try {
|
|
58
|
-
const first = initStateDb(file);
|
|
59
|
-
const firstRows = first.db.prepare('SELECT COUNT(*) AS n FROM schema_version').get().n;
|
|
60
|
-
closeStateDb(first.db);
|
|
61
|
-
|
|
62
|
-
const second = initStateDb(file);
|
|
63
|
-
assert.equal(second.created, false, 'second init should not recreate schema');
|
|
64
|
-
assert.equal(second.schema_version, EXPECTED_VERSION);
|
|
65
|
-
|
|
66
|
-
const secondRows = second.db.prepare('SELECT COUNT(*) AS n FROM schema_version').get().n;
|
|
67
|
-
assert.equal(secondRows, firstRows, 'schema_version row count must not grow on re-init');
|
|
68
|
-
closeStateDb(second.db);
|
|
69
|
-
} finally {
|
|
70
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('openStateDb on an empty file produces no tables until schema is applied', () => {
|
|
75
|
-
const { dir, file } = tmpDbPath();
|
|
76
|
-
try {
|
|
77
|
-
const db = openStateDb(file);
|
|
78
|
-
assert.deepEqual(tableNames(db), []);
|
|
79
|
-
closeStateDb(db);
|
|
80
|
-
} finally {
|
|
81
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('initStateDb upgrades an exact schema 11 project without replacing its approved baseline', () => {
|
|
86
|
-
const { dir, file } = tmpDbPath('ubp-schema-11-upgrade');
|
|
87
|
-
try {
|
|
88
|
-
const legacy = openStateDb(file);
|
|
89
|
-
legacy.exec(`
|
|
90
|
-
CREATE TABLE schema_version (
|
|
91
|
-
version TEXT PRIMARY KEY,
|
|
92
|
-
applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
93
|
-
description TEXT
|
|
94
|
-
);
|
|
95
|
-
CREATE TABLE migration_history (
|
|
96
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
97
|
-
from_version TEXT NOT NULL,
|
|
98
|
-
to_version TEXT NOT NULL,
|
|
99
|
-
direction TEXT NOT NULL CHECK (direction IN ('forward', 'rollback')),
|
|
100
|
-
ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
101
|
-
status TEXT NOT NULL CHECK (status IN ('success', 'failed', 'dry_run')),
|
|
102
|
-
notes TEXT
|
|
103
|
-
);
|
|
104
|
-
CREATE TABLE baselines (
|
|
105
|
-
id TEXT PRIMARY KEY,
|
|
106
|
-
project_name TEXT NOT NULL,
|
|
107
|
-
project_type TEXT,
|
|
108
|
-
stack TEXT,
|
|
109
|
-
mode TEXT NOT NULL CHECK (mode IN ('greenfield', 'brownfield', 'migrated')),
|
|
110
|
-
status TEXT NOT NULL CHECK (status IN ('draft', 'adopting', 'blocked', 'ready', 'superseded')),
|
|
111
|
-
repository_root TEXT NOT NULL DEFAULT '.',
|
|
112
|
-
scope_json TEXT NOT NULL DEFAULT '["."]',
|
|
113
|
-
repository_revision TEXT,
|
|
114
|
-
spec_refs_json TEXT NOT NULL DEFAULT '[]',
|
|
115
|
-
evidence_json TEXT NOT NULL DEFAULT '[]',
|
|
116
|
-
verification_json TEXT NOT NULL DEFAULT '[]',
|
|
117
|
-
unknowns_json TEXT NOT NULL DEFAULT '[]',
|
|
118
|
-
provider_refs_json TEXT NOT NULL DEFAULT '{}',
|
|
119
|
-
approved_by TEXT,
|
|
120
|
-
approval_note TEXT,
|
|
121
|
-
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
122
|
-
converged_at TEXT,
|
|
123
|
-
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
124
|
-
);
|
|
125
|
-
CREATE TABLE changes (
|
|
126
|
-
id TEXT PRIMARY KEY,
|
|
127
|
-
title TEXT NOT NULL,
|
|
128
|
-
kind TEXT NOT NULL CHECK (kind IN ('quick', 'standard', 'major', 'incident')),
|
|
129
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'blocked', 'ready', 'archived', 'cancelled')),
|
|
130
|
-
intent TEXT NOT NULL,
|
|
131
|
-
docs_impact_json TEXT NOT NULL DEFAULT '{}',
|
|
132
|
-
provider_refs_json TEXT NOT NULL DEFAULT '{}',
|
|
133
|
-
base_commit TEXT,
|
|
134
|
-
artifact_root TEXT,
|
|
135
|
-
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
136
|
-
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
137
|
-
closed_at TEXT
|
|
138
|
-
);
|
|
139
|
-
INSERT INTO schema_version (version, description) VALUES ('11.0', 'baseline authority');
|
|
140
|
-
INSERT INTO baselines
|
|
141
|
-
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
142
|
-
VALUES
|
|
143
|
-
('approved-baseline', 'legacy-project', 'brownfield', 'ready', 'owner', 'approved',
|
|
144
|
-
'2026-07-01T00:00:00.000Z');
|
|
145
|
-
`);
|
|
146
|
-
closeStateDb(legacy);
|
|
147
|
-
|
|
148
|
-
let upgraded;
|
|
149
|
-
try {
|
|
150
|
-
upgraded = initStateDb(file);
|
|
151
|
-
} catch (error) {
|
|
152
|
-
assert.fail(`schema 11 upgrade failed: ${error.message}\n${error.stack || ''}`);
|
|
153
|
-
}
|
|
154
|
-
try {
|
|
155
|
-
assert.equal(upgraded.schema_version, '12.0');
|
|
156
|
-
assert.ok(fs.existsSync(upgraded.backup_path));
|
|
157
|
-
const baseline = upgraded.db.prepare(
|
|
158
|
-
'SELECT id, mode, status, worktree_state, gaps_json FROM baselines',
|
|
159
|
-
).get();
|
|
160
|
-
assert.deepEqual(baseline, {
|
|
161
|
-
id: 'approved-baseline', mode: 'brownfield', status: 'ready',
|
|
162
|
-
worktree_state: 'unavailable', gaps_json: '[]',
|
|
163
|
-
});
|
|
164
|
-
assert.equal(
|
|
165
|
-
upgraded.db.prepare("SELECT COUNT(*) AS count FROM baselines WHERE mode = 'migrated'").get().count,
|
|
166
|
-
0,
|
|
167
|
-
);
|
|
168
|
-
const changeColumns = upgraded.db.prepare('PRAGMA table_info(changes)').all().map((row) => row.name);
|
|
169
|
-
assert.ok(changeColumns.includes('baseline_bypass_json'));
|
|
170
|
-
} finally { closeStateDb(upgraded.db); }
|
|
171
|
-
} finally {
|
|
172
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test('initStateDb exposes its pre-migration backup when an incompatible legacy schema fails', () => {
|
|
177
|
-
const { dir, file } = tmpDbPath('ubp-schema-upgrade-failure');
|
|
178
|
-
try {
|
|
179
|
-
const legacy = openStateDb(file);
|
|
180
|
-
legacy.exec(`
|
|
181
|
-
CREATE TABLE schema_version (
|
|
182
|
-
version TEXT PRIMARY KEY,
|
|
183
|
-
applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
184
|
-
description TEXT
|
|
185
|
-
);
|
|
186
|
-
CREATE TABLE migration_history (
|
|
187
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
188
|
-
from_version TEXT NOT NULL,
|
|
189
|
-
to_version TEXT NOT NULL,
|
|
190
|
-
direction TEXT NOT NULL,
|
|
191
|
-
applied_at TEXT NOT NULL,
|
|
192
|
-
status TEXT NOT NULL,
|
|
193
|
-
notes TEXT
|
|
194
|
-
);
|
|
195
|
-
INSERT INTO schema_version (version, description) VALUES ('11.0', 'incompatible fixture');
|
|
196
|
-
`);
|
|
197
|
-
closeStateDb(legacy);
|
|
198
|
-
|
|
199
|
-
let failure;
|
|
200
|
-
try {
|
|
201
|
-
initStateDb(file);
|
|
202
|
-
} catch (error) {
|
|
203
|
-
failure = error;
|
|
204
|
-
}
|
|
205
|
-
assert.ok(failure, 'the incompatible migration_history table must fail schema application');
|
|
206
|
-
assert.match(failure.message, /no such column: ts/);
|
|
207
|
-
assert.ok(failure.migration_backup_path, 'failure must retain the backup location for recovery');
|
|
208
|
-
assert.ok(fs.existsSync(failure.migration_backup_path));
|
|
209
|
-
} finally {
|
|
210
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test('Phase 8A.1 schema: tasks.parent_id column + tasks_parent partial index + seed row', () => {
|
|
215
|
-
const { dir, file } = tmpDbPath();
|
|
216
|
-
try {
|
|
217
|
-
const { db } = initStateDb(file);
|
|
218
|
-
|
|
219
|
-
const cols = db.prepare("PRAGMA table_info(tasks)").all();
|
|
220
|
-
const parentCol = cols.find((c) => c.name === 'parent_id');
|
|
221
|
-
assert.ok(parentCol, 'tasks.parent_id column must exist');
|
|
222
|
-
assert.equal(parentCol.type, 'TEXT');
|
|
223
|
-
assert.equal(parentCol.notnull, 0, 'parent_id must be nullable (top-level tasks)');
|
|
224
|
-
|
|
225
|
-
const indexRow = db
|
|
226
|
-
.prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'tasks_parent'")
|
|
227
|
-
.get();
|
|
228
|
-
assert.ok(indexRow, 'tasks_parent index must exist');
|
|
229
|
-
|
|
230
|
-
const seedRow = db
|
|
231
|
-
.prepare("SELECT version, description FROM schema_version WHERE version = '8A.1'")
|
|
232
|
-
.get();
|
|
233
|
-
assert.ok(seedRow, 'schema_version row for 8A.1 must be seeded');
|
|
234
|
-
assert.match(seedRow.description, /parent_id/);
|
|
235
|
-
|
|
236
|
-
const fkInfo = db.prepare("PRAGMA foreign_key_list(tasks)").all();
|
|
237
|
-
const parentFk = fkInfo.find((fk) => fk.from === 'parent_id');
|
|
238
|
-
assert.ok(parentFk, 'parent_id must declare a foreign key to tasks(id)');
|
|
239
|
-
assert.equal(parentFk.table, 'tasks');
|
|
240
|
-
assert.equal(parentFk.to, 'id');
|
|
241
|
-
assert.equal(parentFk.on_delete, 'SET NULL');
|
|
242
|
-
|
|
243
|
-
closeStateDb(db);
|
|
244
|
-
} finally {
|
|
245
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test('initStateDb migrates existing runtime constraints to Kimi without losing references', () => {
|
|
250
|
-
const { dir, file } = tmpDbPath('ubp-kimi-runtime-upgrade');
|
|
251
|
-
try {
|
|
252
|
-
const legacy = openStateDb(file);
|
|
253
|
-
const legacySchema = fs.readFileSync(SCHEMA_FILE, 'utf8').replaceAll(", 'kimi'", '');
|
|
254
|
-
legacy.exec(legacySchema);
|
|
255
|
-
legacy.prepare("DELETE FROM schema_version WHERE version IN ('9.1', '10.0', '11.0', '12.0')").run();
|
|
256
|
-
legacy.prepare(
|
|
257
|
-
"INSERT INTO tasks (id, title, type, priority) VALUES ('task-old', 'Old', 'feature', 'P1')",
|
|
258
|
-
).run();
|
|
259
|
-
legacy.prepare(
|
|
260
|
-
`INSERT INTO sessions
|
|
261
|
-
(sid, task_id, runtime, worktree_path, artifact_dir, lease_expires_at)
|
|
262
|
-
VALUES ('session-old', 'task-old', 'codex', '/tmp/worktree', '/tmp/artifacts', '2099-01-01T00:00:00.000Z')`,
|
|
263
|
-
).run();
|
|
264
|
-
legacy.prepare(
|
|
265
|
-
"INSERT INTO telemetry (session_id, event_type, tool_name) VALUES ('session-old', 'tool_call', 'task.list')",
|
|
266
|
-
).run();
|
|
267
|
-
legacy.prepare(
|
|
268
|
-
`INSERT INTO incidents
|
|
269
|
-
(id, code, severity, message, session_id)
|
|
270
|
-
VALUES ('incident-old', 'OLD', 'warning', 'preserve me', 'session-old')`,
|
|
271
|
-
).run();
|
|
272
|
-
legacy.prepare(
|
|
273
|
-
"INSERT INTO events (type, session_id, runtime) VALUES ('session_spawned', 'session-old', 'codex')",
|
|
274
|
-
).run();
|
|
275
|
-
closeStateDb(legacy);
|
|
276
|
-
|
|
277
|
-
const upgraded = initStateDb(file);
|
|
278
|
-
assert.equal(upgraded.schema_version, '12.0');
|
|
279
|
-
assert.ok(upgraded.backup_path);
|
|
280
|
-
assert.ok(fs.existsSync(upgraded.backup_path));
|
|
281
|
-
assert.equal(upgraded.db.prepare("SELECT runtime FROM sessions WHERE sid = 'session-old'").get().runtime, 'codex');
|
|
282
|
-
assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE session_id = 'session-old'").get().n, 1);
|
|
283
|
-
assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM incidents WHERE session_id = 'session-old'").get().n, 1);
|
|
284
|
-
assert.deepEqual(upgraded.db.pragma('foreign_key_check'), []);
|
|
285
|
-
const migrations = upgraded.db.prepare(
|
|
286
|
-
"SELECT to_version, notes FROM migration_history WHERE to_version IN ('9.1', '10.0', '11.0', '12.0') ORDER BY id",
|
|
287
|
-
).all();
|
|
288
|
-
assert.ok(migrations.some((row) => row.to_version === '9.1' && /Kimi/.test(row.notes)));
|
|
289
|
-
assert.ok(migrations.some((row) => row.to_version === '10.0' && /Context Spine/.test(row.notes)));
|
|
290
|
-
assert.ok(migrations.some((row) => row.to_version === '11.0' && /baseline adoption/i.test(row.notes)));
|
|
291
|
-
assert.ok(migrations.some((row) => row.to_version === '12.0' && /gap ledger|re-adoption/i.test(row.notes)));
|
|
292
|
-
assert.deepEqual(
|
|
293
|
-
upgraded.db.prepare("SELECT mode, status FROM baselines WHERE id = 'migrated-baseline'").get(),
|
|
294
|
-
{ mode: 'migrated', status: 'adopting' },
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
upgraded.db.prepare(
|
|
298
|
-
"INSERT INTO tasks (id, title, type, priority) VALUES ('task-kimi', 'Kimi', 'feature', 'P1')",
|
|
299
|
-
).run();
|
|
300
|
-
const session = stateOps.createSession(upgraded.db, {
|
|
301
|
-
sid: 'session-kimi',
|
|
302
|
-
task_id: 'task-kimi',
|
|
303
|
-
runtime: 'kimi',
|
|
304
|
-
worktree_path: '/tmp/kimi-worktree',
|
|
305
|
-
artifact_dir: '/tmp/kimi-artifacts',
|
|
306
|
-
});
|
|
307
|
-
assert.equal(session.runtime, 'kimi');
|
|
308
|
-
const event = stateOps.appendEvent(upgraded.db, { type: 'kimi-ready', runtime: 'kimi' });
|
|
309
|
-
assert.ok(event.event_id > 0);
|
|
310
|
-
closeStateDb(upgraded.db);
|
|
311
|
-
} finally {
|
|
312
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
313
|
-
}
|
|
314
|
-
});
|
|
@@ -1,169 +0,0 @@
|
|
|
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 { initStateDb, closeStateDb } = require('./state-db.cjs');
|
|
10
|
-
const ops = require('./state-ops.cjs');
|
|
11
|
-
const changes = require('./change-workflow.cjs');
|
|
12
|
-
const { expandTask, TaskExpandError } = require('./task-expander.cjs');
|
|
13
|
-
|
|
14
|
-
function tmpDb() {
|
|
15
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-expand-'));
|
|
16
|
-
const { db } = initStateDb(path.join(dir, 'state.db'));
|
|
17
|
-
db.prepare(
|
|
18
|
-
`INSERT INTO baselines
|
|
19
|
-
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
20
|
-
VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'fixture', ?)`,
|
|
21
|
-
).run(new Date().toISOString());
|
|
22
|
-
return { dir, db };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function cleanup({ dir, db }) {
|
|
26
|
-
closeStateDb(db);
|
|
27
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function seedParent(db, overrides = {}) {
|
|
31
|
-
return ops.createTask(db, {
|
|
32
|
-
id: 'parent-1',
|
|
33
|
-
title: 'Build authentication module',
|
|
34
|
-
type: 'feature',
|
|
35
|
-
priority: 'P1',
|
|
36
|
-
complexity: 9,
|
|
37
|
-
files_modified: ['src/auth/index.ts'],
|
|
38
|
-
...overrides,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const HAPPY_CHILDREN = [
|
|
43
|
-
{ id: 'child-1', title: 'Design auth schema', type: 'architecture', priority: 'P1', complexity: 4, deps: [], files_modified: ['src/auth/schema.ts'] },
|
|
44
|
-
{ id: 'child-2', title: 'Implement login endpoint', type: 'feature', priority: 'P1', complexity: 5, deps: ['child-1'], files_modified: ['src/auth/login.ts'] },
|
|
45
|
-
{ id: 'child-3', title: 'Add signup endpoint', type: 'feature', priority: 'P2', complexity: 3, deps: ['child-1'], files_modified: ['src/auth/signup.ts'] },
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
test('expandTask atomically persists host-derived children without a provider client', () => {
|
|
49
|
-
const ctx = tmpDb();
|
|
50
|
-
try {
|
|
51
|
-
seedParent(ctx.db);
|
|
52
|
-
const result = expandTask(ctx.db, { id: 'parent-1', children: HAPPY_CHILDREN });
|
|
53
|
-
assert.equal(result.parent_id, 'parent-1');
|
|
54
|
-
assert.equal(result.children.length, 3);
|
|
55
|
-
assert.equal(ops.readTask(ctx.db, 'parent-1').status, 'expanded');
|
|
56
|
-
for (const child of HAPPY_CHILDREN) {
|
|
57
|
-
const row = ops.readTask(ctx.db, child.id);
|
|
58
|
-
assert.equal(row.parent_id, 'parent-1');
|
|
59
|
-
assert.equal(row.status, 'pending');
|
|
60
|
-
}
|
|
61
|
-
const events = ops.subscribeEventsSince(ctx.db, { since_id: 0, limit: 100 }).events;
|
|
62
|
-
assert.equal(events.filter((event) => event.type === 'task_expanded').length, 1);
|
|
63
|
-
} finally { cleanup(ctx); }
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('expandTask rejects missing and already expanded parents', () => {
|
|
67
|
-
const ctx = tmpDb();
|
|
68
|
-
try {
|
|
69
|
-
assert.throws(
|
|
70
|
-
() => expandTask(ctx.db, { id: 'missing', children: HAPPY_CHILDREN }),
|
|
71
|
-
(err) => err instanceof TaskExpandError && err.code === 'TASK_NOT_FOUND',
|
|
72
|
-
);
|
|
73
|
-
seedParent(ctx.db);
|
|
74
|
-
expandTask(ctx.db, { id: 'parent-1', children: HAPPY_CHILDREN });
|
|
75
|
-
assert.throws(
|
|
76
|
-
() => expandTask(ctx.db, { id: 'parent-1', children: [{ ...HAPPY_CHILDREN[0], id: 'other' }] }),
|
|
77
|
-
(err) => err instanceof TaskExpandError && err.code === 'ALREADY_EXPANDED',
|
|
78
|
-
);
|
|
79
|
-
} finally { cleanup(ctx); }
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('expandTask requires valid host-derived children and leaves the parent untouched on failure', () => {
|
|
83
|
-
const ctx = tmpDb();
|
|
84
|
-
try {
|
|
85
|
-
seedParent(ctx.db);
|
|
86
|
-
for (const children of [undefined, [], [{
|
|
87
|
-
id: 'child-1', title: 'x', type: 'feature', priority: 'P1',
|
|
88
|
-
}]]) {
|
|
89
|
-
assert.throws(
|
|
90
|
-
() => expandTask(ctx.db, { id: 'parent-1', children }),
|
|
91
|
-
(err) => err instanceof TaskExpandError && err.code === 'INVALID_OUTPUT',
|
|
92
|
-
);
|
|
93
|
-
assert.equal(ops.readTask(ctx.db, 'parent-1').status, 'pending');
|
|
94
|
-
}
|
|
95
|
-
} finally { cleanup(ctx); }
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('expandTask rejects duplicate child ids', () => {
|
|
99
|
-
const ctx = tmpDb();
|
|
100
|
-
try {
|
|
101
|
-
seedParent(ctx.db);
|
|
102
|
-
assert.throws(
|
|
103
|
-
() => expandTask(ctx.db, { id: 'parent-1', children: [
|
|
104
|
-
{ id: 'same', title: 'First child', type: 'feature', priority: 'P2' },
|
|
105
|
-
{ id: 'same', title: 'Second child', type: 'feature', priority: 'P2' },
|
|
106
|
-
] }),
|
|
107
|
-
(err) => err instanceof TaskExpandError && err.code === 'INVALID_OUTPUT',
|
|
108
|
-
);
|
|
109
|
-
} finally { cleanup(ctx); }
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('expandTask rolls back every child when one id collides', () => {
|
|
113
|
-
const ctx = tmpDb();
|
|
114
|
-
try {
|
|
115
|
-
seedParent(ctx.db);
|
|
116
|
-
ops.createTask(ctx.db, {
|
|
117
|
-
id: 'child-1', title: 'Pre-existing task', type: 'bugfix', priority: 'P3',
|
|
118
|
-
});
|
|
119
|
-
assert.throws(() => expandTask(ctx.db, { id: 'parent-1', children: HAPPY_CHILDREN }));
|
|
120
|
-
assert.equal(ops.readTask(ctx.db, 'parent-1').status, 'pending');
|
|
121
|
-
assert.equal(ops.readTask(ctx.db, 'child-2'), null);
|
|
122
|
-
assert.equal(ops.readTask(ctx.db, 'child-3'), null);
|
|
123
|
-
} finally { cleanup(ctx); }
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('expandTask children inherit parent tag and change ownership', () => {
|
|
127
|
-
const ctx = tmpDb();
|
|
128
|
-
try {
|
|
129
|
-
const { change } = changes.createChange(ctx.db, {
|
|
130
|
-
id: 'expand-change', title: 'Expand one task', kind: 'quick',
|
|
131
|
-
intent: 'Keep child ownership inside the active change.',
|
|
132
|
-
docs_impact: { status: 'none', rationale: 'test fixture' },
|
|
133
|
-
}, { rootDir: ctx.dir });
|
|
134
|
-
seedParent(ctx.db, { tag: 'feat-auth', change_id: change.id });
|
|
135
|
-
const result = expandTask(ctx.db, {
|
|
136
|
-
id: 'parent-1', children: HAPPY_CHILDREN, rootDir: ctx.dir,
|
|
137
|
-
});
|
|
138
|
-
for (const child of result.children) {
|
|
139
|
-
const row = ops.readTask(ctx.db, child.id);
|
|
140
|
-
assert.equal(row.tag, 'feat-auth');
|
|
141
|
-
assert.equal(row.change_id, change.id);
|
|
142
|
-
}
|
|
143
|
-
} finally { cleanup(ctx); }
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test('expandTask enforces baseline authority before writing children', () => {
|
|
147
|
-
for (const baseline of ['missing', 'draft', 'migrated']) {
|
|
148
|
-
const ctx = tmpDb();
|
|
149
|
-
try {
|
|
150
|
-
seedParent(ctx.db);
|
|
151
|
-
if (baseline === 'missing') ctx.db.prepare('DELETE FROM baselines').run();
|
|
152
|
-
else if (baseline === 'draft') {
|
|
153
|
-
ctx.db.prepare("UPDATE baselines SET status = 'draft' WHERE id = 'test-baseline'").run();
|
|
154
|
-
} else {
|
|
155
|
-
ctx.db.prepare(
|
|
156
|
-
"UPDATE baselines SET mode = 'migrated', status = 'adopting' WHERE id = 'test-baseline'",
|
|
157
|
-
).run();
|
|
158
|
-
}
|
|
159
|
-
assert.throws(
|
|
160
|
-
() => expandTask(ctx.db, {
|
|
161
|
-
id: 'parent-1', children: HAPPY_CHILDREN, rootDir: ctx.dir,
|
|
162
|
-
}),
|
|
163
|
-
(error) => error.code === 'BASELINE_NOT_READY',
|
|
164
|
-
);
|
|
165
|
-
assert.equal(ops.readTask(ctx.db, 'child-1'), null);
|
|
166
|
-
assert.equal(ops.readTask(ctx.db, 'parent-1').status, 'pending');
|
|
167
|
-
} finally { cleanup(ctx); }
|
|
168
|
-
}
|
|
169
|
-
});
|