ultra-builder-pro-cli 0.19.1 → 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 +90 -1
- package/README.md +62 -29
- package/adapters/_shared/codex-assets.cjs +69 -11
- package/adapters/_shared/interaction-contract.cjs +60 -5
- 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/commands/ultra-research.md +1 -1
- package/docs/AGENT-CONTEXT.md +79 -45
- package/docs/ARCHITECTURE.md +75 -38
- package/docs/ARTIFACT-AUTHORITY.md +234 -0
- package/docs/DECISIONS.md +20 -10
- package/docs/LEGACY-CLI-CROSSWALK.md +15 -11
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +4 -3
- package/docs/ROADMAP.md +21 -6
- package/docs/RUNTIME-COMPAT-MATRIX.md +30 -13
- package/docs/STATE-DB-ACCESS-POLICY.md +11 -9
- package/docs/WORKFLOW-LIFECYCLE.md +105 -54
- 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 +427 -12
- 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 +375 -12
- 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 +397 -13
- package/mcp-server/lib/state-ops.cjs +248 -49
- package/mcp-server/lib/telemetry.cjs +12 -3
- package/mcp-server/lib/workflow-state.cjs +614 -62
- package/mcp-server/server.cjs +282 -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 +38 -13
- 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 +68 -18
- package/skills/ultra-research/SKILL.md +45 -32
- package/skills/ultra-review/SKILL.md +9 -5
- package/skills/ultra-status/SKILL.md +9 -4
- package/skills/ultra-test/SKILL.md +15 -7
- package/skills/ultra-think/SKILL.md +6 -0
- package/skills/ultra-think/references/decision-dialogue.md +61 -14
- package/skills/ultra-verify/SKILL.md +4 -1
- package/spec/cli-protocol.md +19 -7
- package/spec/mcp-tools.yaml +531 -46
- 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 +57 -17
- 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 +34 -7
- 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) {
|
|
@@ -120,6 +400,7 @@ function readDecisionThread(db, id) {
|
|
|
120
400
|
items,
|
|
121
401
|
started_at: row.started_at,
|
|
122
402
|
updated_at: row.updated_at,
|
|
403
|
+
completed_at: row.completed_at,
|
|
123
404
|
confirmed_at: row.confirmed_at,
|
|
124
405
|
};
|
|
125
406
|
}
|
|
@@ -384,6 +665,117 @@ function deferDecision(db, input = {}) {
|
|
|
384
665
|
});
|
|
385
666
|
}
|
|
386
667
|
|
|
668
|
+
function completeDecisionThread(db, input = {}, options = {}) {
|
|
669
|
+
const id = requiredText(input.id, 'id');
|
|
670
|
+
const summary = requiredText(input.summary, 'summary');
|
|
671
|
+
const appliedRefs = normalizeAppliedRefs(input.applied_refs);
|
|
672
|
+
return ops.tx(db, () => {
|
|
673
|
+
const thread = readDecisionThread(db, id);
|
|
674
|
+
if (!thread) {
|
|
675
|
+
throw new DecisionDialogueError('DECISION_THREAD_NOT_FOUND', `decision thread ${id} not found`);
|
|
676
|
+
}
|
|
677
|
+
if (thread.status === 'checkpoint_ready') {
|
|
678
|
+
throw new DecisionDialogueError(
|
|
679
|
+
'DECISION_CHECKPOINT_PENDING', `confirm or revise checkpoint ${id} before completing the thread`,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
if (thread.status !== 'active') {
|
|
683
|
+
throw new DecisionDialogueError(
|
|
684
|
+
'DECISION_THREAD_NOT_MUTABLE', `decision thread ${id} is ${thread.status}`,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
const open = thread.items.find((item) => item.status === 'open');
|
|
688
|
+
const deferredBlocking = thread.items.filter((item) => item.blocking && item.status === 'deferred');
|
|
689
|
+
if (open || deferredBlocking.length > 0) {
|
|
690
|
+
const blockers = [
|
|
691
|
+
...(open ? [`DECISION_AWAITING_OWNER:${open.id}`] : []),
|
|
692
|
+
...deferredBlocking.map((item) => `DECISION_DEFERRED_BLOCKING:${item.id}`),
|
|
693
|
+
];
|
|
694
|
+
throw new DecisionDialogueError(
|
|
695
|
+
'DECISION_ALIGNMENT_BLOCKING',
|
|
696
|
+
`decision thread ${id} still contains unresolved blocking state`,
|
|
697
|
+
{ blockers },
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
validateAppliedRefs(db, appliedRefs, options);
|
|
701
|
+
const ts = nowIso();
|
|
702
|
+
db.prepare(
|
|
703
|
+
`UPDATE decision_threads
|
|
704
|
+
SET status = 'completed', summary_json = ?, completed_at = ?, updated_at = ?
|
|
705
|
+
WHERE id = ?`,
|
|
706
|
+
).run(JSON.stringify({
|
|
707
|
+
text: summary,
|
|
708
|
+
completion_kind: 'normalized_state',
|
|
709
|
+
applied_refs: appliedRefs,
|
|
710
|
+
completed_at: ts,
|
|
711
|
+
}), ts, ts, id);
|
|
712
|
+
ops.appendEventInTx(db, {
|
|
713
|
+
type: 'decision_thread_completed',
|
|
714
|
+
change_id: thread.change_id,
|
|
715
|
+
payload: {
|
|
716
|
+
thread_id: id,
|
|
717
|
+
item_count: thread.items.length,
|
|
718
|
+
applied_ref_count: appliedRefs.length,
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
return readDecisionThread(db, id);
|
|
722
|
+
});
|
|
723
|
+
}
|
|
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
|
+
|
|
387
779
|
function supersedeDecision(db, input = {}) {
|
|
388
780
|
return ops.tx(db, () => {
|
|
389
781
|
const id = requiredText(input.id, 'id');
|
|
@@ -411,6 +803,15 @@ function supersedeDecision(db, input = {}) {
|
|
|
411
803
|
checkpoint_json = json_set(checkpoint_json, '$.invalidated', json('true'),
|
|
412
804
|
'$.invalidated_at', ?, '$.invalidation_reason', ?), updated_at = ? WHERE id = ?`,
|
|
413
805
|
).run(ts, reason, ts, thread.id);
|
|
806
|
+
} else if (thread.status === 'completed') {
|
|
807
|
+
db.prepare(
|
|
808
|
+
`UPDATE decision_threads
|
|
809
|
+
SET status = 'active', completed_at = NULL,
|
|
810
|
+
summary_json = json_set(summary_json, '$.invalidated', json('true'),
|
|
811
|
+
'$.invalidated_at', ?, '$.invalidation_reason', ?),
|
|
812
|
+
updated_at = ?
|
|
813
|
+
WHERE id = ?`,
|
|
814
|
+
).run(ts, reason, ts, thread.id);
|
|
414
815
|
}
|
|
415
816
|
insertQuestionInTx(db, readDecisionThread(db, thread.id), replacement, id);
|
|
416
817
|
return readDecisionThread(db, thread.id);
|
|
@@ -515,8 +916,10 @@ function checkpointDecisionThread(db, input = {}, { rootDir = process.cwd() } =
|
|
|
515
916
|
if (!thread) {
|
|
516
917
|
throw new DecisionDialogueError('DECISION_THREAD_NOT_FOUND', `decision thread ${id} not found`);
|
|
517
918
|
}
|
|
518
|
-
if (
|
|
519
|
-
throw new DecisionDialogueError(
|
|
919
|
+
if (['completed', 'cancelled'].includes(thread.status)) {
|
|
920
|
+
throw new DecisionDialogueError(
|
|
921
|
+
'DECISION_THREAD_NOT_MUTABLE', `decision thread ${id} is ${thread.status}`,
|
|
922
|
+
);
|
|
520
923
|
}
|
|
521
924
|
const open = thread.items.find((item) => item.status === 'open');
|
|
522
925
|
const deferredBlocking = thread.items.filter((item) => item.blocking && item.status === 'deferred');
|
|
@@ -601,7 +1004,7 @@ function decisionGate(db, bindings = {}, { rootDir = process.cwd() } = {}) {
|
|
|
601
1004
|
params.change_id = bindings.change_id;
|
|
602
1005
|
}
|
|
603
1006
|
if (bindings.baseline_id) {
|
|
604
|
-
clauses.push('(baseline_id = @baseline_id AND change_id IS NULL
|
|
1007
|
+
clauses.push('(baseline_id = @baseline_id AND change_id IS NULL)');
|
|
605
1008
|
params.baseline_id = bindings.baseline_id;
|
|
606
1009
|
}
|
|
607
1010
|
if (clauses.length === 0) return { ready: true, blockers: [], thread: null, current_decision: null };
|
|
@@ -612,6 +1015,7 @@ function decisionGate(db, bindings = {}, { rootDir = process.cwd() } = {}) {
|
|
|
612
1015
|
).all(params);
|
|
613
1016
|
const blockers = [];
|
|
614
1017
|
let selected = null;
|
|
1018
|
+
let fallback = null;
|
|
615
1019
|
for (const row of rows) {
|
|
616
1020
|
const thread = readDecisionThread(db, row.id);
|
|
617
1021
|
if (thread.status === 'confirmed') {
|
|
@@ -622,23 +1026,25 @@ function decisionGate(db, bindings = {}, { rootDir = process.cwd() } = {}) {
|
|
|
622
1026
|
}
|
|
623
1027
|
continue;
|
|
624
1028
|
}
|
|
625
|
-
|
|
626
|
-
|
|
1029
|
+
fallback ||= thread;
|
|
1030
|
+
const priorBlockerCount = blockers.length;
|
|
1031
|
+
if (thread.current_decision?.blocking) {
|
|
1032
|
+
blockers.push(`DECISION_AWAITING_OWNER:${thread.current_decision.id}`);
|
|
1033
|
+
}
|
|
627
1034
|
for (const item of thread.items) {
|
|
628
1035
|
if (item.blocking && item.status === 'deferred') {
|
|
629
1036
|
blockers.push(`DECISION_DEFERRED_BLOCKING:${item.id}`);
|
|
630
1037
|
}
|
|
631
1038
|
}
|
|
632
1039
|
if (thread.status === 'checkpoint_ready') blockers.push(`DECISION_CHECKPOINT_CONFIRMATION_REQUIRED:${thread.id}`);
|
|
633
|
-
|
|
634
|
-
blockers.push(`DECISION_CHECKPOINT_REQUIRED:${thread.id}`);
|
|
635
|
-
}
|
|
1040
|
+
if (blockers.length > priorBlockerCount) selected ||= thread;
|
|
636
1041
|
}
|
|
1042
|
+
const reported = selected || fallback;
|
|
637
1043
|
return {
|
|
638
1044
|
ready: blockers.length === 0,
|
|
639
1045
|
blockers: [...new Set(blockers)],
|
|
640
|
-
thread:
|
|
641
|
-
current_decision:
|
|
1046
|
+
thread: reported,
|
|
1047
|
+
current_decision: reported?.current_decision || null,
|
|
642
1048
|
};
|
|
643
1049
|
}
|
|
644
1050
|
|
|
@@ -656,8 +1062,10 @@ function assertDecisionGate(db, bindings = {}, options = {}) {
|
|
|
656
1062
|
function inspectDecisionHealth(db, { rootDir = process.cwd() } = {}) {
|
|
657
1063
|
const threads = listDecisionThreads(db, { limit: 500 });
|
|
658
1064
|
const active = threads.filter((thread) => thread.status === 'active');
|
|
1065
|
+
const completed = threads.filter((thread) => thread.status === 'completed');
|
|
659
1066
|
const checkpointReady = threads.filter((thread) => thread.status === 'checkpoint_ready');
|
|
660
1067
|
const awaiting = active.filter((thread) => thread.current_decision);
|
|
1068
|
+
const awaitingBlocking = awaiting.filter((thread) => thread.current_decision.blocking);
|
|
661
1069
|
const deferredBlocking = threads.flatMap((thread) => thread.items.filter(
|
|
662
1070
|
(item) => item.blocking && item.status === 'deferred',
|
|
663
1071
|
));
|
|
@@ -678,15 +1086,20 @@ function inspectDecisionHealth(db, { rootDir = process.cwd() } = {}) {
|
|
|
678
1086
|
}
|
|
679
1087
|
}
|
|
680
1088
|
}
|
|
681
|
-
|
|
682
|
-
|
|
1089
|
+
let currentThread = awaitingBlocking[0] || checkpointReady[0] || awaiting[0] || active[0] || null;
|
|
1090
|
+
if (staleArtifacts.length > 0) {
|
|
1091
|
+
currentThread = threads.find((thread) => thread.id === staleArtifacts[0].thread_id) || currentThread;
|
|
1092
|
+
}
|
|
1093
|
+
const currentThreadId = staleArtifacts[0]?.thread_id || currentThread?.id || null;
|
|
683
1094
|
const status = staleArtifacts.length > 0
|
|
684
1095
|
? 'fail'
|
|
685
1096
|
: (active.length > 0 || checkpointReady.length > 0 || deferredBlocking.length > 0 ? 'warning' : 'pass');
|
|
686
1097
|
return {
|
|
687
1098
|
status,
|
|
688
1099
|
active: active.length,
|
|
1100
|
+
completed: completed.length,
|
|
689
1101
|
awaiting_owner: awaiting.length,
|
|
1102
|
+
awaiting_blocking: awaitingBlocking.length,
|
|
690
1103
|
checkpoint_ready: checkpointReady.length,
|
|
691
1104
|
deferred_blocking: deferredBlocking.length,
|
|
692
1105
|
stale_artifacts: staleArtifacts,
|
|
@@ -704,8 +1117,10 @@ module.exports = {
|
|
|
704
1117
|
resolveDecision,
|
|
705
1118
|
delegateDecision,
|
|
706
1119
|
deferDecision,
|
|
1120
|
+
completeDecisionThread,
|
|
707
1121
|
supersedeDecision,
|
|
708
1122
|
checkpointDecisionThread,
|
|
1123
|
+
acceptedIntent,
|
|
709
1124
|
decisionGate,
|
|
710
1125
|
assertDecisionGate,
|
|
711
1126
|
assertConfirmedDecisionCheckpoint,
|