ultra-builder-pro-cli 0.5.2 → 0.6.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/CHANGELOG.md +44 -7
- package/README.md +14 -16
- package/adapters/_shared/codex-assets.cjs +32 -151
- package/adapters/_shared/frontmatter.cjs +2 -2
- package/adapters/_shared/handbook.cjs +3 -3
- package/adapters/_shared/path-rewrite.cjs +0 -3
- package/adapters/_shared/runtime-assets.cjs +11 -4
- package/adapters/_shared/tests/path-rewrite.test.cjs +1 -3
- package/adapters/_shared/tests/resolve-target.test.cjs +1 -11
- package/adapters/_shared/tests/runtime-assets.test.cjs +17 -6
- package/adapters/opencode.js +1 -6
- package/adapters/tests/claude.test.cjs +1 -4
- package/adapters/tests/codex.test.cjs +5 -6
- package/adapters/tests/opencode.test.cjs +1 -2
- package/agents/tdd-runner.md +4 -3
- package/bin/install.js +6 -9
- package/bin/orchestrator.js +11 -4
- package/docs/AGENT-CONTEXT.md +4 -5
- package/docs/ARCHITECTURE.md +3 -4
- package/docs/LEGACY-HERMES.md +5 -81
- package/docs/PLAN.zh-CN.md +35 -42
- package/docs/ROADMAP.md +3 -3
- package/docs/RUNTIME-COMPAT-MATRIX.md +61 -60
- package/mcp-server/lib/pricing.cjs +0 -6
- package/mcp-server/lib/state-ops.cjs +7 -0
- package/mcp-server/lib/telemetry.cjs +4 -0
- package/mcp-server/tests/server-contract.test.cjs +17 -0
- package/mcp-server/tests/state-ops.test.cjs +23 -0
- package/mcp-server/tests/telemetry.test.cjs +14 -0
- package/orchestrator/dispatch-rules.cjs +15 -4
- package/orchestrator/tests/daemon.test.cjs +13 -12
- package/orchestrator/tests/dispatch-rules.test.cjs +12 -10
- package/package.json +3 -5
- package/skills/ultra-init/SKILL.md +0 -1
- package/skills/ultra-verify/SKILL.md +22 -41
- package/skills/ultra-verify/evals/evals.json +22 -24
- package/skills/ultra-verify/references/confidence-system.md +15 -32
- package/skills/ultra-verify/references/cross-verify-modes.md +14 -59
- package/skills/ultra-verify/scripts/verify_wait.py +44 -81
- package/spec/mcp-tools.yaml +3 -3
- package/spec/schemas/skill-manifest.schema.json +2 -2
- package/spec/schemas/state-db.sql +2 -2
- package/adapters/_shared/md-to-toml.cjs +0 -54
- package/adapters/_shared/tests/md-to-toml.test.cjs +0 -48
- package/adapters/gemini.js +0 -204
- package/adapters/tests/gemini.test.cjs +0 -112
- package/skills/gemini-collab/SKILL.md +0 -51
- package/skills/gemini-collab/evals/evals.json +0 -42
|
@@ -1,23 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""File-based completion waiter for the Ultra Verify advisor.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Two exit conditions only:
|
|
8
|
-
1. Both AIs have output (non-empty + stable size) → exit 0, status="complete"
|
|
9
|
-
2. Timeout → exit 0, status="timeout" (JSON status field tells the story)
|
|
10
|
-
|
|
11
|
-
Expected files:
|
|
12
|
-
- gemini-output.md (Gemini)
|
|
13
|
-
- codex-output.md (Codex)
|
|
14
|
-
|
|
15
|
-
Usage:
|
|
16
|
-
python3 verify_wait.py <session_path> [--timeout SECONDS]
|
|
17
|
-
|
|
18
|
-
Exit codes:
|
|
19
|
-
0 - Always (result expressed via JSON status field)
|
|
20
|
-
2 - Invalid arguments (missing path, bad --timeout)
|
|
4
|
+
Poll the session directory until the expected output is non-empty and stable, or until timeout.
|
|
5
|
+
The JSON result carries the operational status; timeout is not a process error.
|
|
21
6
|
"""
|
|
22
7
|
|
|
23
8
|
import json
|
|
@@ -25,106 +10,84 @@ import sys
|
|
|
25
10
|
import time
|
|
26
11
|
from pathlib import Path
|
|
27
12
|
|
|
28
|
-
POLL_INTERVAL = 3
|
|
29
|
-
DEFAULT_TIMEOUT = 1200
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
EXPECTED = {
|
|
33
|
-
"gemini": "gemini-output.md",
|
|
34
|
-
"codex": "codex-output.md",
|
|
35
|
-
}
|
|
13
|
+
POLL_INTERVAL = 3
|
|
14
|
+
DEFAULT_TIMEOUT = 1200
|
|
15
|
+
ADVISOR = "codex"
|
|
16
|
+
OUTPUT_FILE = "codex-output.md"
|
|
36
17
|
|
|
37
18
|
|
|
38
19
|
def _file_size(path: Path) -> int:
|
|
39
|
-
"""Return file size in bytes, or -1 if not found."""
|
|
40
20
|
try:
|
|
41
21
|
return path.stat().st_size
|
|
42
22
|
except (FileNotFoundError, OSError):
|
|
43
23
|
return -1
|
|
44
24
|
|
|
45
25
|
|
|
46
|
-
def _check_output(session_path: Path
|
|
47
|
-
|
|
48
|
-
output = session_path / EXPECTED[name]
|
|
26
|
+
def _check_output(session_path: Path) -> dict:
|
|
27
|
+
output = session_path / OUTPUT_FILE
|
|
49
28
|
if _file_size(output) > 0:
|
|
50
|
-
return {"name":
|
|
51
|
-
return {"name":
|
|
29
|
+
return {"name": ADVISOR, "status": "complete", "file": str(output)}
|
|
30
|
+
return {"name": ADVISOR, "status": "pending", "file": None}
|
|
52
31
|
|
|
53
32
|
|
|
54
|
-
def _timeout_status(session_path: Path
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
error = session_path / f"{name}-error.log"
|
|
33
|
+
def _timeout_status(session_path: Path) -> dict:
|
|
34
|
+
output = session_path / OUTPUT_FILE
|
|
35
|
+
error = session_path / f"{ADVISOR}-error.log"
|
|
58
36
|
output_size = _file_size(output)
|
|
59
37
|
if output_size > 0:
|
|
60
|
-
return {"name":
|
|
38
|
+
return {"name": ADVISOR, "status": "complete", "file": str(output)}
|
|
61
39
|
if _file_size(error) > 0:
|
|
62
|
-
return {"name":
|
|
40
|
+
return {"name": ADVISOR, "status": "failed", "file": str(error)}
|
|
63
41
|
if output_size == 0:
|
|
64
|
-
return {"name":
|
|
65
|
-
return {"name":
|
|
42
|
+
return {"name": ADVISOR, "status": "empty", "file": str(output)}
|
|
43
|
+
return {"name": ADVISOR, "status": "pending", "file": None}
|
|
66
44
|
|
|
67
45
|
|
|
68
|
-
def main():
|
|
46
|
+
def main() -> None:
|
|
69
47
|
if len(sys.argv) < 2:
|
|
70
48
|
print(__doc__, file=sys.stderr)
|
|
71
49
|
sys.exit(2)
|
|
72
50
|
|
|
73
51
|
session_path = Path(sys.argv[1])
|
|
74
52
|
timeout = DEFAULT_TIMEOUT
|
|
75
|
-
|
|
76
53
|
if "--timeout" in sys.argv:
|
|
77
54
|
idx = sys.argv.index("--timeout")
|
|
78
|
-
if idx + 1
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
55
|
+
if idx + 1 >= len(sys.argv):
|
|
56
|
+
print("Error: --timeout requires an integer", file=sys.stderr)
|
|
57
|
+
sys.exit(2)
|
|
58
|
+
try:
|
|
59
|
+
timeout = int(sys.argv[idx + 1])
|
|
60
|
+
except ValueError:
|
|
61
|
+
print(f"Error: --timeout must be integer: {sys.argv[idx + 1]}", file=sys.stderr)
|
|
62
|
+
sys.exit(2)
|
|
63
|
+
if timeout < 0:
|
|
64
|
+
print("Error: --timeout must be non-negative", file=sys.stderr)
|
|
65
|
+
sys.exit(2)
|
|
84
66
|
|
|
85
67
|
if not session_path.is_dir():
|
|
86
68
|
print(f"Error: session directory not found: {session_path}", file=sys.stderr)
|
|
87
69
|
sys.exit(2)
|
|
88
70
|
|
|
89
71
|
deadline = time.monotonic() + timeout
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
# Poll loop — exit condition 1: both outputs ready + stable
|
|
72
|
+
previous_size = _file_size(session_path / OUTPUT_FILE)
|
|
93
73
|
while time.monotonic() < deadline:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
gemini_done = gemini["status"] == "complete" and cur_sizes["gemini"] == prev_sizes["gemini"]
|
|
99
|
-
codex_done = codex["status"] == "complete" and cur_sizes["codex"] == prev_sizes["codex"]
|
|
100
|
-
|
|
101
|
-
if gemini_done and codex_done:
|
|
74
|
+
current_size = _file_size(session_path / OUTPUT_FILE)
|
|
75
|
+
advisor = _check_output(session_path)
|
|
76
|
+
stable = advisor["status"] == "complete" and current_size == previous_size
|
|
77
|
+
if stable:
|
|
102
78
|
elapsed = int(timeout - (deadline - time.monotonic()))
|
|
103
|
-
print(json.dumps({"status": "complete",
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
remaining = int(deadline - time.monotonic())
|
|
110
|
-
parts = []
|
|
111
|
-
for name, done, result in [("gemini", gemini_done, gemini), ("codex", codex_done, codex)]:
|
|
112
|
-
if done:
|
|
113
|
-
parts.append(f"{name}:complete")
|
|
114
|
-
elif result["status"] == "complete":
|
|
115
|
-
parts.append(f"{name}:stabilizing")
|
|
116
|
-
else:
|
|
117
|
-
parts.append(f"{name}:waiting")
|
|
118
|
-
done_count = int(gemini_done) + int(codex_done)
|
|
119
|
-
sys.stderr.write(f"\r [{done_count}/2] {' | '.join(parts)} ({remaining}s remaining) ")
|
|
79
|
+
print(json.dumps({"status": "complete", ADVISOR: advisor, "elapsed_seconds": elapsed}))
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
state = "stabilizing" if advisor["status"] == "complete" else "waiting"
|
|
83
|
+
remaining = max(0, int(deadline - time.monotonic()))
|
|
84
|
+
sys.stderr.write(f"\r [0/1] {ADVISOR}:{state} ({remaining}s remaining) ")
|
|
120
85
|
sys.stderr.flush()
|
|
86
|
+
previous_size = current_size
|
|
121
87
|
time.sleep(POLL_INTERVAL)
|
|
122
88
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
codex = _timeout_status(session_path, "codex")
|
|
126
|
-
print(json.dumps({"status": "timeout", "gemini": gemini, "codex": codex, "elapsed_seconds": timeout}))
|
|
127
|
-
sys.exit(0)
|
|
89
|
+
advisor = _timeout_status(session_path)
|
|
90
|
+
print(json.dumps({"status": "timeout", ADVISOR: advisor, "elapsed_seconds": timeout}))
|
|
128
91
|
|
|
129
92
|
|
|
130
93
|
if __name__ == "__main__":
|
package/spec/mcp-tools.yaml
CHANGED
|
@@ -359,7 +359,7 @@ tools:
|
|
|
359
359
|
properties:
|
|
360
360
|
task_id: { type: string }
|
|
361
361
|
session_id: { type: string }
|
|
362
|
-
runtime: { type: string, enum: [claude, opencode, codex
|
|
362
|
+
runtime: { type: string, enum: [claude, opencode, codex] }
|
|
363
363
|
type:
|
|
364
364
|
type: string
|
|
365
365
|
enum:
|
|
@@ -439,7 +439,7 @@ tools:
|
|
|
439
439
|
additionalProperties: false
|
|
440
440
|
properties:
|
|
441
441
|
task_id: { type: string }
|
|
442
|
-
runtime: { type: string, enum: [claude, opencode, codex
|
|
442
|
+
runtime: { type: string, enum: [claude, opencode, codex] }
|
|
443
443
|
takeover: { type: boolean, default: false, description: 'override active lease (D33 takeover strategy)' }
|
|
444
444
|
worktree_base: { type: string }
|
|
445
445
|
output_schema:
|
|
@@ -541,7 +541,7 @@ tools:
|
|
|
541
541
|
additionalProperties: false
|
|
542
542
|
properties:
|
|
543
543
|
task_id: { type: string }
|
|
544
|
-
runtime: { type: string, enum: [claude, opencode, codex
|
|
544
|
+
runtime: { type: string, enum: [claude, opencode, codex] }
|
|
545
545
|
output_schema:
|
|
546
546
|
type: object
|
|
547
547
|
required: [can_spawn, recommended_action]
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
},
|
|
20
20
|
"runtime": {
|
|
21
21
|
"oneOf": [
|
|
22
|
-
{ "type": "string", "enum": ["claude", "opencode", "codex", "
|
|
22
|
+
{ "type": "string", "enum": ["claude", "opencode", "codex", "all"] },
|
|
23
23
|
{
|
|
24
24
|
"type": "array",
|
|
25
|
-
"items": { "type": "string", "enum": ["claude", "opencode", "codex"
|
|
25
|
+
"items": { "type": "string", "enum": ["claude", "opencode", "codex"] },
|
|
26
26
|
"minItems": 1,
|
|
27
27
|
"uniqueItems": true
|
|
28
28
|
}
|
|
@@ -54,7 +54,7 @@ CREATE TABLE IF NOT EXISTS events (
|
|
|
54
54
|
type TEXT NOT NULL,
|
|
55
55
|
task_id TEXT,
|
|
56
56
|
session_id TEXT,
|
|
57
|
-
runtime TEXT CHECK (runtime IS NULL OR runtime IN ('claude', 'opencode', 'codex'
|
|
57
|
+
runtime TEXT CHECK (runtime IS NULL OR runtime IN ('claude', 'opencode', 'codex')),
|
|
58
58
|
payload_json TEXT
|
|
59
59
|
);
|
|
60
60
|
|
|
@@ -69,7 +69,7 @@ CREATE INDEX IF NOT EXISTS events_session ON events(session_id, id);
|
|
|
69
69
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
70
70
|
sid TEXT PRIMARY KEY,
|
|
71
71
|
task_id TEXT NOT NULL REFERENCES tasks(id),
|
|
72
|
-
runtime TEXT NOT NULL CHECK (runtime IN ('claude', 'opencode', 'codex'
|
|
72
|
+
runtime TEXT NOT NULL CHECK (runtime IN ('claude', 'opencode', 'codex')),
|
|
73
73
|
pid INTEGER,
|
|
74
74
|
worktree_path TEXT NOT NULL,
|
|
75
75
|
artifact_dir TEXT NOT NULL,
|
|
@@ -1,54 +0,0 @@
|
|
|
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 };
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { test } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
|
|
6
|
-
const { mdCommandToToml } = require('../md-to-toml.cjs');
|
|
7
|
-
|
|
8
|
-
test('mdCommandToToml extracts description from frontmatter', () => {
|
|
9
|
-
const md = [
|
|
10
|
-
'---',
|
|
11
|
-
'description: Initialize project',
|
|
12
|
-
'workflow-ref: "@skills/ultra-init/SKILL.md"',
|
|
13
|
-
'---',
|
|
14
|
-
'',
|
|
15
|
-
'# /ultra-init',
|
|
16
|
-
'body content here',
|
|
17
|
-
].join('\n');
|
|
18
|
-
|
|
19
|
-
const toml = mdCommandToToml(md);
|
|
20
|
-
assert.match(toml, /description = "Initialize project"/);
|
|
21
|
-
assert.match(toml, /prompt = """/);
|
|
22
|
-
assert.match(toml, /body content here/);
|
|
23
|
-
assert.match(toml, /workflow reference: @skills\/ultra-init\/SKILL\.md/);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('mdCommandToToml includes mcp_tools_required and cli_fallback when present', () => {
|
|
27
|
-
const md = [
|
|
28
|
-
'---',
|
|
29
|
-
'description: plan tasks',
|
|
30
|
-
'mcp_tools_required:',
|
|
31
|
-
' - task.create',
|
|
32
|
-
' - ask.question',
|
|
33
|
-
'cli_fallback: "task create"',
|
|
34
|
-
'---',
|
|
35
|
-
'body',
|
|
36
|
-
].join('\n');
|
|
37
|
-
|
|
38
|
-
const toml = mdCommandToToml(md);
|
|
39
|
-
assert.match(toml, /MCP tools required: task\.create, ask\.question/);
|
|
40
|
-
assert.match(toml, /CLI fallback: ultra-tools task create/);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('mdCommandToToml defaults description when frontmatter absent', () => {
|
|
44
|
-
const md = '# plain markdown, no frontmatter';
|
|
45
|
-
const toml = mdCommandToToml(md);
|
|
46
|
-
assert.match(toml, /description = "Ultra Builder Pro command"/);
|
|
47
|
-
assert.match(toml, /plain markdown, no frontmatter/);
|
|
48
|
-
});
|
package/adapters/gemini.js
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { test } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const os = require('node:os');
|
|
7
|
-
const path = require('node:path');
|
|
8
|
-
|
|
9
|
-
const gemini = require('../gemini.js');
|
|
10
|
-
const { RETIRED_SKILLS, skillsForRuntime } = require('../_shared/runtime-assets.cjs');
|
|
11
|
-
|
|
12
|
-
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
13
|
-
|
|
14
|
-
function mkTarget() {
|
|
15
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-gemini-'));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
test('install packages into extensions/ultra-builder-pro with manifest + commands.toml + skills', () => {
|
|
19
|
-
const target = mkTarget();
|
|
20
|
-
try {
|
|
21
|
-
const r = gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
22
|
-
const extRoot = r.target;
|
|
23
|
-
assert.equal(extRoot, path.join(target, 'extensions', 'ultra-builder-pro'));
|
|
24
|
-
assert.ok(fs.existsSync(path.join(extRoot, 'gemini-extension.json')));
|
|
25
|
-
assert.ok(fs.existsSync(path.join(extRoot, 'GEMINI.md')));
|
|
26
|
-
assert.ok(r.copied.commands.includes('ultra-init.toml'));
|
|
27
|
-
assert.ok(r.copied.skills.some((p) => p.includes('ultra-init/SKILL.md')));
|
|
28
|
-
|
|
29
|
-
const installedSkills = fs.readdirSync(path.join(extRoot, 'skills')).sort();
|
|
30
|
-
assert.deepEqual(installedSkills, skillsForRuntime('gemini').sort());
|
|
31
|
-
for (const retired of RETIRED_SKILLS) assert.ok(!installedSkills.includes(retired));
|
|
32
|
-
|
|
33
|
-
const tomlContent = fs.readFileSync(path.join(extRoot, 'commands', 'ultra-init.toml'), 'utf8');
|
|
34
|
-
assert.match(tomlContent, /description = "/);
|
|
35
|
-
assert.match(tomlContent, /prompt = """/);
|
|
36
|
-
} finally {
|
|
37
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('reinstall removes stale managed skill assets while preserving runtime state', () => {
|
|
42
|
-
const target = mkTarget();
|
|
43
|
-
try {
|
|
44
|
-
gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
45
|
-
const extRoot = gemini.resolveExtensionRoot({ configDir: target });
|
|
46
|
-
const stale = path.join(extRoot, 'skills', 'recall');
|
|
47
|
-
fs.mkdirSync(stale, { recursive: true });
|
|
48
|
-
fs.writeFileSync(path.join(stale, 'SKILL.md'), 'retired');
|
|
49
|
-
fs.writeFileSync(path.join(extRoot, 'state.db'), 'preserve');
|
|
50
|
-
|
|
51
|
-
gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
52
|
-
assert.equal(fs.existsSync(stale), false);
|
|
53
|
-
assert.equal(fs.readFileSync(path.join(extRoot, 'state.db'), 'utf8'), 'preserve');
|
|
54
|
-
} finally {
|
|
55
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('manifest declares mcpServers with _ubp tag (not in env — P2 #9)', () => {
|
|
60
|
-
const target = mkTarget();
|
|
61
|
-
try {
|
|
62
|
-
gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
63
|
-
const extRoot = gemini.resolveExtensionRoot({ configDir: target });
|
|
64
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(extRoot, 'gemini-extension.json'), 'utf8'));
|
|
65
|
-
assert.equal(manifest.name, gemini.EXTENSION_NAME);
|
|
66
|
-
assert.equal(manifest._ubp.source, gemini.SOURCE_TAG);
|
|
67
|
-
assert.ok(manifest.mcpServers[gemini.MCP_SERVER_NAME]);
|
|
68
|
-
// D45: identification lives outside env so it never leaks into spawned MCP server.
|
|
69
|
-
assert.equal(manifest.mcpServers[gemini.MCP_SERVER_NAME]._ubp.source, gemini.SOURCE_TAG);
|
|
70
|
-
assert.equal(manifest.mcpServers[gemini.MCP_SERVER_NAME].env, undefined, 'project cwd must own MCP state');
|
|
71
|
-
assert.equal(manifest.contextFileName, 'GEMINI.md');
|
|
72
|
-
} finally {
|
|
73
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('manifest uses a durable bundled MCP runtime and project-local state defaults', () => {
|
|
78
|
-
const target = mkTarget();
|
|
79
|
-
try {
|
|
80
|
-
gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
81
|
-
const extRoot = gemini.resolveExtensionRoot({ configDir: target });
|
|
82
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(extRoot, 'gemini-extension.json'), 'utf8'));
|
|
83
|
-
const server = manifest.mcpServers[gemini.MCP_SERVER_NAME];
|
|
84
|
-
const launcher = server.args[0];
|
|
85
|
-
assert.ok(launcher.startsWith(path.join(extRoot, 'runtime') + path.sep));
|
|
86
|
-
assert.ok(fs.existsSync(launcher), 'bundled launcher must survive the installer source directory');
|
|
87
|
-
assert.equal(server.env, undefined, 'Gemini must resolve .ultra/state.db from the active project cwd');
|
|
88
|
-
} finally {
|
|
89
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('uninstall removes extension dir; refuses on foreign manifest', () => {
|
|
94
|
-
const target = mkTarget();
|
|
95
|
-
const extRoot = gemini.resolveExtensionRoot({ configDir: target });
|
|
96
|
-
try {
|
|
97
|
-
gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
98
|
-
assert.ok(fs.existsSync(extRoot));
|
|
99
|
-
gemini.uninstall({ configDir: target });
|
|
100
|
-
assert.ok(!fs.existsSync(extRoot));
|
|
101
|
-
|
|
102
|
-
// Re-install then tamper with the manifest's _ubp identification block.
|
|
103
|
-
gemini.install({ configDir: target, repoRoot: REPO_ROOT });
|
|
104
|
-
const manifestFile = path.join(extRoot, 'gemini-extension.json');
|
|
105
|
-
const m = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
106
|
-
m._ubp = { source: 'someone-else' };
|
|
107
|
-
fs.writeFileSync(manifestFile, JSON.stringify(m, null, 2));
|
|
108
|
-
assert.throws(() => gemini.uninstall({ configDir: target }), /refusing to uninstall/);
|
|
109
|
-
} finally {
|
|
110
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
111
|
-
}
|
|
112
|
-
});
|