ultra-builder-pro-cli 0.24.1 → 0.25.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 +63 -0
- package/README.md +10 -0
- package/adapters/claude.js +104 -0
- package/adapters/codex.js +363 -40
- package/adapters/grok.js +1254 -58
- package/adapters/kimi.js +682 -13
- package/adapters/opencode.js +132 -11
- package/agents/code-reviewer.md +4 -5
- package/agents/debugger.md +8 -8
- package/docs/AGENT-CONTEXT.md +30 -4
- package/docs/ARCHITECTURE.md +24 -5
- package/docs/ARTIFACT-AUTHORITY.md +13 -0
- package/docs/COMMIT-HASH-BACKFILL.md +25 -18
- package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
- package/docs/STATE-DB-ACCESS-POLICY.md +8 -1
- package/mcp-server/lib/artifact-registry.cjs +188 -107
- package/mcp-server/lib/baseline-workflow.cjs +160 -23
- package/mcp-server/lib/change-packet.cjs +4 -32
- package/mcp-server/lib/change-workflow.cjs +36 -2755
- package/mcp-server/lib/context-envelope.cjs +124 -51
- package/mcp-server/lib/decision-records.cjs +164 -78
- package/mcp-server/lib/kernel-change-workflow.cjs +2029 -0
- package/mcp-server/lib/legacy-change-workflow.cjs +3032 -0
- package/mcp-server/lib/managed-file-write.cjs +56 -0
- package/mcp-server/lib/prd-parser.cjs +2 -2
- package/mcp-server/lib/stage-checkpoints.cjs +163 -9
- package/mcp-server/lib/state-db.cjs +186 -8
- package/mcp-server/lib/state-ops.cjs +214 -31
- package/mcp-server/lib/task-contract.cjs +404 -0
- package/mcp-server/lib/task-expander.cjs +8 -3
- package/mcp-server/lib/task-ledger.cjs +175 -62
- package/mcp-server/lib/ultra-facade.cjs +1895 -172
- package/mcp-server/lib/worker-packet.cjs +2 -2
- package/mcp-server/server.cjs +24 -5
- package/orchestrator/parallel-orchestrator.cjs +114 -27
- package/orchestrator/session-runner.cjs +25 -136
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +6 -1
- package/skills/ultra-dev/SKILL.md +9 -4
- package/skills/ultra-plan/SKILL.md +13 -4
- package/skills/ultra-plan/references/semantic-preflight.md +6 -4
- package/spec/mcp-tools.yaml +114 -9
- package/spec/schemas/context-file.v4.5.schema.json +2 -2
- package/spec/schemas/state-db.sql +10 -6
- package/spec/schemas/task-ledger.v2.schema.json +23 -8
- package/spec/schemas/tasks.v4.5.schema.json +34 -12
|
@@ -19,6 +19,22 @@ const GAP_CATEGORIES = new Set([
|
|
|
19
19
|
'technical_debt', 'unknown', 'future_change',
|
|
20
20
|
]);
|
|
21
21
|
const GAP_STATUSES = new Set(['open', 'accepted', 'resolved', 'deferred']);
|
|
22
|
+
const BASELINE_START_FIELDS = new Set([
|
|
23
|
+
'id', 'project_name', 'project_type', 'stack', 'mode', 'repository_revision',
|
|
24
|
+
'replace_migrated', 'replace_ready', 'replacement_authorization', 'scope',
|
|
25
|
+
'provider_refs', 'classification',
|
|
26
|
+
]);
|
|
27
|
+
const BASELINE_REFRESH_FIELDS = new Set([
|
|
28
|
+
'id', 'project_name', 'project_type', 'stack', 'classification',
|
|
29
|
+
]);
|
|
30
|
+
const BASELINE_RECORD_FIELDS = new Set([
|
|
31
|
+
'id', 'repository_revision', 'scope', 'spec_refs', 'evidence', 'verification',
|
|
32
|
+
'unknowns', 'gaps', 'classification', 'provider_refs',
|
|
33
|
+
]);
|
|
34
|
+
const BASELINE_CONVERGE_FIELDS = new Set([
|
|
35
|
+
'id', 'expected_revision', 'approved_by', 'approval_note',
|
|
36
|
+
'accept_dirty_worktree', 'accept_known_red',
|
|
37
|
+
]);
|
|
22
38
|
|
|
23
39
|
class BaselineWorkflowError extends Error {
|
|
24
40
|
constructor(code, message, details) {
|
|
@@ -199,6 +215,46 @@ function nonEmpty(value, field) {
|
|
|
199
215
|
return text;
|
|
200
216
|
}
|
|
201
217
|
|
|
218
|
+
function optionalText(value, field, { nullable = false, emptyAsNull = false } = {}) {
|
|
219
|
+
if (value === undefined) return undefined;
|
|
220
|
+
if (value === null && nullable) return null;
|
|
221
|
+
if (typeof value !== 'string') {
|
|
222
|
+
throw new BaselineWorkflowError(
|
|
223
|
+
'VALIDATION_ERROR',
|
|
224
|
+
`${field} must be a string${nullable ? ' or null' : ''}`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const text = value.trim();
|
|
228
|
+
return emptyAsNull && !text ? null : text;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function optionalBoolean(input, field) {
|
|
232
|
+
if (Object.hasOwn(input, field) && typeof input[field] !== 'boolean') {
|
|
233
|
+
throw new BaselineWorkflowError('VALIDATION_ERROR', `${field} must be a boolean`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function optionalBaselineId(value) {
|
|
238
|
+
if (value === undefined) return undefined;
|
|
239
|
+
const id = nonEmpty(value, 'id');
|
|
240
|
+
if (!BASELINE_ID.test(id)) {
|
|
241
|
+
throw new BaselineWorkflowError('VALIDATION_ERROR', 'baseline id is invalid');
|
|
242
|
+
}
|
|
243
|
+
return id;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function assertExactFields(value, allowed, field) {
|
|
247
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
248
|
+
throw new BaselineWorkflowError('VALIDATION_ERROR', `${field} must be an object`);
|
|
249
|
+
}
|
|
250
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
251
|
+
if (unknown.length > 0) {
|
|
252
|
+
throw new BaselineWorkflowError(
|
|
253
|
+
'VALIDATION_ERROR', `${field}.${unknown[0]} is not allowed`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
202
258
|
function normalizeReplacementAuthorization(value) {
|
|
203
259
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
204
260
|
throw new BaselineWorkflowError(
|
|
@@ -206,6 +262,13 @@ function normalizeReplacementAuthorization(value) {
|
|
|
206
262
|
'replacing a ready baseline requires approved_by and reason',
|
|
207
263
|
);
|
|
208
264
|
}
|
|
265
|
+
const unknown = Object.keys(value).filter((key) => !['approved_by', 'reason'].includes(key));
|
|
266
|
+
if (unknown.length > 0) {
|
|
267
|
+
throw new BaselineWorkflowError(
|
|
268
|
+
'VALIDATION_ERROR',
|
|
269
|
+
`replacement_authorization.${unknown[0]} is not allowed`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
209
272
|
let approvedBy;
|
|
210
273
|
let reason;
|
|
211
274
|
try {
|
|
@@ -245,13 +308,14 @@ function normalizeScope(value, rootDir) {
|
|
|
245
308
|
throw new BaselineWorkflowError('VALIDATION_ERROR', 'scope must be a non-empty array');
|
|
246
309
|
}
|
|
247
310
|
return [...new Set(scope.map((item, index) => {
|
|
311
|
+
const relative = nonEmpty(item, `scope[${index}]`);
|
|
248
312
|
const resolved = safeRelativePath(rootDir, item, `scope[${index}]`);
|
|
249
313
|
if (!fs.existsSync(resolved)) {
|
|
250
314
|
throw new BaselineWorkflowError(
|
|
251
|
-
'BASELINE_SCOPE_MISSING', `baseline scope does not exist: ${
|
|
315
|
+
'BASELINE_SCOPE_MISSING', `baseline scope does not exist: ${relative}`,
|
|
252
316
|
);
|
|
253
317
|
}
|
|
254
|
-
return
|
|
318
|
+
return relative;
|
|
255
319
|
}))];
|
|
256
320
|
}
|
|
257
321
|
|
|
@@ -291,6 +355,11 @@ function normalizeSpecRefs(value, rootDir) {
|
|
|
291
355
|
if (!item || typeof item !== 'object' || Array.isArray(item) || !SPEC_KINDS.has(item.kind)) {
|
|
292
356
|
throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid spec_refs[${index}]`);
|
|
293
357
|
}
|
|
358
|
+
assertExactFields(
|
|
359
|
+
item,
|
|
360
|
+
new Set(['kind', 'path', 'digest']),
|
|
361
|
+
`spec_refs[${index}]`,
|
|
362
|
+
);
|
|
294
363
|
const specPath = nonEmpty(item.path, `spec_refs[${index}].path`);
|
|
295
364
|
const file = safeRelativePath(rootDir, specPath, `spec_refs[${index}].path`);
|
|
296
365
|
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
|
@@ -306,6 +375,11 @@ function normalizeEvidence(value, rootDir) {
|
|
|
306
375
|
if (!item || typeof item !== 'object' || Array.isArray(item) || !EVIDENCE_KINDS.has(item.kind)) {
|
|
307
376
|
throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid evidence[${index}]`);
|
|
308
377
|
}
|
|
378
|
+
assertExactFields(
|
|
379
|
+
item,
|
|
380
|
+
new Set(['kind', 'ref', 'summary', 'digest']),
|
|
381
|
+
`evidence[${index}]`,
|
|
382
|
+
);
|
|
309
383
|
const normalized = {
|
|
310
384
|
kind: item.kind,
|
|
311
385
|
ref: nonEmpty(item.ref, `evidence[${index}].ref`),
|
|
@@ -330,6 +404,11 @@ function normalizeVerification(value) {
|
|
|
330
404
|
if (!item || typeof item !== 'object' || Array.isArray(item) || !VERIFICATION_STATUSES.has(item.status)) {
|
|
331
405
|
throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid verification[${index}]`);
|
|
332
406
|
}
|
|
407
|
+
assertExactFields(
|
|
408
|
+
item,
|
|
409
|
+
new Set(['name', 'command', 'status', 'evidence', 'rationale']),
|
|
410
|
+
`verification[${index}]`,
|
|
411
|
+
);
|
|
333
412
|
const normalized = {
|
|
334
413
|
name: nonEmpty(item.name, `verification[${index}].name`),
|
|
335
414
|
command: nonEmpty(item.command, `verification[${index}].command`),
|
|
@@ -347,6 +426,16 @@ function normalizeUnknowns(value) {
|
|
|
347
426
|
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
348
427
|
throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid unknowns[${index}]`);
|
|
349
428
|
}
|
|
429
|
+
assertExactFields(
|
|
430
|
+
item,
|
|
431
|
+
new Set(['summary', 'blocking', 'owner']),
|
|
432
|
+
`unknowns[${index}]`,
|
|
433
|
+
);
|
|
434
|
+
if (item.blocking !== undefined && typeof item.blocking !== 'boolean') {
|
|
435
|
+
throw new BaselineWorkflowError(
|
|
436
|
+
'VALIDATION_ERROR', `unknowns[${index}].blocking must be a boolean`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
350
439
|
const normalized = {
|
|
351
440
|
summary: nonEmpty(item.summary, `unknowns[${index}].summary`),
|
|
352
441
|
blocking: item.blocking === true,
|
|
@@ -363,6 +452,14 @@ function normalizeGaps(value) {
|
|
|
363
452
|
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
364
453
|
throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid gaps[${index}]`);
|
|
365
454
|
}
|
|
455
|
+
assertExactFields(
|
|
456
|
+
item,
|
|
457
|
+
new Set([
|
|
458
|
+
'id', 'category', 'status', 'blocking', 'summary',
|
|
459
|
+
'evidence_refs', 'owner', 'resolution',
|
|
460
|
+
]),
|
|
461
|
+
`gaps[${index}]`,
|
|
462
|
+
);
|
|
366
463
|
const id = nonEmpty(item.id, `gaps[${index}].id`);
|
|
367
464
|
if (!BASELINE_ID.test(id) || ids.has(id)) {
|
|
368
465
|
throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid or duplicate gap id: ${id}`);
|
|
@@ -371,7 +468,12 @@ function normalizeGaps(value) {
|
|
|
371
468
|
if (!GAP_CATEGORIES.has(item.category)) {
|
|
372
469
|
throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid gap category: ${item.category}`);
|
|
373
470
|
}
|
|
374
|
-
|
|
471
|
+
if (item.blocking !== undefined && typeof item.blocking !== 'boolean') {
|
|
472
|
+
throw new BaselineWorkflowError(
|
|
473
|
+
'VALIDATION_ERROR', `gaps[${index}].blocking must be a boolean`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
const status = item.status === undefined ? 'open' : item.status;
|
|
375
477
|
if (!GAP_STATUSES.has(status)) {
|
|
376
478
|
throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid gap status: ${status}`);
|
|
377
479
|
}
|
|
@@ -418,9 +520,20 @@ function summarizeGaps(gaps = []) {
|
|
|
418
520
|
}
|
|
419
521
|
|
|
420
522
|
function startBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = true } = {}) {
|
|
421
|
-
|
|
422
|
-
|
|
523
|
+
assertExactFields(input, BASELINE_START_FIELDS, 'baseline.start');
|
|
524
|
+
const id = optionalBaselineId(input.id) || 'project-baseline';
|
|
423
525
|
const projectName = nonEmpty(input.project_name, 'project_name');
|
|
526
|
+
const projectType = optionalText(
|
|
527
|
+
input.project_type, 'project_type', { nullable: true, emptyAsNull: true },
|
|
528
|
+
) ?? null;
|
|
529
|
+
const stack = optionalText(input.stack, 'stack', { nullable: true, emptyAsNull: true }) ?? null;
|
|
530
|
+
const repositoryRevision = optionalText(
|
|
531
|
+
input.repository_revision,
|
|
532
|
+
'repository_revision',
|
|
533
|
+
{ nullable: true, emptyAsNull: true },
|
|
534
|
+
) ?? null;
|
|
535
|
+
optionalBoolean(input, 'replace_migrated');
|
|
536
|
+
optionalBoolean(input, 'replace_ready');
|
|
424
537
|
if (!MODES.has(input.mode)) throw new BaselineWorkflowError('VALIDATION_ERROR', `invalid baseline mode: ${input.mode}`);
|
|
425
538
|
if (readBaseline(db, id)) throw new BaselineWorkflowError('DUPLICATE_BASELINE_ID', `baseline ${id} exists`);
|
|
426
539
|
const current = readBaseline(db);
|
|
@@ -464,7 +577,7 @@ function startBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = tr
|
|
|
464
577
|
const providers = providerRefs.normalizeProviderRefs(input.provider_refs, BaselineWorkflowError);
|
|
465
578
|
const classification = normalizeClassification(input.classification);
|
|
466
579
|
const snapshot = gitWorktreeSnapshot(rootDir, scope);
|
|
467
|
-
if (snapshot.head &&
|
|
580
|
+
if (snapshot.head && repositoryRevision && repositoryRevision !== snapshot.head) {
|
|
468
581
|
throw new BaselineWorkflowError(
|
|
469
582
|
'BASELINE_REVISION_MISMATCH', 'repository_revision does not match current Git HEAD',
|
|
470
583
|
);
|
|
@@ -483,8 +596,8 @@ function startBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = tr
|
|
|
483
596
|
worktree_files_json, classification_json, provider_refs_json, started_at, updated_at)
|
|
484
597
|
VALUES (?, ?, ?, ?, ?, ?, '.', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
485
598
|
).run(
|
|
486
|
-
id, projectName,
|
|
487
|
-
JSON.stringify(scope), snapshot.head ||
|
|
599
|
+
id, projectName, projectType, stack, input.mode, status,
|
|
600
|
+
JSON.stringify(scope), snapshot.head || repositoryRevision, snapshot.branch,
|
|
488
601
|
snapshot.state, snapshot.digest, JSON.stringify(snapshot.files), JSON.stringify(classification),
|
|
489
602
|
JSON.stringify(providers), ts, ts,
|
|
490
603
|
);
|
|
@@ -509,7 +622,9 @@ function startBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = tr
|
|
|
509
622
|
function refreshInProgressBaseline(db, input = {}, {
|
|
510
623
|
rootDir = process.cwd(), emitEvent = true,
|
|
511
624
|
} = {}) {
|
|
512
|
-
|
|
625
|
+
assertExactFields(input, BASELINE_REFRESH_FIELDS, 'baseline.refresh');
|
|
626
|
+
const id = optionalBaselineId(input.id);
|
|
627
|
+
const current = readBaseline(db, id);
|
|
513
628
|
if (!current) {
|
|
514
629
|
throw new BaselineWorkflowError('BASELINE_NOT_FOUND', `baseline ${input.id || '(current)'} not found`);
|
|
515
630
|
}
|
|
@@ -517,8 +632,12 @@ function refreshInProgressBaseline(db, input = {}, {
|
|
|
517
632
|
const projectName = input.project_name === undefined
|
|
518
633
|
? current.project_name
|
|
519
634
|
: nonEmpty(input.project_name, 'project_name');
|
|
520
|
-
const projectType = input.project_type === undefined
|
|
521
|
-
|
|
635
|
+
const projectType = input.project_type === undefined
|
|
636
|
+
? current.project_type
|
|
637
|
+
: optionalText(input.project_type, 'project_type', { nullable: true, emptyAsNull: true });
|
|
638
|
+
const stack = input.stack === undefined
|
|
639
|
+
? current.stack
|
|
640
|
+
: optionalText(input.stack, 'stack', { nullable: true, emptyAsNull: true });
|
|
522
641
|
const classification = input.classification === undefined
|
|
523
642
|
? current.classification
|
|
524
643
|
: normalizeClassification(input.classification);
|
|
@@ -533,7 +652,7 @@ function refreshInProgressBaseline(db, input = {}, {
|
|
|
533
652
|
worktree_digest = ?, worktree_files_json = ?, classification_json = ?,
|
|
534
653
|
updated_at = ? WHERE id = ?`,
|
|
535
654
|
).run(
|
|
536
|
-
projectName, projectType
|
|
655
|
+
projectName, projectType, stack,
|
|
537
656
|
repositoryRevision, snapshot.branch, snapshot.state, snapshot.digest,
|
|
538
657
|
JSON.stringify(snapshot.files), JSON.stringify(classification), ts, current.id,
|
|
539
658
|
);
|
|
@@ -543,8 +662,8 @@ function refreshInProgressBaseline(db, input = {}, {
|
|
|
543
662
|
payload: {
|
|
544
663
|
baseline_id: current.id,
|
|
545
664
|
project_name: projectName,
|
|
546
|
-
project_type: projectType
|
|
547
|
-
stack
|
|
665
|
+
project_type: projectType,
|
|
666
|
+
stack,
|
|
548
667
|
repository_revision: repositoryRevision,
|
|
549
668
|
repository_branch: snapshot.branch,
|
|
550
669
|
worktree_state: snapshot.state,
|
|
@@ -556,7 +675,14 @@ function refreshInProgressBaseline(db, input = {}, {
|
|
|
556
675
|
}
|
|
557
676
|
|
|
558
677
|
function recordBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = true } = {}) {
|
|
559
|
-
|
|
678
|
+
assertExactFields(input, BASELINE_RECORD_FIELDS, 'baseline.record');
|
|
679
|
+
const id = optionalBaselineId(input.id);
|
|
680
|
+
const suppliedRevision = optionalText(
|
|
681
|
+
input.repository_revision,
|
|
682
|
+
'repository_revision',
|
|
683
|
+
{ nullable: true, emptyAsNull: true },
|
|
684
|
+
);
|
|
685
|
+
const current = readBaseline(db, id);
|
|
560
686
|
if (!current) throw new BaselineWorkflowError('BASELINE_NOT_FOUND', `baseline ${input.id} not found`);
|
|
561
687
|
if (['ready', 'superseded'].includes(current.status)) {
|
|
562
688
|
throw new BaselineWorkflowError('BASELINE_NOT_MUTABLE', `baseline ${current.id} is ${current.status}`);
|
|
@@ -581,14 +707,14 @@ function recordBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = t
|
|
|
581
707
|
'Git is initialized but has no commit; create an owner-authorized local checkpoint commit before recording the baseline',
|
|
582
708
|
);
|
|
583
709
|
}
|
|
584
|
-
if (snapshot.head &&
|
|
710
|
+
if (snapshot.head && suppliedRevision && suppliedRevision !== snapshot.head) {
|
|
585
711
|
throw new BaselineWorkflowError(
|
|
586
712
|
'BASELINE_REVISION_MISMATCH', 'repository_revision does not match current Git HEAD',
|
|
587
713
|
);
|
|
588
714
|
}
|
|
589
|
-
const revision = snapshot.head || (
|
|
715
|
+
const revision = snapshot.head || (suppliedRevision === undefined
|
|
590
716
|
? (current.repository_revision || workspaceRevision({ scope, specs, evidence }, rootDir))
|
|
591
|
-
:
|
|
717
|
+
: (suppliedRevision || workspaceRevision({ scope, specs, evidence }, rootDir)));
|
|
592
718
|
const status = current.mode === 'brownfield' ? 'adopting' : 'draft';
|
|
593
719
|
const ts = nowIso();
|
|
594
720
|
return ops.tx(db, () => {
|
|
@@ -823,12 +949,22 @@ function convergenceWarnings(db, baseline) {
|
|
|
823
949
|
}
|
|
824
950
|
|
|
825
951
|
function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = true } = {}) {
|
|
826
|
-
|
|
952
|
+
assertExactFields(input, BASELINE_CONVERGE_FIELDS, 'baseline.converge');
|
|
953
|
+
const normalizedInput = {
|
|
954
|
+
...input,
|
|
955
|
+
id: optionalBaselineId(input.id),
|
|
956
|
+
expected_revision: optionalText(input.expected_revision, 'expected_revision'),
|
|
957
|
+
approved_by: optionalText(input.approved_by, 'approved_by'),
|
|
958
|
+
approval_note: optionalText(input.approval_note, 'approval_note'),
|
|
959
|
+
};
|
|
960
|
+
optionalBoolean(input, 'accept_dirty_worktree');
|
|
961
|
+
optionalBoolean(input, 'accept_known_red');
|
|
962
|
+
const current = readBaseline(db, normalizedInput.id);
|
|
827
963
|
if (!current) throw new BaselineWorkflowError('BASELINE_NOT_FOUND', `baseline ${input.id} not found`);
|
|
828
964
|
if (!['draft', 'adopting', 'blocked'].includes(current.status)) {
|
|
829
965
|
throw new BaselineWorkflowError('BASELINE_NOT_CONVERGEABLE', `baseline ${current.id} is ${current.status}`);
|
|
830
966
|
}
|
|
831
|
-
const blockers = convergenceBlockers(db, current,
|
|
967
|
+
const blockers = convergenceBlockers(db, current, normalizedInput, rootDir);
|
|
832
968
|
const warnings = convergenceWarnings(db, current);
|
|
833
969
|
const ts = nowIso();
|
|
834
970
|
if (blockers.length > 0) {
|
|
@@ -857,9 +993,10 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
|
|
|
857
993
|
research_run_id = NULL,
|
|
858
994
|
converged_at = ?, updated_at = ? WHERE id = ?`,
|
|
859
995
|
).run(
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
996
|
+
normalizedInput.approved_by, normalizedInput.approval_note,
|
|
997
|
+
normalizedInput.accept_dirty_worktree === true ? 1 : 0,
|
|
998
|
+
normalizedInput.accept_known_red === true ? 1 : 0,
|
|
999
|
+
research?.id || null, ts, ts, current.id,
|
|
863
1000
|
);
|
|
864
1001
|
if (emitEvent) {
|
|
865
1002
|
ops.appendEventInTx(db, {
|
|
@@ -77,25 +77,8 @@ function requireInside(rootDir, relative, prefix, field) {
|
|
|
77
77
|
return resolved;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
`SELECT id FROM workflow_runs
|
|
83
|
-
WHERE change_id = ?
|
|
84
|
-
ORDER BY CASE
|
|
85
|
-
WHEN status IN ('active', 'blocked', 'ready') THEN 0
|
|
86
|
-
WHEN status = 'completed' THEN 1
|
|
87
|
-
ELSE 2 END,
|
|
88
|
-
CASE kind WHEN 'deliver' THEN 0 WHEN 'plan' THEN 1 WHEN 'research' THEN 2 ELSE 3 END,
|
|
89
|
-
updated_at DESC, rowid DESC
|
|
90
|
-
LIMIT 1`,
|
|
91
|
-
).get(changeId);
|
|
92
|
-
if (!row) {
|
|
93
|
-
throw new ChangePacketError(
|
|
94
|
-
'ARTIFACT_CONSUMER_MISSING',
|
|
95
|
-
`change ${changeId} has no workflow consumer`,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
return { type: 'workflow', id: row.id, relation: 'consumed_by' };
|
|
80
|
+
function deliveryConsumer() {
|
|
81
|
+
return { type: 'external', id: 'ultra-deliver', relation: 'consumed_by' };
|
|
99
82
|
}
|
|
100
83
|
|
|
101
84
|
function readCurrentArtifact(db, changeId, kind) {
|
|
@@ -118,17 +101,6 @@ function exactBaselineAnchor(db, change, input) {
|
|
|
118
101
|
'typed Change delta requires one approved ready baseline',
|
|
119
102
|
);
|
|
120
103
|
}
|
|
121
|
-
const changeRun = db.prepare(
|
|
122
|
-
`SELECT baseline_id FROM workflow_runs
|
|
123
|
-
WHERE kind = 'change' AND change_id = ?
|
|
124
|
-
ORDER BY started_at DESC, rowid DESC LIMIT 1`,
|
|
125
|
-
).get(change.id);
|
|
126
|
-
if (!changeRun || changeRun.baseline_id !== baseline.id) {
|
|
127
|
-
throw new ChangePacketError(
|
|
128
|
-
'CHANGE_BASELINE_AUTHORITY_MISMATCH',
|
|
129
|
-
`change ${change.id} is not bound to baseline ${baseline.id}`,
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
104
|
const anchor = input.baseline_anchor;
|
|
133
105
|
if (!anchor || anchor.baseline_id !== baseline.id
|
|
134
106
|
|| (anchor.repository_revision || null) !== (baseline.repository_revision || null)) {
|
|
@@ -371,7 +343,7 @@ function recordDelta(db, change, input, { rootDir = process.cwd() } = {}) {
|
|
|
371
343
|
let rollbackProgress = null;
|
|
372
344
|
try {
|
|
373
345
|
const result = ops.tx(db, () => {
|
|
374
|
-
const consumer =
|
|
346
|
+
const consumer = deliveryConsumer();
|
|
375
347
|
const payloads = delta.mutations.map((mutation) => (
|
|
376
348
|
artifacts.recordArtifactInTx(db, {
|
|
377
349
|
id: `change-${change.id}-delta-${mutation.id}`,
|
|
@@ -578,7 +550,7 @@ function recordDocumentationReconciliation(
|
|
|
578
550
|
let rollbackProgress = null;
|
|
579
551
|
try {
|
|
580
552
|
return ops.tx(db, () => {
|
|
581
|
-
const fallbackConsumer =
|
|
553
|
+
const fallbackConsumer = deliveryConsumer();
|
|
582
554
|
const payloads = normalized.reconciliation.documents.map((document, index) => (
|
|
583
555
|
artifacts.recordArtifactInTx(db, {
|
|
584
556
|
id: `change-${change.id}-documentation-${index + 1}`,
|