ultra-builder-pro-cli 0.24.2 → 0.25.1
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 +51 -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/docs/AGENT-CONTEXT.md +14 -0
- package/docs/ARTIFACT-AUTHORITY.md +13 -0
- package/docs/COMMIT-HASH-BACKFILL.md +25 -18
- package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
- package/mcp-server/lib/artifact-registry.cjs +181 -51
- package/mcp-server/lib/baseline-workflow.cjs +160 -23
- package/mcp-server/lib/change-workflow.cjs +21 -16
- 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 +294 -71
- package/mcp-server/lib/managed-file-write.cjs +56 -0
- package/mcp-server/lib/prd-parser.cjs +2 -2
- package/mcp-server/lib/runtime-paths.cjs +43 -16
- 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 +2 -2
- package/mcp-server/lib/task-ledger.cjs +175 -62
- package/mcp-server/lib/ultra-facade.cjs +1786 -169
- package/mcp-server/lib/worker-packet.cjs +2 -2
- package/mcp-server/server.cjs +23 -4
- 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, {
|
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const baselines = require('./baseline-workflow.cjs');
|
|
4
|
-
const
|
|
4
|
+
const kernel = require('./kernel-change-workflow.cjs');
|
|
5
5
|
|
|
6
6
|
function createChange(db, input, options = {}) {
|
|
7
|
-
return
|
|
7
|
+
return kernel.createChange(db, input, options);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
function updateChange(db, id, patch = {}, options = {}) {
|
|
11
|
-
return
|
|
11
|
+
return kernel.updateChange(db, id, patch, options);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function supersedeChange(db, input = {}, options = {}) {
|
|
15
|
+
return kernel.supersedeChange(db, input, options);
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
function assertTaskCreationAllowed(db, input = {}, {
|
|
15
19
|
rootDir = process.cwd(),
|
|
16
20
|
} = {}) {
|
|
17
|
-
const change = input.change_id ?
|
|
21
|
+
const change = input.change_id ? kernel.readChange(db, input.change_id) : null;
|
|
18
22
|
if (input.change_id && !change) {
|
|
19
|
-
throw new
|
|
23
|
+
throw new kernel.ChangeWorkflowError(
|
|
20
24
|
'CHANGE_NOT_FOUND',
|
|
21
25
|
`change ${input.change_id} not found`,
|
|
22
26
|
);
|
|
23
27
|
}
|
|
24
28
|
if (change && !['active', 'blocked'].includes(change.status)) {
|
|
25
|
-
throw new
|
|
29
|
+
throw new kernel.ChangeWorkflowError(
|
|
26
30
|
'CHANGE_NOT_MUTABLE',
|
|
27
31
|
`change ${change.id} is ${change.status}`,
|
|
28
32
|
);
|
|
@@ -40,21 +44,22 @@ function assertTaskCreationAllowed(db, input = {}, {
|
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
function archiveChange(db, input, options = {}) {
|
|
43
|
-
return
|
|
47
|
+
return kernel.archiveChange(db, input, options);
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
module.exports = {
|
|
47
|
-
ChangeWorkflowError:
|
|
51
|
+
ChangeWorkflowError: kernel.ChangeWorkflowError,
|
|
48
52
|
createChange,
|
|
49
|
-
readChange:
|
|
50
|
-
listChanges:
|
|
53
|
+
readChange: kernel.readChange,
|
|
54
|
+
listChanges: kernel.listChanges,
|
|
51
55
|
updateChange,
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
supersedeChange,
|
|
57
|
+
recordDelta: kernel.recordDelta,
|
|
58
|
+
recordDocumentationReconciliation: kernel.recordDocumentationReconciliation,
|
|
54
59
|
archiveChange,
|
|
55
|
-
normalizeDocsImpact:
|
|
56
|
-
normalizeProviderRefs:
|
|
57
|
-
normalizeBaselineBypass:
|
|
60
|
+
normalizeDocsImpact: kernel.normalizeDocsImpact,
|
|
61
|
+
normalizeProviderRefs: kernel.normalizeProviderRefs,
|
|
62
|
+
normalizeBaselineBypass: kernel.normalizeBaselineBypass,
|
|
58
63
|
assertTaskCreationAllowed,
|
|
59
|
-
recoverInterruptedArchives:
|
|
64
|
+
recoverInterruptedArchives: kernel.recoverInterruptedArchives,
|
|
60
65
|
};
|
|
@@ -8,7 +8,11 @@ const baselines = require('./baseline-workflow.cjs');
|
|
|
8
8
|
const canonical = require('./canonical-json.cjs');
|
|
9
9
|
const changes = require('./change-workflow.cjs');
|
|
10
10
|
const decisions = require('./decision-records.cjs');
|
|
11
|
-
const {
|
|
11
|
+
const {
|
|
12
|
+
restoreManagedFile,
|
|
13
|
+
snapshotManagedFile,
|
|
14
|
+
writeManagedJson,
|
|
15
|
+
} = require('./managed-file-write.cjs');
|
|
12
16
|
const ops = require('./state-ops.cjs');
|
|
13
17
|
const { readStableProjectFile } = require('./safe-project-file.cjs');
|
|
14
18
|
const checkpoints = require('./stage-checkpoints.cjs');
|
|
@@ -70,8 +74,33 @@ function selectedTask(db, taskId) {
|
|
|
70
74
|
function resolvedScope(db, input = {}) {
|
|
71
75
|
const requested = input.scope || {};
|
|
72
76
|
const task = selectedTask(db, requested.task_id);
|
|
77
|
+
if (requested.task_id && !task) {
|
|
78
|
+
throw new ContextEnvelopeError(
|
|
79
|
+
'CONTEXT_SCOPE_NOT_FOUND',
|
|
80
|
+
`Context task scope does not exist: ${requested.task_id}`,
|
|
81
|
+
{ scope_type: 'task', scope_id: requested.task_id },
|
|
82
|
+
);
|
|
83
|
+
}
|
|
73
84
|
const changeId = requested.change_id || task?.change_id || null;
|
|
74
85
|
const change = selectedChange(db, changeId);
|
|
86
|
+
if (requested.change_id && !change) {
|
|
87
|
+
throw new ContextEnvelopeError(
|
|
88
|
+
'CONTEXT_SCOPE_NOT_FOUND',
|
|
89
|
+
`Context Change scope does not exist: ${requested.change_id}`,
|
|
90
|
+
{ scope_type: 'change', scope_id: requested.change_id },
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (task && requested.change_id && task.change_id !== requested.change_id) {
|
|
94
|
+
throw new ContextEnvelopeError(
|
|
95
|
+
'CONTEXT_SCOPE_MISMATCH',
|
|
96
|
+
`Context task ${task.id} belongs to Change ${task.change_id || '(none)'}, not ${requested.change_id}`,
|
|
97
|
+
{
|
|
98
|
+
task_id: task.id,
|
|
99
|
+
task_change_id: task.change_id || null,
|
|
100
|
+
requested_change_id: requested.change_id,
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
}
|
|
75
104
|
const baseline = baselines.readBaseline(db);
|
|
76
105
|
if (task) return { type: 'task', id: task.id, baseline, change, task };
|
|
77
106
|
if (change) return { type: 'change', id: change.id, baseline, change, task: null };
|
|
@@ -254,6 +283,31 @@ function authorityPayload(db, input, options) {
|
|
|
254
283
|
item.diagnostics.filter((diagnostic) => diagnostic.severity === 'hard_conflict')
|
|
255
284
|
)),
|
|
256
285
|
};
|
|
286
|
+
if (scope.task) {
|
|
287
|
+
diagnostics.warnings.push(...ops.taskContractBlockers(scope.task).map((code) => ({
|
|
288
|
+
code,
|
|
289
|
+
severity: 'warning',
|
|
290
|
+
message: 'Task semantics are incomplete; the active Skill and model own the decision to proceed.',
|
|
291
|
+
})));
|
|
292
|
+
}
|
|
293
|
+
if (scope.change) {
|
|
294
|
+
const advisory = [
|
|
295
|
+
[!String(scope.change.contract?.outcome || '').trim(), 'CHANGE_OUTCOME_UNRESOLVED'],
|
|
296
|
+
[!Array.isArray(scope.change.contract?.acceptance)
|
|
297
|
+
|| scope.change.contract.acceptance.length === 0, 'CHANGE_ACCEPTANCE_UNRESOLVED'],
|
|
298
|
+
[!scope.change.contract?.recovery, 'CHANGE_RECOVERY_UNRESOLVED'],
|
|
299
|
+
[!String(scope.change.classification?.rationale || '').trim(), 'CHANGE_CLASSIFICATION_UNRESOLVED'],
|
|
300
|
+
[scope.change.research_disposition?.status === 'unresolved',
|
|
301
|
+
'CHANGE_RESEARCH_DISPOSITION_UNRESOLVED'],
|
|
302
|
+
];
|
|
303
|
+
diagnostics.warnings.push(...advisory
|
|
304
|
+
.filter(([missing]) => missing)
|
|
305
|
+
.map(([, code]) => ({
|
|
306
|
+
code,
|
|
307
|
+
severity: 'warning',
|
|
308
|
+
message: 'Change semantics remain mutable; the active Skill and model own completion.',
|
|
309
|
+
})));
|
|
310
|
+
}
|
|
257
311
|
for (const code of baselineHealth.blockers || []) {
|
|
258
312
|
diagnostics.needs_attention.push({ code, severity: 'needs_attention' });
|
|
259
313
|
}
|
|
@@ -435,58 +489,77 @@ function persistEnvelope(db, input = {}, options = {}) {
|
|
|
435
489
|
const scope = resolvedScope(db, input);
|
|
436
490
|
const id = `context-${full.digest.slice(0, 24)}`;
|
|
437
491
|
const artifactPath = contextArtifactPath(scope, id);
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
492
|
+
const rootDir = options.rootDir || process.cwd();
|
|
493
|
+
const snapshot = snapshotManagedFile(rootDir, artifactPath);
|
|
494
|
+
const published = writeManagedJson(rootDir, artifactPath, full);
|
|
495
|
+
try {
|
|
496
|
+
ops.tx(db, () => {
|
|
497
|
+
db.prepare(
|
|
498
|
+
`INSERT INTO context_envelopes
|
|
499
|
+
(id, stage, scope_type, scope_id, digest, file_digest, payload_json, artifact_path)
|
|
500
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
501
|
+
).run(
|
|
502
|
+
id,
|
|
503
|
+
input.stage || 'project',
|
|
504
|
+
scope.type,
|
|
505
|
+
scope.id,
|
|
506
|
+
full.digest,
|
|
507
|
+
published.digest,
|
|
508
|
+
JSON.stringify(full.envelope),
|
|
509
|
+
artifactPath,
|
|
510
|
+
);
|
|
511
|
+
if (!artifactPath.startsWith('.ultra/.runtime/')) {
|
|
512
|
+
artifactRegistry.recordArtifactInTx(db, {
|
|
513
|
+
id: `artifact-${id}`,
|
|
514
|
+
owner_type: scope.change ? 'change' : (scope.baseline ? 'baseline' : 'project'),
|
|
515
|
+
owner_id: scope.change?.id || scope.baseline?.id || 'project',
|
|
516
|
+
kind: 'context_envelope',
|
|
517
|
+
path: artifactPath,
|
|
518
|
+
content_digest: published.digest,
|
|
519
|
+
source_refs: [
|
|
520
|
+
...(scope.change
|
|
521
|
+
? [{ type: 'change', id: scope.change.id, relation: 'compiled_from' }]
|
|
522
|
+
: []),
|
|
523
|
+
...(scope.task
|
|
524
|
+
? [{ type: 'task', id: scope.task.id, relation: 'compiled_from_task_contract' }]
|
|
525
|
+
: []),
|
|
526
|
+
...(scope.baseline && !scope.change
|
|
527
|
+
? [{ type: 'baseline', id: scope.baseline.id, relation: 'compiled_from' }]
|
|
528
|
+
: []),
|
|
529
|
+
],
|
|
530
|
+
consumer_refs: [{
|
|
531
|
+
type: 'external',
|
|
532
|
+
id: 'ultra-context-consumer',
|
|
533
|
+
relation: 'consumed_by',
|
|
534
|
+
}],
|
|
535
|
+
provenance: {
|
|
536
|
+
writer: 'context-envelope',
|
|
537
|
+
envelope_digest: full.digest,
|
|
538
|
+
stage: input.stage || 'project',
|
|
539
|
+
},
|
|
540
|
+
metadata: {
|
|
541
|
+
envelope_id: id,
|
|
542
|
+
semantic_digest: full.digest,
|
|
543
|
+
},
|
|
544
|
+
}, { rootDir });
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
} catch (error) {
|
|
548
|
+
try {
|
|
549
|
+
restoreManagedFile(rootDir, snapshot);
|
|
550
|
+
} catch (rollbackError) {
|
|
551
|
+
throw new ContextEnvelopeError(
|
|
552
|
+
'STATE_CORRUPT',
|
|
553
|
+
'Context Envelope publication failed and its managed file could not be restored',
|
|
554
|
+
{
|
|
555
|
+
cause: error.code || error.message,
|
|
556
|
+
rollback: rollbackError.code || rollbackError.message,
|
|
557
|
+
path: artifactPath,
|
|
486
558
|
},
|
|
487
|
-
|
|
559
|
+
);
|
|
488
560
|
}
|
|
489
|
-
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
490
563
|
return {
|
|
491
564
|
id,
|
|
492
565
|
digest: full.digest,
|