ultra-builder-pro-cli 0.15.0 → 0.17.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/specs/research-distillate.md +4 -3
- package/CHANGELOG.md +120 -1
- package/README.md +67 -30
- package/adapters/_shared/codex-assets.cjs +10 -5
- package/adapters/_shared/handbook.cjs +3 -2
- package/adapters/_shared/interaction-contract.cjs +61 -0
- package/adapters/claude.js +1 -1
- package/adapters/opencode.js +1 -1
- package/bin/orchestrator.js +175 -19
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-deliver.md +3 -3
- package/commands/ultra-init.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +2 -2
- package/commands/ultra-review.md +10 -0
- package/commands/ultra-status.md +1 -1
- package/commands/ultra-test.md +1 -1
- package/commands/ultra-think.md +2 -2
- package/docs/AGENT-CONTEXT.md +38 -18
- package/docs/ARCHITECTURE.md +91 -13
- package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
- package/docs/PLAN.zh-CN.md +19 -12
- package/docs/ROADMAP.md +5 -3
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +8 -6
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
- package/docs/WORKFLOW-LIFECYCLE.md +179 -39
- package/hooks/pre_stop_check.py +3 -2
- package/mcp-server/lib/baseline-workflow.cjs +84 -16
- package/mcp-server/lib/change-workflow.cjs +107 -39
- package/mcp-server/lib/context-spine.cjs +215 -87
- package/mcp-server/lib/decision-dialogue.cjs +713 -0
- package/mcp-server/lib/doctor.cjs +9 -0
- package/mcp-server/lib/git-bootstrap.cjs +240 -0
- package/mcp-server/lib/init-project.cjs +178 -41
- package/mcp-server/lib/project-breadcrumb.cjs +23 -14
- package/mcp-server/lib/state-db.cjs +279 -9
- package/mcp-server/lib/state-ops.cjs +64 -1
- package/mcp-server/lib/workflow-state.cjs +313 -205
- package/mcp-server/server.cjs +112 -34
- package/orchestrator/auto-merge.cjs +19 -0
- package/orchestrator/daemon.cjs +104 -0
- package/orchestrator/parallel-orchestrator.cjs +371 -49
- package/orchestrator/session-runner.cjs +354 -25
- package/package.json +2 -1
- package/skills/ultra-change/SKILL.md +69 -65
- package/skills/ultra-deliver/SKILL.md +37 -58
- package/skills/ultra-dev/SKILL.md +55 -49
- package/skills/ultra-doctor/SKILL.md +44 -49
- package/skills/ultra-init/SKILL.md +78 -100
- package/skills/ultra-plan/SKILL.md +78 -79
- package/skills/ultra-research/SKILL.md +99 -98
- package/skills/ultra-review/SKILL.md +38 -47
- package/skills/ultra-review/references/review-modes.md +5 -3
- package/skills/ultra-review/references/unified-schema.md +26 -6
- package/skills/ultra-review/scripts/review_wait.py +12 -0
- package/skills/ultra-status/SKILL.md +34 -40
- package/skills/ultra-test/SKILL.md +61 -48
- package/skills/ultra-think/SKILL.md +47 -47
- package/skills/ultra-think/references/decision-dialogue.md +106 -0
- package/spec/cli-protocol.md +46 -2
- package/spec/mcp-tools.yaml +444 -29
- package/spec/schemas/delivery-report.v1.schema.json +1 -46
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +66 -2
- package/spec/schemas/test-report.v1.schema.json +45 -14
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/session.cjs +10 -3
- package/ultra-tools/commands/status.cjs +34 -14
- package/ultra-tools/commands/system.cjs +2 -2
- package/ultra-tools/commands/task.cjs +3 -0
|
@@ -9,8 +9,8 @@ const { EXPECTED_VERSION, REQUIRED_TABLES } = require('./state-db.cjs');
|
|
|
9
9
|
const { readBreadcrumb } = require('./context-spine.cjs');
|
|
10
10
|
|
|
11
11
|
function blockedBreadcrumb(code, {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
allowedTransitions = [],
|
|
13
|
+
requiredTransition = null,
|
|
14
14
|
} = {}) {
|
|
15
15
|
return {
|
|
16
16
|
change_id: null,
|
|
@@ -20,8 +20,8 @@ function blockedBreadcrumb(code, {
|
|
|
20
20
|
readiness: 'blocked',
|
|
21
21
|
blockers: [code],
|
|
22
22
|
warnings: [],
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
allowed_transitions: allowedTransitions,
|
|
24
|
+
required_transition: requiredTransition,
|
|
25
25
|
context_manifest_path: null,
|
|
26
26
|
context_manifest_hash: null,
|
|
27
27
|
git_head: null,
|
|
@@ -31,15 +31,15 @@ function blockedBreadcrumb(code, {
|
|
|
31
31
|
|
|
32
32
|
function migrationBreadcrumb(code) {
|
|
33
33
|
return blockedBreadcrumb(code, {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
allowedTransitions: ['ultra-init', 'ultra-status'],
|
|
35
|
+
requiredTransition: 'ultra-init',
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function doctorBreadcrumb(code) {
|
|
40
40
|
return blockedBreadcrumb(code, {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
allowedTransitions: ['ultra-doctor', 'ultra-status'],
|
|
42
|
+
requiredTransition: 'ultra-doctor',
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -62,8 +62,8 @@ function readProjectBreadcrumb(rootDir) {
|
|
|
62
62
|
const dbPath = path.join(ultraDir, 'state.db');
|
|
63
63
|
if (!fs.existsSync(dbPath)) {
|
|
64
64
|
return blockedBreadcrumb('STATE_DB_MISSING', {
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
allowedTransitions: ['ultra-init', 'ultra-status'],
|
|
66
|
+
requiredTransition: 'ultra-init',
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -126,10 +126,19 @@ function renderProjectBreadcrumb(rootDir, breadcrumb) {
|
|
|
126
126
|
`Step: ${breadcrumb.workflow.current_step || 'finalize'}`,
|
|
127
127
|
);
|
|
128
128
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
if (breadcrumb.decision) {
|
|
130
|
+
const current = breadcrumb.decision.current;
|
|
131
|
+
lines.push(
|
|
132
|
+
`Decision thread: ${breadcrumb.decision.thread_id} (${breadcrumb.decision.status}/${breadcrumb.decision.mode})`,
|
|
133
|
+
current
|
|
134
|
+
? `Decision: ${current.id} — ${current.question}`
|
|
135
|
+
: 'Decision: checkpoint confirmation required',
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
lines.push(`Allowed transitions: ${(breadcrumb.allowed_transitions || []).join(', ') || 'none'}`);
|
|
139
|
+
if (breadcrumb.required_transition) {
|
|
140
|
+
lines.push(`Required transition: ${breadcrumb.required_transition}`);
|
|
141
|
+
}
|
|
133
142
|
if (breadcrumb.context_manifest_path) {
|
|
134
143
|
lines.push(
|
|
135
144
|
`Context: ${breadcrumb.context_manifest_path} sha256=${breadcrumb.context_manifest_hash || 'unknown'}`,
|
|
@@ -9,13 +9,16 @@ 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 = '18.0';
|
|
13
13
|
const KIMI_SCHEMA_VERSION = '9.1';
|
|
14
14
|
const CONTEXT_SCHEMA_VERSION = '10.0';
|
|
15
15
|
const BASELINE_SCHEMA_VERSION = '11.0';
|
|
16
16
|
const ADOPTION_SCHEMA_VERSION = '12.0';
|
|
17
17
|
const WORKFLOW_SCHEMA_VERSION = '13.0';
|
|
18
18
|
const AUTHORITY_SCHEMA_VERSION = '14.0';
|
|
19
|
+
const SEMANTIC_SCHEMA_VERSION = '15.0';
|
|
20
|
+
const DIALOGUE_SCHEMA_VERSION = '16.0';
|
|
21
|
+
const GIT_AUTHORITY_SCHEMA_VERSION = '17.0';
|
|
19
22
|
|
|
20
23
|
const MIGRATED_GAPS = Object.freeze([{
|
|
21
24
|
id: 'legacy-rebaseline-required',
|
|
@@ -47,6 +50,8 @@ const REQUIRED_TABLES = Object.freeze([
|
|
|
47
50
|
'event_consumers',
|
|
48
51
|
'workflow_runs',
|
|
49
52
|
'workflow_steps',
|
|
53
|
+
'decision_threads',
|
|
54
|
+
'decision_items',
|
|
50
55
|
]);
|
|
51
56
|
|
|
52
57
|
function applyBaselineUpgrade(db, { legacyState = false } = {}) {
|
|
@@ -64,7 +69,7 @@ function applyBaselineUpgrade(db, { legacyState = false } = {}) {
|
|
|
64
69
|
repository_revision TEXT,
|
|
65
70
|
repository_branch TEXT,
|
|
66
71
|
worktree_state TEXT NOT NULL DEFAULT 'unavailable'
|
|
67
|
-
CHECK (worktree_state IN ('clean', 'dirty', 'unavailable')),
|
|
72
|
+
CHECK (worktree_state IN ('clean', 'dirty', 'unborn', 'unavailable')),
|
|
68
73
|
worktree_digest TEXT,
|
|
69
74
|
worktree_files_json TEXT NOT NULL DEFAULT '[]',
|
|
70
75
|
worktree_accepted INTEGER NOT NULL DEFAULT 0 CHECK (worktree_accepted IN (0, 1)),
|
|
@@ -111,7 +116,7 @@ function applyBaselineColumns(db) {
|
|
|
111
116
|
changed = addColumnIfMissing(db, 'baselines', columns, 'repository_branch', 'TEXT') || changed;
|
|
112
117
|
changed = addColumnIfMissing(
|
|
113
118
|
db, 'baselines', columns, 'worktree_state',
|
|
114
|
-
"TEXT NOT NULL DEFAULT 'unavailable' CHECK (worktree_state IN ('clean', 'dirty', 'unavailable'))",
|
|
119
|
+
"TEXT NOT NULL DEFAULT 'unavailable' CHECK (worktree_state IN ('clean', 'dirty', 'unborn', 'unavailable'))",
|
|
115
120
|
) || changed;
|
|
116
121
|
changed = addColumnIfMissing(db, 'baselines', columns, 'worktree_digest', 'TEXT') || changed;
|
|
117
122
|
changed = addColumnIfMissing(
|
|
@@ -143,6 +148,10 @@ function applyChangeColumns(db) {
|
|
|
143
148
|
changed = addColumnIfMissing(
|
|
144
149
|
db, 'changes', columns, 'research_disposition_json', "TEXT NOT NULL DEFAULT '{}'",
|
|
145
150
|
) || changed;
|
|
151
|
+
changed = addColumnIfMissing(
|
|
152
|
+
db, 'changes', columns, 'alignment_thread_id',
|
|
153
|
+
'TEXT REFERENCES decision_threads(id) ON DELETE SET NULL',
|
|
154
|
+
) || changed;
|
|
146
155
|
return changed;
|
|
147
156
|
}
|
|
148
157
|
|
|
@@ -277,7 +286,9 @@ function applyContextSpineUpgrade(db) {
|
|
|
277
286
|
const additions = [
|
|
278
287
|
['role', "TEXT NOT NULL DEFAULT 'plan' CHECK (role IN ('plan', 'implement', 'check', 'review'))"],
|
|
279
288
|
['gate', "TEXT NOT NULL DEFAULT 'alignment' CHECK (gate IN ('alignment', 'planning', 'implementation', 'verification', 'review', 'convergence', 'recovery'))"],
|
|
280
|
-
['next_action', "TEXT NOT NULL DEFAULT '
|
|
289
|
+
['next_action', "TEXT NOT NULL DEFAULT ''"],
|
|
290
|
+
['allowed_transitions_json', "TEXT NOT NULL DEFAULT '[]'"],
|
|
291
|
+
['required_transition', 'TEXT'],
|
|
281
292
|
['readiness', "TEXT NOT NULL DEFAULT 'ready' CHECK (readiness IN ('ready', 'blocked'))"],
|
|
282
293
|
['blockers_json', "TEXT NOT NULL DEFAULT '[]'"],
|
|
283
294
|
['context_json', "TEXT NOT NULL DEFAULT '{}'"],
|
|
@@ -311,6 +322,125 @@ function applyContextSpineUpgrade(db) {
|
|
|
311
322
|
return changed;
|
|
312
323
|
}
|
|
313
324
|
|
|
325
|
+
function migrateAdaptiveWorkflowRuns(db, fromVersion = latestSchemaVersion(db)) {
|
|
326
|
+
const tables = new Set(tableNames(db));
|
|
327
|
+
if (!tables.has('workflow_runs') || !tables.has('workflow_steps')) return false;
|
|
328
|
+
const existing = db.prepare(
|
|
329
|
+
"SELECT 1 FROM migration_history WHERE to_version = '18.0' AND status = 'success' LIMIT 1",
|
|
330
|
+
).get();
|
|
331
|
+
if (fromVersion === EXPECTED_VERSION || existing) return false;
|
|
332
|
+
|
|
333
|
+
const obsoleteSteps = Object.freeze({
|
|
334
|
+
init: ['establish-baseline'],
|
|
335
|
+
change: ['plan-change', 'compile-context', 'verify-readiness'],
|
|
336
|
+
plan: ['select-posture', 'approve-plan'],
|
|
337
|
+
deliver: ['release-if-authorized'],
|
|
338
|
+
});
|
|
339
|
+
const activeRuns = db.prepare(
|
|
340
|
+
`SELECT id, kind, status FROM workflow_runs
|
|
341
|
+
WHERE status IN ('active', 'blocked', 'ready')`,
|
|
342
|
+
).all();
|
|
343
|
+
const isCompleted = db.prepare(
|
|
344
|
+
"SELECT status FROM workflow_steps WHERE run_id = ? AND step_id = ?",
|
|
345
|
+
);
|
|
346
|
+
const currentRequired = db.prepare(
|
|
347
|
+
`SELECT step_id FROM workflow_steps
|
|
348
|
+
WHERE run_id = ? AND required = 1 AND status NOT IN ('completed', 'skipped')
|
|
349
|
+
ORDER BY position ASC LIMIT 1`,
|
|
350
|
+
);
|
|
351
|
+
const skipObsolete = db.prepare(
|
|
352
|
+
`UPDATE workflow_steps
|
|
353
|
+
SET required = 0,
|
|
354
|
+
status = CASE WHEN status IN ('pending', 'in_progress', 'blocked') THEN 'skipped' ELSE status END,
|
|
355
|
+
blockers_json = CASE WHEN status IN ('pending', 'in_progress', 'blocked') THEN '[]' ELSE blockers_json END,
|
|
356
|
+
skip_reason = CASE
|
|
357
|
+
WHEN status IN ('pending', 'in_progress', 'blocked')
|
|
358
|
+
THEN 'Removed by schema 18 adaptive workflow migration; durable evidence is preserved.'
|
|
359
|
+
ELSE skip_reason
|
|
360
|
+
END,
|
|
361
|
+
completed_at = CASE
|
|
362
|
+
WHEN status IN ('pending', 'in_progress', 'blocked')
|
|
363
|
+
THEN strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
364
|
+
ELSE completed_at
|
|
365
|
+
END,
|
|
366
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
367
|
+
WHERE run_id = ? AND step_id = ?`,
|
|
368
|
+
);
|
|
369
|
+
const completeRun = db.prepare(
|
|
370
|
+
`UPDATE workflow_runs
|
|
371
|
+
SET status = 'completed', current_step = NULL, blockers_json = '[]',
|
|
372
|
+
definition_version = '2.0', summary_json = ?,
|
|
373
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
|
|
374
|
+
completed_at = COALESCE(completed_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
375
|
+
WHERE id = ?`,
|
|
376
|
+
);
|
|
377
|
+
for (const run of activeRuns) {
|
|
378
|
+
for (const stepId of obsoleteSteps[run.kind] || []) skipObsolete.run(run.id, stepId);
|
|
379
|
+
|
|
380
|
+
if (run.kind === 'init') {
|
|
381
|
+
const scaffoldReady = ['inspect-authority', 'classify-repository', 'scaffold-authority']
|
|
382
|
+
.every((stepId) => isCompleted.get(run.id, stepId)?.status === 'completed');
|
|
383
|
+
if (scaffoldReady) {
|
|
384
|
+
db.prepare(
|
|
385
|
+
`UPDATE workflow_steps
|
|
386
|
+
SET status = 'completed',
|
|
387
|
+
evidence_json = CASE WHEN evidence_json = '[]' THEN ? ELSE evidence_json END,
|
|
388
|
+
blockers_json = '[]',
|
|
389
|
+
started_at = COALESCE(started_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
390
|
+
completed_at = COALESCE(completed_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
391
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
392
|
+
WHERE run_id = ? AND step_id = 'verify-initialization'
|
|
393
|
+
AND status IN ('pending', 'in_progress', 'blocked')`,
|
|
394
|
+
).run(JSON.stringify([{
|
|
395
|
+
kind: 'migration',
|
|
396
|
+
ref: 'schema:18.0',
|
|
397
|
+
summary: 'Existing scaffold and durable authority were preserved during adaptive migration.',
|
|
398
|
+
}]), run.id);
|
|
399
|
+
if (!currentRequired.get(run.id)) {
|
|
400
|
+
completeRun.run(JSON.stringify({
|
|
401
|
+
migrated: true,
|
|
402
|
+
authority_basis: 'initialized_scaffold',
|
|
403
|
+
research_started: false,
|
|
404
|
+
}), run.id);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (run.kind === 'change') {
|
|
411
|
+
const contractCaptured = ['bind-baseline', 'classify-change', 'record-intent']
|
|
412
|
+
.every((stepId) => isCompleted.get(run.id, stepId)?.status === 'completed');
|
|
413
|
+
if (contractCaptured && !currentRequired.get(run.id)) {
|
|
414
|
+
completeRun.run(JSON.stringify({
|
|
415
|
+
migrated: true,
|
|
416
|
+
authority_basis: 'accepted_change_contract',
|
|
417
|
+
}), run.id);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const next = currentRequired.get(run.id)?.step_id || null;
|
|
423
|
+
db.prepare(
|
|
424
|
+
`UPDATE workflow_runs
|
|
425
|
+
SET definition_version = '2.0', current_step = ?, blockers_json = '[]',
|
|
426
|
+
status = CASE
|
|
427
|
+
WHEN ? IS NULL AND status IN ('active', 'blocked') THEN 'ready'
|
|
428
|
+
WHEN ? IS NOT NULL AND status = 'blocked' THEN 'active'
|
|
429
|
+
ELSE status
|
|
430
|
+
END,
|
|
431
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
432
|
+
WHERE id = ?`,
|
|
433
|
+
).run(next, next, next, run.id);
|
|
434
|
+
}
|
|
435
|
+
db.prepare(
|
|
436
|
+
`UPDATE context_snapshots
|
|
437
|
+
SET allowed_transitions_json = COALESCE(allowed_transitions_json, '[]'),
|
|
438
|
+
required_transition = NULL
|
|
439
|
+
WHERE allowed_transitions_json IS NULL OR required_transition IS NOT NULL`,
|
|
440
|
+
).run();
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
314
444
|
function tableSupportsKimi(db, table) {
|
|
315
445
|
const row = db.prepare(
|
|
316
446
|
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
@@ -318,6 +448,87 @@ function tableSupportsKimi(db, table) {
|
|
|
318
448
|
return typeof row?.sql === 'string' && row.sql.includes("'kimi'");
|
|
319
449
|
}
|
|
320
450
|
|
|
451
|
+
function baselineSupportsUnbornGit(db) {
|
|
452
|
+
const row = db.prepare(
|
|
453
|
+
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'baselines'",
|
|
454
|
+
).get();
|
|
455
|
+
return typeof row?.sql === 'string' && row.sql.includes("'unborn'");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function upgradeBaselineWorktreeConstraint(db, fromVersion = latestSchemaVersion(db)) {
|
|
459
|
+
if (!tableNames(db).includes('baselines') || baselineSupportsUnbornGit(db)) return false;
|
|
460
|
+
const previousVersion = fromVersion || 'unknown';
|
|
461
|
+
const foreignKeys = db.pragma('foreign_keys', { simple: true });
|
|
462
|
+
db.pragma('foreign_keys = OFF');
|
|
463
|
+
try {
|
|
464
|
+
db.transaction(() => {
|
|
465
|
+
db.exec(`
|
|
466
|
+
DROP TABLE IF EXISTS baselines_git_upgrade;
|
|
467
|
+
CREATE TABLE baselines_git_upgrade (
|
|
468
|
+
id TEXT PRIMARY KEY,
|
|
469
|
+
project_name TEXT NOT NULL,
|
|
470
|
+
project_type TEXT,
|
|
471
|
+
stack TEXT,
|
|
472
|
+
mode TEXT NOT NULL CHECK (mode IN ('greenfield', 'brownfield', 'migrated')),
|
|
473
|
+
status TEXT NOT NULL DEFAULT 'draft'
|
|
474
|
+
CHECK (status IN ('draft', 'adopting', 'blocked', 'ready', 'superseded')),
|
|
475
|
+
repository_root TEXT NOT NULL DEFAULT '.',
|
|
476
|
+
scope_json TEXT NOT NULL DEFAULT '["."]',
|
|
477
|
+
repository_revision TEXT,
|
|
478
|
+
repository_branch TEXT,
|
|
479
|
+
worktree_state TEXT NOT NULL DEFAULT 'unavailable'
|
|
480
|
+
CHECK (worktree_state IN ('clean', 'dirty', 'unborn', 'unavailable')),
|
|
481
|
+
worktree_digest TEXT,
|
|
482
|
+
worktree_files_json TEXT NOT NULL DEFAULT '[]',
|
|
483
|
+
worktree_accepted INTEGER NOT NULL DEFAULT 0 CHECK (worktree_accepted IN (0, 1)),
|
|
484
|
+
known_red_accepted INTEGER NOT NULL DEFAULT 0 CHECK (known_red_accepted IN (0, 1)),
|
|
485
|
+
spec_refs_json TEXT NOT NULL DEFAULT '[]',
|
|
486
|
+
evidence_json TEXT NOT NULL DEFAULT '[]',
|
|
487
|
+
verification_json TEXT NOT NULL DEFAULT '[]',
|
|
488
|
+
unknowns_json TEXT NOT NULL DEFAULT '[]',
|
|
489
|
+
gaps_json TEXT NOT NULL DEFAULT '[]',
|
|
490
|
+
classification_json TEXT NOT NULL DEFAULT '{}',
|
|
491
|
+
provider_refs_json TEXT NOT NULL DEFAULT '{}',
|
|
492
|
+
research_run_id TEXT,
|
|
493
|
+
approved_by TEXT,
|
|
494
|
+
approval_note TEXT,
|
|
495
|
+
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
496
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
497
|
+
converged_at TEXT
|
|
498
|
+
);
|
|
499
|
+
INSERT INTO baselines_git_upgrade (
|
|
500
|
+
id, project_name, project_type, stack, mode, status, repository_root, scope_json,
|
|
501
|
+
repository_revision, repository_branch, worktree_state, worktree_digest,
|
|
502
|
+
worktree_files_json, worktree_accepted, known_red_accepted, spec_refs_json,
|
|
503
|
+
evidence_json, verification_json, unknowns_json, gaps_json, classification_json,
|
|
504
|
+
provider_refs_json, research_run_id, approved_by, approval_note, started_at,
|
|
505
|
+
updated_at, converged_at
|
|
506
|
+
)
|
|
507
|
+
SELECT
|
|
508
|
+
id, project_name, project_type, stack, mode, status, repository_root, scope_json,
|
|
509
|
+
repository_revision, repository_branch, worktree_state, worktree_digest,
|
|
510
|
+
worktree_files_json, worktree_accepted, known_red_accepted, spec_refs_json,
|
|
511
|
+
evidence_json, verification_json, unknowns_json, gaps_json, classification_json,
|
|
512
|
+
provider_refs_json, research_run_id, approved_by, approval_note, started_at,
|
|
513
|
+
updated_at, converged_at
|
|
514
|
+
FROM baselines;
|
|
515
|
+
DROP TABLE baselines;
|
|
516
|
+
ALTER TABLE baselines_git_upgrade RENAME TO baselines;
|
|
517
|
+
CREATE INDEX baselines_status ON baselines(status, updated_at);
|
|
518
|
+
`);
|
|
519
|
+
const violations = db.pragma('foreign_key_check');
|
|
520
|
+
if (violations.length > 0) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
`baseline Git-state migration produced ${violations.length} foreign key violation(s)`,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
})();
|
|
526
|
+
} finally {
|
|
527
|
+
db.pragma(`foreign_keys = ${foreignKeys ? 'ON' : 'OFF'}`);
|
|
528
|
+
}
|
|
529
|
+
return previousVersion;
|
|
530
|
+
}
|
|
531
|
+
|
|
321
532
|
function upgradeRuntimeConstraints(db, fromVersion = latestSchemaVersion(db)) {
|
|
322
533
|
if (tableSupportsKimi(db, 'events') && tableSupportsKimi(db, 'sessions')) return false;
|
|
323
534
|
const previousVersion = fromVersion || 'unknown';
|
|
@@ -393,6 +604,13 @@ function upgradeRuntimeConstraints(db, fromVersion = latestSchemaVersion(db)) {
|
|
|
393
604
|
|
|
394
605
|
function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { legacyState = false } = {}) {
|
|
395
606
|
const runtimeChanged = upgradeRuntimeConstraints(db, fromVersion);
|
|
607
|
+
if (tableNames(db).includes('baselines') && !baselineSupportsUnbornGit(db)) {
|
|
608
|
+
db.transaction(() => {
|
|
609
|
+
applyBaselineUpgrade(db, { legacyState });
|
|
610
|
+
applyBaselineColumns(db);
|
|
611
|
+
})();
|
|
612
|
+
}
|
|
613
|
+
const gitStateChangedFrom = upgradeBaselineWorktreeConstraint(db, fromVersion);
|
|
396
614
|
db.transaction(() => {
|
|
397
615
|
applyCompatibleColumns(db);
|
|
398
616
|
const contextChanged = applyContextSpineUpgrade(db);
|
|
@@ -402,7 +620,8 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
402
620
|
const needsContextMigration = Boolean(
|
|
403
621
|
fromVersion && ![
|
|
404
622
|
CONTEXT_SCHEMA_VERSION, BASELINE_SCHEMA_VERSION, ADOPTION_SCHEMA_VERSION,
|
|
405
|
-
WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION,
|
|
623
|
+
WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION,
|
|
624
|
+
DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, EXPECTED_VERSION,
|
|
406
625
|
].includes(fromVersion)
|
|
407
626
|
&& !contextMigration,
|
|
408
627
|
);
|
|
@@ -426,7 +645,8 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
426
645
|
).get();
|
|
427
646
|
if (fromVersion && ![
|
|
428
647
|
BASELINE_SCHEMA_VERSION, ADOPTION_SCHEMA_VERSION, WORKFLOW_SCHEMA_VERSION,
|
|
429
|
-
AUTHORITY_SCHEMA_VERSION,
|
|
648
|
+
AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION,
|
|
649
|
+
GIT_AUTHORITY_SCHEMA_VERSION, EXPECTED_VERSION,
|
|
430
650
|
].includes(fromVersion)
|
|
431
651
|
&& !baselineMigration) {
|
|
432
652
|
db.prepare(
|
|
@@ -530,17 +750,64 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
530
750
|
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
531
751
|
).run(
|
|
532
752
|
fromVersion === AUTHORITY_SCHEMA_VERSION ? fromVersion : AUTHORITY_SCHEMA_VERSION,
|
|
533
|
-
|
|
753
|
+
SEMANTIC_SCHEMA_VERSION,
|
|
534
754
|
'Add typed research semantics, complete change contracts, verified learning application, and reconciliation provenance',
|
|
535
755
|
);
|
|
536
756
|
}
|
|
757
|
+
const dialogueMigration = db.prepare(
|
|
758
|
+
"SELECT 1 FROM migration_history WHERE to_version = '16.0' AND status = 'success' LIMIT 1",
|
|
759
|
+
).get();
|
|
760
|
+
if (fromVersion && ![DIALOGUE_SCHEMA_VERSION, EXPECTED_VERSION].includes(fromVersion)
|
|
761
|
+
&& !dialogueMigration) {
|
|
762
|
+
db.prepare(
|
|
763
|
+
`INSERT INTO migration_history
|
|
764
|
+
(from_version, to_version, direction, status, notes)
|
|
765
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
766
|
+
).run(
|
|
767
|
+
fromVersion === SEMANTIC_SCHEMA_VERSION ? fromVersion : SEMANTIC_SCHEMA_VERSION,
|
|
768
|
+
DIALOGUE_SCHEMA_VERSION,
|
|
769
|
+
'Add durable one-question decision dialogue, owner checkpoints, and workflow alignment gates',
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
const gitStateMigration = db.prepare(
|
|
773
|
+
"SELECT 1 FROM migration_history WHERE to_version = '17.0' AND status = 'success' LIMIT 1",
|
|
774
|
+
).get();
|
|
775
|
+
if (fromVersion && ![GIT_AUTHORITY_SCHEMA_VERSION, EXPECTED_VERSION].includes(fromVersion)
|
|
776
|
+
&& !gitStateMigration) {
|
|
777
|
+
db.prepare(
|
|
778
|
+
`INSERT INTO migration_history
|
|
779
|
+
(from_version, to_version, direction, status, notes)
|
|
780
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
781
|
+
).run(
|
|
782
|
+
gitStateChangedFrom || DIALOGUE_SCHEMA_VERSION,
|
|
783
|
+
GIT_AUTHORITY_SCHEMA_VERSION,
|
|
784
|
+
'Add explicit unborn Git authority and require an owner-authorized checkpoint before baseline recording',
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
const adaptiveChanged = migrateAdaptiveWorkflowRuns(db, fromVersion);
|
|
788
|
+
const adaptiveMigration = db.prepare(
|
|
789
|
+
"SELECT 1 FROM migration_history WHERE to_version = '18.0' AND status = 'success' LIMIT 1",
|
|
790
|
+
).get();
|
|
791
|
+
if (fromVersion && fromVersion !== EXPECTED_VERSION && !adaptiveMigration) {
|
|
792
|
+
db.prepare(
|
|
793
|
+
`INSERT INTO migration_history
|
|
794
|
+
(from_version, to_version, direction, status, notes)
|
|
795
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
796
|
+
).run(
|
|
797
|
+
fromVersion === GIT_AUTHORITY_SCHEMA_VERSION ? fromVersion : GIT_AUTHORITY_SCHEMA_VERSION,
|
|
798
|
+
EXPECTED_VERSION,
|
|
799
|
+
adaptiveChanged
|
|
800
|
+
? 'Migrate active rigid workflows to adaptive capability transitions and independent initialization'
|
|
801
|
+
: 'Add adaptive capability transitions and independent initialization authority',
|
|
802
|
+
);
|
|
803
|
+
}
|
|
537
804
|
db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
|
|
538
805
|
db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
|
|
539
806
|
db.prepare(
|
|
540
807
|
'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
|
|
541
808
|
).run(
|
|
542
809
|
EXPECTED_VERSION,
|
|
543
|
-
'
|
|
810
|
+
'Adaptive capability transitions, independent initialization, and recoverable workflow migration',
|
|
544
811
|
);
|
|
545
812
|
})();
|
|
546
813
|
}
|
|
@@ -568,7 +835,10 @@ function initStateDb(dbPath) {
|
|
|
568
835
|
const existing = new Set(tableNames(db));
|
|
569
836
|
const fromVersion = latestSchemaVersion(db);
|
|
570
837
|
const legacyState = existing.size > 0
|
|
571
|
-
&& ![
|
|
838
|
+
&& ![
|
|
839
|
+
WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION,
|
|
840
|
+
DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, EXPECTED_VERSION,
|
|
841
|
+
].includes(fromVersion);
|
|
572
842
|
backupPath = migrationBackup(db, dbPath, fromVersion, existing);
|
|
573
843
|
applyCompatibleColumns(db);
|
|
574
844
|
const missing = REQUIRED_TABLES.filter((t) => !existing.has(t));
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// here apply the BEGIN IMMEDIATE / retry / status-machine guards specified
|
|
9
9
|
// in PLAN §6 Phase 2.3 + docs/STATE-DB-ACCESS-POLICY.md.
|
|
10
10
|
|
|
11
|
+
const crypto = require('node:crypto');
|
|
11
12
|
const fs = require('node:fs');
|
|
12
13
|
const path = require('node:path');
|
|
13
14
|
const { spawnSync } = require('node:child_process');
|
|
@@ -42,6 +43,17 @@ const PATCHABLE_FIELDS = Object.freeze([
|
|
|
42
43
|
'change_id',
|
|
43
44
|
]);
|
|
44
45
|
|
|
46
|
+
const STALE_RECONCILIATION_FIELDS = Object.freeze([
|
|
47
|
+
'deps', 'files_modified', 'trace_to', 'outcome', 'slice_kind', 'public_seam',
|
|
48
|
+
'verification_command', 'acceptance', 'context_refs', 'docs_impact', 'ownership',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const TASK_CONTRACT_PATCH_FIELDS = new Set([
|
|
52
|
+
'priority', 'complexity', 'estimated_days', 'deps', 'files_modified', 'tag',
|
|
53
|
+
'trace_to', 'outcome', 'slice_kind', 'public_seam', 'verification_command',
|
|
54
|
+
'acceptance', 'context_refs', 'docs_impact', 'ownership',
|
|
55
|
+
]);
|
|
56
|
+
|
|
45
57
|
const SESSION_PATCHABLE = Object.freeze([
|
|
46
58
|
'pid', 'status', 'lease_expires_at', 'heartbeat_at', 'worktree_path', 'artifact_dir',
|
|
47
59
|
]);
|
|
@@ -227,6 +239,30 @@ function assertTaskExecutionContract(task) {
|
|
|
227
239
|
return task;
|
|
228
240
|
}
|
|
229
241
|
|
|
242
|
+
function currentChangeAuthorityDigest(db, changeId) {
|
|
243
|
+
if (!changeId) return null;
|
|
244
|
+
const row = db.prepare(
|
|
245
|
+
`SELECT id, kind, intent, docs_impact_json, provider_refs_json, contract_json,
|
|
246
|
+
classification_json, research_disposition_json
|
|
247
|
+
FROM changes WHERE id = ?`,
|
|
248
|
+
).get(changeId);
|
|
249
|
+
if (!row) return null;
|
|
250
|
+
const parse = (value) => {
|
|
251
|
+
try { return JSON.parse(value || '{}'); } catch { return {}; }
|
|
252
|
+
};
|
|
253
|
+
const payload = {
|
|
254
|
+
id: row.id,
|
|
255
|
+
kind: row.kind,
|
|
256
|
+
intent: row.intent,
|
|
257
|
+
docs_impact: parse(row.docs_impact_json),
|
|
258
|
+
provider_refs: parse(row.provider_refs_json),
|
|
259
|
+
contract: parse(row.contract_json),
|
|
260
|
+
classification: parse(row.classification_json),
|
|
261
|
+
research_disposition: parse(row.research_disposition_json),
|
|
262
|
+
};
|
|
263
|
+
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
264
|
+
}
|
|
265
|
+
|
|
230
266
|
function readTask(db, id) {
|
|
231
267
|
const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
232
268
|
return rowToTask(row);
|
|
@@ -373,6 +409,19 @@ function patchTask(db, id, patch = {}) {
|
|
|
373
409
|
// active execution authority. Ownership is durable once assigned: it may
|
|
374
410
|
// not be detached or moved to another change by a generic task patch.
|
|
375
411
|
assertChangeAcceptsTasks(db, current.change_id);
|
|
412
|
+
const clearingStale = current.stale === true && patch.stale === false;
|
|
413
|
+
if (clearingStale) {
|
|
414
|
+
const missing = STALE_RECONCILIATION_FIELDS.filter(
|
|
415
|
+
(field) => !Object.prototype.hasOwnProperty.call(patch, field),
|
|
416
|
+
);
|
|
417
|
+
if (missing.length > 0) {
|
|
418
|
+
throw new StateOpsError(
|
|
419
|
+
'TASK_STALE_RECONCILIATION_REQUIRED',
|
|
420
|
+
`task ${id} can clear stale only while rebinding its complete execution contract`,
|
|
421
|
+
{ details: { missing_fields: missing } },
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
376
425
|
const sets = [];
|
|
377
426
|
const params = [];
|
|
378
427
|
let nextStatus = null;
|
|
@@ -440,6 +489,8 @@ function patchTask(db, id, patch = {}) {
|
|
|
440
489
|
params.push(nowIso());
|
|
441
490
|
params.push(id);
|
|
442
491
|
db.prepare(`UPDATE tasks SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
492
|
+
const updated = readTask(db, id);
|
|
493
|
+
if (clearingStale) assertTaskExecutionContract(updated);
|
|
443
494
|
|
|
444
495
|
if (nextStatus && nextStatus !== current.status) {
|
|
445
496
|
appendEventInTx(db, {
|
|
@@ -449,7 +500,19 @@ function patchTask(db, id, patch = {}) {
|
|
|
449
500
|
payload: { from: current.status, to: nextStatus },
|
|
450
501
|
});
|
|
451
502
|
}
|
|
452
|
-
|
|
503
|
+
const contractFields = Object.keys(patch).filter((field) => TASK_CONTRACT_PATCH_FIELDS.has(field));
|
|
504
|
+
if (contractFields.length > 0) {
|
|
505
|
+
appendEventInTx(db, {
|
|
506
|
+
type: clearingStale ? 'task_contract_reconciled' : 'task_contract_updated',
|
|
507
|
+
task_id: id,
|
|
508
|
+
change_id: updated.change_id,
|
|
509
|
+
payload: {
|
|
510
|
+
fields: contractFields.sort(),
|
|
511
|
+
change_authority_digest: currentChangeAuthorityDigest(db, updated.change_id),
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return updated;
|
|
453
516
|
});
|
|
454
517
|
}
|
|
455
518
|
|