ultra-builder-pro-cli 0.14.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 (154) 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 +44 -1
  8. package/README.md +34 -19
  9. package/adapters/_shared/codex-assets.cjs +6 -2
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/claude.js +3 -2
  12. package/adapters/codex.js +1 -0
  13. package/adapters/kimi.js +3 -2
  14. package/adapters/opencode.js +3 -2
  15. package/agents/review-coordinator.md +9 -6
  16. package/docs/AGENT-CONTEXT.md +15 -8
  17. package/docs/ARCHITECTURE.md +33 -12
  18. package/docs/PLAN.zh-CN.md +3 -2
  19. package/docs/ROADMAP.md +8 -4
  20. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  21. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  22. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  23. package/hooks/README.md +6 -1
  24. package/hooks/health_check.py +12 -3
  25. package/hooks/subagent_tracker.py +52 -77
  26. package/mcp-server/hook-event.cjs +34 -0
  27. package/mcp-server/lib/archive-journal.cjs +17 -5
  28. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  29. package/mcp-server/lib/change-workflow.cjs +550 -85
  30. package/mcp-server/lib/context-spine.cjs +242 -24
  31. package/mcp-server/lib/doctor.cjs +33 -3
  32. package/mcp-server/lib/init-project.cjs +210 -16
  33. package/mcp-server/lib/plan-store.cjs +4 -1
  34. package/mcp-server/lib/pricing.cjs +7 -10
  35. package/mcp-server/lib/project-breadcrumb.cjs +6 -0
  36. package/mcp-server/lib/projector.cjs +57 -3
  37. package/mcp-server/lib/spec-learning.cjs +79 -2
  38. package/mcp-server/lib/state-db.cjs +145 -8
  39. package/mcp-server/lib/state-ops.cjs +160 -9
  40. package/mcp-server/lib/workflow-state.cjs +2292 -0
  41. package/mcp-server/server.cjs +161 -15
  42. package/orchestrator/daemon.cjs +1 -2
  43. package/orchestrator/dispatch-rules.cjs +5 -31
  44. package/orchestrator/planner/plan-builder.cjs +21 -12
  45. package/package.json +13 -3
  46. package/skills/ultra-change/SKILL.md +53 -68
  47. package/skills/ultra-change/references/change-contract.md +36 -0
  48. package/skills/ultra-deliver/SKILL.md +69 -79
  49. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  50. package/skills/ultra-dev/SKILL.md +54 -59
  51. package/skills/ultra-doctor/SKILL.md +40 -71
  52. package/skills/ultra-init/SKILL.md +96 -103
  53. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  54. package/skills/ultra-plan/SKILL.md +79 -85
  55. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  56. package/skills/ultra-research/SKILL.md +87 -60
  57. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  58. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  59. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  60. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  61. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  62. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  63. package/skills/ultra-research/references/10-user-personas.md +29 -0
  64. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  65. package/skills/ultra-research/references/20-user-stories.md +30 -0
  66. package/skills/ultra-research/references/21-features-scope.md +29 -0
  67. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  68. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  69. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  70. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  71. package/skills/ultra-research/references/40-deployment.md +29 -0
  72. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  73. package/skills/ultra-research/references/99-synthesis.md +35 -0
  74. package/skills/ultra-review/SKILL.md +57 -73
  75. package/skills/ultra-review/references/review-modes.md +27 -0
  76. package/skills/ultra-review/references/unified-schema.md +25 -0
  77. package/skills/ultra-review/scripts/review_wait.py +40 -3
  78. package/skills/ultra-status/SKILL.md +41 -45
  79. package/skills/ultra-test/SKILL.md +48 -55
  80. package/skills/ultra-verify/SKILL.md +1 -1
  81. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  82. package/spec/cli-protocol.md +7 -2
  83. package/spec/mcp-tools.yaml +384 -63
  84. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  85. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  86. package/spec/schemas/mcp-tools.schema.json +1 -1
  87. package/spec/schemas/state-db.sql +82 -1
  88. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  89. package/spec/schemas/test-report.v1.schema.json +194 -0
  90. package/ultra-tools/cli.cjs +1 -1
  91. package/ultra-tools/commands/migrate.cjs +3 -3
  92. package/ultra-tools/commands/status.cjs +213 -12
  93. package/.ultra-template/delivery-report.json +0 -9
  94. package/.ultra-template/test-report.json +0 -8
  95. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  96. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  97. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  98. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  99. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  100. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  101. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
  102. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  103. package/adapters/_shared/tests/validate.test.cjs +0 -30
  104. package/adapters/tests/claude.test.cjs +0 -138
  105. package/adapters/tests/codex-hook.test.cjs +0 -142
  106. package/adapters/tests/codex.test.cjs +0 -430
  107. package/adapters/tests/kimi-hook.test.cjs +0 -149
  108. package/adapters/tests/kimi.test.cjs +0 -259
  109. package/adapters/tests/opencode.test.cjs +0 -357
  110. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  111. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  112. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  113. package/mcp-server/lib/context-spine.test.cjs +0 -93
  114. package/mcp-server/lib/doctor.test.cjs +0 -148
  115. package/mcp-server/lib/plan-store.test.cjs +0 -132
  116. package/mcp-server/lib/prd-parser.test.cjs +0 -82
  117. package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
  118. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  119. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  120. package/mcp-server/lib/state-db.test.cjs +0 -314
  121. package/mcp-server/lib/task-expander.test.cjs +0 -169
  122. package/mcp-server/lib/topo.test.cjs +0 -158
  123. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  124. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  125. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  126. package/mcp-server/tests/concurrency.test.cjs +0 -112
  127. package/mcp-server/tests/db-ops.test.cjs +0 -121
  128. package/mcp-server/tests/init-project.test.cjs +0 -393
  129. package/mcp-server/tests/migrate.test.cjs +0 -232
  130. package/mcp-server/tests/projector.test.cjs +0 -162
  131. package/mcp-server/tests/server-contract.test.cjs +0 -1467
  132. package/mcp-server/tests/staleness.test.cjs +0 -190
  133. package/mcp-server/tests/state-db.test.cjs +0 -106
  134. package/mcp-server/tests/state-ops.test.cjs +0 -449
  135. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  136. package/mcp-server/tests/telemetry.test.cjs +0 -219
  137. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  138. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  139. package/orchestrator/tests/daemon.test.cjs +0 -311
  140. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  141. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  142. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  143. package/orchestrator/tests/recovery.test.cjs +0 -190
  144. package/orchestrator/tests/session-runner.test.cjs +0 -314
  145. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  146. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  147. package/skills/ultra-research/references/architecture.md +0 -38
  148. package/skills/ultra-research/references/problem-product.md +0 -39
  149. package/skills/ultra-research/references/quality-delivery.md +0 -34
  150. package/skills/ultra-research/references/synthesis.md +0 -38
  151. package/skills/ultra-research/references/users-scope.md +0 -40
  152. package/ultra-tools/legacy-memory.test.cjs +0 -80
  153. package/ultra-tools/status-command.test.cjs +0 -181
  154. package/ultra-tools/system-command.test.cjs +0 -309
