ultra-builder-pro-cli 0.13.0 → 0.15.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.
Files changed (172) hide show
  1. package/.ultra-template/docs/research/README.md +11 -7
  2. package/.ultra-template/specs/architecture.md +36 -36
  3. package/.ultra-template/specs/discovery.md +46 -33
  4. package/.ultra-template/specs/product.md +38 -29
  5. package/.ultra-template/specs/research-distillate.md +48 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
  7. package/CHANGELOG.md +86 -1
  8. package/README.md +40 -26
  9. package/adapters/_shared/codex-assets.cjs +29 -165
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/runtime-assets.cjs +1 -0
  12. package/adapters/claude.js +3 -2
  13. package/adapters/codex.js +1 -0
  14. package/adapters/kimi.js +3 -2
  15. package/adapters/opencode.js +33 -122
  16. package/agents/review-coordinator.md +9 -6
  17. package/docs/AGENT-CONTEXT.md +21 -11
  18. package/docs/ARCHITECTURE.md +41 -13
  19. package/docs/PLAN.zh-CN.md +3 -2
  20. package/docs/ROADMAP.md +9 -5
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
  22. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  23. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  24. package/hooks/README.md +6 -1
  25. package/hooks/active_task_context.py +22 -50
  26. package/hooks/context_spine.py +61 -332
  27. package/hooks/health_check.py +29 -18
  28. package/hooks/pre_stop_check.py +28 -33
  29. package/hooks/subagent_tracker.py +54 -85
  30. package/hooks/workflow_checkpoint.py +39 -47
  31. package/hooks/workflow_context.py +4 -9
  32. package/hooks/workflow_resume.py +48 -128
  33. package/mcp-server/breadcrumb.cjs +22 -0
  34. package/mcp-server/hook-event.cjs +34 -0
  35. package/mcp-server/lib/archive-journal.cjs +17 -5
  36. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  37. package/mcp-server/lib/change-workflow.cjs +550 -85
  38. package/mcp-server/lib/context-spine.cjs +242 -24
  39. package/mcp-server/lib/doctor.cjs +33 -3
  40. package/mcp-server/lib/init-project.cjs +210 -16
  41. package/mcp-server/lib/plan-store.cjs +4 -1
  42. package/mcp-server/lib/prd-parser.cjs +14 -41
  43. package/mcp-server/lib/pricing.cjs +7 -10
  44. package/mcp-server/lib/project-breadcrumb.cjs +145 -0
  45. package/mcp-server/lib/projector.cjs +57 -3
  46. package/mcp-server/lib/spec-learning.cjs +79 -2
  47. package/mcp-server/lib/state-db.cjs +145 -8
  48. package/mcp-server/lib/state-ops.cjs +160 -9
  49. package/mcp-server/lib/task-expander.cjs +9 -38
  50. package/mcp-server/lib/workflow-state.cjs +2292 -0
  51. package/mcp-server/server.cjs +182 -50
  52. package/orchestrator/daemon.cjs +1 -2
  53. package/orchestrator/dispatch-rules.cjs +5 -31
  54. package/orchestrator/planner/plan-builder.cjs +21 -12
  55. package/package.json +14 -6
  56. package/skills/cc-collab/SKILL.md +2 -2
  57. package/skills/codex-collab/SKILL.md +2 -3
  58. package/skills/ultra-change/SKILL.md +53 -68
  59. package/skills/ultra-change/references/change-contract.md +36 -0
  60. package/skills/ultra-deliver/SKILL.md +69 -79
  61. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  62. package/skills/ultra-dev/SKILL.md +54 -59
  63. package/skills/ultra-doctor/SKILL.md +40 -71
  64. package/skills/ultra-init/SKILL.md +96 -103
  65. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  66. package/skills/ultra-plan/SKILL.md +79 -75
  67. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  68. package/skills/ultra-research/SKILL.md +87 -60
  69. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  70. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  71. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  72. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  73. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  74. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  75. package/skills/ultra-research/references/10-user-personas.md +29 -0
  76. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  77. package/skills/ultra-research/references/20-user-stories.md +30 -0
  78. package/skills/ultra-research/references/21-features-scope.md +29 -0
  79. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  80. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  81. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  82. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  83. package/skills/ultra-research/references/40-deployment.md +29 -0
  84. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  85. package/skills/ultra-research/references/99-synthesis.md +35 -0
  86. package/skills/ultra-review/SKILL.md +57 -73
  87. package/skills/ultra-review/references/review-modes.md +27 -0
  88. package/skills/ultra-review/references/unified-schema.md +25 -0
  89. package/skills/ultra-review/scripts/review_wait.py +40 -3
  90. package/skills/ultra-status/SKILL.md +41 -45
  91. package/skills/ultra-test/SKILL.md +48 -55
  92. package/skills/ultra-verify/SKILL.md +22 -31
  93. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  94. package/skills/ultra-verify/scripts/verify_wait.py +54 -54
  95. package/spec/cli-protocol.md +7 -2
  96. package/spec/mcp-tools.yaml +422 -73
  97. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  98. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  99. package/spec/schemas/mcp-tools.schema.json +1 -1
  100. package/spec/schemas/state-db.sql +82 -1
  101. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  102. package/spec/schemas/test-report.v1.schema.json +194 -0
  103. package/ultra-tools/cli.cjs +1 -1
  104. package/ultra-tools/commands/migrate.cjs +3 -3
  105. package/ultra-tools/commands/status.cjs +213 -12
  106. package/.ultra-template/delivery-report.json +0 -9
  107. package/.ultra-template/test-report.json +0 -8
  108. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  109. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  110. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  111. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  112. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  113. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  114. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -131
  115. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  116. package/adapters/_shared/tests/validate.test.cjs +0 -30
  117. package/adapters/tests/claude.test.cjs +0 -137
  118. package/adapters/tests/codex-hook.test.cjs +0 -115
  119. package/adapters/tests/codex.test.cjs +0 -423
  120. package/adapters/tests/kimi-hook.test.cjs +0 -122
  121. package/adapters/tests/kimi.test.cjs +0 -259
  122. package/adapters/tests/opencode.test.cjs +0 -371
  123. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  124. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  125. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  126. package/mcp-server/lib/context-spine.test.cjs +0 -93
  127. package/mcp-server/lib/doctor.test.cjs +0 -148
  128. package/mcp-server/lib/llm-client.cjs +0 -157
  129. package/mcp-server/lib/llm-client.test.cjs +0 -156
  130. package/mcp-server/lib/plan-store.test.cjs +0 -132
  131. package/mcp-server/lib/prd-parser.test.cjs +0 -171
  132. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  133. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  134. package/mcp-server/lib/state-db.test.cjs +0 -314
  135. package/mcp-server/lib/task-expander.test.cjs +0 -324
  136. package/mcp-server/lib/topo.test.cjs +0 -158
  137. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  138. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  139. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  140. package/mcp-server/tests/concurrency.test.cjs +0 -112
  141. package/mcp-server/tests/db-ops.test.cjs +0 -121
  142. package/mcp-server/tests/init-project.test.cjs +0 -393
  143. package/mcp-server/tests/migrate.test.cjs +0 -232
  144. package/mcp-server/tests/projector.test.cjs +0 -162
  145. package/mcp-server/tests/server-contract.test.cjs +0 -1471
  146. package/mcp-server/tests/staleness.test.cjs +0 -190
  147. package/mcp-server/tests/state-db.test.cjs +0 -106
  148. package/mcp-server/tests/state-ops.test.cjs +0 -449
  149. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  150. package/mcp-server/tests/telemetry.test.cjs +0 -219
  151. package/orchestrator/planner/expand-prompt.cjs +0 -62
  152. package/orchestrator/planner/expand-prompt.test.cjs +0 -52
  153. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  154. package/orchestrator/planner/prd-prompt.cjs +0 -44
  155. package/orchestrator/planner/prd-prompt.test.cjs +0 -30
  156. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  157. package/orchestrator/tests/daemon.test.cjs +0 -311
  158. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  159. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  160. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  161. package/orchestrator/tests/recovery.test.cjs +0 -190
  162. package/orchestrator/tests/session-runner.test.cjs +0 -314
  163. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  164. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  165. package/skills/ultra-research/references/architecture.md +0 -38
  166. package/skills/ultra-research/references/problem-product.md +0 -39
  167. package/skills/ultra-research/references/quality-delivery.md +0 -34
  168. package/skills/ultra-research/references/synthesis.md +0 -38
  169. package/skills/ultra-research/references/users-scope.md +0 -40
  170. package/ultra-tools/legacy-memory.test.cjs +0 -80
  171. package/ultra-tools/status-command.test.cjs +0 -181
  172. package/ultra-tools/system-command.test.cjs +0 -309
