ultra-builder-pro-cli 0.9.0 → 0.11.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/CHANGELOG.md +92 -1
- package/README.md +11 -6
- package/adapters/_shared/codex-assets.cjs +23 -331
- package/adapters/_shared/handbook.cjs +2 -0
- package/adapters/_shared/runtime-assets.cjs +25 -0
- package/adapters/_shared/tests/handbook.test.cjs +3 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +12 -4
- package/adapters/claude.js +28 -5
- package/adapters/kimi.js +15 -138
- package/adapters/opencode.js +59 -63
- package/adapters/tests/claude.test.cjs +26 -9
- package/adapters/tests/codex.test.cjs +23 -9
- package/adapters/tests/kimi.test.cjs +15 -11
- package/adapters/tests/opencode.test.cjs +57 -10
- package/agents/code-reviewer.md +24 -85
- package/agents/debugger.md +30 -132
- package/agents/review-code.md +24 -104
- package/agents/review-comments.md +20 -83
- package/agents/review-coordinator.md +40 -137
- package/agents/review-design.md +21 -118
- package/agents/review-errors.md +22 -70
- package/agents/review-spec.md +36 -0
- package/agents/review-tests.md +20 -76
- package/agents/tdd-runner.md +26 -84
- package/commands/learn.md +5 -36
- package/commands/ultra-change.md +5 -43
- package/commands/ultra-deliver.md +4 -46
- package/commands/ultra-dev.md +4 -48
- package/commands/ultra-doctor.md +4 -30
- package/commands/ultra-init.md +5 -47
- package/commands/ultra-plan.md +5 -48
- package/commands/ultra-research.md +5 -54
- package/commands/ultra-status.md +5 -44
- package/commands/ultra-test.md +5 -45
- package/commands/ultra-think.md +5 -40
- package/docs/AGENT-CONTEXT.md +34 -12
- package/docs/ARCHITECTURE.md +31 -5
- package/docs/PLAN.zh-CN.md +3 -2
- package/docs/ROADMAP.md +5 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +7 -5
- package/docs/SKILL-AUTHORING.md +75 -0
- package/docs/USER-HANDBOOK-CONTRACT.md +9 -2
- package/hooks/README.md +6 -4
- package/hooks/active_task_context.py +14 -0
- package/hooks/context_spine.py +218 -0
- package/hooks/health_check.py +1 -1
- package/hooks/workflow_context.py +16 -123
- package/hooks/workflow_resume.py +21 -0
- package/mcp-server/lib/change-workflow.cjs +89 -11
- package/mcp-server/lib/change-workflow.test.cjs +202 -7
- package/mcp-server/lib/context-spine.cjs +331 -0
- package/mcp-server/lib/context-spine.test.cjs +73 -0
- package/mcp-server/lib/spec-learning.cjs +191 -0
- package/mcp-server/lib/spec-learning.test.cjs +62 -0
- package/mcp-server/lib/state-db.cjs +72 -10
- package/mcp-server/lib/state-db.test.cjs +7 -2
- package/mcp-server/server.cjs +31 -1
- package/mcp-server/tests/db-ops.test.cjs +3 -3
- package/mcp-server/tests/server-contract.test.cjs +74 -0
- package/mcp-server/tests/state-db.test.cjs +20 -3
- package/package.json +3 -2
- package/skills/cc-collab/SKILL.md +16 -18
- package/skills/code-review-expert/SKILL.md +39 -114
- package/skills/code-review-expert/references/correctness-reliability.md +30 -0
- package/skills/code-review-expert/references/design-boundaries.md +26 -0
- package/skills/code-review-expert/references/removal-plan.md +17 -45
- package/skills/codex-collab/SKILL.md +23 -28
- package/skills/codex-collab/evals/evals.json +18 -23
- package/skills/integration-rules/SKILL.md +28 -67
- package/skills/learn/SKILL.md +46 -105
- package/skills/security-rules/SKILL.md +34 -56
- package/skills/testing-rules/SKILL.md +28 -52
- package/skills/ultra-change/SKILL.md +61 -145
- package/skills/ultra-deliver/SKILL.md +63 -229
- package/skills/ultra-dev/SKILL.md +46 -338
- package/skills/ultra-doctor/SKILL.md +39 -71
- package/skills/ultra-init/SKILL.md +59 -139
- package/skills/ultra-plan/SKILL.md +50 -310
- package/skills/ultra-research/SKILL.md +59 -152
- package/skills/ultra-research/references/architecture.md +38 -0
- package/skills/ultra-research/references/problem-product.md +39 -0
- package/skills/ultra-research/references/quality-delivery.md +34 -0
- package/skills/ultra-research/references/synthesis.md +38 -0
- package/skills/ultra-research/references/users-scope.md +40 -0
- package/skills/ultra-review/SKILL.md +55 -427
- package/skills/ultra-review/references/unified-schema.md +76 -101
- package/skills/ultra-review/scripts/review_wait.py +242 -77
- package/skills/ultra-status/SKILL.md +38 -173
- package/skills/ultra-test/SKILL.md +41 -195
- package/skills/ultra-think/SKILL.md +43 -101
- package/skills/ultra-verify/SKILL.md +44 -84
- package/skills/ultra-verify/evals/evals.json +24 -27
- package/spec/cli-protocol.md +3 -0
- package/spec/mcp-tools.yaml +133 -4
- package/spec/schemas/command-manifest.schema.json +7 -25
- package/spec/schemas/skill-manifest.schema.json +9 -79
- package/spec/schemas/state-db.sql +31 -0
- package/skills/code-review-expert/references/code-quality-checklist.md +0 -112
- package/skills/code-review-expert/references/integration-checklist.md +0 -46
- package/skills/code-review-expert/references/security-checklist.md +0 -100
- package/skills/code-review-expert/references/solid-checklist.md +0 -65
- package/skills/ultra-research/steps/step-00-problem-validation.md +0 -154
- package/skills/ultra-research/steps/step-01-opportunity-discovery.md +0 -172
- package/skills/ultra-research/steps/step-02-market-assessment.md +0 -195
- package/skills/ultra-research/steps/step-03-competitive-landscape.md +0 -197
- package/skills/ultra-research/steps/step-04-product-strategy.md +0 -196
- package/skills/ultra-research/steps/step-05-assumptions-validation.md +0 -234
- package/skills/ultra-research/steps/step-10-user-personas.md +0 -169
- package/skills/ultra-research/steps/step-11-user-scenarios.md +0 -181
- package/skills/ultra-research/steps/step-20-user-stories.md +0 -156
- package/skills/ultra-research/steps/step-21-features-scope.md +0 -157
- package/skills/ultra-research/steps/step-22-success-metrics.md +0 -195
- package/skills/ultra-research/steps/step-30-architecture-context.md +0 -154
- package/skills/ultra-research/steps/step-31-solution-strategy.md +0 -172
- package/skills/ultra-research/steps/step-32-building-blocks.md +0 -188
- package/skills/ultra-research/steps/step-40-deployment.md +0 -123
- package/skills/ultra-research/steps/step-41-quality-risks.md +0 -135
- package/skills/ultra-research/steps/step-99-synthesis.md +0 -188
- package/skills/ultra-review/scripts/review_verdict_update.py +0 -99
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const ops = require('./state-ops.cjs');
|
|
8
|
+
|
|
9
|
+
const CANDIDATE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
10
|
+
const TRANSITIONS = Object.freeze({
|
|
11
|
+
proposed: new Set(['approved', 'rejected']),
|
|
12
|
+
approved: new Set(['applied', 'rejected']),
|
|
13
|
+
rejected: new Set(),
|
|
14
|
+
applied: new Set(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
class SpecLearningError extends Error {
|
|
18
|
+
constructor(code, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'SpecLearningError';
|
|
21
|
+
this.code = code;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nowIso() {
|
|
26
|
+
return new Date().toISOString();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function rowToCandidate(row) {
|
|
30
|
+
if (!row) return null;
|
|
31
|
+
let evidence;
|
|
32
|
+
try { evidence = JSON.parse(row.evidence_json); }
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw new SpecLearningError('STATE_CORRUPT', `invalid spec-learning evidence: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
const candidate = { ...row, evidence };
|
|
37
|
+
delete candidate.evidence_json;
|
|
38
|
+
return candidate;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function listSpecLearning(db, changeId) {
|
|
42
|
+
return db.prepare(
|
|
43
|
+
'SELECT * FROM spec_learning_candidates WHERE change_id = ? ORDER BY proposed_at ASC, id ASC',
|
|
44
|
+
).all(changeId).map(rowToCandidate);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeAtomic(file, contents) {
|
|
48
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
49
|
+
const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
50
|
+
fs.writeFileSync(temporary, contents);
|
|
51
|
+
fs.renameSync(temporary, file);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function upsertProjectionArtifact(db, change, rootDir, candidates) {
|
|
55
|
+
const file = path.resolve(rootDir, change.artifact_root, 'spec-learning.json');
|
|
56
|
+
const generatedAt = candidates.reduce((latest, candidate) => (
|
|
57
|
+
[candidate.applied_at, candidate.resolved_at, candidate.proposed_at]
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.reduce((value, timestamp) => (timestamp > value ? timestamp : value), latest)
|
|
60
|
+
), '1970-01-01T00:00:00.000Z');
|
|
61
|
+
const payload = {
|
|
62
|
+
schema_version: '1.0',
|
|
63
|
+
source: '.ultra/state.db',
|
|
64
|
+
change_id: change.id,
|
|
65
|
+
generated_at: generatedAt,
|
|
66
|
+
candidates,
|
|
67
|
+
};
|
|
68
|
+
const serialized = `${JSON.stringify(payload, null, 2)}\n`;
|
|
69
|
+
writeAtomic(file, serialized);
|
|
70
|
+
const relative = path.relative(rootDir, file);
|
|
71
|
+
const hash = crypto.createHash('sha256').update(serialized).digest('hex');
|
|
72
|
+
db.prepare(
|
|
73
|
+
`INSERT INTO artifacts (id, change_id, kind, path, content_hash, metadata_json)
|
|
74
|
+
VALUES (?, ?, 'spec_learning', ?, ?, ?)
|
|
75
|
+
ON CONFLICT(change_id, kind, path) DO UPDATE SET
|
|
76
|
+
content_hash = excluded.content_hash,
|
|
77
|
+
metadata_json = excluded.metadata_json,
|
|
78
|
+
updated_at = excluded.updated_at`,
|
|
79
|
+
).run(
|
|
80
|
+
`art-${crypto.randomUUID().slice(0, 12)}`, change.id, relative, hash,
|
|
81
|
+
JSON.stringify({ candidates: candidates.length }),
|
|
82
|
+
);
|
|
83
|
+
return { path: file, hash };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function mutableChange(db, changeId) {
|
|
87
|
+
const change = db.prepare('SELECT * FROM changes WHERE id = ?').get(changeId);
|
|
88
|
+
if (!change) throw new SpecLearningError('CHANGE_NOT_FOUND', `change ${changeId} not found`);
|
|
89
|
+
if (!['active', 'blocked'].includes(change.status)) {
|
|
90
|
+
throw new SpecLearningError(
|
|
91
|
+
'CHANGE_NOT_MUTABLE', `change ${changeId} is ${change.status}; learning must resolve before convergence`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return change;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeEvidence(value) {
|
|
98
|
+
if (value === undefined) return [];
|
|
99
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || !entry.trim())) {
|
|
100
|
+
throw new SpecLearningError('VALIDATION_ERROR', 'spec-learning evidence must be non-empty strings');
|
|
101
|
+
}
|
|
102
|
+
return [...new Set(value.map((entry) => entry.trim()))];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function proposeSpecLearning(db, input, { rootDir = process.cwd() } = {}) {
|
|
106
|
+
const change = mutableChange(db, input.change_id);
|
|
107
|
+
const id = String(input.id || `learning-${crypto.randomUUID().slice(0, 12)}`).trim();
|
|
108
|
+
if (!CANDIDATE_ID.test(id)) {
|
|
109
|
+
throw new SpecLearningError('VALIDATION_ERROR', `invalid spec-learning candidate id: ${id}`);
|
|
110
|
+
}
|
|
111
|
+
const targetRef = typeof input.target_ref === 'string' ? input.target_ref.trim() : '';
|
|
112
|
+
const summary = typeof input.summary === 'string' ? input.summary.trim() : '';
|
|
113
|
+
if (!targetRef || !summary) {
|
|
114
|
+
throw new SpecLearningError('VALIDATION_ERROR', 'target_ref and summary are required');
|
|
115
|
+
}
|
|
116
|
+
if (input.task_id) {
|
|
117
|
+
const task = db.prepare('SELECT change_id FROM tasks WHERE id = ?').get(input.task_id);
|
|
118
|
+
if (!task || task.change_id !== change.id) {
|
|
119
|
+
throw new SpecLearningError('TASK_NOT_FOUND', `task ${input.task_id} is not linked to change ${change.id}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const evidence = normalizeEvidence(input.evidence);
|
|
123
|
+
const proposedAt = nowIso();
|
|
124
|
+
return ops.tx(db, () => {
|
|
125
|
+
try {
|
|
126
|
+
db.prepare(
|
|
127
|
+
`INSERT INTO spec_learning_candidates
|
|
128
|
+
(id, change_id, task_id, target_ref, summary, evidence_json, proposed_at)
|
|
129
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
130
|
+
).run(id, change.id, input.task_id || null, targetRef, summary, JSON.stringify(evidence), proposedAt);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (String(error.message).includes('UNIQUE')) {
|
|
133
|
+
throw new SpecLearningError('DUPLICATE_LEARNING_ID', `spec-learning candidate ${id} exists`);
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
ops.appendEventInTx(db, {
|
|
138
|
+
type: 'spec_learning_proposed', change_id: change.id, task_id: input.task_id || null,
|
|
139
|
+
payload: { candidate_id: id, target_ref: targetRef },
|
|
140
|
+
});
|
|
141
|
+
const candidates = listSpecLearning(db, change.id);
|
|
142
|
+
upsertProjectionArtifact(db, change, rootDir, candidates);
|
|
143
|
+
return candidates.find((candidate) => candidate.id === id);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveSpecLearning(db, input, { rootDir = process.cwd() } = {}) {
|
|
148
|
+
const change = mutableChange(db, input.change_id);
|
|
149
|
+
const current = rowToCandidate(db.prepare(
|
|
150
|
+
'SELECT * FROM spec_learning_candidates WHERE id = ? AND change_id = ?',
|
|
151
|
+
).get(input.candidate_id, change.id));
|
|
152
|
+
if (!current) {
|
|
153
|
+
throw new SpecLearningError('LEARNING_NOT_FOUND', `candidate ${input.candidate_id} not found`);
|
|
154
|
+
}
|
|
155
|
+
const status = { approve: 'approved', reject: 'rejected', apply: 'applied' }[input.decision];
|
|
156
|
+
if (!status) throw new SpecLearningError('VALIDATION_ERROR', `invalid learning decision: ${input.decision}`);
|
|
157
|
+
if (!TRANSITIONS[current.status].has(status)) {
|
|
158
|
+
throw new SpecLearningError(
|
|
159
|
+
'ILLEGAL_LEARNING_TRANSITION', `cannot move candidate ${current.id} from ${current.status} to ${status}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const resolution = typeof input.resolution === 'string' ? input.resolution.trim() : '';
|
|
163
|
+
if (['rejected', 'applied'].includes(status) && !resolution) {
|
|
164
|
+
throw new SpecLearningError('VALIDATION_ERROR', `${input.decision} requires a resolution`);
|
|
165
|
+
}
|
|
166
|
+
const timestamp = nowIso();
|
|
167
|
+
return ops.tx(db, () => {
|
|
168
|
+
db.prepare(
|
|
169
|
+
`UPDATE spec_learning_candidates
|
|
170
|
+
SET status = ?, resolution = ?, resolved_at = ?, applied_at = ?
|
|
171
|
+
WHERE id = ? AND change_id = ?`,
|
|
172
|
+
).run(
|
|
173
|
+
status, resolution || null, timestamp, status === 'applied' ? timestamp : null,
|
|
174
|
+
current.id, change.id,
|
|
175
|
+
);
|
|
176
|
+
ops.appendEventInTx(db, {
|
|
177
|
+
type: 'spec_learning_resolved', change_id: change.id, task_id: current.task_id,
|
|
178
|
+
payload: { candidate_id: current.id, decision: input.decision, status },
|
|
179
|
+
});
|
|
180
|
+
const candidates = listSpecLearning(db, change.id);
|
|
181
|
+
upsertProjectionArtifact(db, change, rootDir, candidates);
|
|
182
|
+
return candidates.find((candidate) => candidate.id === current.id);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
SpecLearningError,
|
|
188
|
+
listSpecLearning,
|
|
189
|
+
proposeSpecLearning,
|
|
190
|
+
resolveSpecLearning,
|
|
191
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
changes.createChange(db, {
|
|
24
|
+
id: 'learning-change', title: 'Learning change', kind: 'quick',
|
|
25
|
+
intent: 'Exercise guarded specification-learning transitions.',
|
|
26
|
+
}, { rootDir });
|
|
27
|
+
return { rootDir, db };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test('spec learning cannot skip approval or mutate after apply', () => {
|
|
31
|
+
const fx = fixture();
|
|
32
|
+
try {
|
|
33
|
+
learning.proposeSpecLearning(fx.db, {
|
|
34
|
+
id: 'candidate-1', change_id: 'learning-change', target_ref: 'README.md',
|
|
35
|
+
summary: 'A stable public contract was discovered.',
|
|
36
|
+
}, { rootDir: fx.rootDir });
|
|
37
|
+
assert.throws(
|
|
38
|
+
() => learning.resolveSpecLearning(fx.db, {
|
|
39
|
+
change_id: 'learning-change', candidate_id: 'candidate-1', decision: 'apply',
|
|
40
|
+
resolution: 'Applied.',
|
|
41
|
+
}, { rootDir: fx.rootDir }),
|
|
42
|
+
(error) => error.code === 'ILLEGAL_LEARNING_TRANSITION',
|
|
43
|
+
);
|
|
44
|
+
learning.resolveSpecLearning(fx.db, {
|
|
45
|
+
change_id: 'learning-change', candidate_id: 'candidate-1', decision: 'approve',
|
|
46
|
+
}, { rootDir: fx.rootDir });
|
|
47
|
+
learning.resolveSpecLearning(fx.db, {
|
|
48
|
+
change_id: 'learning-change', candidate_id: 'candidate-1', decision: 'apply',
|
|
49
|
+
resolution: 'Applied to README.md.',
|
|
50
|
+
}, { rootDir: fx.rootDir });
|
|
51
|
+
assert.throws(
|
|
52
|
+
() => learning.resolveSpecLearning(fx.db, {
|
|
53
|
+
change_id: 'learning-change', candidate_id: 'candidate-1', decision: 'reject',
|
|
54
|
+
resolution: 'Too late.',
|
|
55
|
+
}, { rootDir: fx.rootDir }),
|
|
56
|
+
(error) => error.code === 'ILLEGAL_LEARNING_TRANSITION',
|
|
57
|
+
);
|
|
58
|
+
} finally {
|
|
59
|
+
closeStateDb(fx.db);
|
|
60
|
+
fs.rmSync(fx.rootDir, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
@@ -9,7 +9,8 @@ const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
|
9
9
|
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
10
10
|
: path.resolve(__dirname, '..', '..');
|
|
11
11
|
const SCHEMA_FILE = path.join(REPO_ROOT, 'spec', 'schemas', 'state-db.sql');
|
|
12
|
-
const EXPECTED_VERSION = '
|
|
12
|
+
const EXPECTED_VERSION = '10.0';
|
|
13
|
+
const KIMI_SCHEMA_VERSION = '9.1';
|
|
13
14
|
|
|
14
15
|
const REQUIRED_TABLES = Object.freeze([
|
|
15
16
|
'tasks',
|
|
@@ -23,6 +24,7 @@ const REQUIRED_TABLES = Object.freeze([
|
|
|
23
24
|
'changes',
|
|
24
25
|
'artifacts',
|
|
25
26
|
'context_snapshots',
|
|
27
|
+
'spec_learning_candidates',
|
|
26
28
|
'trace_links',
|
|
27
29
|
'incidents',
|
|
28
30
|
'projection_jobs',
|
|
@@ -71,6 +73,13 @@ function columnNames(db, table) {
|
|
|
71
73
|
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
function latestSchemaVersion(db) {
|
|
77
|
+
if (!tableNames(db).includes('schema_version')) return null;
|
|
78
|
+
return db.prepare(
|
|
79
|
+
'SELECT version FROM schema_version ORDER BY applied_at DESC, rowid DESC LIMIT 1',
|
|
80
|
+
).get()?.version || null;
|
|
81
|
+
}
|
|
82
|
+
|
|
74
83
|
function applyCompatibleColumns(db) {
|
|
75
84
|
const tables = new Set(tableNames(db));
|
|
76
85
|
if (!tables.has('tasks')) return;
|
|
@@ -87,6 +96,48 @@ function applyCompatibleColumns(db) {
|
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
98
|
|
|
99
|
+
function applyContextSpineUpgrade(db) {
|
|
100
|
+
const tables = new Set(tableNames(db));
|
|
101
|
+
if (!tables.has('context_snapshots')) return false;
|
|
102
|
+
let changed = !tables.has('spec_learning_candidates');
|
|
103
|
+
const columns = columnNames(db, 'context_snapshots');
|
|
104
|
+
const additions = [
|
|
105
|
+
['role', "TEXT NOT NULL DEFAULT 'plan' CHECK (role IN ('plan', 'implement', 'check', 'review'))"],
|
|
106
|
+
['gate', "TEXT NOT NULL DEFAULT 'alignment' CHECK (gate IN ('alignment', 'planning', 'implementation', 'verification', 'review', 'convergence', 'recovery'))"],
|
|
107
|
+
['next_action', "TEXT NOT NULL DEFAULT 'Resolve the next Ultra workflow action.'"],
|
|
108
|
+
['readiness', "TEXT NOT NULL DEFAULT 'ready' CHECK (readiness IN ('ready', 'blocked'))"],
|
|
109
|
+
['blockers_json', "TEXT NOT NULL DEFAULT '[]'"],
|
|
110
|
+
['context_json', "TEXT NOT NULL DEFAULT '{}'"],
|
|
111
|
+
['token_estimate', 'INTEGER NOT NULL DEFAULT 0 CHECK (token_estimate >= 0)'],
|
|
112
|
+
['token_budget', 'INTEGER NOT NULL DEFAULT 12000 CHECK (token_budget > 0)'],
|
|
113
|
+
];
|
|
114
|
+
for (const [name, definition] of additions) {
|
|
115
|
+
if (!columns.has(name)) {
|
|
116
|
+
db.exec(`ALTER TABLE context_snapshots ADD COLUMN ${name} ${definition}`);
|
|
117
|
+
changed = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
db.exec(`
|
|
121
|
+
CREATE TABLE IF NOT EXISTS spec_learning_candidates (
|
|
122
|
+
id TEXT PRIMARY KEY,
|
|
123
|
+
change_id TEXT NOT NULL REFERENCES changes(id) ON DELETE CASCADE,
|
|
124
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
125
|
+
target_ref TEXT NOT NULL,
|
|
126
|
+
summary TEXT NOT NULL,
|
|
127
|
+
evidence_json TEXT NOT NULL DEFAULT '[]',
|
|
128
|
+
status TEXT NOT NULL DEFAULT 'proposed'
|
|
129
|
+
CHECK (status IN ('proposed', 'approved', 'rejected', 'applied')),
|
|
130
|
+
resolution TEXT,
|
|
131
|
+
proposed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
132
|
+
resolved_at TEXT,
|
|
133
|
+
applied_at TEXT
|
|
134
|
+
);
|
|
135
|
+
CREATE INDEX IF NOT EXISTS spec_learning_change
|
|
136
|
+
ON spec_learning_candidates(change_id, status, proposed_at);
|
|
137
|
+
`);
|
|
138
|
+
return changed;
|
|
139
|
+
}
|
|
140
|
+
|
|
90
141
|
function tableSupportsKimi(db, table) {
|
|
91
142
|
const row = db.prepare(
|
|
92
143
|
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
@@ -94,11 +145,9 @@ function tableSupportsKimi(db, table) {
|
|
|
94
145
|
return typeof row?.sql === 'string' && row.sql.includes("'kimi'");
|
|
95
146
|
}
|
|
96
147
|
|
|
97
|
-
function upgradeRuntimeConstraints(db) {
|
|
148
|
+
function upgradeRuntimeConstraints(db, fromVersion = latestSchemaVersion(db)) {
|
|
98
149
|
if (tableSupportsKimi(db, 'events') && tableSupportsKimi(db, 'sessions')) return false;
|
|
99
|
-
const previousVersion =
|
|
100
|
-
'SELECT version FROM schema_version ORDER BY applied_at DESC LIMIT 1',
|
|
101
|
-
).get()?.version || 'unknown';
|
|
150
|
+
const previousVersion = fromVersion || 'unknown';
|
|
102
151
|
const foreignKeys = db.pragma('foreign_keys', { simple: true });
|
|
103
152
|
db.pragma('foreign_keys = OFF');
|
|
104
153
|
try {
|
|
@@ -155,7 +204,7 @@ function upgradeRuntimeConstraints(db) {
|
|
|
155
204
|
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
156
205
|
).run(
|
|
157
206
|
previousVersion,
|
|
158
|
-
|
|
207
|
+
KIMI_SCHEMA_VERSION,
|
|
159
208
|
'Add Kimi to durable event and session runtime constraints',
|
|
160
209
|
);
|
|
161
210
|
const violations = db.pragma('foreign_key_check');
|
|
@@ -169,15 +218,27 @@ function upgradeRuntimeConstraints(db) {
|
|
|
169
218
|
return true;
|
|
170
219
|
}
|
|
171
220
|
|
|
172
|
-
function applyCompatibleUpgrades(db) {
|
|
173
|
-
upgradeRuntimeConstraints(db);
|
|
221
|
+
function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db)) {
|
|
222
|
+
const runtimeChanged = upgradeRuntimeConstraints(db, fromVersion);
|
|
174
223
|
db.transaction(() => {
|
|
175
224
|
applyCompatibleColumns(db);
|
|
225
|
+
const contextChanged = applyContextSpineUpgrade(db);
|
|
226
|
+
if (contextChanged || (fromVersion && fromVersion !== EXPECTED_VERSION)) {
|
|
227
|
+
db.prepare(
|
|
228
|
+
`INSERT INTO migration_history
|
|
229
|
+
(from_version, to_version, direction, status, notes)
|
|
230
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
231
|
+
).run(
|
|
232
|
+
runtimeChanged ? KIMI_SCHEMA_VERSION : (fromVersion || 'unknown'),
|
|
233
|
+
EXPECTED_VERSION,
|
|
234
|
+
'Add Context Spine role/gate readiness, execution contracts, breadcrumbs, and specification learning',
|
|
235
|
+
);
|
|
236
|
+
}
|
|
176
237
|
db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
|
|
177
238
|
db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
|
|
178
239
|
db.prepare(
|
|
179
240
|
'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
|
|
180
|
-
).run(EXPECTED_VERSION, '
|
|
241
|
+
).run(EXPECTED_VERSION, 'Role-scoped context snapshots, deterministic breadcrumbs, and approval-gated specification learning');
|
|
181
242
|
})();
|
|
182
243
|
}
|
|
183
244
|
|
|
@@ -199,12 +260,13 @@ function ensureSchemaVersion(db) {
|
|
|
199
260
|
function initStateDb(dbPath) {
|
|
200
261
|
const db = openStateDb(dbPath);
|
|
201
262
|
const existing = new Set(tableNames(db));
|
|
263
|
+
const fromVersion = latestSchemaVersion(db);
|
|
202
264
|
applyCompatibleColumns(db);
|
|
203
265
|
const missing = REQUIRED_TABLES.filter((t) => !existing.has(t));
|
|
204
266
|
if (missing.length > 0) {
|
|
205
267
|
applySchema(db);
|
|
206
268
|
}
|
|
207
|
-
applyCompatibleUpgrades(db);
|
|
269
|
+
applyCompatibleUpgrades(db, fromVersion);
|
|
208
270
|
const version = ensureSchemaVersion(db);
|
|
209
271
|
return {
|
|
210
272
|
db,
|
|
@@ -123,7 +123,7 @@ test('initStateDb migrates existing runtime constraints to Kimi without losing r
|
|
|
123
123
|
const legacy = openStateDb(file);
|
|
124
124
|
const legacySchema = fs.readFileSync(SCHEMA_FILE, 'utf8').replaceAll(", 'kimi'", '');
|
|
125
125
|
legacy.exec(legacySchema);
|
|
126
|
-
legacy.prepare("DELETE FROM schema_version WHERE version
|
|
126
|
+
legacy.prepare("DELETE FROM schema_version WHERE version IN ('9.1', '10.0')").run();
|
|
127
127
|
legacy.prepare(
|
|
128
128
|
"INSERT INTO tasks (id, title, type, priority) VALUES ('task-old', 'Old', 'feature', 'P1')",
|
|
129
129
|
).run();
|
|
@@ -146,11 +146,16 @@ test('initStateDb migrates existing runtime constraints to Kimi without losing r
|
|
|
146
146
|
closeStateDb(legacy);
|
|
147
147
|
|
|
148
148
|
const upgraded = initStateDb(file);
|
|
149
|
-
assert.equal(upgraded.schema_version, '
|
|
149
|
+
assert.equal(upgraded.schema_version, '10.0');
|
|
150
150
|
assert.equal(upgraded.db.prepare("SELECT runtime FROM sessions WHERE sid = 'session-old'").get().runtime, 'codex');
|
|
151
151
|
assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE session_id = 'session-old'").get().n, 1);
|
|
152
152
|
assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM incidents WHERE session_id = 'session-old'").get().n, 1);
|
|
153
153
|
assert.deepEqual(upgraded.db.pragma('foreign_key_check'), []);
|
|
154
|
+
const migrations = upgraded.db.prepare(
|
|
155
|
+
"SELECT to_version, notes FROM migration_history WHERE to_version IN ('9.1', '10.0') ORDER BY id",
|
|
156
|
+
).all();
|
|
157
|
+
assert.ok(migrations.some((row) => row.to_version === '9.1' && /Kimi/.test(row.notes)));
|
|
158
|
+
assert.ok(migrations.some((row) => row.to_version === '10.0' && /Context Spine/.test(row.notes)));
|
|
154
159
|
|
|
155
160
|
upgraded.db.prepare(
|
|
156
161
|
"INSERT INTO tasks (id, title, type, priority) VALUES ('task-kimi', 'Kimi', 'feature', 'P1')",
|
package/mcp-server/server.cjs
CHANGED
|
@@ -73,6 +73,9 @@ const CHANGE_TOOLS = Object.freeze([
|
|
|
73
73
|
'change.get',
|
|
74
74
|
'change.list',
|
|
75
75
|
'change.context',
|
|
76
|
+
'change.breadcrumb',
|
|
77
|
+
'change.learning_propose',
|
|
78
|
+
'change.learning_resolve',
|
|
76
79
|
'change.converge',
|
|
77
80
|
'change.archive',
|
|
78
81
|
]);
|
|
@@ -95,6 +98,7 @@ const MUTATING_TOOLS = new Set([
|
|
|
95
98
|
'session.spawn', 'session.close', 'session.heartbeat',
|
|
96
99
|
'plan.export',
|
|
97
100
|
'change.create', 'change.update', 'change.context', 'change.converge', 'change.archive',
|
|
101
|
+
'change.learning_propose', 'change.learning_resolve',
|
|
98
102
|
]);
|
|
99
103
|
|
|
100
104
|
function loadRegisteredTools() {
|
|
@@ -368,7 +372,12 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
368
372
|
err.code = 'CHANGE_NOT_FOUND';
|
|
369
373
|
throw err;
|
|
370
374
|
}
|
|
371
|
-
return {
|
|
375
|
+
return {
|
|
376
|
+
change: {
|
|
377
|
+
...change,
|
|
378
|
+
learning_candidates: changes.listSpecLearning(db, change.id),
|
|
379
|
+
},
|
|
380
|
+
};
|
|
372
381
|
}
|
|
373
382
|
case 'change.list': {
|
|
374
383
|
const rows = changes.listChanges(db, input || {});
|
|
@@ -382,6 +391,27 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
382
391
|
manifest: out.manifest,
|
|
383
392
|
};
|
|
384
393
|
}
|
|
394
|
+
case 'change.breadcrumb': {
|
|
395
|
+
return {
|
|
396
|
+
breadcrumb: changes.readBreadcrumb(
|
|
397
|
+
db, input || {}, { rootDir: ctx.rootDir || process.cwd() },
|
|
398
|
+
),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
case 'change.learning_propose': {
|
|
402
|
+
return {
|
|
403
|
+
candidate: changes.proposeSpecLearning(
|
|
404
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
405
|
+
),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
case 'change.learning_resolve': {
|
|
409
|
+
return {
|
|
410
|
+
candidate: changes.resolveSpecLearning(
|
|
411
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
412
|
+
),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
385
415
|
case 'change.converge': {
|
|
386
416
|
return changes.convergeChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
|
|
387
417
|
}
|
|
@@ -11,7 +11,7 @@ const Database = require('better-sqlite3');
|
|
|
11
11
|
|
|
12
12
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
13
13
|
const CLI = path.join(REPO_ROOT, 'ultra-tools', 'cli.cjs');
|
|
14
|
-
const { initStateDb, closeStateDb } = require('../lib/state-db.cjs');
|
|
14
|
+
const { EXPECTED_VERSION, initStateDb, closeStateDb } = require('../lib/state-db.cjs');
|
|
15
15
|
const ops = require('../lib/state-ops.cjs');
|
|
16
16
|
|
|
17
17
|
function tmpDir() {
|
|
@@ -97,8 +97,8 @@ test('db backup writes a file that opens independently with the same schema', ()
|
|
|
97
97
|
|
|
98
98
|
// Independently open the backup with a fresh connection
|
|
99
99
|
const snap = new Database(backupPath);
|
|
100
|
-
const v = snap.prepare(
|
|
101
|
-
assert.equal(v.version,
|
|
100
|
+
const v = snap.prepare('SELECT version FROM schema_version WHERE version = ?').get(EXPECTED_VERSION);
|
|
101
|
+
assert.equal(v.version, EXPECTED_VERSION);
|
|
102
102
|
const t = snap.prepare("SELECT id FROM tasks WHERE id = 'b-1'").get();
|
|
103
103
|
assert.equal(t.id, 'b-1');
|
|
104
104
|
snap.close();
|
|
@@ -75,10 +75,13 @@ test('listTools returns workflow tools and exposes no Ultra memory API', async (
|
|
|
75
75
|
const names = list.tools.map((t) => t.name).sort();
|
|
76
76
|
assert.deepEqual(names, [
|
|
77
77
|
'change.archive',
|
|
78
|
+
'change.breadcrumb',
|
|
78
79
|
'change.context',
|
|
79
80
|
'change.converge',
|
|
80
81
|
'change.create',
|
|
81
82
|
'change.get',
|
|
83
|
+
'change.learning_propose',
|
|
84
|
+
'change.learning_resolve',
|
|
82
85
|
'change.list',
|
|
83
86
|
'change.update',
|
|
84
87
|
'plan.export',
|
|
@@ -142,9 +145,19 @@ test('change.create + change.context expose a continuous change unit with extern
|
|
|
142
145
|
name: 'change.context', arguments: { id: 'mcp-change' },
|
|
143
146
|
}));
|
|
144
147
|
assert.equal(context.manifest.change.id, 'mcp-change');
|
|
148
|
+
assert.equal(context.manifest.schema_version, '2.0');
|
|
149
|
+
assert.equal(context.manifest.role, 'plan');
|
|
150
|
+
assert.equal(context.manifest.readiness.status, 'ready');
|
|
145
151
|
assert.equal(context.manifest.providers.memory.provider, 'cloud-mem');
|
|
146
152
|
assert.equal(context.manifest.provider_boundary.includes('content remain external'), true);
|
|
147
153
|
|
|
154
|
+
const breadcrumb = readToolPayload(await client.callTool({
|
|
155
|
+
name: 'change.breadcrumb', arguments: { id: 'mcp-change' },
|
|
156
|
+
}));
|
|
157
|
+
assert.equal(breadcrumb.breadcrumb.change_id, 'mcp-change');
|
|
158
|
+
assert.equal(breadcrumb.breadcrumb.recommended_workflow, 'ultra-change');
|
|
159
|
+
assert.match(breadcrumb.breadcrumb.context_manifest_hash, /^[0-9a-f]{64}$/);
|
|
160
|
+
|
|
148
161
|
const listed = readToolPayload(await client.callTool({
|
|
149
162
|
name: 'change.list', arguments: { status: 'active' },
|
|
150
163
|
}));
|
|
@@ -156,6 +169,67 @@ test('change.create + change.context expose a continuous change unit with extern
|
|
|
156
169
|
}
|
|
157
170
|
});
|
|
158
171
|
|
|
172
|
+
test('spec-learning MCP tools persist an approval-gated candidate and projection', async () => {
|
|
173
|
+
const proj = tmpProject();
|
|
174
|
+
try {
|
|
175
|
+
await withClient(proj, async (client) => {
|
|
176
|
+
await client.callTool({
|
|
177
|
+
name: 'change.create',
|
|
178
|
+
arguments: {
|
|
179
|
+
id: 'learning-change', title: 'Learn stable contract', kind: 'quick',
|
|
180
|
+
intent: 'Persist only approved specification discoveries.',
|
|
181
|
+
docs_impact: { status: 'none', files: [], rationale: 'Contract fixture.' },
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
await client.callTool({
|
|
185
|
+
name: 'task.create',
|
|
186
|
+
arguments: {
|
|
187
|
+
id: 'learning-task', title: 'Discover contract', type: 'feature', priority: 'P1',
|
|
188
|
+
change_id: 'learning-change',
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
const proposed = readToolPayload(await client.callTool({
|
|
192
|
+
name: 'change.learning_propose',
|
|
193
|
+
arguments: {
|
|
194
|
+
id: 'learning-contract', change_id: 'learning-change', task_id: 'learning-task',
|
|
195
|
+
target_ref: 'README.md#contract', summary: 'Context compilation fails closed on stale refs.',
|
|
196
|
+
evidence: ['node --test context-spine.test.cjs'],
|
|
197
|
+
},
|
|
198
|
+
}));
|
|
199
|
+
assert.equal(proposed.candidate.status, 'proposed');
|
|
200
|
+
const withLearning = readToolPayload(await client.callTool({
|
|
201
|
+
name: 'change.get', arguments: { id: 'learning-change' },
|
|
202
|
+
}));
|
|
203
|
+
assert.deepEqual(
|
|
204
|
+
withLearning.change.learning_candidates.map((candidate) => candidate.id),
|
|
205
|
+
['learning-contract'],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const approved = readToolPayload(await client.callTool({
|
|
209
|
+
name: 'change.learning_resolve',
|
|
210
|
+
arguments: {
|
|
211
|
+
change_id: 'learning-change', candidate_id: 'learning-contract', decision: 'approve',
|
|
212
|
+
resolution: 'Stable behavior contract.',
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
215
|
+
assert.equal(approved.candidate.status, 'approved');
|
|
216
|
+
const applied = readToolPayload(await client.callTool({
|
|
217
|
+
name: 'change.learning_resolve',
|
|
218
|
+
arguments: {
|
|
219
|
+
change_id: 'learning-change', candidate_id: 'learning-contract', decision: 'apply',
|
|
220
|
+
resolution: 'Applied to README.md#contract.',
|
|
221
|
+
},
|
|
222
|
+
}));
|
|
223
|
+
assert.equal(applied.candidate.status, 'applied');
|
|
224
|
+
assert.ok(fs.existsSync(path.join(
|
|
225
|
+
proj.dir, '.ultra', 'changes', 'active', 'learning-change', 'spec-learning.json',
|
|
226
|
+
)));
|
|
227
|
+
});
|
|
228
|
+
} finally {
|
|
229
|
+
fs.rmSync(proj.dir, { recursive: true, force: true });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
159
233
|
test('system.doctor returns structured provider ownership and projection health', async () => {
|
|
160
234
|
const proj = tmpProject();
|
|
161
235
|
try {
|
|
@@ -8,7 +8,7 @@ const path = require('node:path');
|
|
|
8
8
|
|
|
9
9
|
const stateDb = require('../lib/state-db.cjs');
|
|
10
10
|
|
|
11
|
-
test('initStateDb upgrades a pre-
|
|
11
|
+
test('initStateDb upgrades a pre-10.0 database in place with Context Spine state', () => {
|
|
12
12
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-db-upgrade-'));
|
|
13
13
|
const file = path.join(dir, 'state.db');
|
|
14
14
|
try {
|
|
@@ -59,8 +59,25 @@ test('initStateDb upgrades a pre-9.1 database in place with task/event change li
|
|
|
59
59
|
assert.ok(columns.includes('change_id'));
|
|
60
60
|
const eventColumns = upgraded.db.prepare('PRAGMA table_info(events)').all().map((row) => row.name);
|
|
61
61
|
assert.ok(eventColumns.includes('change_id'));
|
|
62
|
-
assert.equal(upgraded.schema_version, '
|
|
63
|
-
assert.ok(upgraded.db.prepare("SELECT 1 FROM schema_version WHERE version = '
|
|
62
|
+
assert.equal(upgraded.schema_version, '10.0');
|
|
63
|
+
assert.ok(upgraded.db.prepare("SELECT 1 FROM schema_version WHERE version = '10.0'").get());
|
|
64
|
+
const contextColumns = upgraded.db.prepare('PRAGMA table_info(context_snapshots)').all()
|
|
65
|
+
.map((row) => row.name);
|
|
66
|
+
for (const column of ['role', 'gate', 'next_action', 'readiness', 'context_json', 'token_budget']) {
|
|
67
|
+
assert.ok(contextColumns.includes(column), column);
|
|
68
|
+
}
|
|
69
|
+
assert.ok(upgraded.db.prepare(
|
|
70
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'spec_learning_candidates'",
|
|
71
|
+
).get());
|
|
72
|
+
const contextMigration = upgraded.db.prepare(
|
|
73
|
+
"SELECT from_version, to_version, notes FROM migration_history WHERE to_version = '10.0' ORDER BY id DESC LIMIT 1",
|
|
74
|
+
).get();
|
|
75
|
+
assert.equal(contextMigration.from_version, '9.1');
|
|
76
|
+
assert.equal(contextMigration.to_version, '10.0');
|
|
77
|
+
assert.match(contextMigration.notes, /Context Spine/);
|
|
78
|
+
assert.ok(upgraded.db.prepare(
|
|
79
|
+
"SELECT 1 FROM migration_history WHERE from_version = '8A.1' AND to_version = '9.1' AND notes LIKE '%Kimi%'",
|
|
80
|
+
).get());
|
|
64
81
|
stateDb.closeStateDb(upgraded.db);
|
|
65
82
|
} finally {
|
|
66
83
|
fs.rmSync(dir, { recursive: true, force: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra-builder-pro-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Multi-runtime installer for the Ultra Builder Pro agent engineering system — deploys commands, agents, skills, hooks, and MCP server to Claude Code, OpenCode, Codex CLI, and Kimi Code.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ultra-builder-pro-cli": "bin/install.js",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"docs/ROADMAP.md",
|
|
51
51
|
"docs/ARCHITECTURE.md",
|
|
52
52
|
"docs/AGENT-CONTEXT.md",
|
|
53
|
+
"docs/SKILL-AUTHORING.md",
|
|
53
54
|
"docs/COMMIT-HASH-BACKFILL.md",
|
|
54
55
|
"docs/PLAN.zh-CN.md",
|
|
55
56
|
"docs/STATE-DB-ACCESS-POLICY.md",
|
|
@@ -87,7 +88,7 @@
|
|
|
87
88
|
"test:state": "node --test 'mcp-server/**/*.test.cjs'",
|
|
88
89
|
"test:orch": "node --test 'orchestrator/**/*.test.cjs'",
|
|
89
90
|
"test:spec": "node spec/scripts/test-all.cjs",
|
|
90
|
-
"test:rest": "node --test adapters/_shared/tests/*.test.cjs adapters/tests/*.test.cjs ultra-tools/*.test.cjs tests/conformance/**/*.test.cjs tests
|
|
91
|
+
"test:rest": "node --test adapters/_shared/tests/*.test.cjs adapters/tests/*.test.cjs ultra-tools/*.test.cjs tests/conformance/**/*.test.cjs tests/*.test.cjs",
|
|
91
92
|
"test:all": "npm run test:state && npm run test:orch && npm run test:spec && npm run test:rest",
|
|
92
93
|
"test:hooks": "pytest hooks/tests -q",
|
|
93
94
|
"verify:release": "npm run test:all && npm run test:hooks && npm audit --omit=dev --audit-level=high",
|