@@ -0,0 +1,266 @@
1
+ # Ultra Builder Pro workflow lifecycle
2
+
3
+ This document defines when Ultra writes authoritative state, how each workflow
4
+ advances, what invalidates evidence, and which decisions remain with the active
5
+ host and project owner. It is the operational companion to
6
+ [`ARCHITECTURE.md`](./ARCHITECTURE.md) and the machine contracts under `spec/`.
7
+
8
+ ## 1. Authority boundaries
9
+
10
+ | Surface | Owns | Must not own |
11
+ |---|---|---|
12
+ | Active host and owner | Product judgment, research content, implementation, review findings, release authorization | Durable workflow status or fabricated evidence digests |
13
+ | Skill | One reusable procedure and its evidence requirements | Project state, static product doctrine, host-specific tool fiction |
14
+ | MCP | IDs, transitions, task contracts, evidence references, output hashes, gate verdicts, next action | Research prose, code generation, memory, or model calls |
15
+ | `.ultra/state.db` | Baseline, change, task, workflow, session, event, incident, projection, and evidence authority | External memory or code-graph payloads |
16
+ | Generated JSON and Markdown | Read-only projections and durable artifacts | Independent lifecycle authority |
17
+ | Hook | Compact DB-derived lifecycle observation, minimal event metadata, and projection protection | Generic engineering judgment, prompt/transcript capture, parallel lifecycle logs, or arbitrary edit blocking |
18
+
19
+ Prompt input can supply facts, content paths, reasons, owner decisions, and
20
+ evidence references. MCP reads the current repository and DB, validates those
21
+ inputs, hashes outputs, and derives status. Prompt input cannot override a task's
22
+ public seam, verification command, context references, workflow summary, gate
23
+ verdict, output digest, or next action.
24
+
25
+ Every change-bound research, plan, dev, test, review, and deliver run inherits
26
+ the baseline id from the owning change workflow. A Prompt cannot switch that
27
+ authority, and no new stage may start on an archived or cancelled change. Doctor
28
+ recovers a legacy change without this link into an explicitly blocked workflow;
29
+ it does not fabricate the missing provenance.
30
+
31
+ Ultra does not select an MVP. Product scope and delivery posture are owner
32
+ decisions. A reduced research mode is valid only for an explicitly bounded active
33
+ change with a recorded selection reason; initial greenfield and brownfield
34
+ baselines always use all seventeen research steps.
35
+
36
+ ## 2. How `ultra-init` classifies a repository
37
+
38
+ `task.init_project` scans a bounded repository view while ignoring `.git`,
39
+ `.ultra`, dependencies, virtual environments, generated build output, and other
40
+ known non-source directories.
41
+
42
+ `auto` classification is deterministic:
43
+
44
+ - `brownfield` when delivered-system evidence exists: application source, tests,
45
+ deployment configuration, or persisted-state/schema evidence;
46
+ - `greenfield` when none of those signals exists. A Git repository, README,
47
+ package manifest, workspace manifest, or starter documentation alone does not
48
+ prove delivered behavior;
49
+ - `migrated` is never inferred from source. It is a compatibility state created
50
+ when older Ultra authority is preserved and upgraded. It requires a new
51
+ evidence-backed brownfield adoption before normal work.
52
+
53
+ The scan also records bounded signal paths, detected verification commands,
54
+ evidence-backed project type and technology signals, monorepo markers, candidate
55
+ workspace roots, and whether the scan limit was reached. Explicit owner-supplied
56
+ project type or stack metadata overrides detection without deleting the observed
57
+ signals. In a monorepo, the owner selects the repository scope; baseline
58
+ worktree digests and accepted dirty files are restricted to that scope.
59
+
60
+ Classification and readiness are separate. Initialization immediately creates
61
+ or resumes `.ultra/state.db`, installs only the required scaffold, records the
62
+ classification, opens an `init` workflow, and opens `full` research for
63
+ greenfield or `adoption` research for brownfield. Empty templates are not an
64
+ approved baseline.
65
+
66
+ ## 3. Durable status machines
67
+
68
+ ### Baseline
69
+
70
+ ```text
71
+ draft -> adopting -> ready -> superseded
72
+ |
73
+ -> blocked -> adopting
74
+ ```
75
+
76
+ - `draft`: greenfield authority exists but has not been established.
77
+ - `adopting`: brownfield or migrated evidence is being recorded.
78
+ - `blocked`: current baseline evidence, verification, scope, or approval cannot
79
+ converge.
80
+ - `ready`: research, evidence, repository snapshot, gaps, and approval converged.
81
+ - `superseded`: preserved historical baseline after an explicitly authorized
82
+ replacement.
83
+
84
+ `ready` is not trusted as a label alone. Every baseline/status/doctor/change gate
85
+ revalidates its persisted fields, current files, research outputs, revision and
86
+ worktree, known-red acceptance, blocking gaps, and approval provenance. An ordinary
87
+ active change may downgrade only the expected drift created after its completed
88
+ `bind-baseline` step to warnings; it cannot downgrade adoption, migration, missing
89
+ evidence, or structural blockers. Approved incident break-glass is the only other
90
+ execution exception and creates a mandatory reconciliation gap.
91
+
92
+ ### Change
93
+
94
+ ```text
95
+ active <-> blocked -> ready -> archived
96
+ \--------------------------> cancelled
97
+ ```
98
+
99
+ Only a healthy `ready` baseline may authorize a new ordinary change. An incident
100
+ may bypass an unhealthy baseline only with a stored reason and approver; archive
101
+ then creates a blocking baseline-reconciliation gap.
102
+
103
+ ### Task
104
+
105
+ ```text
106
+ pending -> in_progress -> completed
107
+ | |
108
+ v v
109
+ blocked <------+
110
+ |
111
+ -> pending
112
+
113
+ pending -> expanded -> completed
114
+ ```
115
+
116
+ A task is executable only when its DB contract contains an observable outcome,
117
+ traceability, slice kind, public seam, exact verification command, acceptance
118
+ checks, context references, resolved documentation impact, owner, and valid
119
+ dependencies.
120
+
121
+ ### Workflow run and step
122
+
123
+ ```text
124
+ run: active <-> blocked -> ready -> completed
125
+ \----> cancelled
126
+
127
+ step: pending -> in_progress -> completed
128
+ | |
129
+ v v
130
+ blocked ------> in_progress/completed
131
+ ```
132
+
133
+ Required steps execute in definition order. A step that requires evidence or an
134
+ output cannot complete without it. When the final required step completes, the
135
+ run becomes `ready`; `workflow.complete` revalidates the entire stage before
136
+ making it `completed`.
137
+
138
+ ## 4. Write and verification lifecycle
139
+
140
+ | Stage | Authoritative writes | Completion verification | Next route |
141
+ |---|---|---|---|
142
+ | `init` | Baseline classification/scope, init run, research run, initialization event, projection job | DB/schema opens, scaffold/projection succeeds; baseline convergence later completes init | `research`, re-adoption, or `change` for an already-ready baseline |
143
+ | `research` | Ordered step evidence, decisions, typed semantic records with source digests, one immutable report per selected step, output paths and SHA-256 digests | Every report has the required structure; semantic kinds/attributes/source anchors are valid and current; full/adoption synthesis binds the current three specifications and distillate; all digests remain current | `baseline.record`, then approval/convergence; bounded change research returns to its change |
144
+ | baseline convergence | Spec/source/runtime refs, verification results, known failures, unknowns, gaps, branch/HEAD/worktree, approval | Full/adoption research complete; required discovery/product/architecture refs current; revision/worktree exact; blocking gaps resolved or explicitly accepted where allowed | `change` for every initial or daily outcome |
145
+ | `change` | Complete Change Contract, profile/risk rationale, research disposition, intent/delta artifacts, linked change run, initial context snapshot | Blocking decisions resolved; selected change research current; current plan task set matches DB; task-bound context is ready; one next action is derived | selected `research`, then `plan`, or `dev` for an already approved current plan |
146
+ | `plan` | Plan-step evidence, complete change-owned task rows, acceptance coverage matrix, change-bound `.ultra/execution-plan.json`, explicit plan approval | Every Change acceptance id is owned by a task; task trace targets resolve; quick has one task; ownership/dependencies valid; no cycles; exported topology and digest exactly match the current change task set | `dev` for the first dependency-ready task |
147
+ | `dev` | Task/session transitions, step evidence, immutable implementation context, completion/review references | Task completed; no live session; task contract unchanged; starting context and review artifacts valid; completion commits remain ancestors of current HEAD | Next task, then aggregate `test` |
148
+ | `test` | Immutable checking context and `.ultra/reports/tests/<workflow-id>.json` | Report change/task set, full HEAD, worktree digest, context digest, commands, public seam, blockers, and all acceptance/regression/integration/static/build/performance/security/recovery dimensions agree; every omission has a not-applicable rationale | `review` when passing, otherwise resume `dev`/`test` |
149
+ | `review` | Immutable review context, `task`/`change`/`plan` mode, worker selection provenance, two specialist axes, coordinated summary | Current diff/HEAD and context match; selected/skipped workers match recorded rationale; both axes complete; P0/P1 and axis verdicts deterministically derive final verdict | `deliver` only from approved `change` mode, otherwise resume implementation |
150
+ | `deliver` | Convergence context, verified learning resolutions, schema-validated semantic reconciliation manifest, change archive, durable release-authorization decision, immutable delivery report | Tasks/test/change-review current; every baseline update has an anchored before/after semantic record; `change.converge` derives evidence from DB and accepts no Prompt verdicts; archive and baseline transaction agree; external actions have evidence | Report completion or start the next `change` |
151
+ | `status` | None | Reads doctor, baseline, workflows, breadcrumb, tasks, sessions, Git, and DB-referenced reports | One exact current route |
152
+ | `doctor` | None by default; explicit repair writes only mechanical recovery state | Re-runs integrity, schema, workflow-output, projection, session, incident, archive, and install checks | Exact repair, restore/rebaseline decision, or workflow route |
153
+
154
+ Every mutating MCP operation appends an event and enqueues projection work when
155
+ the corresponding read-only view can change. Projection failure is recorded as
156
+ a retryable incident; it does not silently convert a failed write into success.
157
+ `task.append_event` accepts only non-authoritative observations. Lifecycle events are
158
+ emitted by their owning DB mutation and never act as a substitute state transition.
159
+
160
+ ## 5. Context and prompt follow-through
161
+
162
+ `change.context` compiles an immutable Context Manifest v2 snapshot. It combines:
163
+
164
+ - the bound baseline/change/task identity;
165
+ - a role and lifecycle gate;
166
+ - DB-backed task outcome, public seam, verification command, and required refs;
167
+ - local file hashes, current HEAD, branch, and worktree digest;
168
+ - optional metadata-only external memory or graph references;
169
+ - advisory file/token/context-share budgets;
170
+ - readiness blockers and one DB-derived next action.
171
+
172
+ Critical stage workflows bind a matching snapshot as a durable output:
173
+
174
+ | Workflow | Required role | Required gate |
175
+ |---|---|---|
176
+ | `change` | `plan` or `implement` | `planning` or `implementation` |
177
+ | `dev` | `implement` | `implementation` |
178
+ | `test` | `check` | `verification` |
179
+ | `review` | `review` | `review` |
180
+ | `deliver` | `check` | `convergence` |
181
+
182
+ The snapshot path and digest are then carried into test, review, and delivery
183
+ reports. This prevents a later Prompt from claiming evidence gathered under a
184
+ different context. File and token budgets warn; they do not block legitimate
185
+ work. Missing authority, required refs, task contracts, outputs, approval, or
186
+ current gate evidence do block.
187
+
188
+ ## 6. Invalidation rules
189
+
190
+ - Changing a task contract invalidates contexts and gates that depended on the
191
+ former contract.
192
+ - Changing required source/spec content invalidates the recorded output or
193
+ context digest.
194
+ - Changing a research semantic source invalidates that workflow even when its immutable
195
+ step report remains unchanged.
196
+ - Changing full HEAD or the worktree invalidates aggregate test, review, and
197
+ delivery evidence.
198
+ - A completed dev run remains valid across later task commits when its bound
199
+ task contract is unchanged and its recorded commits remain ancestors of the
200
+ current HEAD.
201
+ - Test, review, and delivery reports use workflow-id paths so a later run cannot
202
+ overwrite historical evidence.
203
+ - `.ultra` workflow artifacts are excluded from application-worktree drift, but
204
+ their own recorded SHA-256 digests are still authoritative.
205
+ - Generated `tasks.json` and task-context headers are projections. Manual edits
206
+ are overwritten and never repair DB authority.
207
+
208
+ ## 7. Entry flows
209
+
210
+ ### New project
211
+
212
+ ```text
213
+ ultra-init -> full research (17 steps) -> baseline approval
214
+ -> initial change + plan -> dev per task -> test -> review -> deliver
215
+ ```
216
+
217
+ ### Existing project
218
+
219
+ ```text
220
+ ultra-init -> brownfield adoption research (17 steps)
221
+ -> characterization and known-red evidence -> gap ledger -> owner approval
222
+ -> first selected change -> plan -> dev -> test -> review -> deliver
223
+ ```
224
+
225
+ Adoption records the system that exists. It does not force the owner to recreate
226
+ market, persona, or roadmap claims that cannot be supported; those remain
227
+ `Unknown` or explicit gaps while every semantic step is still processed.
228
+
229
+ ### Earlier Ultra project
230
+
231
+ ```text
232
+ doctor/migrate with backup -> migrated compatibility authority
233
+ -> explicit brownfield re-adoption -> full convergence -> normal change loop
234
+ ```
235
+
236
+ Migration preserves prior files and rows but never carries old approval forward.
237
+
238
+ ### Daily maintenance
239
+
240
+ ```text
241
+ ultra-status -> ultra-change -> plan if task contracts are absent
242
+ -> dev -> test -> review -> deliver -> reconciled baseline
243
+ ```
244
+
245
+ Quick changes shorten the artifact burden only where the machine contract allows;
246
+ they do not skip task ownership, executable acceptance, current checking, review,
247
+ or baseline reconciliation. Incidents add structured diagnosis and recovery
248
+ evidence rather than bypassing them.
249
+
250
+ ## 8. Recovery and diagnosis
251
+
252
+ Use `system.doctor` for project authority and `ubp --doctor` for installed host
253
+ assets. Both are read-only unless an explicit project repair command is chosen.
254
+
255
+ During an explicit repair, a legacy active change that predates durable workflows gets
256
+ one blocked change run at `bind-baseline` with
257
+ `LEGACY_CHANGE_PROVENANCE_REQUIRED`; repair does not invent its evidence. Rebind it to
258
+ the re-adopted baseline and resume the same run before planning. Every plan must use
259
+ the exact baseline id stored by its owning change run; only an approved break-glass
260
+ incident may legitimately bind no baseline id.
261
+
262
+ Resume the same blocked workflow and current step after resolving its blocker.
263
+ Do not create parallel runs to escape state. Supported schema and projection
264
+ repairs are backup-first. A corrupt DB is preserved and requires an explicit
265
+ restore or rebaseline decision; neither action can manufacture research,
266
+ verification, or owner approval.
package/hooks/README.md CHANGED
@@ -14,12 +14,17 @@ advisory workflow conditions into refusal gates.
14
14
  | Before compact | `workflow_checkpoint.py` | Validate and atomically save a minimal workflow checkpoint |
