ultra-builder-pro-cli 0.20.0 → 0.21.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/docs/research/README.md +26 -5
- package/.ultra-template/specs/discovery.md +1 -1
- package/.ultra-template/tasks/tasks.json +1 -1
- package/CHANGELOG.md +66 -0
- package/README.md +54 -24
- package/adapters/_shared/codex-assets.cjs +69 -11
- package/adapters/_shared/interaction-contract.cjs +36 -3
- package/adapters/_shared/runtime-assets.cjs +41 -1
- package/adapters/claude.js +7 -5
- package/adapters/codex.js +1 -1
- package/adapters/kimi.js +2 -8
- package/adapters/opencode.js +30 -28
- package/bin/orchestrator.js +64 -24
- package/docs/AGENT-CONTEXT.md +74 -43
- package/docs/ARCHITECTURE.md +63 -31
- package/docs/ARTIFACT-AUTHORITY.md +234 -0
- package/docs/DECISIONS.md +10 -4
- package/docs/LEGACY-CLI-CROSSWALK.md +6 -4
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +4 -3
- package/docs/ROADMAP.md +19 -5
- package/docs/RUNTIME-COMPAT-MATRIX.md +24 -13
- package/docs/STATE-DB-ACCESS-POLICY.md +11 -9
- package/docs/WORKFLOW-LIFECYCLE.md +70 -39
- package/hooks/README.md +6 -5
- package/hooks/active_task_context.py +97 -12
- package/hooks/context_spine.py +16 -4
- package/hooks/health_check.py +5 -4
- package/hooks/pre_stop_check.py +5 -2
- package/hooks/runtime_paths.py +396 -0
- package/hooks/subagent_tracker.py +5 -2
- package/hooks/workflow_checkpoint.py +16 -3
- package/hooks/workflow_context.py +11 -3
- package/hooks/workflow_resume.py +7 -4
- package/mcp-server/breadcrumb.cjs +20 -2
- package/mcp-server/lib/archive-journal.cjs +1026 -111
- package/mcp-server/lib/archive-mutation-worker.py +365 -0
- package/mcp-server/lib/artifact-registry.cjs +1126 -0
- package/mcp-server/lib/baseline-workflow.cjs +9 -4
- package/mcp-server/lib/change-packet.cjs +818 -0
- package/mcp-server/lib/change-workflow.cjs +932 -176
- package/mcp-server/lib/context-paths.cjs +111 -0
- package/mcp-server/lib/context-spine.cjs +511 -45
- package/mcp-server/lib/decision-dialogue.cjs +345 -3
- package/mcp-server/lib/delivery-transaction.cjs +471 -0
- package/mcp-server/lib/doctor-backup-worker.cjs +328 -0
- package/mcp-server/lib/doctor.cjs +373 -11
- package/mcp-server/lib/git-bootstrap.cjs +459 -47
- package/mcp-server/lib/init-project.cjs +190 -95
- package/mcp-server/lib/plan-publication-worker.cjs +830 -0
- package/mcp-server/lib/plan-store.cjs +700 -5
- package/mcp-server/lib/project-breadcrumb.cjs +23 -4
- package/mcp-server/lib/projector.cjs +174 -40
- package/mcp-server/lib/runtime-paths.cjs +1687 -0
- package/mcp-server/lib/safe-project-file.cjs +385 -0
- package/mcp-server/lib/spec-learning.cjs +28 -12
- package/mcp-server/lib/state-db.cjs +263 -9
- package/mcp-server/lib/state-ops.cjs +248 -49
- package/mcp-server/lib/telemetry.cjs +12 -3
- package/mcp-server/lib/workflow-state.cjs +610 -58
- package/mcp-server/server.cjs +278 -33
- package/orchestrator/daemon.cjs +2 -10
- package/orchestrator/parallel-orchestrator.cjs +314 -50
- package/orchestrator/planner/plan-builder.cjs +1 -1
- package/orchestrator/recovery.cjs +125 -7
- package/orchestrator/session-close-journal-worker.cjs +5 -0
- package/orchestrator/session-close-journal.cjs +1014 -0
- package/orchestrator/session-runner.cjs +679 -69
- package/orchestrator/worktree-manager.cjs +186 -35
- package/package.json +2 -3
- package/skills/cc-collab/SKILL.md +4 -1
- package/skills/codex-collab/SKILL.md +5 -2
- package/skills/ultra-change/SKILL.md +26 -8
- package/skills/ultra-deliver/SKILL.md +23 -11
- package/skills/ultra-dev/SKILL.md +33 -6
- package/skills/ultra-doctor/SKILL.md +6 -2
- package/skills/ultra-init/SKILL.md +4 -2
- package/skills/ultra-plan/SKILL.md +49 -16
- package/skills/ultra-research/SKILL.md +30 -27
- package/skills/ultra-review/SKILL.md +9 -5
- package/skills/ultra-status/SKILL.md +9 -4
- package/skills/ultra-test/SKILL.md +10 -5
- package/skills/ultra-think/SKILL.md +6 -2
- package/skills/ultra-think/references/decision-dialogue.md +17 -6
- package/skills/ultra-verify/SKILL.md +4 -1
- package/spec/cli-protocol.md +18 -7
- package/spec/mcp-tools.yaml +502 -44
- package/spec/schemas/change-delta.v1.schema.json +153 -0
- package/spec/schemas/documentation-reconciliation.v1.schema.json +106 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +50 -13
- package/spec/schemas/tasks.v4.5.schema.json +1 -1
- package/ultra-tools/cli.cjs +3 -2
- package/ultra-tools/commands/db.cjs +76 -25
- package/ultra-tools/commands/migrate.cjs +995 -80
- package/ultra-tools/commands/session.cjs +34 -8
- package/ultra-tools/commands/status.cjs +32 -6
- package/ultra-tools/commands/system.cjs +129 -22
- package/commands/learn.md +0 -10
- package/orchestrator/skill-miner.cjs +0 -133
- package/output-styles/coding-vibes.md +0 -67
- package/output-styles/structural-thinking.md +0 -61
- package/skills/learn/SKILL.md +0 -58
|
@@ -5,10 +5,15 @@ const fs = require('node:fs');
|
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
|
|
7
7
|
const ops = require('./state-ops.cjs');
|
|
8
|
+
const artifactRegistry = require('./artifact-registry.cjs');
|
|
9
|
+
const { readStableProjectFile } = require('./safe-project-file.cjs');
|
|
8
10
|
|
|
9
11
|
const ID = /^[a-zA-Z0-9_-]+$/;
|
|
10
12
|
const MODES = new Set(['guided', 'fast', 'autonomous', 'diagnostic']);
|
|
11
13
|
const ACTIVE_STATUSES = new Set(['active', 'checkpoint_ready']);
|
|
14
|
+
const APPLIED_REF_KINDS = new Set([
|
|
15
|
+
'baseline', 'change', 'workflow', 'task', 'spec', 'artifact',
|
|
16
|
+
]);
|
|
12
17
|
|
|
13
18
|
class DecisionDialogueError extends Error {
|
|
14
19
|
constructor(code, message, details) {
|
|
@@ -53,6 +58,281 @@ function plainObject(value, field, { allowEmpty = true } = {}) {
|
|
|
53
58
|
return JSON.parse(JSON.stringify(value));
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
function normalizeAppliedRefs(value) {
|
|
62
|
+
if (value === undefined) return [];
|
|
63
|
+
if (!Array.isArray(value) || value.length > 20) {
|
|
64
|
+
throw new DecisionDialogueError(
|
|
65
|
+
'VALIDATION_ERROR', 'applied_refs must contain at most twenty authority references',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
return value.map((item, index) => {
|
|
70
|
+
const ref = plainObject(item, `applied_refs[${index}]`, { allowEmpty: false });
|
|
71
|
+
const kind = requiredText(ref.kind, `applied_refs[${index}].kind`);
|
|
72
|
+
if (!APPLIED_REF_KINDS.has(kind)) {
|
|
73
|
+
throw new DecisionDialogueError(
|
|
74
|
+
'VALIDATION_ERROR', `unsupported applied authority kind: ${kind}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const target = requiredText(ref.ref, `applied_refs[${index}].ref`);
|
|
78
|
+
const field = ref.field === undefined
|
|
79
|
+
? undefined
|
|
80
|
+
: requiredText(ref.field, `applied_refs[${index}].field`);
|
|
81
|
+
const digest = ref.digest === undefined
|
|
82
|
+
? undefined
|
|
83
|
+
: requiredText(ref.digest, `applied_refs[${index}].digest`);
|
|
84
|
+
const hasValue = Object.hasOwn(ref, 'value');
|
|
85
|
+
const expectedValue = hasValue ? JSON.parse(JSON.stringify(ref.value)) : undefined;
|
|
86
|
+
if (digest !== undefined && !/^[0-9a-f]{64}$/.test(digest)) {
|
|
87
|
+
throw new DecisionDialogueError(
|
|
88
|
+
'VALIDATION_ERROR', `applied_refs[${index}].digest must be a SHA-256 digest`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const key = `${kind}\0${target}\0${field || ''}`;
|
|
92
|
+
if (seen.has(key)) {
|
|
93
|
+
throw new DecisionDialogueError('VALIDATION_ERROR', `duplicate applied authority ref: ${kind}:${target}`);
|
|
94
|
+
}
|
|
95
|
+
seen.add(key);
|
|
96
|
+
return {
|
|
97
|
+
kind,
|
|
98
|
+
ref: target,
|
|
99
|
+
...(field === undefined ? {} : { field }),
|
|
100
|
+
...(hasValue ? { value: expectedValue } : {}),
|
|
101
|
+
...(digest === undefined ? {} : { digest }),
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function canonicalJson(value) {
|
|
107
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
108
|
+
if (value && typeof value === 'object') {
|
|
109
|
+
return `{${Object.keys(value).sort().map(
|
|
110
|
+
(key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`,
|
|
111
|
+
).join(',')}}`;
|
|
112
|
+
}
|
|
113
|
+
return JSON.stringify(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function valueDigest(value) {
|
|
117
|
+
return crypto.createHash('sha256').update(canonicalJson(value)).digest('hex');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function valueAtField(value, field) {
|
|
121
|
+
const segments = field.split('.').map((item) => item.trim());
|
|
122
|
+
if (segments.some((item) => !item || ['__proto__', 'prototype', 'constructor'].includes(item))) {
|
|
123
|
+
throw new DecisionDialogueError('VALIDATION_ERROR', `invalid applied authority field: ${field}`);
|
|
124
|
+
}
|
|
125
|
+
let current = value;
|
|
126
|
+
for (const segment of segments) {
|
|
127
|
+
if (current === null || current === undefined || typeof current !== 'object'
|
|
128
|
+
|| !Object.hasOwn(current, segment)) {
|
|
129
|
+
throw new DecisionDialogueError(
|
|
130
|
+
'DECISION_APPLIED_REF_FIELD_NOT_FOUND',
|
|
131
|
+
`applied authority field does not exist: ${field}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
current = current[segment];
|
|
135
|
+
}
|
|
136
|
+
return current;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const ROW_FIELD_ALIASES = Object.freeze({
|
|
140
|
+
baseline: {
|
|
141
|
+
scope: 'scope_json',
|
|
142
|
+
worktree_files: 'worktree_files_json',
|
|
143
|
+
spec_refs: 'spec_refs_json',
|
|
144
|
+
evidence: 'evidence_json',
|
|
145
|
+
verification: 'verification_json',
|
|
146
|
+
unknowns: 'unknowns_json',
|
|
147
|
+
gaps: 'gaps_json',
|
|
148
|
+
classification: 'classification_json',
|
|
149
|
+
provider_refs: 'provider_refs_json',
|
|
150
|
+
},
|
|
151
|
+
change: {
|
|
152
|
+
docs_impact: 'docs_impact_json',
|
|
153
|
+
provider_refs: 'provider_refs_json',
|
|
154
|
+
baseline_bypass: 'baseline_bypass_json',
|
|
155
|
+
contract: 'contract_json',
|
|
156
|
+
classification: 'classification_json',
|
|
157
|
+
research_disposition: 'research_disposition_json',
|
|
158
|
+
},
|
|
159
|
+
workflow: {
|
|
160
|
+
metadata: 'metadata_json',
|
|
161
|
+
blockers: 'blockers_json',
|
|
162
|
+
approval: 'approval_json',
|
|
163
|
+
summary: 'summary_json',
|
|
164
|
+
},
|
|
165
|
+
task: {
|
|
166
|
+
acceptance: 'acceptance_json',
|
|
167
|
+
context_refs: 'context_refs_json',
|
|
168
|
+
docs_impact: 'docs_impact_json',
|
|
169
|
+
ownership: 'ownership_json',
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
function rowFieldValue(row, kind, field) {
|
|
174
|
+
const [head, ...tail] = field.split('.');
|
|
175
|
+
const column = ROW_FIELD_ALIASES[kind]?.[head] || head;
|
|
176
|
+
if (!Object.hasOwn(row, column)) {
|
|
177
|
+
throw new DecisionDialogueError(
|
|
178
|
+
'DECISION_APPLIED_REF_FIELD_NOT_FOUND',
|
|
179
|
+
`applied ${kind} authority field does not exist: ${field}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
let value = row[column];
|
|
183
|
+
if (column.endsWith('_json') && typeof value === 'string') {
|
|
184
|
+
try { value = JSON.parse(value); }
|
|
185
|
+
catch (error) {
|
|
186
|
+
throw new DecisionDialogueError(
|
|
187
|
+
'STATE_CORRUPT', `invalid ${kind}.${column}: ${error.message}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return tail.length === 0 ? value : valueAtField(value, tail.join('.'));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function verifyAppliedValue(applied, actual) {
|
|
195
|
+
if (!Object.hasOwn(applied, 'value')) {
|
|
196
|
+
throw new DecisionDialogueError(
|
|
197
|
+
'VALIDATION_ERROR',
|
|
198
|
+
`applied ${applied.kind} references require the exact field value`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (canonicalJson(actual) !== canonicalJson(applied.value)) {
|
|
202
|
+
throw new DecisionDialogueError(
|
|
203
|
+
'DECISION_APPLIED_REF_VALUE_MISMATCH',
|
|
204
|
+
`applied ${applied.kind} field value does not match current authority: ${applied.ref}#${applied.field}`,
|
|
205
|
+
{ expected: applied.value, actual },
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (applied.digest && applied.digest !== valueDigest(actual)) {
|
|
209
|
+
throw new DecisionDialogueError(
|
|
210
|
+
'DECISION_APPLIED_REF_STALE',
|
|
211
|
+
`applied ${applied.kind} field digest is stale: ${applied.ref}#${applied.field}`,
|
|
212
|
+
{ expected: applied.digest, actual: valueDigest(actual) },
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function projectFile(rootDir, relative, field) {
|
|
218
|
+
if (path.isAbsolute(relative)) {
|
|
219
|
+
throw new DecisionDialogueError('VALIDATION_ERROR', `${field} must be project-relative`);
|
|
220
|
+
}
|
|
221
|
+
const root = path.resolve(rootDir);
|
|
222
|
+
const file = path.resolve(root, relative);
|
|
223
|
+
if (file !== root && !file.startsWith(`${root}${path.sep}`)) {
|
|
224
|
+
throw new DecisionDialogueError('VALIDATION_ERROR', `${field} escapes project root`);
|
|
225
|
+
}
|
|
226
|
+
return file;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function readAppliedProjectFile(rootDir, relative, label) {
|
|
230
|
+
try {
|
|
231
|
+
return readStableProjectFile(rootDir, relative);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error?.code === 'PROJECT_FILE_MISSING') {
|
|
234
|
+
throw new DecisionDialogueError(
|
|
235
|
+
'DECISION_APPLIED_REF_NOT_FOUND',
|
|
236
|
+
`${label} is missing: ${relative}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
throw new DecisionDialogueError(
|
|
240
|
+
'DECISION_APPLIED_REF_UNSAFE',
|
|
241
|
+
`${label} cannot be read as physical project authority: ${relative}`,
|
|
242
|
+
{ cause: error.code || error.message },
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function registeredArtifactForRef(db, ref) {
|
|
248
|
+
const byId = db.prepare('SELECT id FROM artifacts WHERE id = ?').get(ref);
|
|
249
|
+
if (byId) return artifactRegistry.getArtifact(db, { id: byId.id });
|
|
250
|
+
try {
|
|
251
|
+
return artifactRegistry.getArtifact(db, { path: ref });
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error?.code === 'ARTIFACT_NOT_FOUND') return null;
|
|
254
|
+
throw new DecisionDialogueError(
|
|
255
|
+
'DECISION_APPLIED_REF_AUTHORITY_CONFLICT',
|
|
256
|
+
`applied artifact authority is ambiguous or invalid: ${ref}`,
|
|
257
|
+
{ cause: error.code || error.message },
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function verifyAppliedFileDigest(applied, read, registered = null) {
|
|
263
|
+
if (!applied.digest) {
|
|
264
|
+
throw new DecisionDialogueError(
|
|
265
|
+
'VALIDATION_ERROR',
|
|
266
|
+
`applied ${applied.kind} references require the exact content digest`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (registered?.status === 'stale'
|
|
270
|
+
|| (registered?.digest && registered.digest !== read.digest)
|
|
271
|
+
|| applied.digest !== read.digest) {
|
|
272
|
+
throw new DecisionDialogueError(
|
|
273
|
+
'DECISION_APPLIED_REF_STALE',
|
|
274
|
+
`applied ${applied.kind} digest is stale: ${registered?.path || applied.ref}`,
|
|
275
|
+
{
|
|
276
|
+
registered_digest: registered?.digest || null,
|
|
277
|
+
expected: applied.digest,
|
|
278
|
+
actual: read.digest,
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function validateAppliedRefs(db, refs, { rootDir = process.cwd() } = {}) {
|
|
285
|
+
const tableByKind = {
|
|
286
|
+
baseline: 'baselines',
|
|
287
|
+
change: 'changes',
|
|
288
|
+
workflow: 'workflow_runs',
|
|
289
|
+
task: 'tasks',
|
|
290
|
+
};
|
|
291
|
+
for (const applied of refs) {
|
|
292
|
+
const table = tableByKind[applied.kind];
|
|
293
|
+
if (table) {
|
|
294
|
+
if (!applied.field) {
|
|
295
|
+
throw new DecisionDialogueError(
|
|
296
|
+
'VALIDATION_ERROR',
|
|
297
|
+
`applied ${applied.kind} references require an exact field`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const row = db.prepare(`SELECT * FROM ${table} WHERE id = ?`).get(applied.ref);
|
|
301
|
+
if (!row) {
|
|
302
|
+
throw new DecisionDialogueError(
|
|
303
|
+
'DECISION_APPLIED_REF_NOT_FOUND',
|
|
304
|
+
`applied ${applied.kind} authority does not exist: ${applied.ref}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
verifyAppliedValue(applied, rowFieldValue(row, applied.kind, applied.field));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (applied.kind === 'artifact') {
|
|
312
|
+
const artifact = registeredArtifactForRef(db, applied.ref);
|
|
313
|
+
if (!artifact) {
|
|
314
|
+
throw new DecisionDialogueError(
|
|
315
|
+
'DECISION_APPLIED_REF_NOT_FOUND',
|
|
316
|
+
`applied artifact authority is not registered: ${applied.ref}`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const read = readAppliedProjectFile(
|
|
320
|
+
rootDir, artifact.path, 'applied artifact file',
|
|
321
|
+
);
|
|
322
|
+
verifyAppliedFileDigest(applied, read, artifact);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const registered = registeredArtifactForRef(db, applied.ref);
|
|
327
|
+
const read = readAppliedProjectFile(
|
|
328
|
+
rootDir,
|
|
329
|
+
registered?.path || applied.ref,
|
|
330
|
+
'applied specification file',
|
|
331
|
+
);
|
|
332
|
+
verifyAppliedFileDigest(applied, read, registered);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
56
336
|
function normalizeOptions(value) {
|
|
57
337
|
if (value === undefined) return [];
|
|
58
338
|
if (!Array.isArray(value) || value.length > 3) {
|
|
@@ -385,9 +665,10 @@ function deferDecision(db, input = {}) {
|
|
|
385
665
|
});
|
|
386
666
|
}
|
|
387
667
|
|
|
388
|
-
function completeDecisionThread(db, input = {}) {
|
|
668
|
+
function completeDecisionThread(db, input = {}, options = {}) {
|
|
389
669
|
const id = requiredText(input.id, 'id');
|
|
390
670
|
const summary = requiredText(input.summary, 'summary');
|
|
671
|
+
const appliedRefs = normalizeAppliedRefs(input.applied_refs);
|
|
391
672
|
return ops.tx(db, () => {
|
|
392
673
|
const thread = readDecisionThread(db, id);
|
|
393
674
|
if (!thread) {
|
|
@@ -416,6 +697,7 @@ function completeDecisionThread(db, input = {}) {
|
|
|
416
697
|
{ blockers },
|
|
417
698
|
);
|
|
418
699
|
}
|
|
700
|
+
validateAppliedRefs(db, appliedRefs, options);
|
|
419
701
|
const ts = nowIso();
|
|
420
702
|
db.prepare(
|
|
421
703
|
`UPDATE decision_threads
|
|
@@ -424,17 +706,76 @@ function completeDecisionThread(db, input = {}) {
|
|
|
424
706
|
).run(JSON.stringify({
|
|
425
707
|
text: summary,
|
|
426
708
|
completion_kind: 'normalized_state',
|
|
709
|
+
applied_refs: appliedRefs,
|
|
427
710
|
completed_at: ts,
|
|
428
711
|
}), ts, ts, id);
|
|
429
712
|
ops.appendEventInTx(db, {
|
|
430
713
|
type: 'decision_thread_completed',
|
|
431
714
|
change_id: thread.change_id,
|
|
432
|
-
payload: {
|
|
715
|
+
payload: {
|
|
716
|
+
thread_id: id,
|
|
717
|
+
item_count: thread.items.length,
|
|
718
|
+
applied_ref_count: appliedRefs.length,
|
|
719
|
+
},
|
|
433
720
|
});
|
|
434
721
|
return readDecisionThread(db, id);
|
|
435
722
|
});
|
|
436
723
|
}
|
|
437
724
|
|
|
725
|
+
function acceptedIntent(db, bindings = {}, { limit = 10 } = {}) {
|
|
726
|
+
const clauses = [];
|
|
727
|
+
const params = {};
|
|
728
|
+
if (bindings.workflow_run_id) {
|
|
729
|
+
clauses.push('workflow_run_id = @workflow_run_id');
|
|
730
|
+
params.workflow_run_id = bindings.workflow_run_id;
|
|
731
|
+
}
|
|
732
|
+
if (bindings.change_id) {
|
|
733
|
+
clauses.push('change_id = @change_id');
|
|
734
|
+
params.change_id = bindings.change_id;
|
|
735
|
+
}
|
|
736
|
+
if (bindings.baseline_id) {
|
|
737
|
+
clauses.push('(baseline_id = @baseline_id AND change_id IS NULL)');
|
|
738
|
+
params.baseline_id = bindings.baseline_id;
|
|
739
|
+
}
|
|
740
|
+
if (clauses.length === 0) return [];
|
|
741
|
+
const threadRows = db.prepare(
|
|
742
|
+
`SELECT id FROM decision_threads
|
|
743
|
+
WHERE status IN ('completed', 'confirmed')
|
|
744
|
+
AND (${clauses.join(' OR ')})
|
|
745
|
+
ORDER BY updated_at DESC, rowid DESC
|
|
746
|
+
LIMIT 100`,
|
|
747
|
+
).all(params);
|
|
748
|
+
const items = [];
|
|
749
|
+
for (const row of threadRows) {
|
|
750
|
+
const thread = readDecisionThread(db, row.id);
|
|
751
|
+
const appliedRefs = Array.isArray(thread.summary.applied_refs)
|
|
752
|
+
? thread.summary.applied_refs
|
|
753
|
+
: [];
|
|
754
|
+
for (const item of thread.items) {
|
|
755
|
+
if (!['answered', 'delegated', 'deferred'].includes(item.status)) continue;
|
|
756
|
+
const resolution = item.resolution || {};
|
|
757
|
+
items.push({
|
|
758
|
+
thread_id: thread.id,
|
|
759
|
+
decision_id: item.id,
|
|
760
|
+
status: item.status,
|
|
761
|
+
authority: resolution.authority || null,
|
|
762
|
+
decision: resolution.decision || null,
|
|
763
|
+
rationale: resolution.rationale || null,
|
|
764
|
+
decided_by: resolution.decided_by || null,
|
|
765
|
+
delegated_to: resolution.delegated_to || null,
|
|
766
|
+
consequences: resolution.consequences || null,
|
|
767
|
+
revisit_condition: resolution.revisit_condition || null,
|
|
768
|
+
effects: item.effects,
|
|
769
|
+
applied_refs: appliedRefs,
|
|
770
|
+
resolved_at: item.resolved_at,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return items
|
|
775
|
+
.sort((left, right) => String(right.resolved_at || '').localeCompare(String(left.resolved_at || '')))
|
|
776
|
+
.slice(0, Math.min(Math.max(Number(limit) || 10, 1), 20));
|
|
777
|
+
}
|
|
778
|
+
|
|
438
779
|
function supersedeDecision(db, input = {}) {
|
|
439
780
|
return ops.tx(db, () => {
|
|
440
781
|
const id = requiredText(input.id, 'id');
|
|
@@ -663,7 +1004,7 @@ function decisionGate(db, bindings = {}, { rootDir = process.cwd() } = {}) {
|
|
|
663
1004
|
params.change_id = bindings.change_id;
|
|
664
1005
|
}
|
|
665
1006
|
if (bindings.baseline_id) {
|
|
666
|
-
clauses.push('(baseline_id = @baseline_id AND change_id IS NULL
|
|
1007
|
+
clauses.push('(baseline_id = @baseline_id AND change_id IS NULL)');
|
|
667
1008
|
params.baseline_id = bindings.baseline_id;
|
|
668
1009
|
}
|
|
669
1010
|
if (clauses.length === 0) return { ready: true, blockers: [], thread: null, current_decision: null };
|
|
@@ -779,6 +1120,7 @@ module.exports = {
|
|
|
779
1120
|
completeDecisionThread,
|
|
780
1121
|
supersedeDecision,
|
|
781
1122
|
checkpointDecisionThread,
|
|
1123
|
+
acceptedIntent,
|
|
782
1124
|
decisionGate,
|
|
783
1125
|
assertDecisionGate,
|
|
784
1126
|
assertConfirmedDecisionCheckpoint,
|