ultra-builder-pro-cli 0.6.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ultra-template/changes/active/.gitkeep +1 -0
- package/.ultra-template/changes/archive/.gitkeep +1 -0
- package/CHANGELOG.md +90 -11
- package/README.md +39 -19
- package/adapters/_shared/handbook.cjs +8 -7
- package/adapters/_shared/provenance.cjs +286 -0
- package/adapters/_shared/runtime-assets.cjs +2 -0
- package/adapters/_shared/tests/handbook.test.cjs +4 -1
- package/adapters/_shared/tests/provenance.test.cjs +133 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +2 -0
- package/adapters/claude.js +33 -0
- package/adapters/codex.js +87 -0
- package/adapters/opencode.js +147 -20
- package/adapters/tests/codex.test.cjs +42 -3
- package/adapters/tests/opencode.test.cjs +16 -9
- package/agents/debugger.md +12 -0
- package/bin/install.js +63 -27
- package/bin/orchestrator.js +1 -26
- package/commands/ultra-change.md +48 -0
- package/commands/ultra-deliver.md +14 -7
- package/commands/ultra-dev.md +6 -3
- package/commands/ultra-doctor.md +36 -0
- package/commands/ultra-init.md +4 -3
- package/commands/ultra-status.md +7 -5
- package/commands/ultra-test.md +2 -1
- package/docs/AGENT-CONTEXT.md +31 -23
- package/docs/ARCHITECTURE.md +80 -17
- package/docs/PLAN.zh-CN.md +37 -29
- package/docs/ROADMAP.md +14 -14
- package/docs/RUNTIME-COMPAT-MATRIX.md +19 -12
- package/hooks/README.md +14 -9
- package/hooks/active_task_context.py +22 -15
- package/hooks/health_check.py +136 -38
- package/hooks/workflow_checkpoint.py +15 -1
- package/hooks/workflow_context.py +120 -30
- package/hooks/workflow_resume.py +96 -7
- package/mcp-server/lib/change-workflow.cjs +625 -0
- package/mcp-server/lib/change-workflow.test.cjs +346 -0
- package/mcp-server/lib/doctor.cjs +112 -0
- package/mcp-server/lib/doctor.test.cjs +86 -0
- package/mcp-server/lib/init-project.cjs +32 -6
- package/mcp-server/lib/runtime-state.cjs +222 -0
- package/mcp-server/lib/runtime-state.test.cjs +92 -0
- package/mcp-server/lib/state-db.cjs +31 -8
- package/mcp-server/lib/state-ops.cjs +17 -3
- package/mcp-server/server.cjs +102 -10
- package/mcp-server/tests/db-ops.test.cjs +5 -1
- package/mcp-server/tests/init-project.test.cjs +45 -0
- package/mcp-server/tests/server-contract.test.cjs +123 -1
- package/mcp-server/tests/state-db.test.cjs +44 -7
- package/mcp-server/tests/state-ops.test.cjs +19 -0
- package/orchestrator/daemon.cjs +39 -0
- package/orchestrator/tests/daemon.test.cjs +50 -0
- package/package.json +3 -3
- package/settings.json +1 -2
- package/skills/ultra-change/SKILL.md +149 -0
- package/skills/ultra-deliver/SKILL.md +83 -15
- package/skills/ultra-dev/SKILL.md +32 -12
- package/skills/ultra-doctor/SKILL.md +97 -0
- package/skills/ultra-init/SKILL.md +8 -5
- package/skills/ultra-review/SKILL.md +16 -0
- package/skills/ultra-status/SKILL.md +30 -7
- package/skills/ultra-test/SKILL.md +14 -2
- package/spec/cli-protocol.md +12 -1
- package/spec/mcp-tools.yaml +337 -1
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +117 -0
- package/ultra-tools/cli.cjs +3 -0
- package/ultra-tools/commands/system.cjs +57 -0
- package/ultra-tools/system-command.test.cjs +53 -0
- package/adapters/_shared/rtk-detect.cjs +0 -86
- package/adapters/_shared/tests/rtk-detect.test.cjs +0 -116
- package/docs/LEGACY-HERMES.md +0 -1099
- package/orchestrator/code-graph-watcher.cjs +0 -121
- package/orchestrator/tests/code-graph-watcher.test.cjs +0 -183
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.1] — 2026-07-17
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Removed a retired-surface name accidentally reintroduced in the D53 decision
|
|
15
|
+
note. The unchanged retirement contract caught it before `v0.8.0` reached npm;
|
|
16
|
+
this immutable patch release carries the complete harness closure forward.
|
|
17
|
+
|
|
18
|
+
## [0.8.0] — 2026-07-17
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Added a first-class incident debug lane: incident changes now create a
|
|
23
|
+
registered `diagnosis.md` artifact whose reproduction, hypotheses, root
|
|
24
|
+
cause, regression-test, and recovery sections are required by convergence.
|
|
25
|
+
- Added normalized install-provenance manifests for Claude Code, OpenCode, and
|
|
26
|
+
Codex plus read-only `ubp --doctor [--json]` checks for asset hashes and
|
|
27
|
+
host-specific plugin, hook, MCP, launcher, and runtime entry points.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Turned workflow compaction checkpoints into a real recovery consumer:
|
|
32
|
+
resume now rejects malformed or terminal checkpoints, selects the newest
|
|
33
|
+
valid live/checkpoint state, and atomically restores missing or older live
|
|
34
|
+
workflow state before re-injecting context.
|
|
35
|
+
|
|
36
|
+
## [0.7.0] — 2026-07-17
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- Added `ultra-change` and `ultra-doctor` as native workflows on Claude Code,
|
|
41
|
+
OpenCode, and Codex. Daily fixes, features, redesigns, and incidents now stay
|
|
42
|
+
attached to an explicit change packet after the initial baseline delivery.
|
|
43
|
+
- Added seven `change.*` MCP tools and `system.doctor`, advancing the live MCP
|
|
44
|
+
contract to 29 tools and `.ultra/state.db` to schema `9.0` with 15 tables for
|
|
45
|
+
changes, artifacts, context snapshots, trace links, incidents, projection
|
|
46
|
+
jobs, and durable event-consumer cursors.
|
|
47
|
+
- Added deterministic convergence and archive gates for task completion, spec
|
|
48
|
+
deltas, tests, documentation impact, review evidence, context freshness, and
|
|
49
|
+
baseline reconciliation.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- `task.init_project` now initializes `state.db` immediately and creates active
|
|
54
|
+
and archived change roots. `tasks.json` remains a generated projection.
|
|
55
|
+
- MCP mutations now commit authority first, enqueue a durable projection job,
|
|
56
|
+
process it, and expose projection status in MCP metadata instead of silently
|
|
57
|
+
swallowing projection failures.
|
|
58
|
+
- Session-start and edit hooks now understand initialized baselines and active
|
|
59
|
+
changes. Health checks surface incidents, projection lag, orphan sessions,
|
|
60
|
+
and missing change artifacts; direct writes to the task projection remain
|
|
61
|
+
blocked even outside a command-scoped workflow.
|
|
62
|
+
- Memory and code-graph content are exclusively owned by separately installed
|
|
63
|
+
providers. Ultra accepts only provider metadata references in compiled change
|
|
64
|
+
contexts and never stores recalled content, graph payloads, or embeddings.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- Wired spec-staleness consumption, crash recovery, projection processing, and
|
|
69
|
+
durable cursors into the real orchestrator loop instead of leaving production
|
|
70
|
+
helpers unconsumed.
|
|
71
|
+
- Kept `intent.md` synchronized with authoritative change updates, linked task
|
|
72
|
+
status events to their newly assigned change, and included `plan.export`
|
|
73
|
+
events in projection accounting.
|
|
74
|
+
- Added atomic projection claims and backup-first recovery for failed or stale
|
|
75
|
+
interrupted jobs so a crashed projector cannot remain invisibly `running`.
|
|
76
|
+
- Kept all three native plugin bundles, MCP launchers, agents, hooks, commands,
|
|
77
|
+
and user-handbook renderings on one explicit Ultra-owned asset allowlist.
|
|
78
|
+
|
|
79
|
+
### Removed
|
|
80
|
+
|
|
81
|
+
- Removed the internal filesystem graph watcher and its dependency; external
|
|
82
|
+
graph providers own indexing and refresh.
|
|
83
|
+
- Removed the retired command-proxy detector, installer option, managed handbook
|
|
84
|
+
block, cache artifacts, prompts, configuration, and documentation surface.
|
|
85
|
+
- Stopped publishing the archived pre-CLI manual in the npm package; it remains
|
|
86
|
+
repository history and cannot be mistaken for an active plugin contract.
|
|
87
|
+
|
|
10
88
|
## [0.6.0] — 2026-07-17
|
|
11
89
|
|
|
12
90
|
### Changed
|
|
@@ -21,9 +99,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
21
99
|
|
|
22
100
|
### Removed
|
|
23
101
|
|
|
24
|
-
- Removed the
|
|
25
|
-
conformance packaging, pricing entries,
|
|
26
|
-
prompts and documentation that offered
|
|
102
|
+
- Removed the retired fourth-runtime adapter, installer surface, extension and
|
|
103
|
+
conformance packaging, pricing entries, collaboration companion, and all
|
|
104
|
+
active Ultra prompts and documentation that offered it as an advisor.
|
|
27
105
|
|
|
28
106
|
### Fixed
|
|
29
107
|
|
|
@@ -189,8 +267,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
189
267
|
|
|
190
268
|
- D49 tech-debt sweep: stale Phase comments (`"scheduled for Phase 1"`,
|
|
191
269
|
`"not implemented in Phase 3.1"`) replaced with accurate `unknown verb`
|
|
192
|
-
messages; `docs/ROADMAP.md` status table synchronized
|
|
193
|
-
excludes `.rtk/` local filter cache.
|
|
270
|
+
messages; `docs/ROADMAP.md` status table synchronized.
|
|
194
271
|
|
|
195
272
|
### Tests
|
|
196
273
|
|
|
@@ -201,13 +278,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
201
278
|
|
|
202
279
|
### Added
|
|
203
280
|
|
|
204
|
-
- **Phase 6 — monitoring + live code graph** (D44):
|
|
205
|
-
|
|
206
|
-
token / cost telemetry with per-runtime pricing table, `telemetry` table +
|
|
281
|
+
- **Phase 6 — monitoring + live code graph** (D44): token / cost telemetry
|
|
282
|
+
with per-runtime pricing table, `telemetry` table +
|
|
207
283
|
daily `.ultra/telemetry/YYYY-MM-DD.jsonl`; `ultra-tools status --cost`
|
|
208
284
|
panel with by-runtime / by-task / by-session aggregation; chokidar-based
|
|
209
|
-
|
|
210
|
-
opt-in via `--with-graph-watcher`.
|
|
285
|
+
then-experimental incremental graph watcher with debounce and batch thresholds.
|
|
211
286
|
- **Phase 5 — execution resilience** (D43): `recovery.cjs` boot-time orphan
|
|
212
287
|
scan using live PID probe; `circuit_breaker` table with `recordTaskFailure`
|
|
213
288
|
counter and `resetCircuitBreaker` escape hatch; spec-change staleness
|
|
@@ -257,7 +332,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
257
332
|
skill manifest, CLI protocol + mapping table; 5 spec validators.
|
|
258
333
|
- **Phase 0 — skeleton**: multi-runtime installer scaffolding.
|
|
259
334
|
|
|
260
|
-
[Unreleased]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.
|
|
335
|
+
[Unreleased]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.8.1...HEAD
|
|
336
|
+
[0.8.1]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.8.0...v0.8.1
|
|
337
|
+
[0.8.0]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.7.0...v0.8.0
|
|
338
|
+
[0.7.0]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.6.0...v0.7.0
|
|
339
|
+
[0.6.0]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.5.3...v0.6.0
|
|
261
340
|
[0.5.3]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.5.2...v0.5.3
|
|
262
341
|
[0.5.2]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.5.1...v0.5.2
|
|
263
342
|
[0.5.1]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.5.0...v0.5.1
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ authoritative `.ultra/state.db`.
|
|
|
8
8
|
|
|
9
9
|
<div align="center">
|
|
10
10
|
|
|
11
|
-
[](./CHANGELOG.md)
|
|
12
12
|
[](#verification)
|
|
13
13
|
[](./LICENSE)
|
|
14
14
|
[](./package.json)
|
|
@@ -23,18 +23,26 @@ authoritative `.ultra/state.db`.
|
|
|
23
23
|
|
|
24
24
|
- **Builds host-native plugins from one allowlist.** Claude Code, OpenCode, and
|
|
25
25
|
Codex receive their own command/skill, agent, hook, and MCP representation.
|
|
26
|
-
Install and uninstall are symmetric on every supported host
|
|
26
|
+
Install and uninstall are symmetric on every supported host; `ubp --doctor`
|
|
27
|
+
verifies the installed asset hashes and host entry points without mutation.
|
|
27
28
|
- **Shares state across runtimes.** `.ultra/state.db` (SQLite + WAL) is the
|
|
28
|
-
authoritative source for tasks, sessions, events,
|
|
29
|
-
and context markdown are
|
|
29
|
+
authoritative source for changes, tasks, sessions, events, incidents,
|
|
30
|
+
projection jobs, and telemetry. `tasks.json` and context markdown are
|
|
31
|
+
generated projections, not handwritten.
|
|
30
32
|
- **Keeps memory ownership explicit.** Ultra Builder Pro does not collect
|
|
31
33
|
prompts, transcripts, observations, summaries, or cross-session memory.
|
|
32
34
|
Install cloud-mem/claude-mem separately if persistent memory is wanted.
|
|
33
35
|
- **Runs real PRDs end-to-end.** `task.parse_prd` → `lib/topo.cjs` waves →
|
|
34
36
|
`.ultra/execution-plan.json` → parallel worktree sessions → auto-merge back.
|
|
35
|
-
- **
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
- **Keeps daily work convergent.** `/ultra-change` creates a bounded delta and
|
|
38
|
+
context manifest; `/ultra-deliver` requires docs/test/review evidence and
|
|
39
|
+
baseline reconciliation before archive. `/ultra-doctor` reports incidents,
|
|
40
|
+
projection lag, orphan sessions, and backup-first mechanical recovery.
|
|
41
|
+
Incident changes additionally require a five-section `diagnosis.md` covering
|
|
42
|
+
reproduction, hypotheses, root cause, regression test, and recovery.
|
|
43
|
+
- **Keeps external context external.** Memory and code-graph providers retain
|
|
44
|
+
their own content; Ultra stores only provider/project/revision/status metadata
|
|
45
|
+
references in a compiled change context.
|
|
38
46
|
|
|
39
47
|
## Quickstart
|
|
40
48
|
|
|
@@ -52,12 +60,16 @@ npx ultra-builder-pro-cli --claude --global
|
|
|
52
60
|
|
|
53
61
|
# Uninstall (symmetric)
|
|
54
62
|
npx ultra-builder-pro-cli --all --local --uninstall
|
|
63
|
+
|
|
64
|
+
# Read-only installation provenance and drift diagnosis
|
|
65
|
+
npx ultra-builder-pro-cli --all --local --doctor
|
|
66
|
+
npx ultra-builder-pro-cli --all --local --doctor --json
|
|
55
67
|
```
|
|
56
68
|
|
|
57
69
|
After install, start a new host session/task and point it at the project.
|
|
58
70
|
Claude Code and OpenCode expose native command forms. Codex exposes the same workflows as namespaced plugin
|
|
59
71
|
skills such as `$ultra-builder-pro:ultra-init`, `$ultra-builder-pro:ultra-plan`,
|
|
60
|
-
and `$ultra-builder-pro:ultra-dev`; `command-map.json` records the
|
|
72
|
+
and `$ultra-builder-pro:ultra-dev`; `command-map.json` records the eleven legacy
|
|
61
73
|
command mappings (`ultra-review` remains a directly invocable skill).
|
|
62
74
|
See [`docs/RUNTIME-COMPAT-MATRIX.md`](./docs/RUNTIME-COMPAT-MATRIX.md)
|
|
63
75
|
for per-runtime capabilities.
|
|
@@ -68,16 +80,17 @@ for per-runtime capabilities.
|
|
|
68
80
|
|-------|---------|----------------|
|
|
69
81
|
| **skill** (`skills/ultra-*/`) | Knowledge — prompts, workflows, prose | Runtime's native skill/prompt loader picks them up after install |
|
|
70
82
|
| **MCP** (`mcp-server/`) | Authoritative state — reads/writes `.ultra/state.db` via stdio JSON-RPC | Primary path for task / session / event / plan operations |
|
|
71
|
-
| **CLI** (`ultra-tools`, `bin/*`) |
|
|
83
|
+
| **CLI** (`ultra-tools`, `bin/*`) | Selected initialization, recovery, diagnostics, and orchestration surfaces | `ultra-tools task init-project`, `ultra-tools system doctor`, `ubp-orchestrator run` |
|
|
72
84
|
|
|
73
|
-
The
|
|
85
|
+
The layers share one `.ultra/state.db`, but the CLI is not a mirror of every MCP
|
|
86
|
+
tool: continuous change mutations are MCP-only and fail closed. See
|
|
74
87
|
[`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) for the full contract and
|
|
75
|
-
[`spec/cli-protocol.md`](./spec/cli-protocol.md) for the
|
|
88
|
+
[`spec/cli-protocol.md`](./spec/cli-protocol.md) for the 29 live contracts.
|
|
76
89
|
Review, impact discovery, skill resolution, and user interaction stay on each
|
|
77
90
|
Host's native surfaces; the generated Codex capability map documents those
|
|
78
91
|
replacements without advertising non-existent MCP tools.
|
|
79
92
|
|
|
80
|
-
The package boundary is deliberate:
|
|
93
|
+
The package boundary is deliberate: twelve public Ultra workflow skills, four
|
|
81
94
|
internal review-rule skills, and host-specific collaboration companions. Browser,
|
|
82
95
|
deployment, skill-discovery, and framework guidance belong to their original plugins.
|
|
83
96
|
|
|
@@ -99,30 +112,33 @@ Full details in [`docs/RUNTIME-COMPAT-MATRIX.md`](./docs/RUNTIME-COMPAT-MATRIX.m
|
|
|
99
112
|
## Typical workflow
|
|
100
113
|
|
|
101
114
|
```bash
|
|
102
|
-
# 1. Initialize a project (writes .ultra/ skeleton
|
|
115
|
+
# 1. Initialize a project (writes .ultra/ skeleton and initializes state.db)
|
|
103
116
|
ultra-tools task init-project --name myapp
|
|
104
117
|
|
|
105
118
|
# 2. Turn a PRD into a task graph + execution plan (human-gate via dry-run)
|
|
106
119
|
# (invoked by /ultra-plan skill or via MCP task.parse_prd + plan.export)
|
|
107
120
|
|
|
108
121
|
# 3. Run the plan — parallel sessions, auto-merge back to main on success
|
|
109
|
-
ubp-orchestrator run
|
|
122
|
+
ubp-orchestrator run
|
|
110
123
|
|
|
111
|
-
# 4. Monitor cost and
|
|
124
|
+
# 4. Monitor cost, progress, and runtime health
|
|
112
125
|
ultra-tools status
|
|
113
126
|
ultra-tools status --cost --since 24h
|
|
114
127
|
ultra-tools session list --json
|
|
128
|
+
ultra-tools system doctor
|
|
115
129
|
```
|
|
116
130
|
|
|
117
131
|
Or let the skills drive it. In Codex, invoke
|
|
118
132
|
`$ultra-builder-pro:ultra-plan` → `$ultra-builder-pro:ultra-dev` →
|
|
119
|
-
`$ultra-builder-pro:ultra-status`;
|
|
133
|
+
`$ultra-builder-pro:ultra-status`; after the baseline is delivered, daily work
|
|
134
|
+
starts with `$ultra-builder-pro:ultra-change`. Other runtimes retain their
|
|
135
|
+
native command form.
|
|
120
136
|
|
|
121
137
|
## CLI surface
|
|
122
138
|
|
|
123
139
|
| Binary | Purpose |
|
|
124
140
|
|--------|---------|
|
|
125
|
-
| `ultra-builder-pro-cli` / `ubp` | Installer — `--claude / --opencode / --codex / --all`, `--local / --global`, `--uninstall
|
|
141
|
+
| `ultra-builder-pro-cli` / `ubp` | Installer — `--claude / --opencode / --codex / --all`, `--local / --global`, `--uninstall`; read-only install checks via `--doctor [--json]` |
|
|
126
142
|
| `ubp-orchestrator` | Session dispatch daemon — `run`, `start`, `stop`, `status` |
|
|
127
143
|
| `ultra-tools` | State-layer CLI — `task`, `session`, `status`, `db`, `migrate`; explicit `legacy-memory` archive/prune migration |
|
|
128
144
|
| `ubp-handbook` | Preview/apply the managed Ultra contract in `CLAUDE.md` / `AGENTS.md`, with backup |
|
|
@@ -156,6 +172,11 @@ Individual suites: `test:state`, `test:orch`, `test:spec`, `test:rest`.
|
|
|
156
172
|
or `./.opencode` etc.; `--global` writes to the user-level dir). The
|
|
157
173
|
install log prints the exact target path. Restart Claude Code/OpenCode or
|
|
158
174
|
start a new Codex task after changing an installed plugin.
|
|
175
|
+
- **Installed hook/MCP path is missing or stale**: run
|
|
176
|
+
`npx ultra-builder-pro-cli --<runtime> --local --doctor` first. It compares
|
|
177
|
+
the recorded install provenance with current asset hashes and validates the
|
|
178
|
+
host-specific hook, MCP, plugin, and runtime-manifest entry points. Reinstall
|
|
179
|
+
only after preserving the degraded report as evidence.
|
|
159
180
|
- **Legacy Ultra memory data remains on disk**: inspect first with
|
|
160
181
|
`ultra-tools legacy-memory inspect`, archive with
|
|
161
182
|
`ultra-tools legacy-memory archive`, then prune only with the explicit
|
|
@@ -169,7 +190,7 @@ Individual suites: `test:state`, `test:orch`, `test:spec`, `test:rest`.
|
|
|
169
190
|
|
|
170
191
|
| Doc | What's in it |
|
|
171
192
|
|-----|--------------|
|
|
172
|
-
| [`docs/PLAN.zh-CN.md`](./docs/PLAN.zh-CN.md) |
|
|
193
|
+
| [`docs/PLAN.zh-CN.md`](./docs/PLAN.zh-CN.md) | Historical 12-phase plan and decision log with a current-boundary overlay |
|
|
173
194
|
| [`docs/ROADMAP.md`](./docs/ROADMAP.md) | One-page English roadmap + phase status |
|
|
174
195
|
| [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) | Single-page system architecture |
|
|
175
196
|
| [`docs/AGENT-CONTEXT.md`](./docs/AGENT-CONTEXT.md) | Canonical runtime context contract |
|
|
@@ -177,7 +198,6 @@ Individual suites: `test:state`, `test:orch`, `test:spec`, `test:rest`.
|
|
|
177
198
|
| [`docs/RUNTIME-COMPAT-MATRIX.md`](./docs/RUNTIME-COMPAT-MATRIX.md) | Per-runtime capability matrix |
|
|
178
199
|
| [`docs/STATE-DB-ACCESS-POLICY.md`](./docs/STATE-DB-ACCESS-POLICY.md) | Multi-process write contract |
|
|
179
200
|
| [`docs/COMMIT-HASH-BACKFILL.md`](./docs/COMMIT-HASH-BACKFILL.md) | Two-commit task-completion flow |
|
|
180
|
-
| [`docs/LEGACY-HERMES.md`](./docs/LEGACY-HERMES.md) | Archived pre-CLI "Hermes 6.6" documentation |
|
|
181
201
|
| [`CHANGELOG.md`](./CHANGELOG.md) | v0.1 → current release notes |
|
|
182
202
|
|
|
183
203
|
## License
|
|
@@ -14,21 +14,21 @@ const HOSTS = Object.freeze({
|
|
|
14
14
|
claude: Object.freeze({
|
|
15
15
|
name: 'Claude Code',
|
|
16
16
|
handbook: ['.claude', 'CLAUDE.md'],
|
|
17
|
-
invocation: '`/ultra-init`, `/ultra-research`, `/ultra-plan`, `/ultra-dev`, `/ultra-test`, `/ultra-review`, and `/ultra-deliver`; diagnostics use `/ultra-status
|
|
17
|
+
invocation: '`/ultra-init`, `/ultra-research`, `/ultra-plan`, `/ultra-change`, `/ultra-dev`, `/ultra-test`, `/ultra-review`, and `/ultra-deliver`; diagnostics use `/ultra-status`, `/ultra-think`, and `/ultra-doctor`',
|
|
18
18
|
collaboration: '`/codex-collab` is an explicitly requested read-only advisor',
|
|
19
19
|
coordination: 'Claude Code task UI is host coordination; it does not replace Ultra task state',
|
|
20
20
|
}),
|
|
21
21
|
codex: Object.freeze({
|
|
22
22
|
name: 'Codex',
|
|
23
23
|
handbook: ['.codex', 'AGENTS.md'],
|
|
24
|
-
invocation: '`$ultra-builder-pro:ultra-init`, `$ultra-builder-pro:ultra-research`, `$ultra-builder-pro:ultra-plan`, `$ultra-builder-pro:ultra-dev`, `$ultra-builder-pro:ultra-test`, `$ultra-builder-pro:ultra-review`, and `$ultra-builder-pro:ultra-deliver`; diagnostics use `$ultra-builder-pro:ultra-status
|
|
24
|
+
invocation: '`$ultra-builder-pro:ultra-init`, `$ultra-builder-pro:ultra-research`, `$ultra-builder-pro:ultra-plan`, `$ultra-builder-pro:ultra-change`, `$ultra-builder-pro:ultra-dev`, `$ultra-builder-pro:ultra-test`, `$ultra-builder-pro:ultra-review`, and `$ultra-builder-pro:ultra-deliver`; diagnostics use `$ultra-builder-pro:ultra-status`, `$ultra-builder-pro:ultra-think`, and `$ultra-builder-pro:ultra-doctor`',
|
|
25
25
|
collaboration: '`$ultra-builder-pro:cc-collab` is an explicitly requested read-only advisor',
|
|
26
26
|
coordination: 'The Codex plan is turn-facing coordination; it does not replace Ultra task state',
|
|
27
27
|
}),
|
|
28
28
|
opencode: Object.freeze({
|
|
29
29
|
name: 'OpenCode',
|
|
30
30
|
handbook: ['.config', 'opencode', 'AGENTS.md'],
|
|
31
|
-
invocation: '`/ultra-init`, `/ultra-research`, `/ultra-plan`, `/ultra-dev`, `/ultra-test`, `/ultra-review`, and `/ultra-deliver`; diagnostics use `/ultra-status
|
|
31
|
+
invocation: '`/ultra-init`, `/ultra-research`, `/ultra-plan`, `/ultra-change`, `/ultra-dev`, `/ultra-test`, `/ultra-review`, and `/ultra-deliver`; diagnostics use `/ultra-status`, `/ultra-think`, and `/ultra-doctor`',
|
|
32
32
|
collaboration: '`/cc-collab` and `/codex-collab` are explicitly requested read-only advisors',
|
|
33
33
|
coordination: 'OpenCode session coordination does not replace Ultra task state',
|
|
34
34
|
}),
|
|
@@ -52,11 +52,12 @@ function renderHandbook(runtime) {
|
|
|
52
52
|
`- Workflow entry points: ${value.invocation}.`,
|
|
53
53
|
`- Collaboration boundary: ${value.collaboration}. The current host remains primary and owns final verification.`,
|
|
54
54
|
`- Coordination boundary: ${value.coordination}.`,
|
|
55
|
-
'- Authority: `.ultra/state.db` is the only durable Ultra authority for tasks, sessions, events, telemetry, and review evidence. Generated JSON and Markdown are projections or workflow artifacts.',
|
|
56
|
-
'- Memory boundary: Ultra Builder Pro does not capture prompts, transcripts, observations, summaries,
|
|
57
|
-
'- Hook boundary: Ultra hooks
|
|
55
|
+
'- Authority: `.ultra/state.db` is the only durable Ultra authority for changes, tasks, sessions, events, incidents, projection state, telemetry, and review evidence. Generated JSON and Markdown are projections or workflow artifacts.',
|
|
56
|
+
'- Memory and graph boundary: Ultra Builder Pro does not capture prompts, transcripts, observations, summaries, cross-session memory, or code-graph content. Separately installed providers own that data; Ultra may store only their metadata references in a change context manifest.',
|
|
57
|
+
'- Hook boundary: Ultra hooks observe workflow/change lifecycle only. Health/context may run when `.ultra/state.db` exists; projection protection is always authoritative, while edit/compact/stop/subagent enforcement remains active-workflow scoped. Generic command blocking and post-edit policy stay in user or repository governance.',
|
|
58
|
+
'- Installation boundary: `ubp --doctor` is the read-only authority for installed asset provenance, content hashes, and host entry-point wiring; `system.doctor` remains scoped to project state and workflow recovery.',
|
|
58
59
|
'- Agent boundary: the bundled review and debugging agents are bounded workers. They use the current checkout and parent-supplied context, do not own private persistent state, and never replace the primary agent.',
|
|
59
|
-
'- Package boundary: only the
|
|
60
|
+
'- Package boundary: only the twelve Ultra workflows, four internal review-rule skills, and host-specific collaboration companions belong to this plugin. General browser, deployment, discovery, and framework skills must be installed from their owners.',
|
|
60
61
|
END_MARKER,
|
|
61
62
|
].join('\n');
|
|
62
63
|
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { spawnSync } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
const { listRelative, writeAtomic } = require('./file-ops.cjs');
|
|
9
|
+
|
|
10
|
+
const SCHEMA_VERSION = 1;
|
|
11
|
+
const DIGEST_ALGORITHM = 'sha256';
|
|
12
|
+
|
|
13
|
+
function issue(code, details = {}) {
|
|
14
|
+
return { code, ...details };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function safeRelative(value) {
|
|
18
|
+
if (typeof value !== 'string' || !value.trim() || path.isAbsolute(value)) {
|
|
19
|
+
throw new Error(`provenance asset path must be relative: ${String(value)}`);
|
|
20
|
+
}
|
|
21
|
+
const normalized = path.normalize(value);
|
|
22
|
+
if (normalized === '..' || normalized.startsWith(`..${path.sep}`)) {
|
|
23
|
+
throw new Error(`provenance asset path escapes its root: ${value}`);
|
|
24
|
+
}
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeRoots(roots) {
|
|
29
|
+
if (!roots || typeof roots !== 'object' || Array.isArray(roots)) {
|
|
30
|
+
throw new Error('provenance roots must be an object');
|
|
31
|
+
}
|
|
32
|
+
const normalized = {};
|
|
33
|
+
for (const [name, root] of Object.entries(roots)) {
|
|
34
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(name) || typeof root !== 'string' || !root.trim()) {
|
|
35
|
+
throw new Error(`invalid provenance root: ${name}`);
|
|
36
|
+
}
|
|
37
|
+
normalized[name] = path.resolve(root);
|
|
38
|
+
}
|
|
39
|
+
if (Object.keys(normalized).length === 0) throw new Error('at least one provenance root is required');
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeRef(ref, roots) {
|
|
44
|
+
if (!ref || typeof ref !== 'object' || Array.isArray(ref) || !roots[ref.root]) {
|
|
45
|
+
throw new Error(`invalid provenance asset root: ${ref && ref.root}`);
|
|
46
|
+
}
|
|
47
|
+
return { root: ref.root, path: safeRelative(ref.path) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveRef(roots, ref) {
|
|
51
|
+
const normalized = normalizeRef(ref, roots);
|
|
52
|
+
const root = roots[normalized.root];
|
|
53
|
+
const target = path.resolve(root, normalized.path);
|
|
54
|
+
if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
|
|
55
|
+
throw new Error(`provenance asset escapes root ${normalized.root}: ${normalized.path}`);
|
|
56
|
+
}
|
|
57
|
+
return { ...normalized, target };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hashFile(file) {
|
|
61
|
+
return crypto.createHash(DIGEST_ALGORITHM).update(fs.readFileSync(file)).digest('hex');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeAssetRefs(assets, roots) {
|
|
65
|
+
if (!Array.isArray(assets) || assets.length === 0) {
|
|
66
|
+
throw new Error('at least one provenance asset is required');
|
|
67
|
+
}
|
|
68
|
+
const unique = new Map();
|
|
69
|
+
for (const input of assets) {
|
|
70
|
+
const ref = normalizeRef(input, roots);
|
|
71
|
+
unique.set(`${ref.root}\0${ref.path}`, ref);
|
|
72
|
+
}
|
|
73
|
+
return [...unique.values()].sort((a, b) => (
|
|
74
|
+
a.root.localeCompare(b.root) || a.path.localeCompare(b.path)
|
|
75
|
+
));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function digestEntries(entries) {
|
|
79
|
+
const hash = crypto.createHash(DIGEST_ALGORITHM);
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
hash.update(entry.root);
|
|
82
|
+
hash.update('\0');
|
|
83
|
+
hash.update(entry.path);
|
|
84
|
+
hash.update('\0');
|
|
85
|
+
hash.update(entry.sha256);
|
|
86
|
+
hash.update('\0');
|
|
87
|
+
hash.update(String(entry.size_bytes));
|
|
88
|
+
hash.update('\0');
|
|
89
|
+
}
|
|
90
|
+
return hash.digest('hex');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sourceCommit(repoRoot) {
|
|
94
|
+
if (!fs.existsSync(path.join(repoRoot, '.git'))) return null;
|
|
95
|
+
const result = spawnSync('git', ['rev-parse', 'HEAD'], {
|
|
96
|
+
cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
97
|
+
});
|
|
98
|
+
const value = result.status === 0 ? result.stdout.trim() : '';
|
|
99
|
+
return /^[0-9a-f]{40,64}$/i.test(value) ? value : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function repositoryUrl(pkg) {
|
|
103
|
+
if (typeof pkg.repository === 'string') return pkg.repository;
|
|
104
|
+
if (pkg.repository && typeof pkg.repository.url === 'string') return pkg.repository.url;
|
|
105
|
+
return typeof pkg.homepage === 'string' ? pkg.homepage : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function packageSource(repoRoot) {
|
|
109
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
110
|
+
return {
|
|
111
|
+
packageInfo: { name: pkg.name, version: pkg.version },
|
|
112
|
+
repository: repositoryUrl(pkg),
|
|
113
|
+
sourceCommit: sourceCommit(repoRoot),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function assetRefsForTree(rootName, root, { exclude = [] } = {}) {
|
|
118
|
+
const excluded = new Set(exclude.map((entry) => path.normalize(entry)));
|
|
119
|
+
return listRelative(root)
|
|
120
|
+
.filter((entry) => !excluded.has(path.normalize(entry)))
|
|
121
|
+
.map((entry) => ({ root: rootName, path: entry }));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeProvenance({
|
|
125
|
+
file, adapter, packageInfo, repository, sourceCommit: commit = null,
|
|
126
|
+
roots: inputRoots, assets: inputAssets, contracts: inputContracts = {},
|
|
127
|
+
}) {
|
|
128
|
+
if (!file || !adapter || !packageInfo?.name || !packageInfo?.version || !repository) {
|
|
129
|
+
throw new Error('provenance file, adapter, package, version, and repository are required');
|
|
130
|
+
}
|
|
131
|
+
const roots = normalizeRoots(inputRoots);
|
|
132
|
+
const refs = normalizeAssetRefs(inputAssets, roots);
|
|
133
|
+
const assets = refs.map((ref) => {
|
|
134
|
+
const { target } = resolveRef(roots, ref);
|
|
135
|
+
const stat = fs.statSync(target);
|
|
136
|
+
if (!stat.isFile()) throw new Error(`provenance asset is not a file: ${target}`);
|
|
137
|
+
return { ...ref, size_bytes: stat.size, sha256: hashFile(target) };
|
|
138
|
+
});
|
|
139
|
+
const contracts = {};
|
|
140
|
+
for (const [name, ref] of Object.entries(inputContracts || {})) {
|
|
141
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`invalid provenance contract: ${name}`);
|
|
142
|
+
contracts[name] = normalizeRef(ref, roots);
|
|
143
|
+
}
|
|
144
|
+
const manifest = {
|
|
145
|
+
schema_version: SCHEMA_VERSION,
|
|
146
|
+
adapter,
|
|
147
|
+
package: { name: packageInfo.name, version: packageInfo.version },
|
|
148
|
+
source: { repository, commit: commit || null },
|
|
149
|
+
installed_at: new Date().toISOString(),
|
|
150
|
+
roots,
|
|
151
|
+
content: { algorithm: DIGEST_ALGORITHM, digest: digestEntries(assets) },
|
|
152
|
+
assets,
|
|
153
|
+
contracts,
|
|
154
|
+
};
|
|
155
|
+
writeAtomic(path.resolve(file), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
156
|
+
return manifest;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function invalidReport(file, status, problem, adapter = null) {
|
|
160
|
+
return {
|
|
161
|
+
adapter,
|
|
162
|
+
status,
|
|
163
|
+
manifest_path: path.resolve(file),
|
|
164
|
+
package: null,
|
|
165
|
+
source: null,
|
|
166
|
+
content_digest: null,
|
|
167
|
+
checks: {
|
|
168
|
+
manifest: { status: 'fail' },
|
|
169
|
+
assets: { status: 'unknown', checked: 0, failed: 0 },
|
|
170
|
+
contracts: { status: 'unknown', checked: 0, failed: 0 },
|
|
171
|
+
},
|
|
172
|
+
issues: [problem],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function inspectProvenance({ file, expectedAdapter = null, expectedPackageVersion = null }) {
|
|
177
|
+
const manifestPath = path.resolve(file);
|
|
178
|
+
if (!fs.existsSync(manifestPath)) {
|
|
179
|
+
return invalidReport(
|
|
180
|
+
manifestPath, 'missing', issue('PROVENANCE_MISSING', { path: manifestPath }), expectedAdapter,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
let manifest;
|
|
184
|
+
try {
|
|
185
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
186
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) throw new Error('manifest must be an object');
|
|
187
|
+
if (manifest.schema_version !== SCHEMA_VERSION) throw new Error(`unsupported schema ${manifest.schema_version}`);
|
|
188
|
+
if (!manifest.package?.name || !manifest.package?.version || !manifest.source?.repository) {
|
|
189
|
+
throw new Error('package and source metadata required');
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return invalidReport(
|
|
193
|
+
manifestPath, 'degraded', issue('PROVENANCE_INVALID', { message: error.message }), expectedAdapter,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const issues = [];
|
|
198
|
+
let roots;
|
|
199
|
+
try {
|
|
200
|
+
roots = normalizeRoots(manifest.roots);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return invalidReport(
|
|
203
|
+
manifestPath, 'degraded', issue('PROVENANCE_INVALID', { message: error.message }), expectedAdapter,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (expectedAdapter && manifest.adapter !== expectedAdapter) {
|
|
207
|
+
issues.push(issue('ADAPTER_MISMATCH', { expected: expectedAdapter, actual: manifest.adapter }));
|
|
208
|
+
}
|
|
209
|
+
if (expectedPackageVersion && manifest.package.version !== expectedPackageVersion) {
|
|
210
|
+
issues.push(issue('PACKAGE_VERSION_MISMATCH', {
|
|
211
|
+
expected: expectedPackageVersion, actual: manifest.package.version,
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const assets = Array.isArray(manifest.assets) ? manifest.assets : [];
|
|
216
|
+
if (assets.length === 0) issues.push(issue('PROVENANCE_INVALID', { message: 'assets required' }));
|
|
217
|
+
let assetFailures = 0;
|
|
218
|
+
for (const asset of assets) {
|
|
219
|
+
try {
|
|
220
|
+
const { target, root, path: assetPath } = resolveRef(roots, asset);
|
|
221
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {
|
|
222
|
+
assetFailures += 1;
|
|
223
|
+
issues.push(issue('ASSET_MISSING', { root, path: assetPath }));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const actual = hashFile(target);
|
|
227
|
+
if (actual !== asset.sha256) {
|
|
228
|
+
assetFailures += 1;
|
|
229
|
+
issues.push(issue('ASSET_HASH_MISMATCH', {
|
|
230
|
+
root, path: assetPath, expected: asset.sha256, actual,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
assetFailures += 1;
|
|
235
|
+
issues.push(issue('ASSET_INVALID', { message: error.message }));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (manifest.content?.algorithm !== DIGEST_ALGORITHM
|
|
239
|
+
|| manifest.content?.digest !== digestEntries(assets)) {
|
|
240
|
+
issues.push(issue('MANIFEST_DIGEST_MISMATCH'));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const contracts = manifest.contracts && typeof manifest.contracts === 'object'
|
|
244
|
+
&& !Array.isArray(manifest.contracts) ? manifest.contracts : {};
|
|
245
|
+
let contractFailures = 0;
|
|
246
|
+
for (const [name, ref] of Object.entries(contracts)) {
|
|
247
|
+
try {
|
|
248
|
+
const { target, root, path: contractPath } = resolveRef(roots, ref);
|
|
249
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {
|
|
250
|
+
contractFailures += 1;
|
|
251
|
+
issues.push(issue('CONTRACT_TARGET_MISSING', { contract: name, root, path: contractPath }));
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
contractFailures += 1;
|
|
255
|
+
issues.push(issue('CONTRACT_INVALID', { contract: name, message: error.message }));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
adapter: manifest.adapter,
|
|
261
|
+
status: issues.length === 0 ? 'healthy' : 'degraded',
|
|
262
|
+
manifest_path: manifestPath,
|
|
263
|
+
package: manifest.package,
|
|
264
|
+
source: manifest.source,
|
|
265
|
+
content_digest: manifest.content?.digest || null,
|
|
266
|
+
checks: {
|
|
267
|
+
manifest: { status: 'pass' },
|
|
268
|
+
assets: { status: assetFailures === 0 ? 'pass' : 'fail', checked: assets.length, failed: assetFailures },
|
|
269
|
+
contracts: {
|
|
270
|
+
status: contractFailures === 0 ? 'pass' : 'fail',
|
|
271
|
+
checked: Object.keys(contracts).length,
|
|
272
|
+
failed: contractFailures,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
issues,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = {
|
|
280
|
+
SCHEMA_VERSION,
|
|
281
|
+
DIGEST_ALGORITHM,
|
|
282
|
+
assetRefsForTree,
|
|
283
|
+
inspectProvenance,
|
|
284
|
+
packageSource,
|
|
285
|
+
writeProvenance,
|
|
286
|
+
};
|
|
@@ -25,7 +25,10 @@ test('renders one common contract with host-native invocation syntax', () => {
|
|
|
25
25
|
assert.match(opencode, /\/ultra-plan/);
|
|
26
26
|
for (const rendered of [claude, codex, opencode]) {
|
|
27
27
|
assert.match(rendered, /\.ultra\/state\.db/);
|
|
28
|
-
assert.match(rendered, /
|
|
28
|
+
assert.match(rendered, /Separately installed providers/);
|
|
29
|
+
assert.match(rendered, /ultra-change/);
|
|
30
|
+
assert.match(rendered, /ultra-doctor/);
|
|
31
|
+
assert.match(rendered, /metadata references/);
|
|
29
32
|
assert.doesNotMatch(rendered, /\.ultra\/memory|memory\.retain|memory\.recall/);
|
|
30
33
|
assert.match(rendered, new RegExp(BEGIN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
31
34
|
assert.match(rendered, new RegExp(END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|