15
15
  | After compact/resume | `workflow_resume.py` | Prefer the DB breadcrumb; restore the minimal file checkpoint only when no active change owns recovery |
16
16
  | Stop | `pre_stop_check.py` | Report an incomplete workflow boundary and allow stop |
17
- | Subagent lifecycle | `subagent_tracker.py` | Append lifecycle evidence under `.ultra/runtime/` |
17
+ | Subagent lifecycle | `subagent_tracker.py` | Append minimal start/stop metadata to the authoritative DB event stream |
18
18
 
19
19
  The plugin deliberately does not capture prompts, tool observations, transcripts, summaries, or
20
20
  cross-session memory or code-graph content. Generic command blocking and post-edit policy are
21
21
  user/repository governance. Persistent Memory/graph content belongs to separately installed providers.
22
22
 
23
+ `subagent_tracker.py` passes only agent id/type and the host session id to the bundled
24
+ `hook-event.cjs` helper. The helper binds the current change/task and appends
25
+ `subagent_started` or `subagent_stopped` to `state.db`; it never stores transcript
26
+ paths, messages, or a parallel JSONL lifecycle log.
27
+
23
28
  `context_spine.py` is an imported library, not an eighth lifecycle registration.
24
29
  Registering it separately would duplicate hook execution. The checkpoint is a recovery artifact, not a second authority. A valid newer
