ultra-builder-pro-cli 0.14.0 → 0.16.0
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 +11 -7
- package/.ultra-template/specs/architecture.md +36 -36
- package/.ultra-template/specs/discovery.md +46 -33
- package/.ultra-template/specs/product.md +38 -29
- package/.ultra-template/specs/research-distillate.md +48 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
- package/CHANGELOG.md +77 -1
- package/README.md +46 -24
- package/adapters/_shared/codex-assets.cjs +6 -2
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/handbook.cjs +2 -1
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +3 -2
- package/agents/review-coordinator.md +9 -6
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +1 -1
- package/commands/ultra-think.md +2 -2
- package/docs/AGENT-CONTEXT.md +43 -19
- package/docs/ARCHITECTURE.md +54 -13
- package/docs/PLAN.zh-CN.md +4 -2
- package/docs/ROADMAP.md +10 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +33 -29
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
- package/docs/WORKFLOW-LIFECYCLE.md +342 -0
- package/hooks/README.md +6 -1
- package/hooks/health_check.py +12 -3
- package/hooks/subagent_tracker.py +52 -77
- package/mcp-server/hook-event.cjs +34 -0
- package/mcp-server/lib/archive-journal.cjs +17 -5
- package/mcp-server/lib/baseline-workflow.cjs +158 -18
- package/mcp-server/lib/change-workflow.cjs +598 -85
- package/mcp-server/lib/context-spine.cjs +305 -25
- package/mcp-server/lib/decision-dialogue.cjs +713 -0
- package/mcp-server/lib/doctor.cjs +42 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +15 -0
- package/mcp-server/lib/projector.cjs +57 -3
- package/mcp-server/lib/spec-learning.cjs +79 -2
- package/mcp-server/lib/state-db.cjs +168 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/workflow-state.cjs +2305 -0
- package/mcp-server/server.cjs +215 -15
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +13 -3
- package/skills/ultra-change/SKILL.md +94 -80
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +76 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +62 -59
- package/skills/ultra-doctor/SKILL.md +61 -82
- package/skills/ultra-init/SKILL.md +101 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +99 -85
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +102 -60
- package/skills/ultra-research/references/00-problem-validation.md +36 -0
- package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
- package/skills/ultra-research/references/02-market-assessment.md +30 -0
- package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
- package/skills/ultra-research/references/04-product-strategy.md +32 -0
- package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
- package/skills/ultra-research/references/10-user-personas.md +29 -0
- package/skills/ultra-research/references/11-user-scenarios.md +28 -0
- package/skills/ultra-research/references/20-user-stories.md +30 -0
- package/skills/ultra-research/references/21-features-scope.md +29 -0
- package/skills/ultra-research/references/22-success-metrics.md +28 -0
- package/skills/ultra-research/references/30-architecture-context.md +30 -0
- package/skills/ultra-research/references/31-solution-strategy.md +28 -0
- package/skills/ultra-research/references/32-building-blocks.md +28 -0
- package/skills/ultra-research/references/40-deployment.md +29 -0
- package/skills/ultra-research/references/41-quality-risks.md +28 -0
- package/skills/ultra-research/references/99-synthesis.md +35 -0
- package/skills/ultra-review/SKILL.md +63 -73
- package/skills/ultra-review/references/review-modes.md +27 -0
- package/skills/ultra-review/references/unified-schema.md +25 -0
- package/skills/ultra-review/scripts/review_wait.py +40 -3
- package/skills/ultra-status/SKILL.md +41 -41
- package/skills/ultra-test/SKILL.md +54 -55
- package/skills/ultra-think/SKILL.md +26 -12
- package/skills/ultra-think/references/decision-dialogue.md +130 -0
- package/skills/ultra-verify/SKILL.md +1 -1
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/spec/cli-protocol.md +17 -3
- package/spec/mcp-tools.yaml +683 -64
- package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
- package/spec/schemas/delivery-report.v1.schema.json +111 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +140 -1
- package/spec/schemas/tasks.v4.5.schema.json +8 -1
- package/spec/schemas/test-report.v1.schema.json +194 -0
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/migrate.cjs +3 -3
- package/ultra-tools/commands/status.cjs +228 -12
- package/.ultra-template/delivery-report.json +0 -9
- package/.ultra-template/test-report.json +0 -8
- package/adapters/_shared/tests/file-ops.test.cjs +0 -54
- package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
- package/adapters/_shared/tests/handbook.test.cjs +0 -101
- package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
- package/adapters/_shared/tests/provenance.test.cjs +0 -133
- package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
- package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
- package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
- package/adapters/_shared/tests/validate.test.cjs +0 -30
- package/adapters/tests/claude.test.cjs +0 -138
- package/adapters/tests/codex-hook.test.cjs +0 -142
- package/adapters/tests/codex.test.cjs +0 -430
- package/adapters/tests/kimi-hook.test.cjs +0 -149
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -357
- package/mcp-server/lib/archive-journal.test.cjs +0 -35
- package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
- package/mcp-server/lib/change-workflow.test.cjs +0 -971
- package/mcp-server/lib/context-spine.test.cjs +0 -93
- package/mcp-server/lib/doctor.test.cjs +0 -148
- package/mcp-server/lib/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -82
- package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
- package/mcp-server/lib/runtime-state.test.cjs +0 -92
- package/mcp-server/lib/spec-learning.test.cjs +0 -67
- package/mcp-server/lib/state-db.test.cjs +0 -314
- package/mcp-server/lib/task-expander.test.cjs +0 -169
- package/mcp-server/lib/topo.test.cjs +0 -158
- package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
- package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
- package/mcp-server/tests/concurrency-update.test.cjs +0 -96
- package/mcp-server/tests/concurrency.test.cjs +0 -112
- package/mcp-server/tests/db-ops.test.cjs +0 -121
- package/mcp-server/tests/init-project.test.cjs +0 -393
- package/mcp-server/tests/migrate.test.cjs +0 -232
- package/mcp-server/tests/projector.test.cjs +0 -162
- package/mcp-server/tests/server-contract.test.cjs +0 -1467
- package/mcp-server/tests/staleness.test.cjs +0 -190
- package/mcp-server/tests/state-db.test.cjs +0 -106
- package/mcp-server/tests/state-ops.test.cjs +0 -449
- package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
- package/mcp-server/tests/telemetry.test.cjs +0 -219
- package/orchestrator/planner/plan-builder.test.cjs +0 -165
- package/orchestrator/tests/auto-merge.test.cjs +0 -216
- package/orchestrator/tests/daemon.test.cjs +0 -311
- package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
- package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
- package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
- package/orchestrator/tests/recovery.test.cjs +0 -190
- package/orchestrator/tests/session-runner.test.cjs +0 -314
- package/orchestrator/tests/skill-miner.test.cjs +0 -147
- package/orchestrator/tests/worktree-manager.test.cjs +0 -148
- package/skills/ultra-research/references/architecture.md +0 -38
- package/skills/ultra-research/references/problem-product.md +0 -39
- package/skills/ultra-research/references/quality-delivery.md +0 -34
- package/skills/ultra-research/references/synthesis.md +0 -38
- package/skills/ultra-research/references/users-scope.md +0 -40
- package/ultra-tools/legacy-memory.test.cjs +0 -80
- package/ultra-tools/status-command.test.cjs +0 -181
- package/ultra-tools/system-command.test.cjs +0 -309
|
@@ -33,8 +33,14 @@ function rowToCandidate(row) {
|
|
|
33
33
|
catch (error) {
|
|
34
34
|
throw new SpecLearningError('STATE_CORRUPT', `invalid spec-learning evidence: ${error.message}`);
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
let applyEvidence;
|
|
37
|
+
try { applyEvidence = JSON.parse(row.apply_evidence_json || '[]'); }
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw new SpecLearningError('STATE_CORRUPT', `invalid spec-learning apply evidence: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
const candidate = { ...row, evidence, apply_evidence: applyEvidence };
|
|
37
42
|
delete candidate.evidence_json;
|
|
43
|
+
delete candidate.apply_evidence_json;
|
|
38
44
|
return candidate;
|
|
39
45
|
}
|
|
40
46
|
|
|
@@ -102,6 +108,66 @@ function normalizeEvidence(value) {
|
|
|
102
108
|
return [...new Set(value.map((entry) => entry.trim()))];
|
|
103
109
|
}
|
|
104
110
|
|
|
111
|
+
function targetFile(rootDir, targetRef) {
|
|
112
|
+
const separator = String(targetRef || '').lastIndexOf('#');
|
|
113
|
+
if (separator <= 0 || separator === targetRef.length - 1) {
|
|
114
|
+
throw new SpecLearningError('VALIDATION_ERROR', 'target_ref must use project-relative path#anchor');
|
|
115
|
+
}
|
|
116
|
+
const relative = targetRef.slice(0, separator);
|
|
117
|
+
if (path.isAbsolute(relative)) {
|
|
118
|
+
throw new SpecLearningError('VALIDATION_ERROR', 'target_ref must be project-relative');
|
|
119
|
+
}
|
|
120
|
+
const root = path.resolve(rootDir);
|
|
121
|
+
const file = path.resolve(root, relative);
|
|
122
|
+
if (file !== root && !file.startsWith(`${root}${path.sep}`)) {
|
|
123
|
+
throw new SpecLearningError('VALIDATION_ERROR', 'target_ref escapes the project root');
|
|
124
|
+
}
|
|
125
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
|
126
|
+
throw new SpecLearningError('LEARNING_TARGET_MISSING', `target file does not exist: ${relative}`);
|
|
127
|
+
}
|
|
128
|
+
return { file, relative };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function digestFile(file) {
|
|
132
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function verifyApplyEvidence(input, current, rootDir) {
|
|
136
|
+
const appliedRef = typeof input.applied_ref === 'string' ? input.applied_ref.trim() : '';
|
|
137
|
+
const beforeDigest = typeof input.before_digest === 'string' ? input.before_digest.trim() : '';
|
|
138
|
+
const afterDigest = typeof input.after_digest === 'string' ? input.after_digest.trim() : '';
|
|
139
|
+
const evidence = normalizeEvidence(input.apply_evidence);
|
|
140
|
+
if (appliedRef !== current.target_ref || !/^[0-9a-f]{64}$/.test(beforeDigest)
|
|
141
|
+
|| !/^[0-9a-f]{64}$/.test(afterDigest) || beforeDigest === afterDigest
|
|
142
|
+
|| evidence.length === 0) {
|
|
143
|
+
throw new SpecLearningError(
|
|
144
|
+
'LEARNING_APPLY_EVIDENCE_REQUIRED',
|
|
145
|
+
'apply requires the exact target ref, distinct before/after digests, and durable evidence refs',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (current.before_digest && current.before_digest !== beforeDigest) {
|
|
149
|
+
throw new SpecLearningError(
|
|
150
|
+
'LEARNING_APPLY_EVIDENCE_MISMATCH', 'before_digest does not match the proposed target state',
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const target = targetFile(rootDir, appliedRef);
|
|
154
|
+
if (digestFile(target.file) !== afterDigest) {
|
|
155
|
+
throw new SpecLearningError(
|
|
156
|
+
'LEARNING_APPLY_EVIDENCE_MISMATCH', 'after_digest does not match the current target file',
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const workflows = require('./workflow-state.cjs');
|
|
160
|
+
try {
|
|
161
|
+
workflows.resolveProjectSourceRef(rootDir, appliedRef, 'applied_ref');
|
|
162
|
+
for (const ref of evidence) workflows.resolveProjectSourceRef(rootDir, ref, 'apply_evidence');
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw new SpecLearningError(
|
|
165
|
+
'LEARNING_APPLY_EVIDENCE_MISMATCH', `applied target or evidence anchor is invalid: ${error.message}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return { appliedRef, beforeDigest, afterDigest, evidence };
|
|
169
|
+
}
|
|
170
|
+
|
|
105
171
|
function proposeSpecLearning(db, input, { rootDir = process.cwd() } = {}) {
|
|
106
172
|
const change = mutableChange(db, input.change_id);
|
|
107
173
|
const id = String(input.id || `learning-${crypto.randomUUID().slice(0, 12)}`).trim();
|
|
@@ -120,6 +186,8 @@ function proposeSpecLearning(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
120
186
|
}
|
|
121
187
|
}
|
|
122
188
|
const evidence = normalizeEvidence(input.evidence);
|
|
189
|
+
const proposedTarget = targetFile(rootDir, targetRef);
|
|
190
|
+
const proposedDigest = digestFile(proposedTarget.file);
|
|
123
191
|
const proposedAt = nowIso();
|
|
124
192
|
return ops.tx(db, () => {
|
|
125
193
|
try {
|
|
@@ -128,6 +196,9 @@ function proposeSpecLearning(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
128
196
|
(id, change_id, task_id, target_ref, summary, evidence_json, proposed_at)
|
|
129
197
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
130
198
|
).run(id, change.id, input.task_id || null, targetRef, summary, JSON.stringify(evidence), proposedAt);
|
|
199
|
+
db.prepare(
|
|
200
|
+
'UPDATE spec_learning_candidates SET before_digest = ? WHERE id = ? AND change_id = ?',
|
|
201
|
+
).run(proposedDigest, id, change.id);
|
|
131
202
|
} catch (error) {
|
|
132
203
|
if (String(error.message).includes('UNIQUE')) {
|
|
133
204
|
throw new SpecLearningError('DUPLICATE_LEARNING_ID', `spec-learning candidate ${id} exists`);
|
|
@@ -164,13 +235,19 @@ function resolveSpecLearning(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
164
235
|
throw new SpecLearningError('VALIDATION_ERROR', `${input.decision} requires a resolution`);
|
|
165
236
|
}
|
|
166
237
|
const timestamp = nowIso();
|
|
238
|
+
const application = status === 'applied'
|
|
239
|
+
? verifyApplyEvidence(input, current, rootDir)
|
|
240
|
+
: null;
|
|
167
241
|
return ops.tx(db, () => {
|
|
168
242
|
db.prepare(
|
|
169
243
|
`UPDATE spec_learning_candidates
|
|
170
|
-
SET status = ?, resolution = ?, resolved_at = ?, applied_at =
|
|
244
|
+
SET status = ?, resolution = ?, resolved_at = ?, applied_at = ?, applied_ref = ?,
|
|
245
|
+
before_digest = COALESCE(?, before_digest), after_digest = ?, apply_evidence_json = ?
|
|
171
246
|
WHERE id = ? AND change_id = ?`,
|
|
172
247
|
).run(
|
|
173
248
|
status, resolution || null, timestamp, status === 'applied' ? timestamp : null,
|
|
249
|
+
application?.appliedRef || null, application?.beforeDigest || null,
|
|
250
|
+
application?.afterDigest || null, JSON.stringify(application?.evidence || []),
|
|
174
251
|
current.id, change.id,
|
|
175
252
|
);
|
|
176
253
|
ops.appendEventInTx(db, {
|
|
@@ -9,10 +9,14 @@ const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
|
9
9
|
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
10
10
|
: path.resolve(__dirname, '..', '..');
|
|
11
11
|
const SCHEMA_FILE = path.join(REPO_ROOT, 'spec', 'schemas', 'state-db.sql');
|
|
12
|
-
const EXPECTED_VERSION = '
|
|
12
|
+
const EXPECTED_VERSION = '16.0';
|
|
13
13
|
const KIMI_SCHEMA_VERSION = '9.1';
|
|
14
14
|
const CONTEXT_SCHEMA_VERSION = '10.0';
|
|
15
15
|
const BASELINE_SCHEMA_VERSION = '11.0';
|
|
16
|
+
const ADOPTION_SCHEMA_VERSION = '12.0';
|
|
17
|
+
const WORKFLOW_SCHEMA_VERSION = '13.0';
|
|
18
|
+
const AUTHORITY_SCHEMA_VERSION = '14.0';
|
|
19
|
+
const SEMANTIC_SCHEMA_VERSION = '15.0';
|
|
16
20
|
|
|
17
21
|
const MIGRATED_GAPS = Object.freeze([{
|
|
18
22
|
id: 'legacy-rebaseline-required',
|
|
@@ -42,6 +46,10 @@ const REQUIRED_TABLES = Object.freeze([
|
|
|
42
46
|
'incidents',
|
|
43
47
|
'projection_jobs',
|
|
44
48
|
'event_consumers',
|
|
49
|
+
'workflow_runs',
|
|
50
|
+
'workflow_steps',
|
|
51
|
+
'decision_threads',
|
|
52
|
+
'decision_items',
|
|
45
53
|
]);
|
|
46
54
|
|
|
47
55
|
function applyBaselineUpgrade(db, { legacyState = false } = {}) {
|
|
@@ -63,6 +71,7 @@ function applyBaselineUpgrade(db, { legacyState = false } = {}) {
|
|
|
63
71
|
worktree_digest TEXT,
|
|
64
72
|
worktree_files_json TEXT NOT NULL DEFAULT '[]',
|
|
65
73
|
worktree_accepted INTEGER NOT NULL DEFAULT 0 CHECK (worktree_accepted IN (0, 1)),
|
|
74
|
+
known_red_accepted INTEGER NOT NULL DEFAULT 0 CHECK (known_red_accepted IN (0, 1)),
|
|
66
75
|
spec_refs_json TEXT NOT NULL DEFAULT '[]',
|
|
67
76
|
evidence_json TEXT NOT NULL DEFAULT '[]',
|
|
68
77
|
verification_json TEXT NOT NULL DEFAULT '[]',
|
|
@@ -70,6 +79,7 @@ function applyBaselineUpgrade(db, { legacyState = false } = {}) {
|
|
|
70
79
|
gaps_json TEXT NOT NULL DEFAULT '[]',
|
|
71
80
|
classification_json TEXT NOT NULL DEFAULT '{}',
|
|
72
81
|
provider_refs_json TEXT NOT NULL DEFAULT '{}',
|
|
82
|
+
research_run_id TEXT,
|
|
73
83
|
approved_by TEXT,
|
|
74
84
|
approval_note TEXT,
|
|
75
85
|
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
@@ -114,17 +124,55 @@ function applyBaselineColumns(db) {
|
|
|
114
124
|
db, 'baselines', columns, 'worktree_accepted',
|
|
115
125
|
'INTEGER NOT NULL DEFAULT 0 CHECK (worktree_accepted IN (0, 1))',
|
|
116
126
|
) || changed;
|
|
127
|
+
changed = addColumnIfMissing(
|
|
128
|
+
db, 'baselines', columns, 'known_red_accepted',
|
|
129
|
+
'INTEGER NOT NULL DEFAULT 0 CHECK (known_red_accepted IN (0, 1))',
|
|
130
|
+
) || changed;
|
|
117
131
|
changed = addColumnIfMissing(db, 'baselines', columns, 'gaps_json', "TEXT NOT NULL DEFAULT '[]'") || changed;
|
|
118
132
|
changed = addColumnIfMissing(
|
|
119
133
|
db, 'baselines', columns, 'classification_json', "TEXT NOT NULL DEFAULT '{}'",
|
|
120
134
|
) || changed;
|
|
135
|
+
changed = addColumnIfMissing(db, 'baselines', columns, 'research_run_id', 'TEXT') || changed;
|
|
121
136
|
return changed;
|
|
122
137
|
}
|
|
123
138
|
|
|
124
139
|
function applyChangeColumns(db) {
|
|
125
140
|
if (!tableNames(db).includes('changes')) return false;
|
|
126
141
|
const columns = columnNames(db, 'changes');
|
|
127
|
-
|
|
142
|
+
let changed = false;
|
|
143
|
+
changed = addColumnIfMissing(db, 'changes', columns, 'baseline_bypass_json', 'TEXT') || changed;
|
|
144
|
+
changed = addColumnIfMissing(db, 'changes', columns, 'contract_json', "TEXT NOT NULL DEFAULT '{}'") || changed;
|
|
145
|
+
changed = addColumnIfMissing(db, 'changes', columns, 'classification_json', "TEXT NOT NULL DEFAULT '{}'") || changed;
|
|
146
|
+
changed = addColumnIfMissing(
|
|
147
|
+
db, 'changes', columns, 'research_disposition_json', "TEXT NOT NULL DEFAULT '{}'",
|
|
148
|
+
) || changed;
|
|
149
|
+
changed = addColumnIfMissing(
|
|
150
|
+
db, 'changes', columns, 'alignment_thread_id',
|
|
151
|
+
'TEXT REFERENCES decision_threads(id) ON DELETE SET NULL',
|
|
152
|
+
) || changed;
|
|
153
|
+
return changed;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function applySemanticAuthorityColumns(db) {
|
|
157
|
+
let changed = false;
|
|
158
|
+
if (tableNames(db).includes('workflow_steps')) {
|
|
159
|
+
const columns = columnNames(db, 'workflow_steps');
|
|
160
|
+
changed = addColumnIfMissing(
|
|
161
|
+
db, 'workflow_steps', columns, 'semantic_records_json', "TEXT NOT NULL DEFAULT '[]'",
|
|
162
|
+
) || changed;
|
|
163
|
+
}
|
|
164
|
+
if (tableNames(db).includes('spec_learning_candidates')) {
|
|
165
|
+
const columns = columnNames(db, 'spec_learning_candidates');
|
|
166
|
+
for (const [name, definition] of [
|
|
167
|
+
['applied_ref', 'TEXT'],
|
|
168
|
+
['before_digest', 'TEXT'],
|
|
169
|
+
['after_digest', 'TEXT'],
|
|
170
|
+
['apply_evidence_json', "TEXT NOT NULL DEFAULT '[]'"],
|
|
171
|
+
]) {
|
|
172
|
+
changed = addColumnIfMissing(db, 'spec_learning_candidates', columns, name, definition) || changed;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return changed;
|
|
128
176
|
}
|
|
129
177
|
|
|
130
178
|
function quoteSqlString(value) {
|
|
@@ -151,7 +199,14 @@ function readSchemaSql() {
|
|
|
151
199
|
}
|
|
152
200
|
|
|
153
201
|
function applyPragmas(db) {
|
|
154
|
-
db.pragma('journal_mode = WAL');
|
|
202
|
+
const journalMode = db.pragma('journal_mode = WAL', { simple: true });
|
|
203
|
+
if (String(journalMode).toLowerCase() !== 'wal') {
|
|
204
|
+
const error = new Error(
|
|
205
|
+
`state.db requires SQLite WAL mode; storage returned ${String(journalMode || 'unknown')}`,
|
|
206
|
+
);
|
|
207
|
+
error.code = 'STATE_DB_WAL_UNAVAILABLE';
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
155
210
|
db.pragma('synchronous = NORMAL');
|
|
156
211
|
db.pragma('busy_timeout = 5000');
|
|
157
212
|
db.pragma('foreign_keys = ON');
|
|
@@ -202,6 +257,19 @@ function applyCompatibleColumns(db) {
|
|
|
202
257
|
if (!taskColumns.has('change_id')) {
|
|
203
258
|
db.exec('ALTER TABLE tasks ADD COLUMN change_id TEXT REFERENCES changes(id) ON DELETE SET NULL');
|
|
204
259
|
}
|
|
260
|
+
const taskAdditions = [
|
|
261
|
+
['outcome', 'TEXT'],
|
|
262
|
+
['slice_kind', "TEXT CHECK (slice_kind IS NULL OR slice_kind IN ('tracer_bullet', 'expand_contract', 'integration_checkpoint'))"],
|
|
263
|
+
['public_seam', 'TEXT'],
|
|
264
|
+
['verification_command', 'TEXT'],
|
|
265
|
+
['acceptance_json', "TEXT NOT NULL DEFAULT '[]'"],
|
|
266
|
+
['context_refs_json', "TEXT NOT NULL DEFAULT '[]'"],
|
|
267
|
+
['docs_impact_json', "TEXT NOT NULL DEFAULT '{\"status\":\"unknown\",\"files\":[],\"rationale\":null}'"],
|
|
268
|
+
['ownership_json', "TEXT NOT NULL DEFAULT '{}'"],
|
|
269
|
+
];
|
|
270
|
+
for (const [name, definition] of taskAdditions) {
|
|
271
|
+
if (!taskColumns.has(name)) db.exec(`ALTER TABLE tasks ADD COLUMN ${name} ${definition}`);
|
|
272
|
+
}
|
|
205
273
|
if (tables.has('events')) {
|
|
206
274
|
const eventColumns = columnNames(db, 'events');
|
|
207
275
|
if (!eventColumns.has('change_id')) db.exec('ALTER TABLE events ADD COLUMN change_id TEXT');
|
|
@@ -339,7 +407,10 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
339
407
|
"SELECT 1 FROM migration_history WHERE to_version = '10.0' AND status = 'success' LIMIT 1",
|
|
340
408
|
).get();
|
|
341
409
|
const needsContextMigration = Boolean(
|
|
342
|
-
fromVersion && ![
|
|
410
|
+
fromVersion && ![
|
|
411
|
+
CONTEXT_SCHEMA_VERSION, BASELINE_SCHEMA_VERSION, ADOPTION_SCHEMA_VERSION,
|
|
412
|
+
WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION, EXPECTED_VERSION,
|
|
413
|
+
].includes(fromVersion)
|
|
343
414
|
&& !contextMigration,
|
|
344
415
|
);
|
|
345
416
|
if (contextChanged || needsContextMigration) {
|
|
@@ -356,10 +427,14 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
356
427
|
applyBaselineUpgrade(db, { legacyState });
|
|
357
428
|
applyBaselineColumns(db);
|
|
358
429
|
applyChangeColumns(db);
|
|
430
|
+
applySemanticAuthorityColumns(db);
|
|
359
431
|
const baselineMigration = db.prepare(
|
|
360
432
|
"SELECT 1 FROM migration_history WHERE to_version = '11.0' AND status = 'success' LIMIT 1",
|
|
361
433
|
).get();
|
|
362
|
-
if (fromVersion && ![
|
|
434
|
+
if (fromVersion && ![
|
|
435
|
+
BASELINE_SCHEMA_VERSION, ADOPTION_SCHEMA_VERSION, WORKFLOW_SCHEMA_VERSION,
|
|
436
|
+
AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION, EXPECTED_VERSION,
|
|
437
|
+
].includes(fromVersion)
|
|
363
438
|
&& !baselineMigration) {
|
|
364
439
|
db.prepare(
|
|
365
440
|
`INSERT INTO migration_history
|
|
@@ -372,6 +447,32 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
372
447
|
);
|
|
373
448
|
}
|
|
374
449
|
if (legacyState) {
|
|
450
|
+
const legacyBaselines = db.prepare(
|
|
451
|
+
"SELECT id, mode, status, classification_json, gaps_json FROM baselines WHERE status != 'superseded'",
|
|
452
|
+
).all();
|
|
453
|
+
for (const baseline of legacyBaselines) {
|
|
454
|
+
let classification = {};
|
|
455
|
+
let gaps = [];
|
|
456
|
+
try { classification = JSON.parse(baseline.classification_json || '{}'); } catch { classification = {}; }
|
|
457
|
+
try { gaps = JSON.parse(baseline.gaps_json || '[]'); } catch { gaps = []; }
|
|
458
|
+
if (!gaps.some((gap) => gap?.id === MIGRATED_GAPS[0].id)) gaps.push(MIGRATED_GAPS[0]);
|
|
459
|
+
classification.migration = {
|
|
460
|
+
previous_mode: baseline.mode,
|
|
461
|
+
previous_status: baseline.status,
|
|
462
|
+
from_schema: fromVersion || 'unknown',
|
|
463
|
+
requires_research_workflow: true,
|
|
464
|
+
};
|
|
465
|
+
db.prepare(
|
|
466
|
+
`UPDATE baselines SET mode = 'migrated', status = 'adopting',
|
|
467
|
+
classification_json = ?, gaps_json = ?, approved_by = NULL, approval_note = ?,
|
|
468
|
+
converged_at = NULL, research_run_id = NULL, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
469
|
+
WHERE id = ?`,
|
|
470
|
+
).run(
|
|
471
|
+
JSON.stringify(classification), JSON.stringify(gaps),
|
|
472
|
+
'Legacy project preserved; evidence-backed brownfield re-adoption is required',
|
|
473
|
+
baseline.id,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
375
476
|
db.prepare(
|
|
376
477
|
`UPDATE baselines SET status = 'adopting', approved_by = NULL, approval_note = ?,
|
|
377
478
|
converged_at = NULL, worktree_state = 'unavailable', worktree_digest = NULL,
|
|
@@ -393,18 +494,74 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
393
494
|
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
394
495
|
).run(
|
|
395
496
|
BASELINE_SCHEMA_VERSION,
|
|
396
|
-
|
|
497
|
+
ADOPTION_SCHEMA_VERSION,
|
|
397
498
|
'Add repository snapshots, authoritative gap ledger, incident governance, and required re-adoption',
|
|
398
499
|
);
|
|
399
500
|
}
|
|
400
501
|
}
|
|
502
|
+
const workflowMigration = db.prepare(
|
|
503
|
+
"SELECT 1 FROM migration_history WHERE to_version = '13.0' AND status = 'success' LIMIT 1",
|
|
504
|
+
).get();
|
|
505
|
+
if (fromVersion && fromVersion !== EXPECTED_VERSION && !workflowMigration) {
|
|
506
|
+
db.prepare(
|
|
507
|
+
`INSERT INTO migration_history
|
|
508
|
+
(from_version, to_version, direction, status, notes)
|
|
509
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
510
|
+
).run(
|
|
511
|
+
fromVersion === ADOPTION_SCHEMA_VERSION ? fromVersion : ADOPTION_SCHEMA_VERSION,
|
|
512
|
+
WORKFLOW_SCHEMA_VERSION,
|
|
513
|
+
'Add durable cross-stage workflow runs, step evidence, output digests, blockers, and recovery',
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
const authorityMigration = db.prepare(
|
|
517
|
+
"SELECT 1 FROM migration_history WHERE to_version = '14.0' AND status = 'success' LIMIT 1",
|
|
518
|
+
).get();
|
|
519
|
+
if (fromVersion && fromVersion !== EXPECTED_VERSION && !authorityMigration) {
|
|
520
|
+
db.prepare(
|
|
521
|
+
`INSERT INTO migration_history
|
|
522
|
+
(from_version, to_version, direction, status, notes)
|
|
523
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
524
|
+
).run(
|
|
525
|
+
fromVersion === WORKFLOW_SCHEMA_VERSION ? fromVersion : WORKFLOW_SCHEMA_VERSION,
|
|
526
|
+
AUTHORITY_SCHEMA_VERSION,
|
|
527
|
+
'Persist known-red acceptance and continuously revalidate ready baseline authority',
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
const semanticMigration = db.prepare(
|
|
531
|
+
"SELECT 1 FROM migration_history WHERE to_version = '15.0' AND status = 'success' LIMIT 1",
|
|
532
|
+
).get();
|
|
533
|
+
if (fromVersion && fromVersion !== EXPECTED_VERSION && !semanticMigration) {
|
|
534
|
+
db.prepare(
|
|
535
|
+
`INSERT INTO migration_history
|
|
536
|
+
(from_version, to_version, direction, status, notes)
|
|
537
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
538
|
+
).run(
|
|
539
|
+
fromVersion === AUTHORITY_SCHEMA_VERSION ? fromVersion : AUTHORITY_SCHEMA_VERSION,
|
|
540
|
+
SEMANTIC_SCHEMA_VERSION,
|
|
541
|
+
'Add typed research semantics, complete change contracts, verified learning application, and reconciliation provenance',
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
const dialogueMigration = db.prepare(
|
|
545
|
+
"SELECT 1 FROM migration_history WHERE to_version = '16.0' AND status = 'success' LIMIT 1",
|
|
546
|
+
).get();
|
|
547
|
+
if (fromVersion && fromVersion !== EXPECTED_VERSION && !dialogueMigration) {
|
|
548
|
+
db.prepare(
|
|
549
|
+
`INSERT INTO migration_history
|
|
550
|
+
(from_version, to_version, direction, status, notes)
|
|
551
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
552
|
+
).run(
|
|
553
|
+
fromVersion === SEMANTIC_SCHEMA_VERSION ? fromVersion : SEMANTIC_SCHEMA_VERSION,
|
|
554
|
+
EXPECTED_VERSION,
|
|
555
|
+
'Add durable one-question decision dialogue, owner checkpoints, and workflow alignment gates',
|
|
556
|
+
);
|
|
557
|
+
}
|
|
401
558
|
db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
|
|
402
559
|
db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
|
|
403
560
|
db.prepare(
|
|
404
561
|
'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
|
|
405
562
|
).run(
|
|
406
563
|
EXPECTED_VERSION,
|
|
407
|
-
'
|
|
564
|
+
'Durable one-question decision dialogue, owner checkpoints, and workflow alignment gates',
|
|
408
565
|
);
|
|
409
566
|
})();
|
|
410
567
|
}
|
|
@@ -431,7 +588,10 @@ function initStateDb(dbPath) {
|
|
|
431
588
|
db = openStateDb(dbPath);
|
|
432
589
|
const existing = new Set(tableNames(db));
|
|
433
590
|
const fromVersion = latestSchemaVersion(db);
|
|
434
|
-
const legacyState = existing.size > 0
|
|
591
|
+
const legacyState = existing.size > 0
|
|
592
|
+
&& ![
|
|
593
|
+
WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION, EXPECTED_VERSION,
|
|
594
|
+
].includes(fromVersion);
|
|
435
595
|
backupPath = migrationBackup(db, dbPath, fromVersion, existing);
|
|
436
596
|
applyCompatibleColumns(db);
|
|
437
597
|
const missing = REQUIRED_TABLES.filter((t) => !existing.has(t));
|
|
@@ -25,15 +25,19 @@ const STATUS_TRANSITIONS = Object.freeze({
|
|
|
25
25
|
|
|
26
26
|
const TASK_FIELDS = Object.freeze([
|
|
27
27
|
'id', 'title', 'type', 'priority', 'complexity', 'estimated_days', 'status',
|
|
28
|
-
'deps', 'files_modified', 'session_id', 'stale',
|
|
29
|
-
'tag', 'trace_to', '
|
|
28
|
+
'deps', 'files_modified', 'session_id', 'stale',
|
|
29
|
+
'tag', 'trace_to', 'outcome', 'slice_kind', 'public_seam', 'verification_command',
|
|
30
|
+
'acceptance_json', 'context_refs_json', 'docs_impact_json', 'ownership_json',
|
|
31
|
+
'context_file', 'completion_commit', 'parent_id',
|
|
30
32
|
'change_id',
|
|
31
33
|
'created_at', 'updated_at',
|
|
32
34
|
]);
|
|
33
35
|
|
|
34
36
|
const PATCHABLE_FIELDS = Object.freeze([
|
|
35
37
|
'priority', 'complexity', 'estimated_days', 'deps', 'files_modified',
|
|
36
|
-
'session_id', 'stale', '
|
|
38
|
+
'session_id', 'stale', 'tag', 'trace_to',
|
|
39
|
+
'outcome', 'slice_kind', 'public_seam', 'verification_command',
|
|
40
|
+
'acceptance', 'context_refs', 'docs_impact', 'ownership',
|
|
37
41
|
'context_file', 'completion_commit',
|
|
38
42
|
'change_id',
|
|
39
43
|
]);
|
|
@@ -89,7 +93,7 @@ function withRetry(fn, { attempts = 10, baseMs = 50, capMs = 2000 } = {}) {
|
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
function tx(db, fn) {
|
|
92
|
-
return withRetry(() => db.transaction(fn)());
|
|
96
|
+
return withRetry(() => db.transaction(fn).immediate());
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
// ─── tasks ───────────────────────────────────────────────────────────────
|
|
@@ -97,17 +101,132 @@ function tx(db, fn) {
|
|
|
97
101
|
function rowToTask(row) {
|
|
98
102
|
if (!row) return null;
|
|
99
103
|
const out = { ...row };
|
|
104
|
+
// Pre-13 databases may retain this retired Claude-specific column. It is
|
|
105
|
+
// intentionally ignored instead of leaking host/model assumptions into the
|
|
106
|
+
// current task contract or projections.
|
|
107
|
+
delete out.complexity_hint;
|
|
100
108
|
for (const k of ['deps', 'files_modified']) {
|
|
101
109
|
if (typeof out[k] === 'string') {
|
|
102
110
|
try { out[k] = JSON.parse(out[k]); } catch { out[k] = null; }
|
|
103
111
|
}
|
|
104
112
|
}
|
|
113
|
+
const jsonFields = {
|
|
114
|
+
acceptance_json: ['acceptance', []],
|
|
115
|
+
context_refs_json: ['context_refs', []],
|
|
116
|
+
docs_impact_json: ['docs_impact', { status: 'unknown', files: [], rationale: null }],
|
|
117
|
+
ownership_json: ['ownership', {}],
|
|
118
|
+
};
|
|
119
|
+
for (const [column, [field, fallback]] of Object.entries(jsonFields)) {
|
|
120
|
+
try { out[field] = typeof out[column] === 'string' ? JSON.parse(out[column]) : fallback; }
|
|
121
|
+
catch { out[field] = fallback; }
|
|
122
|
+
delete out[column];
|
|
123
|
+
}
|
|
105
124
|
if (out.stale !== undefined && out.stale !== null) {
|
|
106
125
|
out.stale = Boolean(out.stale);
|
|
107
126
|
}
|
|
108
127
|
return out;
|
|
109
128
|
}
|
|
110
129
|
|
|
130
|
+
function normalizeObjectArray(value, field) {
|
|
131
|
+
if (!Array.isArray(value) || value.some((item) => !item || typeof item !== 'object' || Array.isArray(item))) {
|
|
132
|
+
throw new StateOpsError('VALIDATION_ERROR', `${field} must be an array of objects`);
|
|
133
|
+
}
|
|
134
|
+
return JSON.parse(JSON.stringify(value));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeAcceptance(value) {
|
|
138
|
+
const items = normalizeObjectArray(value, 'acceptance');
|
|
139
|
+
const ids = new Set();
|
|
140
|
+
return items.map((item, index) => {
|
|
141
|
+
const id = String(item.id || '').trim();
|
|
142
|
+
const criterion = String(item.criterion || '').trim();
|
|
143
|
+
const verification = String(item.verification || '').trim();
|
|
144
|
+
if (!id || !criterion || !verification || ids.has(id)) {
|
|
145
|
+
throw new StateOpsError(
|
|
146
|
+
'VALIDATION_ERROR', `acceptance[${index}] requires a unique id, criterion, and verification`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
ids.add(id);
|
|
150
|
+
return { id, criterion, verification };
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeContextRefs(value) {
|
|
155
|
+
return normalizeObjectArray(value, 'context_refs').map((item, index) => {
|
|
156
|
+
const ref = String(item.ref || '').trim();
|
|
157
|
+
const reason = String(item.reason || '').trim();
|
|
158
|
+
if (!ref || !reason) {
|
|
159
|
+
throw new StateOpsError('VALIDATION_ERROR', `context_refs[${index}] requires ref and reason`);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
ref, reason, kind: String(item.kind || 'source').trim(), required: item.required !== false,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeDocsImpact(value) {
|
|
168
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
169
|
+
throw new StateOpsError('VALIDATION_ERROR', 'docs_impact must be an object');
|
|
170
|
+
}
|
|
171
|
+
const status = String(value.status || 'unknown').trim();
|
|
172
|
+
if (!['unknown', 'required', 'none'].includes(status)) {
|
|
173
|
+
throw new StateOpsError('VALIDATION_ERROR', 'docs_impact.status must be unknown, required, or none');
|
|
174
|
+
}
|
|
175
|
+
const files = value.files === undefined ? [] : value.files;
|
|
176
|
+
if (!Array.isArray(files) || files.some((item) => typeof item !== 'string' || !item.trim())) {
|
|
177
|
+
throw new StateOpsError('VALIDATION_ERROR', 'docs_impact.files must be an array of paths');
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
status, files: files.map((item) => item.trim()),
|
|
181
|
+
rationale: value.rationale == null ? null : String(value.rationale).trim(),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeOwnership(value) {
|
|
186
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
187
|
+
throw new StateOpsError('VALIDATION_ERROR', 'ownership must be an object');
|
|
188
|
+
}
|
|
189
|
+
const reviewers = value.reviewers === undefined ? [] : value.reviewers;
|
|
190
|
+
if (!Array.isArray(reviewers) || reviewers.some((item) => typeof item !== 'string' || !item.trim())) {
|
|
191
|
+
throw new StateOpsError('VALIDATION_ERROR', 'ownership.reviewers must be an array of names');
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
owner: value.owner == null ? null : String(value.owner).trim(),
|
|
195
|
+
reviewers: reviewers.map((item) => item.trim()),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function taskContractBlockers(task) {
|
|
200
|
+
const blockers = [];
|
|
201
|
+
if (!String(task?.outcome || '').trim()) blockers.push('TASK_OUTCOME_MISSING');
|
|
202
|
+
if (!['tracer_bullet', 'expand_contract', 'integration_checkpoint'].includes(task?.slice_kind)) {
|
|
203
|
+
blockers.push('TASK_SLICE_KIND_MISSING');
|
|
204
|
+
}
|
|
205
|
+
if (!String(task?.public_seam || '').trim()) blockers.push('TASK_PUBLIC_SEAM_MISSING');
|
|
206
|
+
if (!String(task?.verification_command || '').trim()) blockers.push('TASK_VERIFICATION_COMMAND_MISSING');
|
|
207
|
+
if (!Array.isArray(task?.acceptance) || task.acceptance.length === 0) blockers.push('TASK_ACCEPTANCE_MISSING');
|
|
208
|
+
if (!Array.isArray(task?.context_refs) || task.context_refs.length === 0) blockers.push('TASK_CONTEXT_REFS_MISSING');
|
|
209
|
+
if (!String(task?.trace_to || '').trim()) blockers.push('TASK_TRACEABILITY_MISSING');
|
|
210
|
+
if (!task?.docs_impact || task.docs_impact.status === 'unknown') blockers.push('TASK_DOCS_IMPACT_UNRESOLVED');
|
|
211
|
+
else if (task.docs_impact.status === 'required' && task.docs_impact.files.length === 0) {
|
|
212
|
+
blockers.push('TASK_DOCS_FILES_MISSING');
|
|
213
|
+
} else if (!String(task.docs_impact.rationale || '').trim()) blockers.push('TASK_DOCS_RATIONALE_MISSING');
|
|
214
|
+
if (!String(task?.ownership?.owner || '').trim()) blockers.push('TASK_OWNER_MISSING');
|
|
215
|
+
return blockers;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function assertTaskExecutionContract(task) {
|
|
219
|
+
const blockers = taskContractBlockers(task);
|
|
220
|
+
if (blockers.length > 0) {
|
|
221
|
+
throw new StateOpsError(
|
|
222
|
+
'TASK_EXECUTION_CONTRACT_INCOMPLETE',
|
|
223
|
+
`task ${task?.id || '(unknown)'} execution contract is incomplete`,
|
|
224
|
+
{ details: { blockers } },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return task;
|
|
228
|
+
}
|
|
229
|
+
|
|
111
230
|
function readTask(db, id) {
|
|
112
231
|
const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
113
232
|
return rowToTask(row);
|
|
@@ -116,13 +235,14 @@ function readTask(db, id) {
|
|
|
116
235
|
// Single static SQL with named parameters and NULL-pass-through filters.
|
|
117
236
|
// Every value flows through @bindings; the string is a frozen literal so no
|
|
118
237
|
// concatenation / interpolation hooks can flag injection risk.
|
|
119
|
-
const LIST_TASKS_SQL = "SELECT * FROM tasks WHERE (@status IS NULL OR status = @status) AND (@tag IS NULL OR tag = @tag) AND (@since IS NULL OR updated_at >= @since) ORDER BY created_at ASC LIMIT IIF(@maxn IS NULL, -1, @maxn)";
|
|
238
|
+
const LIST_TASKS_SQL = "SELECT * FROM tasks WHERE (@status IS NULL OR status = @status) AND (@tag IS NULL OR tag = @tag) AND (@change_id IS NULL OR change_id = @change_id) AND (@since IS NULL OR updated_at >= @since) ORDER BY created_at ASC LIMIT IIF(@maxn IS NULL, -1, @maxn)";
|
|
120
239
|
|
|
121
240
|
function listTasks(db, filter = {}) {
|
|
122
241
|
const status = filter.status && filter.status !== 'any' ? filter.status : null;
|
|
123
242
|
return db.prepare(LIST_TASKS_SQL).all({
|
|
124
243
|
status,
|
|
125
244
|
tag: filter.tag || null,
|
|
245
|
+
change_id: filter.change_id || null,
|
|
126
246
|
since: filter.since || null,
|
|
127
247
|
maxn: filter.limit || null,
|
|
128
248
|
}).map(rowToTask);
|
|
@@ -203,9 +323,18 @@ function createTask(db, input) {
|
|
|
203
323
|
files_modified: input.files_modified ? JSON.stringify(input.files_modified) : null,
|
|
204
324
|
session_id: input.session_id ?? null,
|
|
205
325
|
stale: 0,
|
|
206
|
-
complexity_hint: input.complexity_hint ?? null,
|
|
207
326
|
tag: input.tag ?? derivedTag,
|
|
208
327
|
trace_to: input.trace_to ?? null,
|
|
328
|
+
outcome: input.outcome ?? null,
|
|
329
|
+
slice_kind: input.slice_kind ?? null,
|
|
330
|
+
public_seam: input.public_seam ?? null,
|
|
331
|
+
verification_command: input.verification_command ?? null,
|
|
332
|
+
acceptance_json: JSON.stringify(input.acceptance === undefined ? [] : normalizeAcceptance(input.acceptance)),
|
|
333
|
+
context_refs_json: JSON.stringify(input.context_refs === undefined ? [] : normalizeContextRefs(input.context_refs)),
|
|
334
|
+
docs_impact_json: JSON.stringify(input.docs_impact === undefined
|
|
335
|
+
? { status: 'unknown', files: [], rationale: null }
|
|
336
|
+
: normalizeDocsImpact(input.docs_impact)),
|
|
337
|
+
ownership_json: JSON.stringify(input.ownership === undefined ? {} : normalizeOwnership(input.ownership)),
|
|
209
338
|
context_file: input.context_file ?? null,
|
|
210
339
|
completion_commit: null,
|
|
211
340
|
parent_id: input.parent_id ?? null,
|
|
@@ -271,6 +400,26 @@ function patchTask(db, id, patch = {}) {
|
|
|
271
400
|
}
|
|
272
401
|
value = value === null ? null : JSON.stringify(value);
|
|
273
402
|
}
|
|
403
|
+
if (key === 'acceptance') {
|
|
404
|
+
sets.push('acceptance_json = ?');
|
|
405
|
+
params.push(JSON.stringify(normalizeAcceptance(value)));
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (key === 'context_refs') {
|
|
409
|
+
sets.push('context_refs_json = ?');
|
|
410
|
+
params.push(JSON.stringify(normalizeContextRefs(value)));
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (key === 'docs_impact') {
|
|
414
|
+
sets.push('docs_impact_json = ?');
|
|
415
|
+
params.push(JSON.stringify(normalizeDocsImpact(value)));
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (key === 'ownership') {
|
|
419
|
+
sets.push('ownership_json = ?');
|
|
420
|
+
params.push(JSON.stringify(normalizeOwnership(value)));
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
274
423
|
if (key === 'stale') value = value ? 1 : 0;
|
|
275
424
|
sets.push(`${key} = ?`);
|
|
276
425
|
params.push(value);
|
|
@@ -709,11 +858,11 @@ function markTasksStaleBySpecSections(db, sections) {
|
|
|
709
858
|
// shows up under a synthetic "unknown" runtime bucket. Writes go through
|
|
710
859
|
// mcp-server/lib/telemetry.cjs — don't insert telemetry rows directly.
|
|
711
860
|
|
|
712
|
-
const AGGREGATE_BY_RUNTIME_SQL = "SELECT COALESCE(s.runtime, 'unknown') AS runtime, COUNT(*) AS calls, COALESCE(SUM(t.tokens_input), 0) AS tokens_in, COALESCE(SUM(t.tokens_output), 0) AS tokens_out, COALESCE(SUM(t.cost_usd), 0.0) AS cost_usd FROM telemetry t LEFT JOIN sessions s ON t.session_id = s.sid WHERE (@since IS NULL OR t.ts >= @since) GROUP BY COALESCE(s.runtime, 'unknown') ORDER BY cost_usd DESC";
|
|
861
|
+
const AGGREGATE_BY_RUNTIME_SQL = "SELECT COALESCE(s.runtime, 'unknown') AS runtime, COUNT(*) AS calls, COALESCE(SUM(t.tokens_input), 0) AS tokens_in, COALESCE(SUM(t.tokens_output), 0) AS tokens_out, COALESCE(SUM(t.cost_usd), 0.0) AS cost_usd, SUM(CASE WHEN t.event_type = 'token_usage' AND (COALESCE(t.tokens_input, 0) > 0 OR COALESCE(t.tokens_output, 0) > 0) AND t.cost_usd IS NULL THEN 1 ELSE 0 END) AS unpriced_usage_events, SUM(CASE WHEN t.event_type = 'token_usage' AND (COALESCE(t.tokens_input, 0) > 0 OR COALESCE(t.tokens_output, 0) > 0) AND t.cost_usd IS NOT NULL THEN 1 ELSE 0 END) AS priced_usage_events FROM telemetry t LEFT JOIN sessions s ON t.session_id = s.sid WHERE (@since IS NULL OR t.ts >= @since) GROUP BY COALESCE(s.runtime, 'unknown') ORDER BY cost_usd DESC";
|
|
713
862
|
|
|
714
|
-
const AGGREGATE_BY_TASK_SQL = "SELECT s.task_id AS task_id, COUNT(*) AS calls, COALESCE(SUM(t.tokens_input), 0) AS tokens_in, COALESCE(SUM(t.tokens_output), 0) AS tokens_out, COALESCE(SUM(t.cost_usd), 0.0) AS cost_usd FROM telemetry t JOIN sessions s ON t.session_id = s.sid WHERE s.task_id IS NOT NULL AND (@since IS NULL OR t.ts >= @since) GROUP BY s.task_id ORDER BY cost_usd DESC LIMIT @maxn";
|
|
863
|
+
const AGGREGATE_BY_TASK_SQL = "SELECT s.task_id AS task_id, COUNT(*) AS calls, COALESCE(SUM(t.tokens_input), 0) AS tokens_in, COALESCE(SUM(t.tokens_output), 0) AS tokens_out, COALESCE(SUM(t.cost_usd), 0.0) AS cost_usd, SUM(CASE WHEN t.event_type = 'token_usage' AND (COALESCE(t.tokens_input, 0) > 0 OR COALESCE(t.tokens_output, 0) > 0) AND t.cost_usd IS NULL THEN 1 ELSE 0 END) AS unpriced_usage_events, SUM(CASE WHEN t.event_type = 'token_usage' AND (COALESCE(t.tokens_input, 0) > 0 OR COALESCE(t.tokens_output, 0) > 0) AND t.cost_usd IS NOT NULL THEN 1 ELSE 0 END) AS priced_usage_events FROM telemetry t JOIN sessions s ON t.session_id = s.sid WHERE s.task_id IS NOT NULL AND (@since IS NULL OR t.ts >= @since) GROUP BY s.task_id ORDER BY cost_usd DESC LIMIT @maxn";
|
|
715
864
|
|
|
716
|
-
const AGGREGATE_BY_SESSION_SQL = "SELECT @sid AS session_id, COUNT(*) AS tool_calls, COALESCE(SUM(tokens_input), 0) AS tokens_in, COALESCE(SUM(tokens_output), 0) AS tokens_out, COALESCE(SUM(cost_usd), 0.0) AS cost_usd FROM telemetry WHERE session_id = @sid";
|
|
865
|
+
const AGGREGATE_BY_SESSION_SQL = "SELECT @sid AS session_id, COUNT(*) AS tool_calls, COALESCE(SUM(tokens_input), 0) AS tokens_in, COALESCE(SUM(tokens_output), 0) AS tokens_out, COALESCE(SUM(cost_usd), 0.0) AS cost_usd, SUM(CASE WHEN event_type = 'token_usage' AND (COALESCE(tokens_input, 0) > 0 OR COALESCE(tokens_output, 0) > 0) AND cost_usd IS NULL THEN 1 ELSE 0 END) AS unpriced_usage_events, SUM(CASE WHEN event_type = 'token_usage' AND (COALESCE(tokens_input, 0) > 0 OR COALESCE(tokens_output, 0) > 0) AND cost_usd IS NOT NULL THEN 1 ELSE 0 END) AS priced_usage_events FROM telemetry WHERE session_id = @sid";
|
|
717
866
|
|
|
718
867
|
function aggregateTelemetryByRuntime(db, { since = null } = {}) {
|
|
719
868
|
return db.prepare(AGGREGATE_BY_RUNTIME_SQL).all({ since });
|
|
@@ -765,6 +914,8 @@ module.exports = {
|
|
|
765
914
|
StateOpsError,
|
|
766
915
|
STATUS_TRANSITIONS,
|
|
767
916
|
PATCHABLE_FIELDS,
|
|
917
|
+
taskContractBlockers,
|
|
918
|
+
assertTaskExecutionContract,
|
|
768
919
|
SESSION_PATCHABLE,
|
|
769
920
|
openStateDb,
|
|
770
921
|
tx,
|