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
|
@@ -40,20 +40,33 @@ function gitHead(rootDir) {
|
|
|
40
40
|
return result.status === 0 ? result.stdout.trim() : null;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function gitRepositoryRoot(rootDir) {
|
|
44
|
+
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
45
|
+
cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
46
|
+
});
|
|
47
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
function gitBranch(rootDir) {
|
|
44
|
-
|
|
51
|
+
let result = spawnSync('git', ['symbolic-ref', '--quiet', '--short', 'HEAD'], {
|
|
45
52
|
cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
46
53
|
});
|
|
54
|
+
if (result.status !== 0) {
|
|
55
|
+
result = spawnSync('git', ['branch', '--show-current'], {
|
|
56
|
+
cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
47
59
|
return result.status === 0 ? (result.stdout.trim() || null) : null;
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
function gitWorktreeSnapshot(rootDir, scope = ['.']) {
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
63
|
+
const repositoryRoot = gitRepositoryRoot(rootDir);
|
|
64
|
+
if (!repositoryRoot) {
|
|
53
65
|
return {
|
|
54
66
|
head: null, branch: null, state: 'unavailable', digest: null, files: [],
|
|
55
67
|
};
|
|
56
68
|
}
|
|
69
|
+
const head = gitHead(rootDir);
|
|
57
70
|
const selectedScope = Array.isArray(scope) && scope.length > 0 ? scope : ['.'];
|
|
58
71
|
const pathspecs = selectedScope.map((item) => (
|
|
59
72
|
item === '.' ? '.' : `:(literal)${item}`
|
|
@@ -63,10 +76,12 @@ function gitWorktreeSnapshot(rootDir, scope = ['.']) {
|
|
|
63
76
|
'git', ['status', '--porcelain=v1', '-z', '--untracked-files=all', ...args],
|
|
64
77
|
{ cwd: rootDir, encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 32 * 1024 * 1024 },
|
|
65
78
|
);
|
|
66
|
-
const diff =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
const diff = head
|
|
80
|
+
? spawnSync(
|
|
81
|
+
'git', ['diff', '--binary', '--no-ext-diff', 'HEAD', ...args],
|
|
82
|
+
{ cwd: rootDir, encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 32 * 1024 * 1024 },
|
|
83
|
+
)
|
|
84
|
+
: { status: 0, stdout: Buffer.alloc(0) };
|
|
70
85
|
const untracked = spawnSync(
|
|
71
86
|
'git', ['ls-files', '--others', '--exclude-standard', '-z', ...args],
|
|
72
87
|
{ cwd: rootDir, encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 32 * 1024 * 1024 },
|
|
@@ -79,7 +94,7 @@ function gitWorktreeSnapshot(rootDir, scope = ['.']) {
|
|
|
79
94
|
const statusText = status.stdout.toString('utf8');
|
|
80
95
|
const files = statusText.split('\0').filter(Boolean);
|
|
81
96
|
const digest = crypto.createHash('sha256');
|
|
82
|
-
digest.update(head).update('\0').update(gitBranch(rootDir) || '').update('\0');
|
|
97
|
+
digest.update(head || 'unborn').update('\0').update(gitBranch(rootDir) || '').update('\0');
|
|
83
98
|
digest.update(status.stdout).update(diff.stdout);
|
|
84
99
|
for (const relative of untracked.stdout.toString('utf8').split('\0').filter(Boolean).sort()) {
|
|
85
100
|
digest.update(relative).update('\0');
|
|
@@ -93,7 +108,7 @@ function gitWorktreeSnapshot(rootDir, scope = ['.']) {
|
|
|
93
108
|
return {
|
|
94
109
|
head,
|
|
95
110
|
branch: gitBranch(rootDir),
|
|
96
|
-
state: files.length > 0 ? 'dirty' : 'clean',
|
|
111
|
+
state: head ? (files.length > 0 ? 'dirty' : 'clean') : 'unborn',
|
|
97
112
|
digest: digest.digest('hex'),
|
|
98
113
|
files,
|
|
99
114
|
};
|
|
@@ -464,6 +479,55 @@ function startBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = tr
|
|
|
464
479
|
});
|
|
465
480
|
}
|
|
466
481
|
|
|
482
|
+
function refreshInProgressBaseline(db, input = {}, {
|
|
483
|
+
rootDir = process.cwd(), emitEvent = true,
|
|
484
|
+
} = {}) {
|
|
485
|
+
const current = readBaseline(db, input.id);
|
|
486
|
+
if (!current) {
|
|
487
|
+
throw new BaselineWorkflowError('BASELINE_NOT_FOUND', `baseline ${input.id || '(current)'} not found`);
|
|
488
|
+
}
|
|
489
|
+
if (!['draft', 'adopting', 'blocked'].includes(current.status)) return current;
|
|
490
|
+
const projectName = input.project_name === undefined
|
|
491
|
+
? current.project_name
|
|
492
|
+
: nonEmpty(input.project_name, 'project_name');
|
|
493
|
+
const projectType = input.project_type === undefined ? current.project_type : input.project_type;
|
|
494
|
+
const stack = input.stack === undefined ? current.stack : input.stack;
|
|
495
|
+
const classification = input.classification === undefined
|
|
496
|
+
? current.classification
|
|
497
|
+
: normalizeClassification(input.classification);
|
|
498
|
+
const snapshot = gitWorktreeSnapshot(rootDir, current.scope);
|
|
499
|
+
const repositoryRevision = snapshot.head
|
|
500
|
+
|| (snapshot.state === 'unborn' ? null : current.repository_revision);
|
|
501
|
+
const ts = nowIso();
|
|
502
|
+
return ops.tx(db, () => {
|
|
503
|
+
db.prepare(
|
|
504
|
+
`UPDATE baselines SET project_name = ?, project_type = ?, stack = ?,
|
|
505
|
+
repository_revision = ?, repository_branch = ?, worktree_state = ?,
|
|
506
|
+
worktree_digest = ?, worktree_files_json = ?, classification_json = ?,
|
|
507
|
+
updated_at = ? WHERE id = ?`,
|
|
508
|
+
).run(
|
|
509
|
+
projectName, projectType || null, stack || null,
|
|
510
|
+
repositoryRevision, snapshot.branch, snapshot.state, snapshot.digest,
|
|
511
|
+
JSON.stringify(snapshot.files), JSON.stringify(classification), ts, current.id,
|
|
512
|
+
);
|
|
513
|
+
if (emitEvent) {
|
|
514
|
+
ops.appendEventInTx(db, {
|
|
515
|
+
type: 'baseline_metadata_refreshed',
|
|
516
|
+
payload: {
|
|
517
|
+
baseline_id: current.id,
|
|
518
|
+
project_name: projectName,
|
|
519
|
+
project_type: projectType || null,
|
|
520
|
+
stack: stack || null,
|
|
521
|
+
repository_revision: repositoryRevision,
|
|
522
|
+
repository_branch: snapshot.branch,
|
|
523
|
+
worktree_state: snapshot.state,
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return readBaseline(db, current.id);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
467
531
|
function recordBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = true } = {}) {
|
|
468
532
|
const current = readBaseline(db, input.id);
|
|
469
533
|
if (!current) throw new BaselineWorkflowError('BASELINE_NOT_FOUND', `baseline ${input.id} not found`);
|
|
@@ -484,6 +548,12 @@ function recordBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = t
|
|
|
484
548
|
? current.provider_refs
|
|
485
549
|
: providerRefs.normalizeProviderRefs(input.provider_refs, BaselineWorkflowError);
|
|
486
550
|
const snapshot = gitWorktreeSnapshot(rootDir, scope);
|
|
551
|
+
if (snapshot.state === 'unborn') {
|
|
552
|
+
throw new BaselineWorkflowError(
|
|
553
|
+
'BASELINE_GIT_HEAD_REQUIRED',
|
|
554
|
+
'Git is initialized but has no commit; create an owner-authorized local checkpoint commit before recording the baseline',
|
|
555
|
+
);
|
|
556
|
+
}
|
|
487
557
|
if (snapshot.head && input.repository_revision && input.repository_revision !== snapshot.head) {
|
|
488
558
|
throw new BaselineWorkflowError(
|
|
489
559
|
'BASELINE_REVISION_MISMATCH', 'repository_revision does not match current Git HEAD',
|
|
@@ -667,6 +737,7 @@ function convergenceBlockers(db, baseline, input, rootDir) {
|
|
|
667
737
|
if (!expected) blockers.add('BASELINE_REVISION_REQUIRED');
|
|
668
738
|
else if (baseline.repository_revision !== expected) blockers.add('BASELINE_REVISION_MISMATCH');
|
|
669
739
|
const snapshot = gitWorktreeSnapshot(rootDir, baseline.scope);
|
|
740
|
+
if (snapshot.state === 'unborn') blockers.add('BASELINE_GIT_HEAD_REQUIRED');
|
|
670
741
|
if (snapshot.head && expected && snapshot.head !== expected) blockers.add('BASELINE_HEAD_STALE');
|
|
671
742
|
if (baseline.repository_branch && snapshot.branch
|
|
672
743
|
&& baseline.repository_branch !== snapshot.branch) blockers.add('BASELINE_BRANCH_STALE');
|
|
@@ -725,7 +796,6 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
|
|
|
725
796
|
type: 'baseline_blocked', payload: { baseline_id: current.id, blockers },
|
|
726
797
|
});
|
|
727
798
|
}
|
|
728
|
-
workflows.blockInitializationInTx(db, current.id, blockers);
|
|
729
799
|
});
|
|
730
800
|
return { ready: false, status: 'blocked', blockers, baseline: readBaseline(db, current.id) };
|
|
731
801
|
}
|
|
@@ -745,12 +815,6 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
|
|
|
745
815
|
type: 'baseline_converged', payload: { baseline_id: current.id, revision: current.repository_revision },
|
|
746
816
|
});
|
|
747
817
|
}
|
|
748
|
-
workflows.completeInitializationInTx(db, {
|
|
749
|
-
...current,
|
|
750
|
-
approved_by: String(input.approved_by).trim(),
|
|
751
|
-
research_run_id: research.id,
|
|
752
|
-
converged_at: ts,
|
|
753
|
-
});
|
|
754
818
|
});
|
|
755
819
|
return { ready: true, status: 'ready', blockers: [], baseline: readBaseline(db, current.id) };
|
|
756
820
|
}
|
|
@@ -786,6 +850,9 @@ function inspectBaseline(db, { rootDir = process.cwd(), id } = {}) {
|
|
|
786
850
|
const snapshot = gitWorktreeSnapshot(
|
|
787
851
|
rootDir, Array.isArray(baseline.scope) && baseline.scope.length > 0 ? baseline.scope : ['.'],
|
|
788
852
|
);
|
|
853
|
+
if (snapshot.state === 'unborn') {
|
|
854
|
+
blockers.push('BASELINE_GIT_HEAD_REQUIRED');
|
|
855
|
+
}
|
|
789
856
|
if (snapshot.head && baseline.repository_revision && snapshot.head !== baseline.repository_revision) {
|
|
790
857
|
blockers.push('BASELINE_HEAD_STALE');
|
|
791
858
|
} else if (!snapshot.head && baseline.repository_revision?.startsWith('workspace:')) {
|
|
@@ -905,6 +972,7 @@ function appendGap(db, input = {}, options = {}) {
|
|
|
905
972
|
module.exports = {
|
|
906
973
|
BaselineWorkflowError,
|
|
907
974
|
startBaseline,
|
|
975
|
+
refreshInProgressBaseline,
|
|
908
976
|
recordBaseline,
|
|
909
977
|
convergeBaseline,
|
|
910
978
|
readBaseline,
|
|
@@ -24,6 +24,10 @@ const CHANGE_PATCH_FIELDS = new Set([
|
|
|
24
24
|
'title', 'intent', 'status', 'docs_impact', 'provider_refs',
|
|
25
25
|
'contract', 'classification', 'research_disposition',
|
|
26
26
|
]);
|
|
27
|
+
const SEMANTIC_AUTHORITY_FIELDS = new Set([
|
|
28
|
+
'intent', 'docs_impact', 'provider_refs', 'contract', 'classification',
|
|
29
|
+
'research_disposition',
|
|
30
|
+
]);
|
|
27
31
|
const CHANGE_TRANSITIONS = Object.freeze({
|
|
28
32
|
active: new Set(['active', 'blocked', 'cancelled']),
|
|
29
33
|
blocked: new Set(['active', 'blocked', 'cancelled']),
|
|
@@ -544,9 +548,12 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
544
548
|
).all(change.id).map((task) => ops.readTask(db, task.id));
|
|
545
549
|
const specs = Array.isArray(input.spec_refs) ? input.spec_refs : [];
|
|
546
550
|
const spine = contextSpine.compileRoleContext(db, { input, change, tasks, rootDir });
|
|
551
|
+
const recommendation = spine.recommendation;
|
|
552
|
+
const durableSpine = { ...spine };
|
|
553
|
+
delete durableSpine.recommendation;
|
|
547
554
|
const snapshotId = `ctx-${crypto.randomUUID().slice(0, 12)}`;
|
|
548
555
|
const manifest = {
|
|
549
|
-
schema_version: '
|
|
556
|
+
schema_version: '3.0',
|
|
550
557
|
snapshot_id: snapshotId,
|
|
551
558
|
generated_at: nowIso(),
|
|
552
559
|
change: {
|
|
@@ -566,7 +573,7 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
566
573
|
specs,
|
|
567
574
|
role: spine.role,
|
|
568
575
|
gate: spine.gate,
|
|
569
|
-
|
|
576
|
+
control: spine.control,
|
|
570
577
|
readiness: spine.readiness,
|
|
571
578
|
context: spine.context,
|
|
572
579
|
execution_contract: spine.execution_contract,
|
|
@@ -588,14 +595,16 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
588
595
|
db.prepare(
|
|
589
596
|
`INSERT INTO context_snapshots
|
|
590
597
|
(id, change_id, task_id, git_head, provider_refs_json, manifest_path, manifest_hash,
|
|
591
|
-
role, gate, next_action,
|
|
592
|
-
token_estimate, token_budget)
|
|
593
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
598
|
+
role, gate, next_action, allowed_transitions_json, required_transition,
|
|
599
|
+
readiness, blockers_json, context_json, token_estimate, token_budget)
|
|
600
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
594
601
|
).run(
|
|
595
602
|
snapshotId, change.id, input.task_id || null,
|
|
596
603
|
spine.resume.git_head, JSON.stringify(providers), relative, hash,
|
|
597
|
-
spine.role, spine.gate,
|
|
598
|
-
JSON.stringify(spine.
|
|
604
|
+
spine.role, spine.gate, '',
|
|
605
|
+
JSON.stringify(spine.control.allowed_transitions), spine.control.required_transition,
|
|
606
|
+
spine.readiness.status,
|
|
607
|
+
JSON.stringify(spine.readiness.blockers), JSON.stringify(durableSpine),
|
|
599
608
|
spine.context.token_estimate, spine.context.budget.max_tokens,
|
|
600
609
|
);
|
|
601
610
|
upsertArtifact(db, {
|
|
@@ -623,34 +632,17 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
623
632
|
manifest_path: relative, manifest_hash: hash, git_head: spine.resume.git_head,
|
|
624
633
|
worktree_digest: spine.resume.worktree_digest,
|
|
625
634
|
role: spine.role, gate: spine.gate, readiness: spine.readiness.status,
|
|
626
|
-
|
|
635
|
+
allowed_transitions: spine.control.allowed_transitions,
|
|
636
|
+
required_transition: spine.control.required_transition,
|
|
627
637
|
},
|
|
628
638
|
});
|
|
629
|
-
const linkedWorkflow = db.prepare(
|
|
630
|
-
`SELECT id FROM workflow_runs
|
|
631
|
-
WHERE kind = 'change' AND change_id = ? AND current_step = 'compile-context'
|
|
632
|
-
AND status IN ('active', 'blocked')
|
|
633
|
-
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
634
|
-
).get(change.id);
|
|
635
|
-
if (linkedWorkflow) {
|
|
636
|
-
if (spine.readiness.status === 'ready') {
|
|
637
|
-
workflows.recordWorkflowStep(db, {
|
|
638
|
-
id: linkedWorkflow.id, step_id: 'compile-context', status: 'completed',
|
|
639
|
-
evidence: [{
|
|
640
|
-
kind: 'context', ref: relative,
|
|
641
|
-
summary: `Role ${spine.role} context is ready for ${input.task_id || 'change planning'}.`,
|
|
642
|
-
}],
|
|
643
|
-
outputs: [{ path: relative, kind: 'context-manifest' }],
|
|
644
|
-
}, { rootDir });
|
|
645
|
-
} else {
|
|
646
|
-
workflows.recordWorkflowStep(db, {
|
|
647
|
-
id: linkedWorkflow.id, step_id: 'compile-context', status: 'blocked',
|
|
648
|
-
blockers: spine.readiness.blockers,
|
|
649
|
-
}, { rootDir });
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
639
|
});
|
|
653
|
-
return {
|
|
640
|
+
return {
|
|
641
|
+
manifest,
|
|
642
|
+
recommendation,
|
|
643
|
+
context_manifest_path: manifestPath,
|
|
644
|
+
manifest_hash: hash,
|
|
645
|
+
};
|
|
654
646
|
}
|
|
655
647
|
|
|
656
648
|
function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
@@ -793,7 +785,6 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
793
785
|
change_id: row.id, subject: `Establish executable readiness for ${row.title}.`,
|
|
794
786
|
metadata: { change_kind: row.kind, base_commit: row.base_commit },
|
|
795
787
|
}, { rootDir });
|
|
796
|
-
const context = compileContext(db, { id: row.id }, { rootDir });
|
|
797
788
|
workflow = workflows.recordWorkflowStep(db, {
|
|
798
789
|
id: workflow.id, step_id: 'bind-baseline', status: 'completed',
|
|
799
790
|
evidence: [{
|
|
@@ -819,9 +810,10 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
819
810
|
research_disposition: row.research_disposition,
|
|
820
811
|
}],
|
|
821
812
|
}, { rootDir });
|
|
813
|
+
workflow = workflows.completeWorkflow(db, { id: workflow.id }, { rootDir });
|
|
822
814
|
return {
|
|
823
815
|
change: readChange(db, row.id), intent_path: intentPath,
|
|
824
|
-
context_manifest_path:
|
|
816
|
+
context_manifest_path: null,
|
|
825
817
|
workflow,
|
|
826
818
|
};
|
|
827
819
|
});
|
|
@@ -914,6 +906,30 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
|
|
|
914
906
|
sets.push('updated_at = ?'); values.push(nowIso(), id);
|
|
915
907
|
db.prepare(`UPDATE changes SET ${sets.join(', ')} WHERE id = ?`).run(...values);
|
|
916
908
|
const updated = readChange(db, id);
|
|
909
|
+
const invalidatedTasks = [];
|
|
910
|
+
if (contextSpine.changeStateDigest(current) !== contextSpine.changeStateDigest(updated)) {
|
|
911
|
+
const tasks = db.prepare(
|
|
912
|
+
'SELECT id, stale FROM tasks WHERE change_id = ? ORDER BY created_at ASC, id ASC',
|
|
913
|
+
).all(id);
|
|
914
|
+
const markStale = db.prepare(
|
|
915
|
+
'UPDATE tasks SET stale = 1, updated_at = ? WHERE id = ?',
|
|
916
|
+
);
|
|
917
|
+
const invalidatedAt = nowIso();
|
|
918
|
+
for (const task of tasks) {
|
|
919
|
+
if (task.stale) continue;
|
|
920
|
+
markStale.run(invalidatedAt, task.id);
|
|
921
|
+
invalidatedTasks.push(task.id);
|
|
922
|
+
ops.appendEventInTx(db, {
|
|
923
|
+
type: 'task_stale_marked',
|
|
924
|
+
task_id: task.id,
|
|
925
|
+
change_id: id,
|
|
926
|
+
payload: {
|
|
927
|
+
reason: 'change_semantic_authority_updated',
|
|
928
|
+
fields: Object.keys(patch).filter((field) => SEMANTIC_AUTHORITY_FIELDS.has(field)),
|
|
929
|
+
},
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
917
933
|
if (syncIntent) {
|
|
918
934
|
fs.mkdirSync(path.dirname(intentPath), { recursive: true });
|
|
919
935
|
writeIntent(intentPath, updated);
|
|
@@ -924,7 +940,11 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
|
|
|
924
940
|
contentHash: crypto.createHash('sha256').update(fs.readFileSync(intentPath)).digest('hex'),
|
|
925
941
|
});
|
|
926
942
|
}
|
|
927
|
-
ops.appendEventInTx(db, {
|
|
943
|
+
ops.appendEventInTx(db, {
|
|
944
|
+
type: 'change_updated',
|
|
945
|
+
change_id: id,
|
|
946
|
+
payload: { fields: Object.keys(patch), invalidated_tasks: invalidatedTasks },
|
|
947
|
+
});
|
|
928
948
|
return updated;
|
|
929
949
|
});
|
|
930
950
|
} catch (error) {
|