25
30
  live workflow wins; a newer valid checkpoint may restore a missing or older
@@ -14,7 +14,7 @@ REQUIRED_TABLES = {
14
14
  "tasks", "events", "sessions", "schema_version", "migration_history",
15
15
  "telemetry", "specs_refs", "circuit_breaker", "changes", "artifacts",
16
16
  "context_snapshots", "spec_learning_candidates", "trace_links", "incidents", "projection_jobs",
17
- "event_consumers",
17
+ "event_consumers", "workflow_runs", "workflow_steps",
18
18
  }
19
19
 
20
20
 
@@ -72,12 +72,21 @@ def inspect(root: Path) -> dict:
72
72
  ]
73
73
  if str(code).startswith("BASELINE_")
74
74
  ]
75
+ baseline = breadcrumb.get("baseline") if breadcrumb else None
76
+ baseline_is_ready = isinstance(baseline, dict) and baseline.get("status") == "ready"
77
+ baseline_status = (
78
+ "fail" if baseline_codes and baseline_is_ready
79
+ else "warning" if baseline_codes
80
+ else "pass"
81
+ )
75
82
  report["checks"]["baseline"] = {
76
- "baseline": breadcrumb.get("baseline") if breadcrumb else None,
83
+ "baseline": baseline,
77
84
  "blockers": baseline_codes,
78
85
  "readiness": "blocked" if baseline_codes else "ready",
79
- "status": "warning" if baseline_codes else "pass",
86
+ "status": baseline_status,
80
87
  }