@@ -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', 'complexity_hint',
29
- 'tag', 'trace_to', 'context_file', 'completion_commit', 'parent_id',
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', 'complexity_hint', 'tag', 'trace_to',
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,
@@ -1,21 +1,11 @@
1
1
  'use strict';
2
2
 
3
- // Phase 8A.3 — Expand a complex task into subtasks via injected llmClient.
4
- //
5
- // expandTask(db, { id, sub_count, strategy, llmClient }) reads the parent,
6
- // asks the LLM for children, and persists them atomically (children rows
7
- // with parent_id set, parent status → 'expanded'). The status transition
8
- // automatically emits task_expanded via state-ops' status→event map.
9
- // Strategy 'manual' is reserved for future CLI interactive flows.
3
+ // Persist host-derived child tasks atomically. Model judgment stays with the
4
+ // current host; this module validates ownership and the database contract.
10
5
 
11
6
  const Ajv = require('ajv/dist/2020');
12
7
  const ops = require('./state-ops.cjs');
13
8
  const changes = require('./change-workflow.cjs');
14
- const {
15
- buildExpandSystemPrompt,
16
- buildExpandUserPrompt,
17
- } = require('../../orchestrator/planner/expand-prompt.cjs');
18
-
19
9
  const CHILDREN_SHAPE = Object.freeze({
20
10
  type: 'object',
21
11
  required: ['children'],
@@ -52,8 +42,8 @@ class TaskExpandError extends Error {
52
42
  }
53
43
  }
54
44
 
55
- async function expandTask(db, {
56
- id, sub_count, strategy = 'llm', llmClient, rootDir = process.cwd(),
45
+ function expandTask(db, {
46
+ id, children, rootDir = process.cwd(),
57
47
  } = {}) {
58
48
  if (!id) throw new TaskExpandError('VALIDATION_ERROR', 'id required');
59
49
 
@@ -62,35 +52,16 @@ async function expandTask(db, {
62
52
  if (parent.status === 'expanded') {
63
53
  throw new TaskExpandError('ALREADY_EXPANDED', `task ${id} is already expanded`);
64
54
  }
65
- if (strategy === 'manual') {
66
- throw new TaskExpandError('NOT_IMPLEMENTED', 'strategy "manual" not implemented; use "llm"');
67
- }
68
- if (strategy !== 'llm') {
69
- throw new TaskExpandError('VALIDATION_ERROR', `unknown strategy "${strategy}"`);
70
- }
71
55
  changes.assertTaskCreationAllowed(db, { change_id: parent.change_id }, { rootDir });
72
- if (!llmClient || typeof llmClient.completeJson !== 'function') {
73
- throw new TaskExpandError('NO_LLM_CLIENT', 'llmClient.completeJson is required');
74
- }
75
-
76
- const system = buildExpandSystemPrompt({ sub_count });
77
- const user = buildExpandUserPrompt(parent);
78
-
79
- let reply;
80
- try {
81
- reply = await llmClient.completeJson({ system, user });
82
- } catch (err) {
83
- throw new TaskExpandError('LLM_CALL_FAILED', `LLM call failed: ${err.message}`, err);
84
- }
85
- if (!validateChildren(reply.json)) {
56
+ if (!validateChildren({ children })) {
86
57
  throw new TaskExpandError(
87
58
  'INVALID_OUTPUT',
88
- `LLM output failed schema: ${ajv.errorsText(validateChildren.errors)}`,
59
+ `child task graph failed schema: ${ajv.errorsText(validateChildren.errors)}`,
89
60
  );
90
61
  }
91
62
 
92
63
  const seen = new Set();
93
- const children = reply.json.children.map((c) => {
64
+ const normalized = children.map((c) => {
94
65
  if (seen.has(c.id)) {
95
66
  throw new TaskExpandError('INVALID_OUTPUT', `duplicate child id "${c.id}"`);
96
67
  }
@@ -111,13 +82,13 @@ async function expandTask(db, {
111
82
 
112
83
  return ops.tx(db, () => {
113
84
  changes.assertTaskCreationAllowed(db, { change_id: parent.change_id }, { rootDir });
114
- for (const child of children) {
85
+ for (const child of normalized) {
115
86
  ops.createTask(db, child);
116
87
  }
117
88
  ops.patchTask(db, parent.id, { status: 'expanded' });
118
89
  return {
119
90
  parent_id: parent.id,
120
- children,
91
+ children: normalized,
121
92
  };
122
93
  });
123
94
  }