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.
- package/.ultra-template/delivery-report.json +9 -0
- package/.ultra-template/docs/research/README.md +53 -0
- package/.ultra-template/specs/architecture.md +250 -0
- package/.ultra-template/specs/discovery.md +248 -0
- package/.ultra-template/specs/product.md +223 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +57 -0
- package/.ultra-template/tasks/tasks.json +33 -0
- package/.ultra-template/test-report.json +8 -0
- package/CHANGELOG.md +194 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/adapters/_shared/codex-assets.cjs +1128 -0
- package/adapters/_shared/codex-ultra-tools-entry.cjs +11 -0
- package/adapters/_shared/file-ops.cjs +115 -0
- package/adapters/_shared/frontmatter.cjs +52 -0
- package/adapters/_shared/handbook.cjs +144 -0
- package/adapters/_shared/md-to-toml.cjs +54 -0
- package/adapters/_shared/path-rewrite.cjs +79 -0
- package/adapters/_shared/rtk-detect.cjs +86 -0
- package/adapters/_shared/runtime-assets.cjs +70 -0
- package/adapters/_shared/settings-merge.cjs +76 -0
- package/adapters/_shared/tests/file-ops.test.cjs +54 -0
- package/adapters/_shared/tests/frontmatter.test.cjs +47 -0
- package/adapters/_shared/tests/handbook.test.cjs +86 -0
- package/adapters/_shared/tests/md-to-toml.test.cjs +48 -0
- package/adapters/_shared/tests/path-rewrite.test.cjs +60 -0
- package/adapters/_shared/tests/resolve-target.test.cjs +85 -0
- package/adapters/_shared/tests/rtk-detect.test.cjs +116 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +107 -0
- package/adapters/_shared/tests/settings-merge.test.cjs +85 -0
- package/adapters/_shared/tests/validate.test.cjs +30 -0
- package/adapters/_shared/validate.cjs +24 -0
- package/adapters/claude.js +182 -0
- package/adapters/codex.js +385 -0
- package/adapters/gemini.js +204 -0
- package/adapters/opencode.js +506 -0
- package/adapters/tests/claude.test.cjs +117 -0
- package/adapters/tests/codex-hook.test.cjs +90 -0
- package/adapters/tests/codex.test.cjs +356 -0
- package/adapters/tests/gemini.test.cjs +112 -0
- package/adapters/tests/opencode.test.cjs +246 -0
- package/agents/code-reviewer.md +102 -0
- package/agents/debugger.md +127 -0
- package/agents/review-code.md +125 -0
- package/agents/review-comments.md +98 -0
- package/agents/review-coordinator.md +146 -0
- package/agents/review-design.md +133 -0
- package/agents/review-errors.md +87 -0
- package/agents/review-tests.md +93 -0
- package/agents/tdd-runner.md +92 -0
- package/bin/handbook.js +54 -0
- package/bin/install.js +254 -0
- package/bin/orchestrator.js +214 -0
- package/commands/learn.md +41 -0
- package/commands/ultra-deliver.md +45 -0
- package/commands/ultra-dev.md +50 -0
- package/commands/ultra-init.md +51 -0
- package/commands/ultra-plan.md +49 -0
- package/commands/ultra-research.md +59 -0
- package/commands/ultra-status.md +46 -0
- package/commands/ultra-test.md +48 -0
- package/commands/ultra-think.md +45 -0
- package/docs/AGENT-CONTEXT.md +143 -0
- package/docs/ARCHITECTURE.md +197 -0
- package/docs/COMMIT-HASH-BACKFILL.md +101 -0
- package/docs/LEGACY-HERMES.md +1175 -0
- package/docs/PLAN.zh-CN.md +1683 -0
- package/docs/ROADMAP.md +102 -0
- package/docs/RUNTIME-COMPAT-MATRIX.md +98 -0
- package/docs/STATE-DB-ACCESS-POLICY.md +109 -0
- package/docs/USER-HANDBOOK-CONTRACT.md +61 -0
- package/hooks/README.md +24 -0
- package/hooks/active_task_context.py +44 -0
- package/hooks/adapters/codex.py +302 -0
- package/hooks/health_check.py +61 -0
- package/hooks/pre_stop_check.py +56 -0
- package/hooks/subagent_tracker.py +116 -0
- package/hooks/workflow_checkpoint.py +56 -0
- package/hooks/workflow_context.py +63 -0
- package/hooks/workflow_resume.py +50 -0
- package/mcp-server/lib/init-project.cjs +162 -0
- package/mcp-server/lib/llm-client.cjs +157 -0
- package/mcp-server/lib/llm-client.test.cjs +156 -0
- package/mcp-server/lib/plan-store.cjs +96 -0
- package/mcp-server/lib/plan-store.test.cjs +132 -0
- package/mcp-server/lib/prd-parser.cjs +110 -0
- package/mcp-server/lib/prd-parser.test.cjs +171 -0
- package/mcp-server/lib/pricing.cjs +53 -0
- package/mcp-server/lib/projector.cjs +158 -0
- package/mcp-server/lib/state-db.cjs +113 -0
- package/mcp-server/lib/state-db.test.cjs +116 -0
- package/mcp-server/lib/state-ops.cjs +725 -0
- package/mcp-server/lib/task-expander.cjs +123 -0
- package/mcp-server/lib/task-expander.test.cjs +253 -0
- package/mcp-server/lib/telemetry.cjs +72 -0
- package/mcp-server/lib/topo.cjs +131 -0
- package/mcp-server/lib/topo.test.cjs +158 -0
- package/mcp-server/server.cjs +482 -0
- package/mcp-server/tests/circuit-breaker.test.cjs +125 -0
- package/mcp-server/tests/commit-hash-flow.test.cjs +117 -0
- package/mcp-server/tests/concurrency-update.test.cjs +96 -0
- package/mcp-server/tests/concurrency.test.cjs +112 -0
- package/mcp-server/tests/db-ops.test.cjs +117 -0
- package/mcp-server/tests/init-project.test.cjs +141 -0
- package/mcp-server/tests/migrate.test.cjs +144 -0
- package/mcp-server/tests/projector.test.cjs +130 -0
- package/mcp-server/tests/server-contract.test.cjs +750 -0
- package/mcp-server/tests/staleness.test.cjs +190 -0
- package/mcp-server/tests/state-ops.test.cjs +286 -0
- package/mcp-server/tests/tagged-tasks.test.cjs +163 -0
- package/mcp-server/tests/telemetry.test.cjs +205 -0
- package/orchestrator/auto-merge.cjs +109 -0
- package/orchestrator/code-graph-watcher.cjs +121 -0
- package/orchestrator/daemon.cjs +112 -0
- package/orchestrator/dispatch-rules.cjs +93 -0
- package/orchestrator/parallel-orchestrator.cjs +195 -0
- package/orchestrator/planner/expand-prompt.cjs +62 -0
- package/orchestrator/planner/expand-prompt.test.cjs +52 -0
- package/orchestrator/planner/plan-builder.cjs +116 -0
- package/orchestrator/planner/plan-builder.test.cjs +165 -0
- package/orchestrator/planner/prd-prompt.cjs +44 -0
- package/orchestrator/planner/prd-prompt.test.cjs +30 -0
- package/orchestrator/recovery.cjs +70 -0
- package/orchestrator/session-runner.cjs +224 -0
- package/orchestrator/skill-miner.cjs +133 -0
- package/orchestrator/tests/auto-merge.test.cjs +216 -0
- package/orchestrator/tests/code-graph-watcher.test.cjs +183 -0
- package/orchestrator/tests/daemon.test.cjs +260 -0
- package/orchestrator/tests/dispatch-rules.test.cjs +122 -0
- package/orchestrator/tests/parallel-orchestrator.test.cjs +233 -0
- package/orchestrator/tests/phase-8b-gate.test.cjs +183 -0
- package/orchestrator/tests/recovery.test.cjs +190 -0
- package/orchestrator/tests/session-runner.test.cjs +314 -0
- package/orchestrator/tests/skill-miner.test.cjs +147 -0
- package/orchestrator/tests/worktree-manager.test.cjs +148 -0
- package/orchestrator/worktree-manager.cjs +131 -0
- package/output-styles/coding-vibes.md +67 -0
- package/output-styles/structural-thinking.md +61 -0
- package/package.json +109 -0
- package/settings.json +8 -0
- package/skills/cc-collab/SKILL.md +41 -0
- package/skills/code-review-expert/SKILL.md +118 -0
- package/skills/code-review-expert/references/code-quality-checklist.md +112 -0
- package/skills/code-review-expert/references/integration-checklist.md +46 -0
- package/skills/code-review-expert/references/removal-plan.md +52 -0
- package/skills/code-review-expert/references/security-checklist.md +100 -0
- package/skills/code-review-expert/references/solid-checklist.md +65 -0
- package/skills/codex-collab/SKILL.md +52 -0
- package/skills/codex-collab/evals/evals.json +40 -0
- package/skills/gemini-collab/SKILL.md +51 -0
- package/skills/gemini-collab/evals/evals.json +42 -0
- package/skills/integration-rules/SKILL.md +71 -0
- package/skills/learn/SKILL.md +117 -0
- package/skills/security-rules/SKILL.md +60 -0
- package/skills/testing-rules/SKILL.md +56 -0
- package/skills/ultra-deliver/SKILL.md +171 -0
- package/skills/ultra-dev/SKILL.md +324 -0
- package/skills/ultra-init/SKILL.md +148 -0
- package/skills/ultra-plan/SKILL.md +314 -0
- package/skills/ultra-research/SKILL.md +156 -0
- package/skills/ultra-research/steps/step-00-problem-validation.md +154 -0
- package/skills/ultra-research/steps/step-01-opportunity-discovery.md +172 -0
- package/skills/ultra-research/steps/step-02-market-assessment.md +195 -0
- package/skills/ultra-research/steps/step-03-competitive-landscape.md +197 -0
- package/skills/ultra-research/steps/step-04-product-strategy.md +196 -0
- package/skills/ultra-research/steps/step-05-assumptions-validation.md +234 -0
- package/skills/ultra-research/steps/step-10-user-personas.md +169 -0
- package/skills/ultra-research/steps/step-11-user-scenarios.md +181 -0
- package/skills/ultra-research/steps/step-20-user-stories.md +156 -0
- package/skills/ultra-research/steps/step-21-features-scope.md +157 -0
- package/skills/ultra-research/steps/step-22-success-metrics.md +195 -0
- package/skills/ultra-research/steps/step-30-architecture-context.md +154 -0
- package/skills/ultra-research/steps/step-31-solution-strategy.md +172 -0
- package/skills/ultra-research/steps/step-32-building-blocks.md +188 -0
- package/skills/ultra-research/steps/step-40-deployment.md +123 -0
- package/skills/ultra-research/steps/step-41-quality-risks.md +135 -0
- package/skills/ultra-research/steps/step-99-synthesis.md +188 -0
- package/skills/ultra-review/SKILL.md +437 -0
- package/skills/ultra-review/references/unified-schema.md +127 -0
- package/skills/ultra-review/scripts/review_verdict_update.py +99 -0
- package/skills/ultra-review/scripts/review_wait.py +120 -0
- package/skills/ultra-status/SKILL.md +162 -0
- package/skills/ultra-test/SKILL.md +195 -0
- package/skills/ultra-think/SKILL.md +109 -0
- package/skills/ultra-verify/SKILL.md +111 -0
- package/skills/ultra-verify/evals/evals.json +51 -0
- package/skills/ultra-verify/references/confidence-system.md +38 -0
- package/skills/ultra-verify/references/cross-verify-modes.md +73 -0
- package/skills/ultra-verify/scripts/verify_wait.py +131 -0
- package/spec/cli-protocol.md +118 -0
- package/spec/mcp-tools.yaml +658 -0
- package/spec/schemas/command-manifest.schema.json +47 -0
- package/spec/schemas/context-file.v4.5.schema.json +54 -0
- package/spec/schemas/mcp-tools.schema.json +136 -0
- package/spec/schemas/skill-manifest.schema.json +94 -0
- package/spec/schemas/state-db.sql +154 -0
- package/spec/schemas/tasks.v4.5.schema.json +66 -0
- package/ultra-tools/cli.cjs +121 -0
- package/ultra-tools/commands/db.cjs +175 -0
- package/ultra-tools/commands/legacy-memory.cjs +159 -0
- package/ultra-tools/commands/migrate.cjs +354 -0
- package/ultra-tools/commands/session.cjs +180 -0
- package/ultra-tools/commands/status.cjs +158 -0
- package/ultra-tools/commands/task.cjs +95 -0
- package/ultra-tools/legacy-memory.test.cjs +80 -0
- package/ultra-tools/status-command.test.cjs +132 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Codex adapter.
|
|
5
|
+
*
|
|
6
|
+
* Codex-native mapping:
|
|
7
|
+
* commands/*.md -> explicit plugin skills ($ultra-builder-pro:<name>)
|
|
8
|
+
* skills/** -> ~/plugins/ultra-builder-pro/skills/**
|
|
9
|
+
* agents/*.md -> ~/.codex/agents/*.toml
|
|
10
|
+
* hooks/*.py -> plugin hooks with a Codex wire adapter
|
|
11
|
+
* mcp-server -> plugin .mcp.json (project cwd owns .ultra/state.db)
|
|
12
|
+
* plugin marketplace -> ~/.agents/plugins/marketplace.json
|
|
13
|
+
*
|
|
14
|
+
* Deprecated ~/.codex/prompts and ~/.codex/skills projection is intentionally
|
|
15
|
+
* not used. The plugin remains the single distribution boundary for skills,
|
|
16
|
+
* hooks, and MCP; custom agents use Codex's standalone TOML surface.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const os = require('node:os');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
const { spawnSync } = require('node:child_process');
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
ensureDir,
|
|
26
|
+
isManaged,
|
|
27
|
+
removeTree,
|
|
28
|
+
writeAtomic,
|
|
29
|
+
} = require('./_shared/file-ops.cjs');
|
|
30
|
+
const {
|
|
31
|
+
PLUGIN_NAME,
|
|
32
|
+
MANAGED_MARKER,
|
|
33
|
+
buildPlugin,
|
|
34
|
+
installAgents,
|
|
35
|
+
} = require('./_shared/codex-assets.cjs');
|
|
36
|
+
|
|
37
|
+
const MCP_SERVER_NAME = PLUGIN_NAME;
|
|
38
|
+
const SOURCE_TAG = 'ubp';
|
|
39
|
+
const MARKER_BEGIN = '# >>> ultra-builder-pro managed block — do not edit by hand';
|
|
40
|
+
const MARKER_END = '# <<< ultra-builder-pro managed block';
|
|
41
|
+
const RUNTIME_MANIFEST_DIR = 'ultra-builder-pro';
|
|
42
|
+
const RUNTIME_MANIFEST_FILE = 'install-manifest.json';
|
|
43
|
+
const HOOK_ADAPTER_RELATIVE = path.join('hooks', 'adapters', 'codex.py');
|
|
44
|
+
|
|
45
|
+
function resolveTarget(ctx = {}) {
|
|
46
|
+
if (ctx.configDir) return path.resolve(ctx.configDir);
|
|
47
|
+
if (ctx.scope === 'global') {
|
|
48
|
+
return process.env.CODEX_HOME || path.join(ctx.homeDir || os.homedir(), '.codex');
|
|
49
|
+
}
|
|
50
|
+
return path.join(ctx.cwd || process.cwd(), '.codex');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveHomeDir(ctx = {}) {
|
|
54
|
+
return path.resolve(ctx.homeDir || os.homedir());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveRepoRoot(ctx = {}) {
|
|
58
|
+
return ctx.repoRoot || path.resolve(__dirname, '..');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolvePluginRoot(ctx = {}) {
|
|
62
|
+
return path.join(resolveHomeDir(ctx), 'plugins', PLUGIN_NAME);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveMarketplaceFile(ctx = {}) {
|
|
66
|
+
return path.join(resolveHomeDir(ctx), '.agents', 'plugins', 'marketplace.json');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function runtimeManifestFile(configDir) {
|
|
70
|
+
return path.join(configDir, RUNTIME_MANIFEST_DIR, RUNTIME_MANIFEST_FILE);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stripManagedBlock(text) {
|
|
74
|
+
const begin = text.indexOf(MARKER_BEGIN);
|
|
75
|
+
if (begin === -1) return text;
|
|
76
|
+
const end = text.indexOf(MARKER_END, begin);
|
|
77
|
+
if (end === -1) return text;
|
|
78
|
+
const endLine = text.indexOf('\n', end);
|
|
79
|
+
const after = endLine === -1 ? text.length : endLine + 1;
|
|
80
|
+
const leading = text.slice(0, begin).replace(/\n+$/, '\n');
|
|
81
|
+
return leading + text.slice(after);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasManagedBlock(text) {
|
|
85
|
+
return text.includes(MARKER_BEGIN);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function cleanupLegacyConfig(configDir) {
|
|
89
|
+
const configFile = path.join(configDir, 'config.toml');
|
|
90
|
+
let configUpdated = false;
|
|
91
|
+
if (fs.existsSync(configFile)) {
|
|
92
|
+
const existing = fs.readFileSync(configFile, 'utf8');
|
|
93
|
+
if (hasManagedBlock(existing)) {
|
|
94
|
+
const stripped = stripManagedBlock(existing);
|
|
95
|
+
if (stripped.trim()) writeAtomic(configFile, stripped);
|
|
96
|
+
else fs.unlinkSync(configFile);
|
|
97
|
+
configUpdated = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const removed = [];
|
|
102
|
+
for (const sub of ['skills', 'prompts']) {
|
|
103
|
+
const dir = path.join(configDir, sub);
|
|
104
|
+
if (fs.existsSync(dir) && isManaged(dir)) {
|
|
105
|
+
removeTree(dir);
|
|
106
|
+
removed.push(sub);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { configUpdated, removed };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function loadMarketplace(file) {
|
|
113
|
+
if (!fs.existsSync(file)) {
|
|
114
|
+
return {
|
|
115
|
+
name: 'personal',
|
|
116
|
+
interface: { displayName: 'Personal' },
|
|
117
|
+
plugins: [],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const payload = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
121
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
122
|
+
throw new Error(`${file} must contain a JSON object`);
|
|
123
|
+
}
|
|
124
|
+
if (typeof payload.name !== 'string' || !payload.name.trim()) {
|
|
125
|
+
throw new Error(`${file} must contain a non-empty marketplace name`);
|
|
126
|
+
}
|
|
127
|
+
if (!payload.interface || typeof payload.interface !== 'object' || Array.isArray(payload.interface)) {
|
|
128
|
+
payload.interface = { displayName: 'Personal' };
|
|
129
|
+
}
|
|
130
|
+
if (!Array.isArray(payload.plugins)) {
|
|
131
|
+
throw new Error(`${file} field plugins must be an array`);
|
|
132
|
+
}
|
|
133
|
+
return payload;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function marketplaceEntry() {
|
|
137
|
+
return {
|
|
138
|
+
name: PLUGIN_NAME,
|
|
139
|
+
source: { source: 'local', path: `./plugins/${PLUGIN_NAME}` },
|
|
140
|
+
policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },
|
|
141
|
+
category: 'Developer Tools',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function upsertMarketplace(file) {
|
|
146
|
+
const payload = loadMarketplace(file);
|
|
147
|
+
const entry = marketplaceEntry();
|
|
148
|
+
const index = payload.plugins.findIndex((plugin) => plugin && plugin.name === PLUGIN_NAME);
|
|
149
|
+
if (index === -1) payload.plugins.push(entry);
|
|
150
|
+
else payload.plugins[index] = entry;
|
|
151
|
+
writeAtomic(file, JSON.stringify(payload, null, 2) + '\n');
|
|
152
|
+
return { file, name: payload.name, entry };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function removeMarketplaceEntry(file) {
|
|
156
|
+
if (!fs.existsSync(file)) return { file, updated: false, name: 'personal' };
|
|
157
|
+
const payload = loadMarketplace(file);
|
|
158
|
+
const next = payload.plugins.filter((plugin) => !plugin || plugin.name !== PLUGIN_NAME);
|
|
159
|
+
if (next.length === payload.plugins.length) {
|
|
160
|
+
return { file, updated: false, name: payload.name };
|
|
161
|
+
}
|
|
162
|
+
payload.plugins = next;
|
|
163
|
+
writeAtomic(file, JSON.stringify(payload, null, 2) + '\n');
|
|
164
|
+
return { file, updated: true, name: payload.name };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function shouldRunPluginCli(ctx = {}) {
|
|
168
|
+
if (typeof ctx.runPluginCli === 'boolean') return ctx.runPluginCli;
|
|
169
|
+
return ctx.scope === 'global' && !ctx.configDir;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function runPluginCli(action, marketplaceName, ctx = {}) {
|
|
173
|
+
const codexBin = ctx.codexBin || 'codex';
|
|
174
|
+
const selector = `${PLUGIN_NAME}@${marketplaceName}`;
|
|
175
|
+
const args = action === 'add'
|
|
176
|
+
? ['plugin', 'add', selector, '--json']
|
|
177
|
+
: ['plugin', 'remove', selector];
|
|
178
|
+
const result = spawnSync(codexBin, args, { encoding: 'utf8' });
|
|
179
|
+
if (result.error) throw result.error;
|
|
180
|
+
if (result.status !== 0) {
|
|
181
|
+
const detail = (result.stderr || result.stdout || '').trim();
|
|
182
|
+
throw new Error(`codex plugin ${action} failed${detail ? `: ${detail}` : ''}`);
|
|
183
|
+
}
|
|
184
|
+
return { selector, stdout: result.stdout.trim() };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function pluginCacheRoot(configDir, marketplaceName) {
|
|
188
|
+
if (typeof marketplaceName !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(marketplaceName)) {
|
|
189
|
+
throw new Error(`invalid Codex marketplace name: ${marketplaceName}`);
|
|
190
|
+
}
|
|
191
|
+
return path.join(configDir, 'plugins', 'cache', marketplaceName, PLUGIN_NAME);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function listCachedHookAdapters(configDir, marketplaceName) {
|
|
195
|
+
const root = pluginCacheRoot(configDir, marketplaceName);
|
|
196
|
+
if (!fs.existsSync(root)) return [];
|
|
197
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
198
|
+
.filter((entry) => entry.isDirectory())
|
|
199
|
+
.map((entry) => path.join(root, entry.name, HOOK_ADAPTER_RELATIVE))
|
|
200
|
+
.filter((file) => fs.existsSync(file))
|
|
201
|
+
.sort();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function hookForwarder(target) {
|
|
205
|
+
return `#!/usr/bin/env python3
|
|
206
|
+
"""Forward a live Codex task from a retired plugin cache to the current adapter."""
|
|
207
|
+
|
|
208
|
+
import runpy
|
|
209
|
+
|
|
210
|
+
runpy.run_path(${JSON.stringify(path.resolve(target))}, run_name="__main__")
|
|
211
|
+
`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function restoreCachedHookAdapters(previousAdapters, target) {
|
|
215
|
+
const resolvedTarget = path.resolve(target);
|
|
216
|
+
if (!fs.existsSync(resolvedTarget)) {
|
|
217
|
+
throw new Error(`current Codex hook adapter is missing: ${resolvedTarget}`);
|
|
218
|
+
}
|
|
219
|
+
const restored = [];
|
|
220
|
+
for (const previous of previousAdapters) {
|
|
221
|
+
if (path.resolve(previous) === resolvedTarget || fs.existsSync(previous)) continue;
|
|
222
|
+
writeAtomic(previous, hookForwarder(resolvedTarget));
|
|
223
|
+
restored.push(previous);
|
|
224
|
+
}
|
|
225
|
+
return { target: resolvedTarget, restored };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function currentHookAdapter(configDir, marketplaceName, plugin) {
|
|
229
|
+
const cached = path.join(pluginCacheRoot(configDir, marketplaceName), plugin.version, HOOK_ADAPTER_RELATIVE);
|
|
230
|
+
if (fs.existsSync(cached)) return cached;
|
|
231
|
+
return path.join(plugin.root, HOOK_ADAPTER_RELATIVE);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function writeRuntimeManifest(configDir, data) {
|
|
235
|
+
const file = runtimeManifestFile(configDir);
|
|
236
|
+
writeAtomic(file, JSON.stringify({
|
|
237
|
+
source: SOURCE_TAG,
|
|
238
|
+
adapter: 'codex',
|
|
239
|
+
plugin: data.plugin,
|
|
240
|
+
marketplace: data.marketplace,
|
|
241
|
+
agents: data.agents,
|
|
242
|
+
}, null, 2) + '\n');
|
|
243
|
+
return file;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function install(ctx = {}) {
|
|
247
|
+
const configDir = resolveTarget(ctx);
|
|
248
|
+
const repoRoot = resolveRepoRoot(ctx);
|
|
249
|
+
const pluginRoot = resolvePluginRoot(ctx);
|
|
250
|
+
const marketplaceFile = resolveMarketplaceFile(ctx);
|
|
251
|
+
ensureDir(configDir);
|
|
252
|
+
|
|
253
|
+
const legacy = cleanupLegacyConfig(configDir);
|
|
254
|
+
const plugin = buildPlugin({ repoRoot, pluginRoot });
|
|
255
|
+
const agents = installAgents({ repoRoot, configDir });
|
|
256
|
+
const marketplace = upsertMarketplace(marketplaceFile);
|
|
257
|
+
const manifestFile = writeRuntimeManifest(configDir, {
|
|
258
|
+
plugin: { root: plugin.root, version: plugin.version },
|
|
259
|
+
marketplace: { file: marketplace.file, name: marketplace.name },
|
|
260
|
+
agents: agents.installed,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
let registration = null;
|
|
264
|
+
let hookCompatibility = null;
|
|
265
|
+
if (shouldRunPluginCli(ctx)) {
|
|
266
|
+
const previousAdapters = listCachedHookAdapters(configDir, marketplace.name);
|
|
267
|
+
try {
|
|
268
|
+
registration = runPluginCli('add', marketplace.name, ctx);
|
|
269
|
+
} catch (registrationError) {
|
|
270
|
+
try {
|
|
271
|
+
hookCompatibility = restoreCachedHookAdapters(
|
|
272
|
+
previousAdapters,
|
|
273
|
+
currentHookAdapter(configDir, marketplace.name, plugin),
|
|
274
|
+
);
|
|
275
|
+
} catch (recoveryError) {
|
|
276
|
+
throw new AggregateError(
|
|
277
|
+
[registrationError, recoveryError],
|
|
278
|
+
`codex plugin add and hook cache recovery both failed`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
throw registrationError;
|
|
282
|
+
}
|
|
283
|
+
hookCompatibility = restoreCachedHookAdapters(
|
|
284
|
+
previousAdapters,
|
|
285
|
+
currentHookAdapter(configDir, marketplace.name, plugin),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
target: configDir,
|
|
291
|
+
plugin,
|
|
292
|
+
agents,
|
|
293
|
+
marketplace,
|
|
294
|
+
registration,
|
|
295
|
+
hookCompatibility,
|
|
296
|
+
legacy,
|
|
297
|
+
manifestFile,
|
|
298
|
+
config: { updated: legacy.configUpdated },
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function removeManagedAgents(configDir, manifest) {
|
|
303
|
+
const agentRoot = path.join(configDir, 'agents');
|
|
304
|
+
const requested = manifest && Array.isArray(manifest.agents)
|
|
305
|
+
? manifest.agents
|
|
306
|
+
: (fs.existsSync(agentRoot) ? fs.readdirSync(agentRoot).filter((name) => name.endsWith('.toml')) : []);
|
|
307
|
+
const removed = [];
|
|
308
|
+
for (const file of requested) {
|
|
309
|
+
const target = path.join(agentRoot, path.basename(file));
|
|
310
|
+
if (!fs.existsSync(target)) continue;
|
|
311
|
+
const contents = fs.readFileSync(target, 'utf8');
|
|
312
|
+
if (!contents.startsWith(`# ${MANAGED_MARKER}`)) continue;
|
|
313
|
+
fs.unlinkSync(target);
|
|
314
|
+
removed.push(path.basename(target));
|
|
315
|
+
}
|
|
316
|
+
return removed;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function readRuntimeManifest(configDir) {
|
|
320
|
+
const file = runtimeManifestFile(configDir);
|
|
321
|
+
if (!fs.existsSync(file)) return null;
|
|
322
|
+
try {
|
|
323
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
324
|
+
} catch (error) {
|
|
325
|
+
throw new Error(`invalid Codex runtime manifest ${file}: ${error.message}`, { cause: error });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function uninstall(ctx = {}) {
|
|
330
|
+
const configDir = resolveTarget(ctx);
|
|
331
|
+
const pluginRoot = resolvePluginRoot(ctx);
|
|
332
|
+
const marketplaceFile = resolveMarketplaceFile(ctx);
|
|
333
|
+
const manifest = readRuntimeManifest(configDir);
|
|
334
|
+
const marketplaceBefore = loadMarketplace(marketplaceFile);
|
|
335
|
+
|
|
336
|
+
const registration = shouldRunPluginCli(ctx)
|
|
337
|
+
? runPluginCli('remove', marketplaceBefore.name, ctx)
|
|
338
|
+
: null;
|
|
339
|
+
const agents = removeManagedAgents(configDir, manifest);
|
|
340
|
+
|
|
341
|
+
let pluginRemoved = false;
|
|
342
|
+
if (fs.existsSync(pluginRoot) && fs.existsSync(path.join(pluginRoot, '.ubp-managed'))) {
|
|
343
|
+
removeTree(pluginRoot);
|
|
344
|
+
pluginRemoved = true;
|
|
345
|
+
}
|
|
346
|
+
const marketplace = removeMarketplaceEntry(marketplaceFile);
|
|
347
|
+
const legacy = cleanupLegacyConfig(configDir);
|
|
348
|
+
const manifestDir = path.join(configDir, RUNTIME_MANIFEST_DIR);
|
|
349
|
+
if (fs.existsSync(manifestDir)) removeTree(manifestDir);
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
target: configDir,
|
|
353
|
+
removed: { plugin: pluginRemoved, agents },
|
|
354
|
+
marketplace,
|
|
355
|
+
registration,
|
|
356
|
+
legacy,
|
|
357
|
+
config: { updated: legacy.configUpdated },
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
module.exports = {
|
|
362
|
+
name: 'codex',
|
|
363
|
+
MCP_SERVER_NAME,
|
|
364
|
+
MARKER_BEGIN,
|
|
365
|
+
MARKER_END,
|
|
366
|
+
SOURCE_TAG,
|
|
367
|
+
resolveTarget,
|
|
368
|
+
resolveHomeDir,
|
|
369
|
+
resolvePluginRoot,
|
|
370
|
+
resolveMarketplaceFile,
|
|
371
|
+
install,
|
|
372
|
+
uninstall,
|
|
373
|
+
_internal: {
|
|
374
|
+
stripManagedBlock,
|
|
375
|
+
hasManagedBlock,
|
|
376
|
+
cleanupLegacyConfig,
|
|
377
|
+
loadMarketplace,
|
|
378
|
+
upsertMarketplace,
|
|
379
|
+
removeMarketplaceEntry,
|
|
380
|
+
runPluginCli,
|
|
381
|
+
pluginCacheRoot,
|
|
382
|
+
listCachedHookAdapters,
|
|
383
|
+
restoreCachedHookAdapters,
|
|
384
|
+
},
|
|
385
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gemini CLI adapter — Phase 4.5 installer (extension-packaged).
|
|
5
|
+
*
|
|
6
|
+
* Target config dirs:
|
|
7
|
+
* global: $GEMINI_CONFIG_DIR → ~/.gemini
|
|
8
|
+
* local: <cwd>/.gemini
|
|
9
|
+
*
|
|
10
|
+
* Layout — everything is packaged as a Gemini extension under
|
|
11
|
+
* <target>/extensions/ultra-builder-pro/:
|
|
12
|
+
*
|
|
13
|
+
* gemini-extension.json → extension manifest (mcpServers declared here)
|
|
14
|
+
* commands/*.toml → generated from commands/*.md via md-to-toml
|
|
15
|
+
* skills/** → Ultra-owned allowlist (Gemini reads these as prompt includes)
|
|
16
|
+
* GEMINI.md → canonical agent context (docs/AGENT-CONTEXT.md copy)
|
|
17
|
+
* hooks/ → NOT installed; Gemini has no hook surface.
|
|
18
|
+
* prompt-guard downgrade is described inside GEMINI.md.
|
|
19
|
+
*
|
|
20
|
+
* Scope boundaries (documented):
|
|
21
|
+
* - Gemini has no hook event loop; runtime-enforced guards degrade to
|
|
22
|
+
* prompt-level guidance (hooks/adapters/gemini.py explains).
|
|
23
|
+
* - Subagent registration requires the upstream spec to settle — Phase 4.5
|
|
24
|
+
* spike captures whatever shape lands.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const os = require('node:os');
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
copyTree,
|
|
33
|
+
writeAtomic,
|
|
34
|
+
ensureDir,
|
|
35
|
+
removeTree,
|
|
36
|
+
} = require('./_shared/file-ops.cjs');
|
|
37
|
+
const { mdCommandToToml } = require('./_shared/md-to-toml.cjs');
|
|
38
|
+
const { buildMcpRuntime } = require('./_shared/codex-assets.cjs');
|
|
39
|
+
const { skillsForRuntime } = require('./_shared/runtime-assets.cjs');
|
|
40
|
+
|
|
41
|
+
const EXTENSION_NAME = 'ultra-builder-pro';
|
|
42
|
+
const MCP_SERVER_NAME = 'ultra-builder-pro';
|
|
43
|
+
const SOURCE_TAG = 'ubp';
|
|
44
|
+
|
|
45
|
+
function resolveTarget(ctx) {
|
|
46
|
+
if (ctx.configDir) return ctx.configDir;
|
|
47
|
+
if (ctx.scope === 'global') {
|
|
48
|
+
return process.env.GEMINI_CONFIG_DIR || path.join(ctx.homeDir || os.homedir(), '.gemini');
|
|
49
|
+
}
|
|
50
|
+
return path.join(ctx.cwd || process.cwd(), '.gemini');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveExtensionRoot(ctx) {
|
|
54
|
+
return path.join(resolveTarget(ctx), 'extensions', EXTENSION_NAME);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveRepoRoot(ctx) {
|
|
58
|
+
return ctx.repoRoot || path.resolve(__dirname, '..');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadPkgVersion(repoRoot) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')).version;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// Fall back to 0.0.0 but surface the reason — silent swallow hid a
|
|
66
|
+
// real "package.json moved" regression in a Phase 4 code review.
|
|
67
|
+
process.stderr.write(`gemini adapter: loadPkgVersion fell back to 0.0.0 (${err.message})\n`);
|
|
68
|
+
return '0.0.0';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildManifest(repoRoot, runtime) {
|
|
73
|
+
return {
|
|
74
|
+
name: EXTENSION_NAME,
|
|
75
|
+
version: loadPkgVersion(repoRoot),
|
|
76
|
+
description: 'Ultra Builder Pro — skill/MCP/CLI agent engineering system (Gemini packaging).',
|
|
77
|
+
// Sibling identification block — outside env so it cannot leak into the
|
|
78
|
+
// MCP server's child process (P2 #9). Uninstall reads manifest._ubp.source.
|
|
79
|
+
_ubp: { source: SOURCE_TAG },
|
|
80
|
+
mcpServers: {
|
|
81
|
+
[MCP_SERVER_NAME]: {
|
|
82
|
+
command: process.execPath,
|
|
83
|
+
args: [runtime.launcher],
|
|
84
|
+
_ubp: { source: SOURCE_TAG },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
contextFileName: 'GEMINI.md',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function ownsExtension(extensionRoot) {
|
|
92
|
+
const manifestFile = path.join(extensionRoot, 'gemini-extension.json');
|
|
93
|
+
if (!fs.existsSync(manifestFile)) return false;
|
|
94
|
+
try {
|
|
95
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
96
|
+
return manifest?._ubp?.source === SOURCE_TAG;
|
|
97
|
+
} catch (_error) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function install(ctx) {
|
|
103
|
+
const extensionRoot = resolveExtensionRoot(ctx);
|
|
104
|
+
const repoRoot = resolveRepoRoot(ctx);
|
|
105
|
+
if (fs.existsSync(extensionRoot)) {
|
|
106
|
+
const entries = fs.readdirSync(extensionRoot);
|
|
107
|
+
if (entries.length > 0 && !ownsExtension(extensionRoot)) {
|
|
108
|
+
throw new Error(`refusing to replace unmanaged Gemini extension: ${extensionRoot}`);
|
|
109
|
+
}
|
|
110
|
+
if (entries.length > 0) {
|
|
111
|
+
for (const managed of ['skills', 'commands', 'runtime', 'spec', 'templates']) {
|
|
112
|
+
removeTree(path.join(extensionRoot, managed));
|
|
113
|
+
}
|
|
114
|
+
for (const managed of ['GEMINI.md', 'gemini-extension.json']) {
|
|
115
|
+
const file = path.join(extensionRoot, managed);
|
|
116
|
+
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
ensureDir(extensionRoot);
|
|
121
|
+
|
|
122
|
+
const report = { target: extensionRoot, copied: {}, config: { updated: false } };
|
|
123
|
+
|
|
124
|
+
// 1. Skills — copy only the canonical Ultra-owned allowlist.
|
|
125
|
+
const skillsSrc = path.join(repoRoot, 'skills');
|
|
126
|
+
report.copied.skills = [];
|
|
127
|
+
for (const name of skillsForRuntime('gemini')) {
|
|
128
|
+
const source = path.join(skillsSrc, name);
|
|
129
|
+
if (!fs.existsSync(path.join(source, 'SKILL.md'))) {
|
|
130
|
+
throw new Error(`missing allowlisted Gemini skill: ${name}`);
|
|
131
|
+
}
|
|
132
|
+
const files = copyTree(source, path.join(extensionRoot, 'skills', name));
|
|
133
|
+
report.copied.skills.push(...files.map((file) => path.join(name, file)));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 2. Commands — md → toml
|
|
137
|
+
const commandsSrc = path.join(repoRoot, 'commands');
|
|
138
|
+
if (fs.existsSync(commandsSrc)) {
|
|
139
|
+
const dstCommands = path.join(extensionRoot, 'commands');
|
|
140
|
+
ensureDir(dstCommands);
|
|
141
|
+
const tomlFiles = [];
|
|
142
|
+
for (const f of fs.readdirSync(commandsSrc)) {
|
|
143
|
+
if (!f.endsWith('.md')) continue;
|
|
144
|
+
const md = fs.readFileSync(path.join(commandsSrc, f), 'utf8');
|
|
145
|
+
const tomlName = f.replace(/\.md$/, '.toml');
|
|
146
|
+
writeAtomic(path.join(dstCommands, tomlName), mdCommandToToml(md));
|
|
147
|
+
tomlFiles.push(tomlName);
|
|
148
|
+
}
|
|
149
|
+
report.copied.commands = tomlFiles;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 3. GEMINI.md — copy docs/AGENT-CONTEXT.md into the extension context file
|
|
153
|
+
const agentContext = path.join(repoRoot, 'docs', 'AGENT-CONTEXT.md');
|
|
154
|
+
if (fs.existsSync(agentContext)) {
|
|
155
|
+
const dst = path.join(extensionRoot, 'GEMINI.md');
|
|
156
|
+
fs.copyFileSync(agentContext, dst);
|
|
157
|
+
report.copied.context = 'GEMINI.md';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 4. Manifest
|
|
161
|
+
const runtime = buildMcpRuntime(repoRoot, extensionRoot, { runtime: 'gemini' });
|
|
162
|
+
const manifest = buildManifest(repoRoot, runtime);
|
|
163
|
+
writeAtomic(
|
|
164
|
+
path.join(extensionRoot, 'gemini-extension.json'),
|
|
165
|
+
JSON.stringify(manifest, null, 2) + '\n',
|
|
166
|
+
);
|
|
167
|
+
report.config.updated = true;
|
|
168
|
+
return report;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function uninstall(ctx) {
|
|
172
|
+
const extensionRoot = resolveExtensionRoot(ctx);
|
|
173
|
+
const report = { target: extensionRoot, removed: {}, config: { updated: false } };
|
|
174
|
+
if (fs.existsSync(extensionRoot)) {
|
|
175
|
+
// Manifest carries the _ubp sibling block — if tampered with, bail loud.
|
|
176
|
+
const manifestFile = path.join(extensionRoot, 'gemini-extension.json');
|
|
177
|
+
if (fs.existsSync(manifestFile)) {
|
|
178
|
+
try {
|
|
179
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
180
|
+
if (!manifest._ubp || manifest._ubp.source !== SOURCE_TAG) {
|
|
181
|
+
throw new Error(`refusing to uninstall: ${manifestFile} was not generated by this adapter`);
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (err.message.startsWith('refusing')) throw err;
|
|
185
|
+
// malformed manifest: proceed — we still own the directory name
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
removeTree(extensionRoot);
|
|
189
|
+
report.removed.extension = true;
|
|
190
|
+
report.config.updated = true;
|
|
191
|
+
}
|
|
192
|
+
return report;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
name: 'gemini',
|
|
197
|
+
EXTENSION_NAME,
|
|
198
|
+
MCP_SERVER_NAME,
|
|
199
|
+
SOURCE_TAG,
|
|
200
|
+
resolveTarget,
|
|
201
|
+
resolveExtensionRoot,
|
|
202
|
+
install,
|
|
203
|
+
uninstall,
|
|
204
|
+
};
|