ultra-builder-pro-cli 0.16.0 → 0.17.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/.ultra-template/reports/templates/delivery-report.json +17 -0
- package/.ultra-template/reports/templates/test-report.json +18 -0
- package/.ultra-template/specs/research-distillate.md +4 -3
- package/CHANGELOG.md +95 -0
- package/README.md +54 -24
- package/adapters/_shared/codex-assets.cjs +10 -5
- package/adapters/_shared/handbook.cjs +1 -1
- 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/docs/AGENT-CONTEXT.md +11 -8
- package/docs/ARCHITECTURE.md +67 -9
- package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
- package/docs/PLAN.zh-CN.md +19 -13
- package/docs/RUNTIME-COMPAT-MATRIX.md +2 -2
- package/docs/WORKFLOW-LIFECYCLE.md +123 -59
- package/hooks/pre_stop_check.py +3 -2
- package/mcp-server/lib/baseline-workflow.cjs +84 -16
- package/mcp-server/lib/change-workflow.cjs +55 -35
- package/mcp-server/lib/context-spine.cjs +171 -105
- 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 +14 -14
- package/mcp-server/lib/state-db.cjs +257 -10
- package/mcp-server/lib/state-ops.cjs +64 -1
- package/mcp-server/lib/workflow-state.cjs +300 -205
- package/mcp-server/server.cjs +58 -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 -94
- package/skills/ultra-deliver/SKILL.md +37 -65
- package/skills/ultra-dev/SKILL.md +50 -52
- package/skills/ultra-doctor/SKILL.md +34 -49
- package/skills/ultra-init/SKILL.md +78 -105
- package/skills/ultra-plan/SKILL.md +78 -99
- package/skills/ultra-research/SKILL.md +99 -113
- package/skills/ultra-review/SKILL.md +38 -53
- 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 -44
- package/skills/ultra-test/SKILL.md +61 -54
- package/skills/ultra-think/SKILL.md +37 -51
- package/skills/ultra-think/references/decision-dialogue.md +87 -111
- package/spec/cli-protocol.md +35 -0
- package/spec/mcp-tools.yaml +145 -28
- package/spec/schemas/delivery-report.v1.schema.json +1 -46
- package/spec/schemas/state-db.sql +8 -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 +19 -14
- package/ultra-tools/commands/system.cjs +2 -2
- package/ultra-tools/commands/task.cjs +3 -0
|
@@ -52,6 +52,10 @@ function parseJson(value, fallback = {}) {
|
|
|
52
52
|
try { return JSON.parse(value); } catch { return fallback; }
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function uniqueStrings(values) {
|
|
56
|
+
return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))];
|
|
57
|
+
}
|
|
58
|
+
|
|
55
59
|
function hasIncidentBreakGlass(change) {
|
|
56
60
|
const bypass = change?.baseline_bypass
|
|
57
61
|
|| parseJson(change?.baseline_bypass_json, null);
|
|
@@ -128,6 +132,22 @@ function taskStateDigest(task) {
|
|
|
128
132
|
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
function changeStateDigest(change) {
|
|
136
|
+
if (!change) return null;
|
|
137
|
+
const payload = {
|
|
138
|
+
id: change.id,
|
|
139
|
+
kind: change.kind,
|
|
140
|
+
intent: change.intent,
|
|
141
|
+
docs_impact: change.docs_impact || parseJson(change.docs_impact_json, {}),
|
|
142
|
+
provider_refs: change.provider_refs || parseJson(change.provider_refs_json, {}),
|
|
143
|
+
contract: change.contract || parseJson(change.contract_json, {}),
|
|
144
|
+
classification: change.classification || parseJson(change.classification_json, {}),
|
|
145
|
+
research_disposition: change.research_disposition
|
|
146
|
+
|| parseJson(change.research_disposition_json, {}),
|
|
147
|
+
};
|
|
148
|
+
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
149
|
+
}
|
|
150
|
+
|
|
131
151
|
function nonEmpty(value, field) {
|
|
132
152
|
const text = typeof value === 'string' ? value.trim() : '';
|
|
133
153
|
if (!text) throw new ContextSpineError('VALIDATION_ERROR', `${field} must be a non-empty string`);
|
|
@@ -291,27 +311,106 @@ function defaultGate(role) {
|
|
|
291
311
|
}[role];
|
|
292
312
|
}
|
|
293
313
|
|
|
294
|
-
function
|
|
314
|
+
function deriveTransitions({
|
|
315
|
+
change,
|
|
316
|
+
tasks = [],
|
|
317
|
+
task,
|
|
318
|
+
role,
|
|
319
|
+
readiness,
|
|
320
|
+
blockers = [],
|
|
321
|
+
activeWorkflow = null,
|
|
322
|
+
deliveryReady = false,
|
|
323
|
+
}) {
|
|
324
|
+
const unique = (values) => [...new Set(values.filter(Boolean))];
|
|
325
|
+
const has = (prefix) => blockers.some((blocker) => blocker === prefix || blocker.startsWith(`${prefix}:`));
|
|
326
|
+
if (has('STATE_DB_UNREADABLE') || has('STATE_SCHEMA_INCOMPLETE') || has('STATE_AUTHORITY_CONFLICT')) {
|
|
327
|
+
return {
|
|
328
|
+
allowed_transitions: ['ultra-doctor', 'ultra-status'],
|
|
329
|
+
required_transition: 'ultra-doctor',
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (has('STATE_DB_MISSING') || has('BASELINE_MISSING')) {
|
|
333
|
+
return {
|
|
334
|
+
allowed_transitions: ['ultra-init', 'ultra-status'],
|
|
335
|
+
required_transition: 'ultra-init',
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (blockers.some((blocker) => blocker.startsWith('DECISION_'))) {
|
|
339
|
+
return {
|
|
340
|
+
allowed_transitions: ['ultra-think', 'ultra-status'],
|
|
341
|
+
required_transition: 'ultra-think',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (blockers.some((blocker) => blocker.startsWith('BASELINE_SCHEMA_')
|
|
345
|
+
|| blocker.startsWith('STATE_SCHEMA_MIGRATION_'))) {
|
|
346
|
+
return {
|
|
347
|
+
allowed_transitions: ['ultra-doctor', 'ultra-init', 'ultra-status'],
|
|
348
|
+
required_transition: 'ultra-doctor',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const transitions = [];
|
|
353
|
+
if (activeWorkflow) transitions.push(`ultra-${activeWorkflow.kind}`);
|
|
354
|
+
if (!change) {
|
|
355
|
+
transitions.push('ultra-research', 'ultra-change', 'ultra-think', 'ultra-status', 'ultra-doctor');
|
|
356
|
+
return { allowed_transitions: unique(transitions), required_transition: null };
|
|
357
|
+
}
|
|
295
358
|
if (readiness === 'blocked') {
|
|
359
|
+
if (blockers.some((blocker) => blocker.startsWith('CONTEXT_')
|
|
360
|
+
|| blocker.startsWith('EXECUTION_'))) {
|
|
361
|
+
transitions.push('change.context');
|
|
362
|
+
}
|
|
296
363
|
if (blockers.some((blocker) => blocker.startsWith('BASELINE_'))) {
|
|
297
|
-
|
|
364
|
+
transitions.push('ultra-research', 'ultra-doctor');
|
|
298
365
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (change.status === '
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
366
|
+
transitions.push('ultra-change', 'ultra-think', 'ultra-status');
|
|
367
|
+
return { allowed_transitions: unique(transitions), required_transition: null };
|
|
368
|
+
}
|
|
369
|
+
if (change.status === 'ready') {
|
|
370
|
+
transitions.push('ultra-deliver', 'ultra-review', 'ultra-test');
|
|
371
|
+
} else if (change.status === 'blocked') {
|
|
372
|
+
transitions.push('ultra-think', 'ultra-change');
|
|
373
|
+
} else if (!task && tasks.length === 0) {
|
|
374
|
+
transitions.push('ultra-plan', 'ultra-research', 'ultra-change');
|
|
375
|
+
} else if (task?.status === 'pending' || task?.status === 'in_progress') {
|
|
376
|
+
transitions.push('ultra-dev', 'ultra-plan');
|
|
377
|
+
if (role === 'review') transitions.push('ultra-review');
|
|
378
|
+
} else if (tasks.length > 0 && tasks.every((row) => ['completed', 'expanded'].includes(row.status))) {
|
|
379
|
+
transitions.push('ultra-test', 'ultra-review');
|
|
380
|
+
if (deliveryReady) transitions.push('ultra-deliver');
|
|
381
|
+
} else {
|
|
382
|
+
transitions.push('ultra-plan', 'ultra-change');
|
|
383
|
+
}
|
|
384
|
+
transitions.push('ultra-think', 'ultra-status');
|
|
385
|
+
return { allowed_transitions: unique(transitions), required_transition: null };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function normalizeRecommendation(value) {
|
|
389
|
+
if (value === undefined || value === null) return null;
|
|
390
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
391
|
+
throw new ContextSpineError('VALIDATION_ERROR', 'recommendation must be an object');
|
|
392
|
+
}
|
|
393
|
+
const workflow = String(value.workflow || '').trim();
|
|
394
|
+
const rationale = String(value.rationale || '').trim();
|
|
395
|
+
if (!workflow || rationale.length < 3) {
|
|
396
|
+
throw new ContextSpineError(
|
|
397
|
+
'VALIDATION_ERROR', 'recommendation requires workflow and rationale',
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return { workflow, rationale, authoritative: false };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function deliveryCapabilityReady(db, change, tasks, rootDir) {
|
|
404
|
+
if (!change || !Array.isArray(tasks) || tasks.length === 0
|
|
405
|
+
|| !tasks.every((row) => ['completed', 'expanded'].includes(row.status))) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
workflows.validateDeliveryPrerequisites(db, { change_id: change.id }, rootDir);
|
|
410
|
+
return true;
|
|
411
|
+
} catch {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
315
414
|
}
|
|
316
415
|
|
|
317
416
|
function latestContext(db, changeId, taskId) {
|
|
@@ -328,8 +427,8 @@ function latestContext(db, changeId, taskId) {
|
|
|
328
427
|
function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
329
428
|
if (input.next_action !== undefined) {
|
|
330
429
|
throw new ContextSpineError(
|
|
331
|
-
'
|
|
332
|
-
'next_action is
|
|
430
|
+
'CONTEXT_LEGACY_NEXT_ACTION_UNSUPPORTED',
|
|
431
|
+
'next_action is not an authority field; provide a non-authoritative recommendation instead',
|
|
333
432
|
);
|
|
334
433
|
}
|
|
335
434
|
const task = input.task_id ? tasks.find((row) => row.id === input.task_id) : null;
|
|
@@ -376,13 +475,20 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
376
475
|
}
|
|
377
476
|
}
|
|
378
477
|
const readiness = blockers.length === 0 ? 'ready' : 'blocked';
|
|
379
|
-
const
|
|
380
|
-
change,
|
|
478
|
+
const control = deriveTransitions({
|
|
479
|
+
change,
|
|
480
|
+
tasks,
|
|
481
|
+
task,
|
|
482
|
+
role,
|
|
483
|
+
readiness,
|
|
484
|
+
blockers,
|
|
485
|
+
deliveryReady: deliveryCapabilityReady(db, change, tasks, rootDir),
|
|
381
486
|
});
|
|
382
487
|
return {
|
|
383
488
|
role,
|
|
384
489
|
gate,
|
|
385
|
-
|
|
490
|
+
control,
|
|
491
|
+
recommendation: normalizeRecommendation(input.recommendation),
|
|
386
492
|
readiness: { status: readiness, blockers, warnings: [...new Set(warnings)] },
|
|
387
493
|
context: { items, budget, token_estimate: tokenEstimate, file_count: fileCount },
|
|
388
494
|
execution_contract: executionContract,
|
|
@@ -397,6 +503,7 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
397
503
|
selected_task: task,
|
|
398
504
|
resume: {
|
|
399
505
|
change_id: change.id,
|
|
506
|
+
change_state_digest: changeStateDigest(change),
|
|
400
507
|
task_id: task?.id || null,
|
|
401
508
|
task_status: task?.status || null,
|
|
402
509
|
task_state_digest: taskStateDigest(task),
|
|
@@ -410,15 +517,6 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
410
517
|
};
|
|
411
518
|
}
|
|
412
519
|
|
|
413
|
-
function recommendedWorkflow(change, task, tasks, readiness) {
|
|
414
|
-
if (!change) return 'ultra-change';
|
|
415
|
-
if (readiness !== 'ready' || change.status === 'blocked') return 'ultra-change';
|
|
416
|
-
if (change.status === 'ready') return 'ultra-deliver';
|
|
417
|
-
if (task && ['pending', 'in_progress'].includes(task.status)) return 'ultra-dev';
|
|
418
|
-
if (tasks.length > 0 && tasks.every((row) => ['completed', 'expanded'].includes(row.status))) return 'ultra-test';
|
|
419
|
-
return 'ultra-change';
|
|
420
|
-
}
|
|
421
|
-
|
|
422
520
|
function actionableBaselineWorkflow(db, baselineId, rootDir) {
|
|
423
521
|
if (!baselineId) return null;
|
|
424
522
|
const candidates = workflows.listWorkflows(db, { baseline_id: baselineId, limit: 100 }, { rootDir })
|
|
@@ -443,16 +541,6 @@ function summarizeWorkflow(run) {
|
|
|
443
541
|
};
|
|
444
542
|
}
|
|
445
543
|
|
|
446
|
-
function workflowNextAction(run) {
|
|
447
|
-
if (run.status === 'blocked') {
|
|
448
|
-
return `Resolve ${run.kind} workflow blockers at ${run.current_step}: ${run.blockers.join(', ')}.`;
|
|
449
|
-
}
|
|
450
|
-
if (run.status === 'ready') {
|
|
451
|
-
return `Finalize ${run.kind} workflow ${run.id} after verifying its recorded evidence and outputs.`;
|
|
452
|
-
}
|
|
453
|
-
return `Continue ${run.kind} workflow ${run.id} at ${run.current_step}.`;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
544
|
function summarizeDecision(gate) {
|
|
457
545
|
if (!gate?.thread) return null;
|
|
458
546
|
const current = gate.current_decision;
|
|
@@ -472,19 +560,6 @@ function summarizeDecision(gate) {
|
|
|
472
560
|
};
|
|
473
561
|
}
|
|
474
562
|
|
|
475
|
-
function decisionNextAction(gate) {
|
|
476
|
-
if (gate.blockers?.some((blocker) => blocker.startsWith('DECISION_CHECKPOINT_ARTIFACT_STALE:'))) {
|
|
477
|
-
return `Reprepare and confirm decision checkpoint ${gate.thread?.id || 'for the active alignment thread'} against the current artifact.`;
|
|
478
|
-
}
|
|
479
|
-
if (gate.current_decision) {
|
|
480
|
-
return `Answer decision ${gate.current_decision.id}: ${gate.current_decision.question}`;
|
|
481
|
-
}
|
|
482
|
-
if (gate.thread?.status === 'checkpoint_ready') {
|
|
483
|
-
return `Confirm decision checkpoint ${gate.thread.id} after reviewing its durable effects.`;
|
|
484
|
-
}
|
|
485
|
-
return `Prepare the confirmed decision checkpoint for ${gate.thread?.id || 'the active alignment thread'}.`;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
563
|
function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
489
564
|
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
490
565
|
const change = id
|
|
@@ -505,7 +580,7 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
505
580
|
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'blocked',
|
|
506
581
|
blockers: [...new Set([...baselineHealth.blockers, ...decisionGate.blockers])],
|
|
507
582
|
warnings: baselineHealth.warnings,
|
|
508
|
-
|
|
583
|
+
allowed_transitions: ['ultra-think', 'ultra-status'], required_transition: 'ultra-think',
|
|
509
584
|
workflow: null, decision: summarizeDecision(decisionGate),
|
|
510
585
|
context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
511
586
|
baseline: baseline ? {
|
|
@@ -516,15 +591,24 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
516
591
|
}
|
|
517
592
|
if (baselineHealth.status !== 'pass') {
|
|
518
593
|
const activeWorkflow = actionableBaselineWorkflow(db, baseline?.id, rootDir);
|
|
519
|
-
const
|
|
520
|
-
?
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
594
|
+
const control = baseline
|
|
595
|
+
? {
|
|
596
|
+
allowed_transitions: uniqueStrings([
|
|
597
|
+
activeWorkflow ? `ultra-${activeWorkflow.kind}` : 'ultra-research',
|
|
598
|
+
'ultra-status',
|
|
599
|
+
'ultra-doctor',
|
|
600
|
+
]),
|
|
601
|
+
required_transition: null,
|
|
602
|
+
}
|
|
603
|
+
: {
|
|
604
|
+
allowed_transitions: ['ultra-init', 'ultra-status'],
|
|
605
|
+
required_transition: 'ultra-init',
|
|
606
|
+
};
|
|
524
607
|
return {
|
|
525
608
|
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'blocked',
|
|
526
|
-
blockers: baselineHealth.blockers, warnings: baselineHealth.warnings,
|
|
527
|
-
|
|
609
|
+
blockers: baselineHealth.blockers, warnings: baselineHealth.warnings,
|
|
610
|
+
allowed_transitions: control.allowed_transitions,
|
|
611
|
+
required_transition: control.required_transition,
|
|
528
612
|
workflow: summarizeWorkflow(activeWorkflow), decision: null, context_manifest_path: null,
|
|
529
613
|
context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
530
614
|
baseline: baseline ? {
|
|
@@ -535,7 +619,8 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
535
619
|
}
|
|
536
620
|
return {
|
|
537
621
|
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'ready',
|
|
538
|
-
|
|
622
|
+
allowed_transitions: ['ultra-change', 'ultra-research', 'ultra-think', 'ultra-status'],
|
|
623
|
+
required_transition: null,
|
|
539
624
|
workflow: null,
|
|
540
625
|
decision: null,
|
|
541
626
|
context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
@@ -564,6 +649,7 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
564
649
|
!context.resume || !context.context || !context.readiness || !('baseline' in context)
|
|
565
650
|
|| !Object.prototype.hasOwnProperty.call(context.resume, 'worktree_digest')
|
|
566
651
|
|| !Object.prototype.hasOwnProperty.call(context.resume, 'task_state_digest')
|
|
652
|
+
|| !Object.prototype.hasOwnProperty.call(context.resume, 'change_state_digest')
|
|
567
653
|
),
|
|
568
654
|
);
|
|
569
655
|
const headStale = Boolean(snapshot?.git_head && head && snapshot.git_head !== head);
|
|
@@ -576,6 +662,9 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
576
662
|
const taskContractChanged = Boolean(
|
|
577
663
|
context.resume && context.resume.task_state_digest !== taskStateDigest(task),
|
|
578
664
|
);
|
|
665
|
+
const changeContractChanged = Boolean(
|
|
666
|
+
context.resume && context.resume.change_state_digest !== changeStateDigest(change),
|
|
667
|
+
);
|
|
579
668
|
const blockers = [];
|
|
580
669
|
const baselineGate = baselineGateForChange(db, change, baselineHealth);
|
|
581
670
|
const decisionGate = decisions.decisionGate(db, {
|
|
@@ -586,13 +675,15 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
586
675
|
const warnings = [...baselineGate.warnings];
|
|
587
676
|
blockers.push(...baselineGate.blockers);
|
|
588
677
|
blockers.push(...decisionGate.blockers);
|
|
589
|
-
if (snapshotMissing) blockers.push('CONTEXT_NOT_COMPILED');
|
|
678
|
+
if (snapshotMissing && tasks.length > 0) blockers.push('CONTEXT_NOT_COMPILED');
|
|
679
|
+
else if (snapshotMissing) warnings.push('CONTEXT_NOT_COMPILED');
|
|
590
680
|
else {
|
|
591
681
|
if (legacyContext) blockers.push('CONTEXT_SNAPSHOT_UPGRADE_REQUIRED');
|
|
592
682
|
if (headStale) blockers.push('CONTEXT_HEAD_STALE');
|
|
593
683
|
if (worktreeStale) blockers.push('CONTEXT_WORKTREE_STALE');
|
|
594
684
|
if (stateChanged) blockers.push('CONTEXT_TASK_STATE_STALE');
|
|
595
685
|
if (taskContractChanged) blockers.push('CONTEXT_TASK_CONTRACT_STALE');
|
|
686
|
+
if (changeContractChanged) blockers.push('CONTEXT_CHANGE_CONTRACT_STALE');
|
|
596
687
|
for (const blocker of parseJson(snapshot.blockers_json, [])) {
|
|
597
688
|
if (ADVISORY_CONTEXT_CODES.has(blocker)
|
|
598
689
|
|| (blocker.startsWith('BASELINE_') && baselineGate.blockers.length === 0)) {
|
|
@@ -603,38 +694,18 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
603
694
|
if (!warnings.includes(warning)) warnings.push(warning);
|
|
604
695
|
}
|
|
605
696
|
}
|
|
606
|
-
const readiness = blockers.length > 0 ? 'blocked' : snapshot
|
|
697
|
+
const readiness = blockers.length > 0 ? 'blocked' : (snapshot?.readiness || 'ready');
|
|
607
698
|
const activeWorkflow = actionableChangeWorkflow(db, change.id, rootDir);
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
} else if (snapshotMissing) {
|
|
619
|
-
nextAction = `Compile change.context for change ${change.id} before continuing.`;
|
|
620
|
-
} else if (legacyContext) {
|
|
621
|
-
nextAction = 'Recompile change.context to upgrade this legacy context snapshot.';
|
|
622
|
-
} else if (headStale) {
|
|
623
|
-
nextAction = 'Git HEAD changed after context compilation; recompile change.context.';
|
|
624
|
-
} else if (worktreeStale) {
|
|
625
|
-
nextAction = 'The worktree changed after context compilation; recompile change.context.';
|
|
626
|
-
} else if (stateChanged) {
|
|
627
|
-
nextAction = 'Task state changed after context compilation; recompile change.context.';
|
|
628
|
-
} else if (taskContractChanged) {
|
|
629
|
-
nextAction = 'The task execution contract changed after context compilation; recompile change.context.';
|
|
630
|
-
} else if (activeWorkflow) {
|
|
631
|
-
nextAction = workflowNextAction(activeWorkflow);
|
|
632
|
-
} else {
|
|
633
|
-
nextAction = deriveNextAction({
|
|
634
|
-
change, tasks, task, role: snapshot?.role || 'plan', readiness,
|
|
635
|
-
blockers,
|
|
636
|
-
});
|
|
637
|
-
}
|
|
699
|
+
const control = deriveTransitions({
|
|
700
|
+
change,
|
|
701
|
+
tasks,
|
|
702
|
+
task,
|
|
703
|
+
role: snapshot?.role || 'plan',
|
|
704
|
+
readiness,
|
|
705
|
+
blockers,
|
|
706
|
+
activeWorkflow,
|
|
707
|
+
deliveryReady: deliveryCapabilityReady(db, change, tasks, rootDir),
|
|
708
|
+
});
|
|
638
709
|
return {
|
|
639
710
|
change_id: change.id,
|
|
640
711
|
change_status: change.status,
|
|
@@ -646,14 +717,8 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
646
717
|
readiness,
|
|
647
718
|
blockers,
|
|
648
719
|
warnings: [...new Set(warnings)],
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
? (baselineHealth.baseline?.status === 'ready' ? 'ultra-doctor' : 'ultra-init')
|
|
652
|
-
: !decisionGate.ready
|
|
653
|
-
? 'ultra-think'
|
|
654
|
-
: activeWorkflow
|
|
655
|
-
? `ultra-${activeWorkflow.kind}`
|
|
656
|
-
: recommendedWorkflow(change, task, tasks, readiness),
|
|
720
|
+
allowed_transitions: control.allowed_transitions,
|
|
721
|
+
required_transition: control.required_transition,
|
|
657
722
|
context_manifest_path: snapshot?.manifest_path || null,
|
|
658
723
|
context_manifest_hash: snapshot?.manifest_hash || null,
|
|
659
724
|
git_head: head,
|
|
@@ -670,7 +735,8 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
670
735
|
module.exports = {
|
|
671
736
|
ContextSpineError,
|
|
672
737
|
baselineGateForChange,
|
|
738
|
+
changeStateDigest,
|
|
673
739
|
compileRoleContext,
|
|
674
|
-
|
|
740
|
+
deriveTransitions,
|
|
675
741
|
readBreadcrumb,
|
|
676
742
|
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const GIT_MODES = new Set(['auto', 'initialize', 'skip']);
|
|
8
|
+
// No trailing slash: isolated session worktrees expose `.ultra` as a symlink
|
|
9
|
+
// to the central authority, and Git's directory-only pattern does not ignore
|
|
10
|
+
// that symlink reliably.
|
|
11
|
+
const ULTRA_IGNORE_LINE = '.ultra';
|
|
12
|
+
|
|
13
|
+
class GitBootstrapError extends Error {
|
|
14
|
+
constructor(code, message, { cause } = {}) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'GitBootstrapError';
|
|
17
|
+
this.code = code;
|
|
18
|
+
if (cause) this.cause = cause;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function runGit(rootDir, args) {
|
|
23
|
+
return spawnSync('git', args, {
|
|
24
|
+
cwd: rootDir,
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function gitValue(rootDir, args) {
|
|
31
|
+
const result = runGit(rootDir, args);
|
|
32
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function inspectGitRepository(rootDir) {
|
|
36
|
+
const repositoryRoot = gitValue(rootDir, ['rev-parse', '--show-toplevel']);
|
|
37
|
+
if (!repositoryRoot) {
|
|
38
|
+
return {
|
|
39
|
+
present: false,
|
|
40
|
+
repository_root: null,
|
|
41
|
+
branch: null,
|
|
42
|
+
head: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
present: true,
|
|
47
|
+
repository_root: path.resolve(repositoryRoot),
|
|
48
|
+
branch: gitValue(rootDir, ['symbolic-ref', '--quiet', '--short', 'HEAD'])
|
|
49
|
+
|| gitValue(rootDir, ['branch', '--show-current']),
|
|
50
|
+
head: gitValue(rootDir, ['rev-parse', '--verify', 'HEAD']),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function initializeRepository(rootDir) {
|
|
55
|
+
let result = runGit(rootDir, ['init', '-q', '-b', 'main']);
|
|
56
|
+
if (result.status !== 0) {
|
|
57
|
+
result = runGit(rootDir, ['init', '-q']);
|
|
58
|
+
if (result.status === 0) {
|
|
59
|
+
const branch = runGit(rootDir, ['symbolic-ref', 'HEAD', 'refs/heads/main']);
|
|
60
|
+
if (branch.status !== 0) result = branch;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (result.status !== 0) {
|
|
64
|
+
throw new GitBootstrapError(
|
|
65
|
+
'GIT_INIT_FAILED',
|
|
66
|
+
`git init failed: ${(result.stderr || result.stdout || 'unknown error').trim()}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function containsUltraIgnore(contents, { requireSymlinkSafe = false } = {}) {
|
|
72
|
+
const accepted = requireSymlinkSafe
|
|
73
|
+
? ['.ultra', '/.ultra']
|
|
74
|
+
: ['.ultra', '.ultra/', '/.ultra', '/.ultra/'];
|
|
75
|
+
return contents.split(/\r?\n/).some((line) => {
|
|
76
|
+
const value = line.trim();
|
|
77
|
+
return accepted.includes(value);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function effectiveUltraIgnore(rootDir) {
|
|
82
|
+
return runGit(rootDir, ['check-ignore', '--quiet', '--no-index', '--', '.ultra']).status === 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function ensureUltraIgnored(rootDir, { requireSymlinkSafe = false } = {}) {
|
|
86
|
+
const file = path.join(rootDir, '.gitignore');
|
|
87
|
+
let stat = null;
|
|
88
|
+
try { stat = fs.lstatSync(file); }
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error.code !== 'ENOENT') throw error;
|
|
91
|
+
}
|
|
92
|
+
const existed = stat !== null;
|
|
93
|
+
if (stat) {
|
|
94
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
95
|
+
throw new GitBootstrapError(
|
|
96
|
+
'GITIGNORE_UNSAFE',
|
|
97
|
+
`refusing to update non-regular root .gitignore: ${file}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const before = existed ? fs.readFileSync(file, 'utf8') : null;
|
|
102
|
+
if (!requireSymlinkSafe && effectiveUltraIgnore(rootDir)) {
|
|
103
|
+
return {
|
|
104
|
+
changed: false,
|
|
105
|
+
path: file,
|
|
106
|
+
rollback: { existed, before },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (before !== null && containsUltraIgnore(before, { requireSymlinkSafe })) {
|
|
110
|
+
return {
|
|
111
|
+
changed: false,
|
|
112
|
+
path: file,
|
|
113
|
+
rollback: { existed, before },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const prefix = before && !before.endsWith('\n') ? `${before}\n` : (before || '');
|
|
117
|
+
const separator = prefix && !prefix.endsWith('\n\n') ? '\n' : '';
|
|
118
|
+
const next = `${prefix}${separator}# Ultra Builder Pro local authority\n${ULTRA_IGNORE_LINE}\n`;
|
|
119
|
+
fs.writeFileSync(file, next);
|
|
120
|
+
return {
|
|
121
|
+
changed: true,
|
|
122
|
+
path: file,
|
|
123
|
+
rollback: { existed, before },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function assertUltraIgnored(rootDir) {
|
|
128
|
+
const result = runGit(rootDir, ['check-ignore', '--quiet', '--no-index', '--', '.ultra']);
|
|
129
|
+
if (result.status === 0) return;
|
|
130
|
+
const detail = result.status === 1
|
|
131
|
+
? 'the effective Git rules re-include .ultra'
|
|
132
|
+
: (result.stderr || result.stdout || 'Git could not evaluate the rule').trim();
|
|
133
|
+
throw new GitBootstrapError(
|
|
134
|
+
'GITIGNORE_INEFFECTIVE',
|
|
135
|
+
`Ultra authority is not ignored by Git: ${detail}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function rollbackGitBootstrap(rootDir, internal) {
|
|
140
|
+
const errors = [];
|
|
141
|
+
if (!internal) return errors;
|
|
142
|
+
const gitignore = internal.gitignore;
|
|
143
|
+
if (gitignore?.changed) {
|
|
144
|
+
try {
|
|
145
|
+
if (gitignore.rollback.existed) {
|
|
146
|
+
fs.writeFileSync(gitignore.path, gitignore.rollback.before);
|
|
147
|
+
} else {
|
|
148
|
+
fs.rmSync(gitignore.path, { force: true });
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
errors.push(`gitignore restore failed: ${error.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (internal.created_repository) {
|
|
155
|
+
try {
|
|
156
|
+
fs.rmSync(path.join(rootDir, '.git'), { recursive: true, force: true });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
errors.push(`git repository rollback failed: ${error.message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return errors;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function bootstrapGit(rootDir, { mode = 'auto' } = {}) {
|
|
165
|
+
if (!GIT_MODES.has(mode)) {
|
|
166
|
+
throw new GitBootstrapError(
|
|
167
|
+
'VALIDATION_ERROR',
|
|
168
|
+
`git_mode must be one of: ${[...GIT_MODES].join(', ')}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const before = inspectGitRepository(rootDir);
|
|
172
|
+
if (!before.present && mode === 'skip') {
|
|
173
|
+
return {
|
|
174
|
+
result: {
|
|
175
|
+
requested_mode: mode,
|
|
176
|
+
status: 'skipped',
|
|
177
|
+
repository_root: null,
|
|
178
|
+
branch: null,
|
|
179
|
+
head: null,
|
|
180
|
+
gitignore_path: null,
|
|
181
|
+
gitignore_changed: false,
|
|
182
|
+
initial_commit_required: false,
|
|
183
|
+
},
|
|
184
|
+
internal: null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const internal = {
|
|
189
|
+
created_repository: false,
|
|
190
|
+
gitignore: null,
|
|
191
|
+
};
|
|
192
|
+
try {
|
|
193
|
+
if (!before.present) {
|
|
194
|
+
initializeRepository(rootDir);
|
|
195
|
+
internal.created_repository = true;
|
|
196
|
+
}
|
|
197
|
+
internal.gitignore = ensureUltraIgnored(rootDir, {
|
|
198
|
+
// A repository created in this call has no tracked baseline to preserve,
|
|
199
|
+
// so its ignore rule can be made safe for the session authority symlink.
|
|
200
|
+
requireSymlinkSafe: internal.created_repository,
|
|
201
|
+
});
|
|
202
|
+
assertUltraIgnored(rootDir);
|
|
203
|
+
const after = inspectGitRepository(rootDir);
|
|
204
|
+
if (!after.present) {
|
|
205
|
+
throw new GitBootstrapError(
|
|
206
|
+
'GIT_INIT_FAILED',
|
|
207
|
+
'git initialization completed without a discoverable repository',
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
result: {
|
|
212
|
+
requested_mode: mode,
|
|
213
|
+
status: internal.created_repository ? 'initialized' : 'existing',
|
|
214
|
+
repository_root: after.repository_root,
|
|
215
|
+
branch: after.branch,
|
|
216
|
+
head: after.head,
|
|
217
|
+
gitignore_path: path.relative(rootDir, internal.gitignore.path) || '.gitignore',
|
|
218
|
+
gitignore_changed: internal.gitignore.changed,
|
|
219
|
+
initial_commit_required: after.head === null,
|
|
220
|
+
},
|
|
221
|
+
internal,
|
|
222
|
+
};
|
|
223
|
+
} catch (error) {
|
|
224
|
+
rollbackGitBootstrap(rootDir, internal);
|
|
225
|
+
if (error instanceof GitBootstrapError) throw error;
|
|
226
|
+
throw new GitBootstrapError(
|
|
227
|
+
'GIT_INIT_FAILED',
|
|
228
|
+
`Git bootstrap failed: ${error.message}`,
|
|
229
|
+
{ cause: error },
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
GitBootstrapError,
|
|
236
|
+
GIT_MODES,
|
|
237
|
+
bootstrapGit,
|
|
238
|
+
inspectGitRepository,
|
|
239
|
+
rollbackGitBootstrap,
|
|
240
|
+
};
|