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.
- 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 +44 -1
- package/README.md +34 -19
- package/adapters/_shared/codex-assets.cjs +6 -2
- package/adapters/_shared/file-ops.cjs +6 -2
- 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/docs/AGENT-CONTEXT.md +15 -8
- package/docs/ARCHITECTURE.md +33 -12
- package/docs/PLAN.zh-CN.md +3 -2
- package/docs/ROADMAP.md +8 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
- package/docs/WORKFLOW-LIFECYCLE.md +266 -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 +550 -85
- package/mcp-server/lib/context-spine.cjs +242 -24
- package/mcp-server/lib/doctor.cjs +33 -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 +6 -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 +145 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/workflow-state.cjs +2292 -0
- package/mcp-server/server.cjs +161 -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 +53 -68
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +69 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +54 -59
- package/skills/ultra-doctor/SKILL.md +40 -71
- package/skills/ultra-init/SKILL.md +96 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +79 -85
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +87 -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 +57 -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 -45
- package/skills/ultra-test/SKILL.md +48 -55
- 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 +7 -2
- package/spec/mcp-tools.yaml +384 -63
- 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 +82 -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 +213 -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
|
@@ -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
|
|
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
|
package/hooks/health_check.py
CHANGED
|
@@ -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":
|
|
83
|
+
"baseline": baseline,
|
|
77
84
|
"blockers": baseline_codes,
|
|
78
85
|
"readiness": "blocked" if baseline_codes else "ready",
|
|
79
|
-
"status":
|
|
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
|
-
"""
|
|
2
|
+
"""Append minimal Ultra subagent lifecycle metadata to state.db events.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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, {
|
|
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,
|
|
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
|
};
|