ultra-builder-pro-cli 0.5.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 (206) hide show
  1. package/.ultra-template/delivery-report.json +9 -0
  2. package/.ultra-template/docs/research/README.md +53 -0
  3. package/.ultra-template/specs/architecture.md +250 -0
  4. package/.ultra-template/specs/discovery.md +248 -0
  5. package/.ultra-template/specs/product.md +223 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +57 -0
  7. package/.ultra-template/tasks/tasks.json +33 -0
  8. package/.ultra-template/test-report.json +8 -0
  9. package/CHANGELOG.md +194 -0
  10. package/LICENSE +21 -0
  11. package/README.md +187 -0
  12. package/adapters/_shared/codex-assets.cjs +1128 -0
  13. package/adapters/_shared/codex-ultra-tools-entry.cjs +11 -0
  14. package/adapters/_shared/file-ops.cjs +115 -0
  15. package/adapters/_shared/frontmatter.cjs +52 -0
  16. package/adapters/_shared/handbook.cjs +144 -0
  17. package/adapters/_shared/md-to-toml.cjs +54 -0
  18. package/adapters/_shared/path-rewrite.cjs +79 -0
  19. package/adapters/_shared/rtk-detect.cjs +86 -0
  20. package/adapters/_shared/runtime-assets.cjs +70 -0
  21. package/adapters/_shared/settings-merge.cjs +76 -0
  22. package/adapters/_shared/tests/file-ops.test.cjs +54 -0
  23. package/adapters/_shared/tests/frontmatter.test.cjs +47 -0
  24. package/adapters/_shared/tests/handbook.test.cjs +86 -0
  25. package/adapters/_shared/tests/md-to-toml.test.cjs +48 -0
  26. package/adapters/_shared/tests/path-rewrite.test.cjs +60 -0
  27. package/adapters/_shared/tests/resolve-target.test.cjs +85 -0
  28. package/adapters/_shared/tests/rtk-detect.test.cjs +116 -0
  29. package/adapters/_shared/tests/runtime-assets.test.cjs +107 -0
  30. package/adapters/_shared/tests/settings-merge.test.cjs +85 -0
  31. package/adapters/_shared/tests/validate.test.cjs +30 -0
  32. package/adapters/_shared/validate.cjs +24 -0
  33. package/adapters/claude.js +182 -0
  34. package/adapters/codex.js +385 -0
  35. package/adapters/gemini.js +204 -0
  36. package/adapters/opencode.js +506 -0
  37. package/adapters/tests/claude.test.cjs +117 -0
  38. package/adapters/tests/codex-hook.test.cjs +90 -0
  39. package/adapters/tests/codex.test.cjs +356 -0
  40. package/adapters/tests/gemini.test.cjs +112 -0
  41. package/adapters/tests/opencode.test.cjs +246 -0
  42. package/agents/code-reviewer.md +102 -0
  43. package/agents/debugger.md +127 -0
  44. package/agents/review-code.md +125 -0
  45. package/agents/review-comments.md +98 -0
  46. package/agents/review-coordinator.md +146 -0
  47. package/agents/review-design.md +133 -0
  48. package/agents/review-errors.md +87 -0
  49. package/agents/review-tests.md +93 -0
  50. package/agents/tdd-runner.md +92 -0
  51. package/bin/handbook.js +54 -0
  52. package/bin/install.js +254 -0
  53. package/bin/orchestrator.js +214 -0
  54. package/commands/learn.md +41 -0
  55. package/commands/ultra-deliver.md +45 -0
  56. package/commands/ultra-dev.md +50 -0
  57. package/commands/ultra-init.md +51 -0
  58. package/commands/ultra-plan.md +49 -0
  59. package/commands/ultra-research.md +59 -0
  60. package/commands/ultra-status.md +46 -0
  61. package/commands/ultra-test.md +48 -0
  62. package/commands/ultra-think.md +45 -0
  63. package/docs/AGENT-CONTEXT.md +143 -0
  64. package/docs/ARCHITECTURE.md +197 -0
  65. package/docs/COMMIT-HASH-BACKFILL.md +101 -0
  66. package/docs/LEGACY-HERMES.md +1175 -0
  67. package/docs/PLAN.zh-CN.md +1683 -0
  68. package/docs/ROADMAP.md +102 -0
  69. package/docs/RUNTIME-COMPAT-MATRIX.md +98 -0
  70. package/docs/STATE-DB-ACCESS-POLICY.md +109 -0
  71. package/docs/USER-HANDBOOK-CONTRACT.md +61 -0
  72. package/hooks/README.md +24 -0
  73. package/hooks/active_task_context.py +44 -0
  74. package/hooks/adapters/codex.py +302 -0
  75. package/hooks/health_check.py +61 -0
  76. package/hooks/pre_stop_check.py +56 -0
  77. package/hooks/subagent_tracker.py +116 -0
  78. package/hooks/workflow_checkpoint.py +56 -0
  79. package/hooks/workflow_context.py +63 -0
  80. package/hooks/workflow_resume.py +50 -0
  81. package/mcp-server/lib/init-project.cjs +162 -0
  82. package/mcp-server/lib/llm-client.cjs +157 -0
  83. package/mcp-server/lib/llm-client.test.cjs +156 -0
  84. package/mcp-server/lib/plan-store.cjs +96 -0
  85. package/mcp-server/lib/plan-store.test.cjs +132 -0
  86. package/mcp-server/lib/prd-parser.cjs +110 -0
  87. package/mcp-server/lib/prd-parser.test.cjs +171 -0
  88. package/mcp-server/lib/pricing.cjs +53 -0
  89. package/mcp-server/lib/projector.cjs +158 -0
  90. package/mcp-server/lib/state-db.cjs +113 -0
  91. package/mcp-server/lib/state-db.test.cjs +116 -0
  92. package/mcp-server/lib/state-ops.cjs +725 -0
  93. package/mcp-server/lib/task-expander.cjs +123 -0
  94. package/mcp-server/lib/task-expander.test.cjs +253 -0
  95. package/mcp-server/lib/telemetry.cjs +72 -0
  96. package/mcp-server/lib/topo.cjs +131 -0
  97. package/mcp-server/lib/topo.test.cjs +158 -0
  98. package/mcp-server/server.cjs +482 -0
  99. package/mcp-server/tests/circuit-breaker.test.cjs +125 -0
  100. package/mcp-server/tests/commit-hash-flow.test.cjs +117 -0
  101. package/mcp-server/tests/concurrency-update.test.cjs +96 -0
  102. package/mcp-server/tests/concurrency.test.cjs +112 -0
  103. package/mcp-server/tests/db-ops.test.cjs +117 -0
  104. package/mcp-server/tests/init-project.test.cjs +141 -0
  105. package/mcp-server/tests/migrate.test.cjs +144 -0
  106. package/mcp-server/tests/projector.test.cjs +130 -0
  107. package/mcp-server/tests/server-contract.test.cjs +750 -0
  108. package/mcp-server/tests/staleness.test.cjs +190 -0
  109. package/mcp-server/tests/state-ops.test.cjs +286 -0
  110. package/mcp-server/tests/tagged-tasks.test.cjs +163 -0
  111. package/mcp-server/tests/telemetry.test.cjs +205 -0
  112. package/orchestrator/auto-merge.cjs +109 -0
  113. package/orchestrator/code-graph-watcher.cjs +121 -0
  114. package/orchestrator/daemon.cjs +112 -0
  115. package/orchestrator/dispatch-rules.cjs +93 -0
  116. package/orchestrator/parallel-orchestrator.cjs +195 -0
  117. package/orchestrator/planner/expand-prompt.cjs +62 -0
  118. package/orchestrator/planner/expand-prompt.test.cjs +52 -0
  119. package/orchestrator/planner/plan-builder.cjs +116 -0
  120. package/orchestrator/planner/plan-builder.test.cjs +165 -0
  121. package/orchestrator/planner/prd-prompt.cjs +44 -0
  122. package/orchestrator/planner/prd-prompt.test.cjs +30 -0
  123. package/orchestrator/recovery.cjs +70 -0
  124. package/orchestrator/session-runner.cjs +224 -0
  125. package/orchestrator/skill-miner.cjs +133 -0
  126. package/orchestrator/tests/auto-merge.test.cjs +216 -0
  127. package/orchestrator/tests/code-graph-watcher.test.cjs +183 -0
  128. package/orchestrator/tests/daemon.test.cjs +260 -0
  129. package/orchestrator/tests/dispatch-rules.test.cjs +122 -0
  130. package/orchestrator/tests/parallel-orchestrator.test.cjs +233 -0
  131. package/orchestrator/tests/phase-8b-gate.test.cjs +183 -0
  132. package/orchestrator/tests/recovery.test.cjs +190 -0
  133. package/orchestrator/tests/session-runner.test.cjs +314 -0
  134. package/orchestrator/tests/skill-miner.test.cjs +147 -0
  135. package/orchestrator/tests/worktree-manager.test.cjs +148 -0
  136. package/orchestrator/worktree-manager.cjs +131 -0
  137. package/output-styles/coding-vibes.md +67 -0
  138. package/output-styles/structural-thinking.md +61 -0
  139. package/package.json +109 -0
  140. package/settings.json +8 -0
  141. package/skills/cc-collab/SKILL.md +41 -0
  142. package/skills/code-review-expert/SKILL.md +118 -0
  143. package/skills/code-review-expert/references/code-quality-checklist.md +112 -0
  144. package/skills/code-review-expert/references/integration-checklist.md +46 -0
  145. package/skills/code-review-expert/references/removal-plan.md +52 -0
  146. package/skills/code-review-expert/references/security-checklist.md +100 -0
  147. package/skills/code-review-expert/references/solid-checklist.md +65 -0
  148. package/skills/codex-collab/SKILL.md +52 -0
  149. package/skills/codex-collab/evals/evals.json +40 -0
  150. package/skills/gemini-collab/SKILL.md +51 -0
  151. package/skills/gemini-collab/evals/evals.json +42 -0
  152. package/skills/integration-rules/SKILL.md +71 -0
  153. package/skills/learn/SKILL.md +117 -0
  154. package/skills/security-rules/SKILL.md +60 -0
  155. package/skills/testing-rules/SKILL.md +56 -0
  156. package/skills/ultra-deliver/SKILL.md +171 -0
  157. package/skills/ultra-dev/SKILL.md +324 -0
  158. package/skills/ultra-init/SKILL.md +148 -0
  159. package/skills/ultra-plan/SKILL.md +314 -0
  160. package/skills/ultra-research/SKILL.md +156 -0
  161. package/skills/ultra-research/steps/step-00-problem-validation.md +154 -0
  162. package/skills/ultra-research/steps/step-01-opportunity-discovery.md +172 -0
  163. package/skills/ultra-research/steps/step-02-market-assessment.md +195 -0
  164. package/skills/ultra-research/steps/step-03-competitive-landscape.md +197 -0
  165. package/skills/ultra-research/steps/step-04-product-strategy.md +196 -0
  166. package/skills/ultra-research/steps/step-05-assumptions-validation.md +234 -0
  167. package/skills/ultra-research/steps/step-10-user-personas.md +169 -0
  168. package/skills/ultra-research/steps/step-11-user-scenarios.md +181 -0
  169. package/skills/ultra-research/steps/step-20-user-stories.md +156 -0
  170. package/skills/ultra-research/steps/step-21-features-scope.md +157 -0
  171. package/skills/ultra-research/steps/step-22-success-metrics.md +195 -0
  172. package/skills/ultra-research/steps/step-30-architecture-context.md +154 -0
  173. package/skills/ultra-research/steps/step-31-solution-strategy.md +172 -0
  174. package/skills/ultra-research/steps/step-32-building-blocks.md +188 -0
  175. package/skills/ultra-research/steps/step-40-deployment.md +123 -0
  176. package/skills/ultra-research/steps/step-41-quality-risks.md +135 -0
  177. package/skills/ultra-research/steps/step-99-synthesis.md +188 -0
  178. package/skills/ultra-review/SKILL.md +437 -0
  179. package/skills/ultra-review/references/unified-schema.md +127 -0
  180. package/skills/ultra-review/scripts/review_verdict_update.py +99 -0
  181. package/skills/ultra-review/scripts/review_wait.py +120 -0
  182. package/skills/ultra-status/SKILL.md +162 -0
  183. package/skills/ultra-test/SKILL.md +195 -0
  184. package/skills/ultra-think/SKILL.md +109 -0
  185. package/skills/ultra-verify/SKILL.md +111 -0
  186. package/skills/ultra-verify/evals/evals.json +51 -0
  187. package/skills/ultra-verify/references/confidence-system.md +38 -0
  188. package/skills/ultra-verify/references/cross-verify-modes.md +73 -0
  189. package/skills/ultra-verify/scripts/verify_wait.py +131 -0
  190. package/spec/cli-protocol.md +118 -0
  191. package/spec/mcp-tools.yaml +658 -0
  192. package/spec/schemas/command-manifest.schema.json +47 -0
  193. package/spec/schemas/context-file.v4.5.schema.json +54 -0
  194. package/spec/schemas/mcp-tools.schema.json +136 -0
  195. package/spec/schemas/skill-manifest.schema.json +94 -0
  196. package/spec/schemas/state-db.sql +154 -0
  197. package/spec/schemas/tasks.v4.5.schema.json +66 -0
  198. package/ultra-tools/cli.cjs +121 -0
  199. package/ultra-tools/commands/db.cjs +175 -0
  200. package/ultra-tools/commands/legacy-memory.cjs +159 -0
  201. package/ultra-tools/commands/migrate.cjs +354 -0
  202. package/ultra-tools/commands/session.cjs +180 -0
  203. package/ultra-tools/commands/status.cjs +158 -0
  204. package/ultra-tools/commands/task.cjs +95 -0
  205. package/ultra-tools/legacy-memory.test.cjs +80 -0
  206. package/ultra-tools/status-command.test.cjs +132 -0
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ // ncc emits this entrypoint into <plugin>/runtime/ultra-tools.cjs. Set the
6
+ // runtime asset root before loading modules that read schemas or templates.
7
+ process.env.UBP_RUNTIME_ROOT = path.resolve(__dirname, '..');
8
+
9
+ const { main } = require('../../ultra-tools/cli.cjs');
10
+
11
+ main(process.argv);
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ // File-system primitives shared by every runtime adapter.
4
+ // Pure filesystem — no runtime coupling. Kept small; each adapter layers
5
+ // its own runtime-specific behavior on top.
6
+
7
+ const fs = require('node:fs');
8
+ const path = require('node:path');
9
+ const crypto = require('node:crypto');
10
+
11
+ const DEFAULT_EXCLUDES = new Set(['.DS_Store', 'Thumbs.db']);
12
+
13
+ function ensureDir(dir) {
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ }
16
+
17
+ function writeAtomic(file, content) {
18
+ ensureDir(path.dirname(file));
19
+ // randomUUID defeats predictable tmp-name attacks and PID+ms collisions
20
+ // when two writers hit the same path inside one millisecond.
21
+ const tmp = `${file}.tmp-${crypto.randomUUID()}`;
22
+ fs.writeFileSync(tmp, content);
23
+ fs.renameSync(tmp, file);
24
+ }
25
+
26
+ function listRelative(root, { exclude = DEFAULT_EXCLUDES } = {}) {
27
+ const out = [];
28
+ (function walk(dir) {
29
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
30
+ if (exclude.has(entry.name)) continue;
31
+ const full = path.join(dir, entry.name);
32
+ if (entry.isDirectory()) walk(full);
33
+ else if (entry.isFile()) out.push(path.relative(root, full));
34
+ }
35
+ })(root);
36
+ return out.sort();
37
+ }
38
+
39
+ function copyTree(src, dst, { exclude = DEFAULT_EXCLUDES, transform } = {}) {
40
+ const files = listRelative(src, { exclude });
41
+ for (const rel of files) {
42
+ const from = path.join(src, rel);
43
+ const to = path.join(dst, rel);
44
+ ensureDir(path.dirname(to));
45
+ if (transform) {
46
+ const original = fs.readFileSync(from);
47
+ const rewritten = transform(original, rel);
48
+ writeAtomic(to, rewritten);
49
+ } else {
50
+ fs.copyFileSync(from, to);
51
+ }
52
+ }
53
+ return files;
54
+ }
55
+
56
+ function removeTree(target, { allowRoot = false } = {}) {
57
+ const abs = path.resolve(target);
58
+ if (!allowRoot && (abs === '/' || abs === path.parse(abs).root)) {
59
+ throw new Error(`refusing to remove filesystem root: ${abs}`);
60
+ }
61
+ // Guard against a target that is itself a symlink: unlink the link,
62
+ // don't rmSync through it (which would delete the target's contents).
63
+ try {
64
+ const lst = fs.lstatSync(abs);
65
+ if (lst.isSymbolicLink()) {
66
+ fs.unlinkSync(abs);
67
+ return;
68
+ }
69
+ } catch (err) {
70
+ if (err.code === 'ENOENT') return;
71
+ throw err;
72
+ }
73
+ fs.rmSync(abs, { recursive: true, force: true });
74
+ }
75
+
76
+ const UBP_SENTINEL_FILE = '.ubp-managed';
77
+
78
+ // Write a sentinel file into a managed directory so uninstall can verify
79
+ // ownership before deleting (P1 #3).
80
+ function markManaged(dir, meta = {}) {
81
+ ensureDir(dir);
82
+ writeAtomic(path.join(dir, UBP_SENTINEL_FILE), JSON.stringify({
83
+ source: 'ubp',
84
+ installed_at: new Date().toISOString(),
85
+ ...meta,
86
+ }, null, 2) + '\n');
87
+ }
88
+
89
+ function isManaged(dir) {
90
+ return fs.existsSync(path.join(dir, UBP_SENTINEL_FILE));
91
+ }
92
+
93
+ function copyFlatByExt(srcDir, dstDir, ext) {
94
+ if (!fs.existsSync(srcDir)) return [];
95
+ const files = fs.readdirSync(srcDir).filter((f) => f.endsWith(ext));
96
+ if (files.length === 0) return [];
97
+ ensureDir(dstDir);
98
+ for (const f of files) {
99
+ fs.copyFileSync(path.join(srcDir, f), path.join(dstDir, f));
100
+ }
101
+ return files;
102
+ }
103
+
104
+ module.exports = {
105
+ DEFAULT_EXCLUDES,
106
+ UBP_SENTINEL_FILE,
107
+ ensureDir,
108
+ writeAtomic,
109
+ listRelative,
110
+ copyTree,
111
+ removeTree,
112
+ markManaged,
113
+ isManaged,
114
+ copyFlatByExt,
115
+ };
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ // YAML frontmatter parsing / serialization for markdown assets.
4
+ // Every runtime adapter consumes this — OpenCode lowercases keys,
5
+ // Gemini extracts description for toml, Claude preserves verbatim.
6
+
7
+ const yaml = require('js-yaml');
8
+
9
+ const DELIMITER = '---';
10
+
11
+ function parse(text) {
12
+ if (typeof text !== 'string') throw new TypeError('parse() expects string');
13
+ if (!text.startsWith(DELIMITER)) {
14
+ return { fm: null, body: text, bodyStart: 0 };
15
+ }
16
+ const end = text.indexOf(`\n${DELIMITER}`, DELIMITER.length);
17
+ if (end === -1) {
18
+ return { fm: null, body: text, bodyStart: 0 };
19
+ }
20
+ const raw = text.slice(DELIMITER.length, end).trim();
21
+ const fm = yaml.load(raw) || null;
22
+ // end points at "\n---"; skip "\n---" (4 chars) to land on whatever follows the closing delimiter
23
+ const bodyStart = end + DELIMITER.length + 1;
24
+ let body = text.slice(bodyStart);
25
+ // Strip leading blank line(s) separating frontmatter from body (Markdown convention)
26
+ body = body.replace(/^\n+/, '');
27
+ return { fm, body, bodyStart };
28
+ }
29
+
30
+ function serialize(fm, body = '') {
31
+ if (fm == null) return body;
32
+ const yamlBody = yaml.dump(fm, { lineWidth: -1, noRefs: true }).trimEnd();
33
+ return `${DELIMITER}\n${yamlBody}\n${DELIMITER}\n${body}`;
34
+ }
35
+
36
+ function lowercaseKeys(obj) {
37
+ if (Array.isArray(obj)) return obj.map(lowercaseKeys);
38
+ if (obj === null || typeof obj !== 'object') return obj;
39
+ const out = {};
40
+ for (const [k, v] of Object.entries(obj)) {
41
+ out[k.toLowerCase()] = lowercaseKeys(v);
42
+ }
43
+ return out;
44
+ }
45
+
46
+ function patch(text, patchFn) {
47
+ const { fm, body } = parse(text);
48
+ const nextFm = patchFn(fm ? { ...fm } : {});
49
+ return serialize(nextFm, body);
50
+ }
51
+
52
+ module.exports = { parse, serialize, lowercaseKeys, patch };
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const { writeAtomic } = require('./file-ops.cjs');
8
+
9
+ const BEGIN_MARKER = '<!-- ultra-builder-pro:handbook:start -->';
10
+ const END_MARKER = '<!-- ultra-builder-pro:handbook:end -->';
11
+ const LEGACY_HEADING = '## Ultra Builder Pro Runtime Contract';
12
+
13
+ const HOSTS = Object.freeze({
14
+ claude: Object.freeze({
15
+ name: 'Claude Code',
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` and `/ultra-think`',
18
+ collaboration: '`/codex-collab` and `/gemini-collab` are explicitly requested read-only advisors',
19
+ coordination: 'Claude Code task UI is host coordination; it does not replace Ultra task state',
20
+ }),
21
+ codex: Object.freeze({
22
+ name: 'Codex',
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` and `$ultra-builder-pro:ultra-think`',
25
+ collaboration: '`$ultra-builder-pro:cc-collab` and `$ultra-builder-pro:gemini-collab` are explicitly requested read-only advisors',
26
+ coordination: 'The Codex plan is turn-facing coordination; it does not replace Ultra task state',
27
+ }),
28
+ opencode: Object.freeze({
29
+ name: 'OpenCode',
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` and `/ultra-think`',
32
+ collaboration: '`/cc-collab`, `/codex-collab`, and `/gemini-collab` are explicitly requested read-only advisors',
33
+ coordination: 'OpenCode session coordination does not replace Ultra task state',
34
+ }),
35
+ });
36
+
37
+ function host(runtime) {
38
+ const value = HOSTS[runtime];
39
+ if (!value) throw new Error(`unsupported handbook runtime: ${runtime}`);
40
+ return value;
41
+ }
42
+
43
+ function renderHandbook(runtime) {
44
+ const value = host(runtime);
45
+ return [
46
+ BEGIN_MARKER,
47
+ '## Ultra Builder Pro Runtime Contract',
48
+ '',
49
+ `Ultra Builder Pro is installed as a native ${value.name} plugin. Keep general engineering`,
50
+ 'policy in this handbook and detailed Ultra procedures in the plugin skills.',
51
+ '',
52
+ `- Workflow entry points: ${value.invocation}.`,
53
+ `- Collaboration boundary: ${value.collaboration}. The current host remains primary and owns final verification.`,
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, or cross-session memory. Persistent memory belongs to a separately installed provider such as cloud-mem or claude-mem.',
57
+ '- Hook boundary: Ultra hooks are workflow-only lifecycle adapters and are no-ops unless `.ultra/workflow-state.json` describes an active non-terminal workflow. Generic command blocking and post-edit policy stay in user or repository governance.',
58
+ '- 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 ten 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
+ END_MARKER,
61
+ ].join('\n');
62
+ }
63
+
64
+ function markerCount(text, marker) {
65
+ return text.split(marker).length - 1;
66
+ }
67
+
68
+ function replaceManagedBlock(existing, block) {
69
+ const beginCount = markerCount(existing, BEGIN_MARKER);
70
+ const endCount = markerCount(existing, END_MARKER);
71
+ if (beginCount !== endCount || beginCount > 1) {
72
+ throw new Error('handbook contains malformed or duplicate Ultra managed markers');
73
+ }
74
+ if (beginCount === 0) return null;
75
+
76
+ const start = existing.indexOf(BEGIN_MARKER);
77
+ const end = existing.indexOf(END_MARKER, start) + END_MARKER.length;
78
+ return existing.slice(0, start) + block + existing.slice(end);
79
+ }
80
+
81
+ function replaceLegacyCodexSection(existing, block) {
82
+ const heading = new RegExp(`^${LEGACY_HEADING.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^\\n]*$`, 'm');
83
+ const match = heading.exec(existing);
84
+ if (!match) return null;
85
+
86
+ const start = match.index;
87
+ const remainder = existing.slice(start + match[0].length);
88
+ const nextHeading = /^##\s+/m.exec(remainder);
89
+ const end = nextHeading ? start + match[0].length + nextHeading.index : existing.length;
90
+ const before = existing.slice(0, start).trimEnd();
91
+ const after = existing.slice(end).trimStart();
92
+ return `${before}${before ? '\n\n' : ''}${block}${after ? `\n\n${after}` : '\n'}`;
93
+ }
94
+
95
+ function mergeHandbook(existing, runtime) {
96
+ host(runtime);
97
+ const current = typeof existing === 'string' ? existing : '';
98
+ const block = renderHandbook(runtime);
99
+ const managed = replaceManagedBlock(current, block);
100
+ if (managed !== null) return managed;
101
+
102
+ if (runtime === 'codex') {
103
+ const migrated = replaceLegacyCodexSection(current, block);
104
+ if (migrated !== null) return migrated;
105
+ }
106
+
107
+ const before = current.trimEnd();
108
+ return `${before}${before ? '\n\n' : ''}${block}\n`;
109
+ }
110
+
111
+ function resolveHandbookFile(runtime, { homeDir = os.homedir() } = {}) {
112
+ return path.join(homeDir, ...host(runtime).handbook);
113
+ }
114
+
115
+ function timestamp() {
116
+ return new Date().toISOString().replace(/[-:.]/g, '').replace(/\.\d{3}Z$/, 'Z');
117
+ }
118
+
119
+ function applyHandbook({ runtime, file, homeDir, now = timestamp() }) {
120
+ const target = path.resolve(file || resolveHandbookFile(runtime, { homeDir }));
121
+ const exists = fs.existsSync(target);
122
+ const existing = exists ? fs.readFileSync(target, 'utf8') : '';
123
+ const merged = mergeHandbook(existing, runtime);
124
+ if (merged === existing) return { runtime, file: target, changed: false, backup: null };
125
+
126
+ let backup = null;
127
+ if (exists) {
128
+ backup = `${target}.ubp-backup-${now}`;
129
+ if (fs.existsSync(backup)) throw new Error(`handbook backup already exists: ${backup}`);
130
+ writeAtomic(backup, existing);
131
+ }
132
+ writeAtomic(target, merged);
133
+ return { runtime, file: target, changed: true, backup };
134
+ }
135
+
136
+ module.exports = {
137
+ BEGIN_MARKER,
138
+ END_MARKER,
139
+ HOSTS,
140
+ applyHandbook,
141
+ mergeHandbook,
142
+ renderHandbook,
143
+ resolveHandbookFile,
144
+ };
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ // Convert a thin-shell command .md file to a Gemini custom-command TOML.
4
+ // Gemini commands live in `~/.gemini/commands/<name>.toml` with shape:
5
+ //
6
+ // description = "…"
7
+ // prompt = """
8
+ // <command body>
9
+ // """
10
+ //
11
+ // Only the description (from frontmatter) and the body text cross over.
12
+ // Frontmatter keys like `workflow-ref` are inlined into the prompt so the
13
+ // agent still sees the skill reference.
14
+
15
+ const { parse: parseFrontmatter } = require('./frontmatter.cjs');
16
+
17
+ function escapeTomlLiteral(str) {
18
+ // Gemini accepts """multi-line""" literals. Escape any triple-quote to be safe.
19
+ return String(str).replace(/"""/g, '\\"\\"\\"');
20
+ }
21
+
22
+ function buildPromptSection(fm, body) {
23
+ const lines = [];
24
+ if (fm && fm['workflow-ref']) {
25
+ lines.push(`[ultra-builder-pro] workflow reference: ${fm['workflow-ref']}`);
26
+ }
27
+ if (fm && Array.isArray(fm.mcp_tools_required) && fm.mcp_tools_required.length > 0) {
28
+ lines.push(`[ultra-builder-pro] MCP tools required: ${fm.mcp_tools_required.join(', ')}`);
29
+ }
30
+ if (fm && fm.cli_fallback) {
31
+ lines.push(`[ultra-builder-pro] CLI fallback: ultra-tools ${fm.cli_fallback}`);
32
+ }
33
+ if (lines.length > 0) lines.push('');
34
+ lines.push(body.trimEnd());
35
+ return lines.join('\n');
36
+ }
37
+
38
+ function mdCommandToToml(mdText) {
39
+ if (typeof mdText !== 'string') throw new TypeError('mdCommandToToml: expects string');
40
+ const { fm, body } = parseFrontmatter(mdText);
41
+ const description = (fm && fm.description) || 'Ultra Builder Pro command';
42
+ const prompt = buildPromptSection(fm, body);
43
+
44
+ const tomlLines = [
45
+ `description = ${JSON.stringify(description)}`,
46
+ 'prompt = """',
47
+ escapeTomlLiteral(prompt),
48
+ '"""',
49
+ '',
50
+ ];
51
+ return tomlLines.join('\n');
52
+ }
53
+
54
+ module.exports = { mdCommandToToml };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ // Path-rewriting helpers for workflow-ref resolution per runtime.
4
+ // The canonical form in commands/*.md is `@skills/<name>/SKILL.md`. Each
5
+ // runtime stores skills under a different root — this module converts
6
+ // the symbolic ref into a concrete path (or leaves it symbolic for
7
+ // runtimes that understand the shorthand).
8
+
9
+ const path = require('node:path');
10
+
11
+ const SKILL_REF_RE = /^@skills\/([a-z][a-z0-9\-]*)\/SKILL\.md$/;
12
+
13
+ const RUNTIME_SKILL_ROOT = {
14
+ claude: '.claude/skills',
15
+ opencode: '.config/opencode/skills',
16
+ codex: '.agents/skills',
17
+ gemini: '.gemini/extensions/ultra-builder-pro/skills',
18
+ };
19
+
20
+ function parseSkillRef(ref) {
21
+ if (typeof ref !== 'string') return null;
22
+ const m = ref.match(SKILL_REF_RE);
23
+ if (!m) return null;
24
+ return { name: m[1], filename: 'SKILL.md' };
25
+ }
26
+
27
+ function resolveSkillRef(ref, { runtime, homeDir }) {
28
+ const parsed = parseSkillRef(ref);
29
+ if (!parsed) throw new Error(`invalid skill ref: ${ref}`);
30
+ const root = RUNTIME_SKILL_ROOT[runtime];
31
+ if (!root) throw new Error(`unknown runtime: ${runtime}`);
32
+ return path.join(homeDir, root, parsed.name, parsed.filename);
33
+ }
34
+
35
+ function rewriteWorkflowRefInText(text, { runtime, homeDir }) {
36
+ return text.replace(/@skills\/([a-z][a-z0-9\-]*)\/SKILL\.md/g, (match) => {
37
+ const parsed = parseSkillRef(match);
38
+ if (!parsed) return match;
39
+ return resolveSkillRef(match, { runtime, homeDir });
40
+ });
41
+ }
42
+
43
+ function resolveAssetTarget(srcRel, { runtime, scope, homeDir, cwd }) {
44
+ const base = scope === 'global'
45
+ ? path.join(homeDir, runtimeRoot(runtime))
46
+ : path.join(cwd, localRoot(runtime));
47
+ return path.join(base, srcRel);
48
+ }
49
+
50
+ function runtimeRoot(runtime) {
51
+ switch (runtime) {
52
+ case 'claude': return '.claude';
53
+ case 'opencode': return '.config/opencode';
54
+ case 'codex': return '.agents';
55
+ case 'gemini': return '.gemini/extensions/ultra-builder-pro';
56
+ default: throw new Error(`unknown runtime: ${runtime}`);
57
+ }
58
+ }
59
+
60
+ function localRoot(runtime) {
61
+ switch (runtime) {
62
+ case 'claude': return '.claude';
63
+ case 'opencode': return '.opencode';
64
+ case 'codex': return '.agents';
65
+ case 'gemini': return '.gemini/extensions/ultra-builder-pro';
66
+ default: throw new Error(`unknown runtime: ${runtime}`);
67
+ }
68
+ }
69
+
70
+ module.exports = {
71
+ SKILL_REF_RE,
72
+ RUNTIME_SKILL_ROOT,
73
+ parseSkillRef,
74
+ resolveSkillRef,
75
+ rewriteWorkflowRefInText,
76
+ resolveAssetTarget,
77
+ runtimeRoot,
78
+ localRoot,
79
+ };
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ // Phase 6.1 — RTK soft dependency.
4
+ //
5
+ // RTK (Rust Token Killer) is an external CLI proxy the user installs
6
+ // separately. We never hard-require it: installation continues even when
7
+ // rtk is missing, we just print an install hint. This mirrors PLAN Phase
8
+ // 6.1: "无 rtk → 提示安装 + 跳过(不硬依赖)".
9
+ //
10
+ // Tests inject a PATH override so we can exercise the happy/missing paths
11
+ // without touching the real rtk binary on the machine.
12
+
13
+ const { execFileSync } = require('node:child_process');
14
+
15
+ const INSTALL_HINT =
16
+ 'rtk not found. RTK is a token-saving proxy that rewrites Bash/git/npm\n' +
17
+ ' commands transparently. Install: https://github.com/rcrocky/rtk\n' +
18
+ ' (or run `cargo install rtk`). Continuing without RTK.';
19
+
20
+ function envWithPath(pathOverride) {
21
+ if (!pathOverride) return process.env;
22
+ // Isolate rtk lookup to pathOverride + minimal system dirs (so shebangs
23
+ // like `/usr/bin/env bash` still resolve). Tests use this to cut off the
24
+ // real rtk binary on the developer's machine.
25
+ return { ...process.env, PATH: `${pathOverride}:/usr/bin:/bin` };
26
+ }
27
+
28
+ function detectRtk({ pathOverride = null, timeoutMs = 3000 } = {}) {
29
+ try {
30
+ const stdout = execFileSync('rtk', ['--version'], {
31
+ env: envWithPath(pathOverride),
32
+ stdio: ['ignore', 'pipe', 'pipe'],
33
+ timeout: timeoutMs,
34
+ });
35
+ return { available: true, version: stdout.toString().trim() };
36
+ } catch (err) {
37
+ return {
38
+ available: false,
39
+ reason: err && err.code === 'ENOENT' ? 'binary not on PATH' : (err && err.message) || 'unknown',
40
+ };
41
+ }
42
+ }
43
+
44
+ function initRtk({ pathOverride = null, scope = 'global', timeoutMs = 10000 } = {}) {
45
+ const args = ['init'];
46
+ if (scope === 'global') args.push('-g');
47
+ try {
48
+ const stdout = execFileSync('rtk', args, {
49
+ env: envWithPath(pathOverride),
50
+ stdio: ['ignore', 'pipe', 'pipe'],
51
+ timeout: timeoutMs,
52
+ });
53
+ return { ok: true, stdout: stdout.toString() };
54
+ } catch (err) {
55
+ return { ok: false, error: (err && err.message) || 'unknown' };
56
+ }
57
+ }
58
+
59
+ // High-level entry: detect + optionally init, never throws.
60
+ // Caller passes {skip:true} to honor --skip-rtk flag.
61
+ function installHook({ skip = false, pathOverride = null, scope = 'global' } = {}) {
62
+ if (skip) return { skipped: true };
63
+ const detection = detectRtk({ pathOverride });
64
+ if (!detection.available) {
65
+ return {
66
+ available: false,
67
+ initialized: false,
68
+ reason: detection.reason,
69
+ hint: INSTALL_HINT,
70
+ };
71
+ }
72
+ const initResult = initRtk({ pathOverride, scope });
73
+ return {
74
+ available: true,
75
+ initialized: initResult.ok,
76
+ version: detection.version,
77
+ init_error: initResult.ok ? undefined : initResult.error,
78
+ };
79
+ }
80
+
81
+ module.exports = {
82
+ detectRtk,
83
+ initRtk,
84
+ installHook,
85
+ INSTALL_HINT,
86
+ };
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Canonical Ultra Builder Pro package boundary.
5
+ *
6
+ * Adapters must build from this allowlist. A new directory under skills/ or
7
+ * hooks/ is not packaged until it is deliberately classified here.
8
+ */
9
+
10
+ const CORE_PUBLIC_SKILLS = Object.freeze([
11
+ 'learn',
12
+ 'ultra-init',
13
+ 'ultra-research',
14
+ 'ultra-plan',
15
+ 'ultra-dev',
16
+ 'ultra-test',
17
+ 'ultra-review',
18
+ 'ultra-deliver',
19
+ 'ultra-status',
20
+ 'ultra-think',
21
+ ]);
22
+
23
+ const INTERNAL_AGENT_SKILLS = Object.freeze([
24
+ 'code-review-expert',
25
+ 'security-rules',
26
+ 'integration-rules',
27
+ 'testing-rules',
28
+ ]);
29
+
30
+ const COLLAB_SKILLS_BY_RUNTIME = Object.freeze({
31
+ claude: Object.freeze(['codex-collab', 'gemini-collab', 'ultra-verify']),
32
+ codex: Object.freeze(['cc-collab', 'gemini-collab', 'ultra-verify']),
33
+ opencode: Object.freeze(['cc-collab', 'codex-collab', 'gemini-collab', 'ultra-verify']),
34
+ gemini: Object.freeze(['cc-collab', 'codex-collab', 'ultra-verify']),
35
+ });
36
+
37
+ const RETIRED_SKILLS = Object.freeze([
38
+ 'agent-browser',
39
+ 'find-skills',
40
+ 'recall',
41
+ 'use-railway',
42
+ 'vercel-composition-patterns',
43
+ 'vercel-react-best-practices',
44
+ 'vercel-react-native-skills',
45
+ ]);
46
+
47
+ const WORKFLOW_HOOK_FILES = Object.freeze([
48
+ 'active_task_context.py',
49
+ 'health_check.py',
50
+ 'pre_stop_check.py',
51
+ 'subagent_tracker.py',
52
+ 'workflow_checkpoint.py',
53
+ 'workflow_context.py',
54
+ 'workflow_resume.py',
55
+ ]);
56
+
57
+ function skillsForRuntime(runtime) {
58
+ const collab = COLLAB_SKILLS_BY_RUNTIME[runtime];
59
+ if (!collab) throw new Error(`unsupported Ultra runtime: ${runtime}`);
60
+ return [...CORE_PUBLIC_SKILLS, ...INTERNAL_AGENT_SKILLS, ...collab];
61
+ }
62
+
63
+ module.exports = {
64
+ CORE_PUBLIC_SKILLS,
65
+ INTERNAL_AGENT_SKILLS,
66
+ COLLAB_SKILLS_BY_RUNTIME,
67
+ RETIRED_SKILLS,
68
+ WORKFLOW_HOOK_FILES,
69
+ skillsForRuntime,
70
+ };
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ // Sentinel-block merge/remove for JSON settings files (D7).
4
+ // Protects user-authored settings by confining our writes to a single
5
+ // well-known key (the sentinel). Install writes/updates the sentinel value;
6
+ // uninstall deletes it. Everything else in the JSON is left untouched.
7
+
8
+ const SENTINEL_VERSION = 1;
9
+
10
+ function readJsonSafe(filepath, { fs = require('node:fs'), rescue = false } = {}) {
11
+ if (!fs.existsSync(filepath)) return {};
12
+ const raw = fs.readFileSync(filepath, 'utf8').trim();
13
+ if (!raw) return {};
14
+ try { return JSON.parse(raw); }
15
+ catch (err) {
16
+ // rescue mode: back up the corrupt file and treat as empty so install
17
+ // can proceed instead of exploding mid-setup (P2 #7 from Phase 4 review).
18
+ // Callers that need strict semantics leave rescue=false (default).
19
+ if (rescue) {
20
+ const backup = `${filepath}.bak-${Date.now()}`;
21
+ try {
22
+ fs.copyFileSync(filepath, backup);
23
+ process.stderr.write(`settings-merge: corrupt ${filepath}; backed up to ${backup}, continuing with empty object\n`);
24
+ } catch (_backupErr) { /* best-effort */ }
25
+ return {};
26
+ }
27
+ throw new Error(`settings-merge: cannot parse ${filepath}: ${err.message}`);
28
+ }
29
+ }
30
+
31
+ function withSentinelBlock(existing, sentinelKey, block) {
32
+ if (!sentinelKey || typeof sentinelKey !== 'string') {
33
+ throw new TypeError('withSentinelBlock: sentinelKey must be non-empty string');
34
+ }
35
+ const next = { ...existing };
36
+ next[sentinelKey] = {
37
+ __sentinel: SENTINEL_VERSION,
38
+ __generated_by: 'ultra-builder-pro-cli',
39
+ __generated_at: new Date().toISOString(),
40
+ ...block,
41
+ };
42
+ return next;
43
+ }
44
+
45
+ function removeSentinelBlock(existing, sentinelKey) {
46
+ if (!(sentinelKey in existing)) return existing;
47
+ const next = { ...existing };
48
+ delete next[sentinelKey];
49
+ return next;
50
+ }
51
+
52
+ function hasSentinelBlock(existing, sentinelKey) {
53
+ return Boolean(existing && existing[sentinelKey] && existing[sentinelKey].__sentinel);
54
+ }
55
+
56
+ function mergeArrayField(existing, key, items) {
57
+ const current = Array.isArray(existing[key]) ? existing[key] : [];
58
+ const seen = new Set(current.map((x) => JSON.stringify(x)));
59
+ const appended = [...current];
60
+ for (const item of items) {
61
+ const tag = JSON.stringify(item);
62
+ if (seen.has(tag)) continue;
63
+ appended.push(item);
64
+ seen.add(tag);
65
+ }
66
+ return { ...existing, [key]: appended };
67
+ }
68
+
69
+ module.exports = {
70
+ SENTINEL_VERSION,
71
+ readJsonSafe,
72
+ withSentinelBlock,
73
+ removeSentinelBlock,
74
+ hasSentinelBlock,
75
+ mergeArrayField,
76
+ };