88
+ if baseline_status == "fail":
89
+ issues.append("baseline")
81
90
 
82
91
  incidents = [
83
92
  {"id": row[0], "code": row[1], "severity": row[2], "retryable": bool(row[3])}
@@ -1,69 +1,67 @@
1
1
  #!/usr/bin/env python3
2
- """Subagent lifecycle tracker.
2
+ """Append minimal Ultra subagent lifecycle metadata to state.db events.
3
3
 
4
- Logs SubagentStart/Stop events to .ultra/runtime/subagent-log.jsonl (project-level)
5
- for debugging and cost analysis.
6
-
7
- Protocol fields:
8
- SubagentStart: agent_id, agent_type, session_id
9
- SubagentStop: agent_id, agent_type, session_id, agent_transcript_path,
10
- last_assistant_message, stop_hook_active
11
-
12
- Usage:
13
- python3 subagent_tracker.py start # called by SubagentStart hook
14
- python3 subagent_tracker.py stop # called by SubagentStop hook
4
+ The hook never stores prompts, messages, transcript paths, or a parallel JSONL log.
5
+ It is advisory: a failed lifecycle event is reported on stderr but never blocks the
6
+ host's subagent operation.
15
7
  """
16
8
 
17
9
  import json
10
+ import os
11
+ import shutil
12
+ import subprocess
18
13
  import sys
19
- from datetime import datetime, timezone
20
14
  from pathlib import Path
21
15
 
22
- from context_spine import ContextSpineError, find_root, read_breadcrumb
16
+ from context_spine import find_root
23
17
 
24
- MAX_LOG_LINES = 5000
25
18
 
19
+ def helper_path() -> Path:
20
+ hook_root = Path(__file__).resolve().parent
21
+ installed = hook_root.parent / "runtime" / "hook-event.cjs"
22
+ if installed.is_file():
23
+ return installed
24
+ source = hook_root.parent / "mcp-server" / "hook-event.cjs"
25
+ if source.is_file():
26
+ return source
27
+ raise RuntimeError("bundled hook lifecycle helper is missing")
26
28
 
27
- def get_log_dir(hook_input: dict):
28
- root = find_root(Path(hook_input.get("cwd") or Path.cwd()).resolve())
29
- if root is None:
30
- return None
31
- try:
32
- breadcrumb = read_breadcrumb(root)
33
- except ContextSpineError as exc:
34
- print(f"[subagent_tracker] cannot inspect Context Spine: {exc}", file=sys.stderr)
35
- return None
36
- if breadcrumb and breadcrumb.get("change_id"):
37
- return root / ".ultra" / "runtime"
38
- return None
39
29
 
30
+ def node_binary() -> str:
31
+ configured = os.environ.get("UBP_NODE")
32
+ if configured:
33
+ return configured
34
+ discovered = shutil.which("node")
35
+ if discovered:
36
+ return discovered
37
+ raise RuntimeError("node executable is unavailable")
40
38
 
41
- def rotate_log(log_file: Path) -> None:
42
- """Keep last MAX_LOG_LINES entries to prevent unbounded growth."""
43
- try:
44
- if not log_file.exists():
45
- return
46
- with open(log_file, "r", encoding="utf-8") as f:
47
- lines = f.readlines()
48
- if len(lines) <= MAX_LOG_LINES:
49
- return
50
- with open(log_file, "w", encoding="utf-8") as f:
51
- f.writelines(lines[-MAX_LOG_LINES:])
52
- except OSError as exc:
53
- print(f"[subagent_tracker] log rotation failed: {exc}", file=sys.stderr)
54
-
55
-
56
- def main():
57
- if len(sys.argv) < 2:
58
- print(json.dumps({}))
59
- return
60
39
 
61
- action = sys.argv[1]
40
+ def append_event(root: Path, action: str, hook_input: dict) -> None:
41
+ payload = {
42
+ "agent_id": hook_input.get("agent_id", "unknown"),
43
+ "agent_type": hook_input.get("agent_type", "unknown"),
44
+ "session_id": hook_input.get("session_id", ""),
45
+ }
46
+ result = subprocess.run(
47
+ [node_binary(), str(helper_path()), str(root.resolve()), action],
48
+ input=json.dumps(payload),
49
+ text=True,
50
+ capture_output=True,
51
+ check=False,
52
+ timeout=5,
53
+ )
54
+ if result.returncode != 0:
55
+ detail = result.stderr.strip() or f"exit {result.returncode}"
56
+ raise RuntimeError(detail)
57
+
58
+
59
+ def main() -> None:
60
+ action = sys.argv[1] if len(sys.argv) > 1 else ""
62
61
  if action not in ("start", "stop"):
63
62
  print(json.dumps({}))
64
63
  return
65
64
 
66
- # Read hook input from stdin
67
65
  try:
68
66
  raw = sys.stdin.read()
69
67
  hook_input = json.loads(raw) if raw.strip() else {}
@@ -73,35 +71,12 @@ def main():
73
71
  print(f"[subagent_tracker] invalid hook input: {exc}", file=sys.stderr)
74
72
  hook_input = {}
75
73
 
76
- # Lazy init: avoid module-level subprocess
77
- log_dir = get_log_dir(hook_input)
78
- if log_dir is None:
79
- print(json.dumps({}))
80
- return
81
- log_file = log_dir / "subagent-log.jsonl"
82
- log_dir.mkdir(parents=True, exist_ok=True)
83
-
84
- entry = {
85
- "timestamp": datetime.now(timezone.utc).isoformat(),
86
- "event": f"subagent_{action}",
87
- "agent_id": hook_input.get("agent_id", "unknown"),
88
- "agent_type": hook_input.get("agent_type", "unknown"),
89
- "session_id": hook_input.get("session_id", ""),
90
- }
91
-
92
- # SubagentStop provides additional fields
93
- if action == "stop":
94
- transcript = hook_input.get("agent_transcript_path", "")
95
- if transcript:
96
- entry["agent_transcript_path"] = transcript
97
-
98
- try:
99
- with open(log_file, "a", encoding="utf-8") as f:
100
- f.write(json.dumps(entry, ensure_ascii=False) + "\n")
101
- except OSError as exc:
102
- print(f"[subagent_tracker] event write failed: {exc}", file=sys.stderr)
103
-
104
- rotate_log(log_file)
74
+ root = find_root(Path(hook_input.get("cwd") or Path.cwd()).resolve())
75
+ if root is not None:
76
+ try:
77
+ append_event(root, action, hook_input)
78
+ except (OSError, RuntimeError, subprocess.TimeoutExpired) as exc:
79
+ print(f"[subagent_tracker] lifecycle event unavailable: {exc}", file=sys.stderr)
105
80
 
106
81
  print(json.dumps({}))
107
82
 
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const runtime = fs.existsSync(path.join(__dirname, 'index.cjs'))
8
+ ? require('./index.cjs')
9
+ : require('./server.cjs');
10
+
11
+ function main() {
12
+ const rootDir = path.resolve(process.argv[2] || process.cwd());
13
+ const action = process.argv[3];
14
+ let hookInput = {};
15
+ const raw = fs.readFileSync(0, 'utf8');
16
+ if (raw.trim()) {
17
+ hookInput = JSON.parse(raw);
18
+ if (!hookInput || typeof hookInput !== 'object' || Array.isArray(hookInput)) {
19
+ throw new Error('hook input must be a JSON object');
20
+ }
21
+ }
22
+ const result = runtime.appendHookLifecycleEvent({ rootDir, action, hookInput });
23
+ process.stdout.write(`${JSON.stringify(result)}\n`);
24
+ }
25
+
26
+ if (require.main === module) {
27
+ try { main(); }
28
+ catch (error) {
29
+ process.stderr.write(`hook lifecycle event failed: ${error.message}\n`);
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ module.exports = { main };
@@ -31,6 +31,9 @@ function assertIntent(intent, rootDir) {
31
31
  || intent.schema_version !== INTENT_VERSION
32
32
  || typeof intent.change_id !== 'string' || !intent.change_id
33
33
  || !Array.isArray(intent.baseline_updates)
34
+ || typeof intent.reconciliation_path !== 'string' || !intent.reconciliation_path
35
+ || !/^[0-9a-f]{64}$/.test(String(intent.reconciliation_digest || ''))
36
+ || !intent.reconciliation_manifest || typeof intent.reconciliation_manifest !== 'object'
34
37
  || typeof intent.summary !== 'string' || intent.summary.trim().length < 3) {
35
38
  throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', 'archive intent shape is invalid');
36
39
  }
@@ -46,6 +49,7 @@ function assertIntent(intent, rootDir) {
46
49
  throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', 'archive intent destination is not the change archive root');
47
50
  }
48
51
  for (const update of intent.baseline_updates) resolveInside(rootDir, update, 'baseline update');
52
+ resolveInside(rootDir, intent.reconciliation_path, 'reconciliation path');
49
53
  if (intent.previous_summary !== null && typeof intent.previous_summary !== 'string') {
50
54
  throw new ArchiveJournalError('ARCHIVE_INTENT_INVALID', 'previous_summary must be string or null');
51
55
  }
@@ -77,16 +81,21 @@ function renderSummary(change, summary, baselineUpdates, noBaselineChangeReason)
77
81
  ].join('\n');
78
82
  }
79
83
 
80
- function sameOperation(intent, { change, summary, baselineUpdates, noBaselineChangeReason }) {
84
+ function sameOperation(intent, {
85
+ change, summary, baselineUpdates, noBaselineChangeReason, reconciliationPath, reconciliationDigest,
86
+ }) {
81
87
  return intent.change_id === change.id
82
88
  && intent.source === change.artifact_root
83
89
  && intent.summary === summary.trim()
84
90
  && JSON.stringify(intent.baseline_updates) === JSON.stringify(baselineUpdates)
85
- && (intent.no_baseline_change_reason || null) === (noBaselineChangeReason || null);
91
+ && (intent.no_baseline_change_reason || null) === (noBaselineChangeReason || null)
92
+ && intent.reconciliation_path === reconciliationPath
93
+ && intent.reconciliation_digest === reconciliationDigest;
86
94
  }
87
95
 
88
96
  function prepareArchiveMove({
89
- rootDir, change, summary, baselineUpdates, noBaselineChangeReason, now = new Date(),
97
+ rootDir, change, summary, baselineUpdates, noBaselineChangeReason,
98
+ reconciliationPath, reconciliationDigest, reconciliationManifest, now = new Date(),
90
99
  }) {
91
100
  const date = now.toISOString().slice(0, 10);
92
101
  const source = resolveInside(rootDir, change.artifact_root, 'source');
@@ -98,7 +107,7 @@ function prepareArchiveMove({
98
107
  if (!fs.existsSync(source) && fs.existsSync(destinationIntentFile)) {
99
108
  const resumed = readIntentFile(destinationIntentFile, rootDir);
100
109
  if (!sameOperation(resumed.intent, {
101
- change, summary, baselineUpdates, noBaselineChangeReason,
110
+ change, summary, baselineUpdates, noBaselineChangeReason, reconciliationPath, reconciliationDigest,
102
111
  })) {
103
112
  throw new ArchiveJournalError('ARCHIVE_INTENT_CONFLICT', 'existing archive intent differs from retry input');
104
113
  }
@@ -115,7 +124,7 @@ function prepareArchiveMove({
115
124
  if (fs.existsSync(sourceIntentFile)) {
116
125
  prepared = readIntentFile(sourceIntentFile, rootDir);
117
126
  if (!sameOperation(prepared.intent, {
118
- change, summary, baselineUpdates, noBaselineChangeReason,
127
+ change, summary, baselineUpdates, noBaselineChangeReason, reconciliationPath, reconciliationDigest,
119
128
  })) {
120
129
  throw new ArchiveJournalError('ARCHIVE_INTENT_CONFLICT', 'prepared archive intent differs from retry input');
121
130
  }
@@ -130,6 +139,9 @@ function prepareArchiveMove({
130
139
  summary: summary.trim(),
131
140
  baseline_updates: baselineUpdates,
132
141
  no_baseline_change_reason: noBaselineChangeReason || null,
142
+ reconciliation_path: reconciliationPath,
143
+ reconciliation_digest: reconciliationDigest,
144
+ reconciliation_manifest: reconciliationManifest,
133
145
  previous_summary: previousSummary,
134
146
  created_at: now.toISOString(),
135
147
  };