@jaguilar87/gaia 5.1.0-rc.3 → 5.1.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/.claude-plugin/marketplace.json +3 -2
- package/.claude-plugin/plugin.json +2 -174
- package/CHANGELOG.md +4 -0
- package/bin/plugin-dryrun.sh +20 -11
- package/bin/pre-publish-validate.js +16 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/build-plugin.py +20 -19
- package/scripts/check_hooks_drift.py +13 -7
- package/scripts/release-prepare.mjs +18 -2
- package/skills/gaia-patterns/reference.md +2 -2
- package/skills/gaia-release/SKILL.md +6 -6
- package/skills/gaia-release/reference.md +6 -6
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "gaia",
|
|
11
11
|
"description": "Security-first multi-agent orchestration for Claude Code. Specialized agents cover the full development lifecycle — analysis, planning, execution, deployment — with codebase-aware context injection. Every command is risk-classified: read-only runs freely, state changes pause for your approval, and irreversible operations are permanently blocked.",
|
|
12
|
-
"version": "5.1.0
|
|
12
|
+
"version": "5.1.0",
|
|
13
13
|
"category": "devops",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "jaguilar87",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"homepage": "https://github.com/metraton/gaia#readme",
|
|
19
19
|
"source": {
|
|
20
20
|
"source": "github",
|
|
21
|
-
"repo": "metraton/gaia"
|
|
21
|
+
"repo": "metraton/gaia",
|
|
22
|
+
"ref": "v5.1.0"
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gaia",
|
|
3
|
-
"version": "5.1.0
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Security-first multi-agent orchestration for Claude Code. Agents span the full lifecycle; commands are risk-classified \u2014 reads run free, state changes need approval, irreversible ops blocked.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "jaguilar87",
|
|
@@ -20,177 +20,5 @@
|
|
|
20
20
|
"devops",
|
|
21
21
|
"security",
|
|
22
22
|
"orchestration"
|
|
23
|
-
]
|
|
24
|
-
"hooks": {
|
|
25
|
-
"PreToolUse": [
|
|
26
|
-
{
|
|
27
|
-
"matcher": "Bash",
|
|
28
|
-
"hooks": [
|
|
29
|
-
{
|
|
30
|
-
"type": "command",
|
|
31
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pre_tool_use.py"
|
|
32
|
-
}
|
|
33
|
-
]
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"matcher": "Task",
|
|
37
|
-
"hooks": [
|
|
38
|
-
{
|
|
39
|
-
"type": "command",
|
|
40
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pre_tool_use.py"
|
|
41
|
-
}
|
|
42
|
-
]
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"matcher": "Agent",
|
|
46
|
-
"hooks": [
|
|
47
|
-
{
|
|
48
|
-
"type": "command",
|
|
49
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pre_tool_use.py"
|
|
50
|
-
}
|
|
51
|
-
]
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
"matcher": "SendMessage",
|
|
55
|
-
"hooks": [
|
|
56
|
-
{
|
|
57
|
-
"type": "command",
|
|
58
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pre_tool_use.py"
|
|
59
|
-
}
|
|
60
|
-
]
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
"matcher": "Read|Edit|Write|Glob|Grep|WebSearch|WebFetch|NotebookEdit",
|
|
64
|
-
"hooks": [
|
|
65
|
-
{
|
|
66
|
-
"type": "command",
|
|
67
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pre_tool_use.py"
|
|
68
|
-
}
|
|
69
|
-
]
|
|
70
|
-
}
|
|
71
|
-
],
|
|
72
|
-
"PostToolUse": [
|
|
73
|
-
{
|
|
74
|
-
"matcher": "Bash",
|
|
75
|
-
"hooks": [
|
|
76
|
-
{
|
|
77
|
-
"type": "command",
|
|
78
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/post_tool_use.py"
|
|
79
|
-
}
|
|
80
|
-
]
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
"matcher": "AskUserQuestion",
|
|
84
|
-
"hooks": [
|
|
85
|
-
{
|
|
86
|
-
"type": "command",
|
|
87
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/post_tool_use.py"
|
|
88
|
-
}
|
|
89
|
-
]
|
|
90
|
-
}
|
|
91
|
-
],
|
|
92
|
-
"SubagentStop": [
|
|
93
|
-
{
|
|
94
|
-
"matcher": "*",
|
|
95
|
-
"hooks": [
|
|
96
|
-
{
|
|
97
|
-
"type": "command",
|
|
98
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/subagent_stop.py"
|
|
99
|
-
}
|
|
100
|
-
]
|
|
101
|
-
}
|
|
102
|
-
],
|
|
103
|
-
"SessionStart": [
|
|
104
|
-
{
|
|
105
|
-
"matcher": "startup",
|
|
106
|
-
"hooks": [
|
|
107
|
-
{
|
|
108
|
-
"type": "command",
|
|
109
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session_start.py"
|
|
110
|
-
}
|
|
111
|
-
]
|
|
112
|
-
}
|
|
113
|
-
],
|
|
114
|
-
"SessionEnd": [
|
|
115
|
-
{
|
|
116
|
-
"hooks": [
|
|
117
|
-
{
|
|
118
|
-
"type": "command",
|
|
119
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session_end_hook.py"
|
|
120
|
-
}
|
|
121
|
-
]
|
|
122
|
-
}
|
|
123
|
-
],
|
|
124
|
-
"PreCompact": [
|
|
125
|
-
{
|
|
126
|
-
"hooks": [
|
|
127
|
-
{
|
|
128
|
-
"type": "command",
|
|
129
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pre_compact.py"
|
|
130
|
-
}
|
|
131
|
-
]
|
|
132
|
-
}
|
|
133
|
-
],
|
|
134
|
-
"PostCompact": [
|
|
135
|
-
{
|
|
136
|
-
"hooks": [
|
|
137
|
-
{
|
|
138
|
-
"type": "command",
|
|
139
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/post_compact.py"
|
|
140
|
-
}
|
|
141
|
-
]
|
|
142
|
-
}
|
|
143
|
-
],
|
|
144
|
-
"Stop": [
|
|
145
|
-
{
|
|
146
|
-
"hooks": [
|
|
147
|
-
{
|
|
148
|
-
"type": "command",
|
|
149
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/stop_hook.py"
|
|
150
|
-
}
|
|
151
|
-
]
|
|
152
|
-
}
|
|
153
|
-
],
|
|
154
|
-
"TaskCompleted": [
|
|
155
|
-
{
|
|
156
|
-
"hooks": [
|
|
157
|
-
{
|
|
158
|
-
"type": "command",
|
|
159
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/task_completed.py"
|
|
160
|
-
}
|
|
161
|
-
]
|
|
162
|
-
}
|
|
163
|
-
],
|
|
164
|
-
"SubagentStart": [
|
|
165
|
-
{
|
|
166
|
-
"matcher": "*",
|
|
167
|
-
"hooks": [
|
|
168
|
-
{
|
|
169
|
-
"type": "command",
|
|
170
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/subagent_start.py"
|
|
171
|
-
}
|
|
172
|
-
]
|
|
173
|
-
}
|
|
174
|
-
],
|
|
175
|
-
"UserPromptSubmit": [
|
|
176
|
-
{
|
|
177
|
-
"hooks": [
|
|
178
|
-
{
|
|
179
|
-
"type": "command",
|
|
180
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/user_prompt_submit.py"
|
|
181
|
-
}
|
|
182
|
-
]
|
|
183
|
-
}
|
|
184
|
-
],
|
|
185
|
-
"ElicitationResult": [
|
|
186
|
-
{
|
|
187
|
-
"hooks": [
|
|
188
|
-
{
|
|
189
|
-
"type": "command",
|
|
190
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/elicitation_result.py"
|
|
191
|
-
}
|
|
192
|
-
]
|
|
193
|
-
}
|
|
194
|
-
]
|
|
195
|
-
}
|
|
23
|
+
]
|
|
196
24
|
}
|
package/CHANGELOG.md
CHANGED
package/bin/plugin-dryrun.sh
CHANGED
|
@@ -31,7 +31,7 @@ if [[ "${1:-}" == "--functional" ]]; then
|
|
|
31
31
|
fi
|
|
32
32
|
|
|
33
33
|
# ---------------------------------------------------------------------------
|
|
34
|
-
# (a) Pack -> prepack regenerates the root
|
|
34
|
+
# (a) Pack -> prepack regenerates the root plugin.json (metadata only) + hooks/hooks.json
|
|
35
35
|
# ---------------------------------------------------------------------------
|
|
36
36
|
cd "${REPO_ROOT}"
|
|
37
37
|
echo "[dryrun] packing tarball (prepack regenerates root manifests)..."
|
|
@@ -118,15 +118,24 @@ else
|
|
|
118
118
|
echo " [PASS] no dist/ in tarball (package root is the plugin)"
|
|
119
119
|
fi
|
|
120
120
|
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
|
|
121
|
+
# Hooks are declared ONLY in hooks/hooks.json. plugin.json must NOT embed an
|
|
122
|
+
# inline 'hooks' block -- CC reads both, so an inline block double-registers
|
|
123
|
+
# every hook. Assert its absence, and assert every entry point referenced by
|
|
124
|
+
# hooks/hooks.json exists under the plugin root.
|
|
125
|
+
if python3 - "${ROOT}/.claude-plugin/plugin.json" "${ROOT}/hooks/hooks.json" "${ROOT}" <<'PY'
|
|
124
126
|
import json, os, re, sys
|
|
125
127
|
|
|
126
|
-
plugin_json, root = sys.argv[1], sys.argv[2]
|
|
127
|
-
|
|
128
|
+
plugin_json, hooks_json, root = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
129
|
+
|
|
130
|
+
plugin = json.load(open(plugin_json))
|
|
131
|
+
assert "hooks" not in plugin, (
|
|
132
|
+
"plugin.json embeds an inline 'hooks' block -- hooks must live only in "
|
|
133
|
+
"hooks/hooks.json (an inline block double-registers every hook)"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
data = json.load(open(hooks_json))
|
|
128
137
|
hooks = data.get("hooks")
|
|
129
|
-
assert isinstance(hooks, dict) and hooks, "
|
|
138
|
+
assert isinstance(hooks, dict) and hooks, "hooks/hooks.json is missing a non-empty 'hooks' block"
|
|
130
139
|
|
|
131
140
|
missing = []
|
|
132
141
|
for event, entries in hooks.items():
|
|
@@ -135,13 +144,13 @@ for event, entries in hooks.items():
|
|
|
135
144
|
m = re.search(r"\$\{CLAUDE_PLUGIN_ROOT\}/(\S+\.py)", h.get("command", ""))
|
|
136
145
|
if m and not os.path.isfile(os.path.join(root, m.group(1))):
|
|
137
146
|
missing.append(m.group(1))
|
|
138
|
-
assert not missing, f"
|
|
139
|
-
print(f"
|
|
147
|
+
assert not missing, f"hook entry points not found under plugin root: {sorted(set(missing))}"
|
|
148
|
+
print(f" hooks.json: {len(hooks)} events, all entry points resolve; plugin.json has no inline hooks")
|
|
140
149
|
PY
|
|
141
150
|
then
|
|
142
|
-
echo " [PASS]
|
|
151
|
+
echo " [PASS] hooks declared once (hooks.json only) + entry points resolve"
|
|
143
152
|
else
|
|
144
|
-
echo " [FAIL]
|
|
153
|
+
echo " [FAIL] hooks declaration invalid (inline block present or entry points missing)"
|
|
145
154
|
fail=1
|
|
146
155
|
fi
|
|
147
156
|
|
|
@@ -601,9 +601,24 @@ class PrePublishValidator {
|
|
|
601
601
|
if (plugin.version !== expectedVersion) {
|
|
602
602
|
versionMismatches.push(`marketplace/${plugin.name}: ${plugin.version}`);
|
|
603
603
|
}
|
|
604
|
+
// For git/github object sources a `source.ref` pins `/plugin install`
|
|
605
|
+
// to a fixed release tag (`v<version>`). release:prepare's
|
|
606
|
+
// bumpMarketplace writes it atomically with the version. Tolerant
|
|
607
|
+
// when absent (refless resolves the repo's default HEAD -- a valid
|
|
608
|
+
// pre-pin state); strict when present so a stale/hand-edited ref that
|
|
609
|
+
// would serve the wrong tag is caught here rather than after publish.
|
|
610
|
+
const src = plugin.source;
|
|
611
|
+
if (src && typeof src === 'object' &&
|
|
612
|
+
(src.source === 'github' || src.source === 'git') &&
|
|
613
|
+
src.ref !== undefined) {
|
|
614
|
+
const expectedRef = `v${expectedVersion}`;
|
|
615
|
+
if (src.ref !== expectedRef) {
|
|
616
|
+
versionMismatches.push(`marketplace/${plugin.name}.source.ref: ${src.ref}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
604
619
|
}
|
|
605
620
|
if (!versionMismatches.some(m => m.startsWith('marketplace/'))) {
|
|
606
|
-
this.log(` ✓ marketplace.json plugin versions match`, 'success');
|
|
621
|
+
this.log(` ✓ marketplace.json plugin versions${marketplaceData.plugins.some(p => p.source && p.source.ref) ? ' + source.ref pins' : ''} match`, 'success');
|
|
607
622
|
}
|
|
608
623
|
}
|
|
609
624
|
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/scripts/build-plugin.py
CHANGED
|
@@ -4,9 +4,14 @@ Build script for the gaia plugin.
|
|
|
4
4
|
|
|
5
5
|
Under the `source: npm` delivery model the published package root IS the
|
|
6
6
|
plugin -- there is no dist/ bundle. This script only regenerates the two
|
|
7
|
-
generated manifests (.claude-plugin/plugin.json
|
|
7
|
+
generated manifests (.claude-plugin/plugin.json and the canonical
|
|
8
8
|
hooks/hooks.json) in place; it never cleans or copies component files.
|
|
9
9
|
|
|
10
|
+
Hooks are declared in exactly ONE place: hooks/hooks.json (the standard
|
|
11
|
+
plugin convention Claude Code reads). plugin.json does NOT embed an inline
|
|
12
|
+
`hooks` block -- doing so made Claude Code register every hook twice (once
|
|
13
|
+
from the inline block, once from hooks.json), so every event fired twice.
|
|
14
|
+
|
|
10
15
|
Usage:
|
|
11
16
|
python3 scripts/build-plugin.py <plugin-name> --manifests-only [--output-dir <path>]
|
|
12
17
|
|
|
@@ -233,10 +238,11 @@ def _get_entry_point(event_name: str, entries: list[str]) -> str:
|
|
|
233
238
|
def generate_plugin_json(manifest: dict) -> dict:
|
|
234
239
|
"""Generate .claude-plugin/plugin.json from manifest.
|
|
235
240
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
Does NOT embed a `hooks` key. Hooks are declared solely in
|
|
242
|
+
hooks/hooks.json (the standard plugin convention Claude Code reads).
|
|
243
|
+
Embedding them inline here as well made CC read BOTH sources and register
|
|
244
|
+
every hook twice, so every event (SessionStart, SessionEnd, ...) fired
|
|
245
|
+
twice. Emitting hooks in exactly one place -- hooks.json -- fixes that.
|
|
240
246
|
"""
|
|
241
247
|
version = manifest.get("version", "0.0.0")
|
|
242
248
|
if version == "from:package.json":
|
|
@@ -248,12 +254,9 @@ def generate_plugin_json(manifest: dict) -> dict:
|
|
|
248
254
|
plugin_name = manifest["plugin_name"]
|
|
249
255
|
homepage = "https://github.com/metraton/gaia#readme"
|
|
250
256
|
|
|
251
|
-
#
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
hooks_data = generate_hooks_json(manifest)
|
|
255
|
-
inline_hooks = hooks_data["hooks"]
|
|
256
|
-
|
|
257
|
+
# NOTE: no `hooks` key here on purpose. Hooks live only in hooks/hooks.json
|
|
258
|
+
# (generated by generate_hooks_json / write_root_manifests). Declaring them
|
|
259
|
+
# here too caused CC to register each hook twice (double-firing).
|
|
257
260
|
return {
|
|
258
261
|
"name": plugin_name,
|
|
259
262
|
"version": version,
|
|
@@ -268,7 +271,6 @@ def generate_plugin_json(manifest: dict) -> dict:
|
|
|
268
271
|
"keywords": ["security", "devops"],
|
|
269
272
|
"engines": {"claude-code": ">=2.1.0"},
|
|
270
273
|
"categories": ["devops", "security", "orchestration"],
|
|
271
|
-
"hooks": inline_hooks,
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
|
|
@@ -290,11 +292,11 @@ def write_root_manifests(plugin_name: str, output_dir: Path) -> None:
|
|
|
290
292
|
"""Regenerate ONLY the two plugin manifests into an existing directory.
|
|
291
293
|
|
|
292
294
|
Writes:
|
|
293
|
-
- <output_dir>/.claude-plugin/plugin.json (
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
- <output_dir>/.claude-plugin/plugin.json (metadata only -- NO inline
|
|
296
|
+
`hooks` block; declaring hooks here as well made CC register them twice)
|
|
297
|
+
- <output_dir>/hooks/hooks.json (canonical, manifest-derived,
|
|
298
|
+
the SINGLE source of hook declarations; the npm surface reads this via
|
|
299
|
+
merge_local_hooks() in _install_helpers.py)
|
|
298
300
|
|
|
299
301
|
Unlike build_plugin(), this NEVER cleans (`rmtree`) or copies component files.
|
|
300
302
|
It is the pack-time / release-prepare mechanism that makes the repo root (and
|
|
@@ -345,8 +347,7 @@ def write_root_manifests(plugin_name: str, output_dir: Path) -> None:
|
|
|
345
347
|
with open(plugin_json_path, "w") as f:
|
|
346
348
|
json.dump(plugin_json, f, indent=2)
|
|
347
349
|
f.write("\n")
|
|
348
|
-
|
|
349
|
-
print(f" Generated: .claude-plugin/plugin.json (inline hooks: {inline_events} events)", file=sys.stderr)
|
|
350
|
+
print(" Generated: .claude-plugin/plugin.json (metadata only, no inline hooks)", file=sys.stderr)
|
|
350
351
|
print("Root manifests regenerated.", file=sys.stderr)
|
|
351
352
|
|
|
352
353
|
|
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
"""Hooks-drift guard: fail if the committed generated hook artifacts diverge
|
|
3
3
|
from what the manifest would produce.
|
|
4
4
|
|
|
5
|
-
Background: ``hooks/hooks.json``
|
|
6
|
-
``.claude-plugin/plugin.json`` are GENERATED from ``build/gaia.manifest.json``
|
|
5
|
+
Background: ``hooks/hooks.json`` is GENERATED from ``build/gaia.manifest.json``
|
|
7
6
|
via ``generate_hooks_json`` (scripts/build-plugin.py), invoked by
|
|
8
|
-
``npm run generate:plugin-root``. The manifest is the single source of truth
|
|
7
|
+
``npm run generate:plugin-root``. The manifest is the single source of truth,
|
|
8
|
+
and hooks/hooks.json is the SINGLE place hooks are declared.
|
|
9
|
+
|
|
10
|
+
``.claude-plugin/plugin.json`` must NOT embed an inline ``hooks`` block:
|
|
11
|
+
Claude Code reads both the inline block and hooks/hooks.json, so declaring
|
|
12
|
+
hooks in both registered every hook twice and made every event fire twice.
|
|
13
|
+
This guard asserts hooks/hooks.json matches the manifest AND that plugin.json
|
|
14
|
+
carries no ``hooks`` key.
|
|
9
15
|
|
|
10
16
|
If a committed artifact no longer matches the manifest, someone either
|
|
11
17
|
hand-edited a generated file or forgot to re-run the generator after editing
|
|
@@ -78,14 +84,14 @@ def main() -> int:
|
|
|
78
84
|
)
|
|
79
85
|
_report_diff("hooks/hooks.json", expected, hooks_json)
|
|
80
86
|
|
|
81
|
-
if plugin_hooks
|
|
87
|
+
if plugin_hooks:
|
|
82
88
|
drift = True
|
|
83
89
|
print(
|
|
84
|
-
"hooks-drift guard: .claude-plugin/plugin.json
|
|
85
|
-
"
|
|
90
|
+
"hooks-drift guard: .claude-plugin/plugin.json embeds an inline "
|
|
91
|
+
"'hooks' block -- hooks must be declared ONLY in hooks/hooks.json. "
|
|
92
|
+
"An inline block makes Claude Code register every hook twice.",
|
|
86
93
|
file=sys.stderr,
|
|
87
94
|
)
|
|
88
|
-
_report_diff(".claude-plugin/plugin.json", expected, plugin_hooks)
|
|
89
95
|
|
|
90
96
|
if drift:
|
|
91
97
|
print(
|
|
@@ -94,9 +94,25 @@ function bumpMarketplace(rel, version) {
|
|
|
94
94
|
const plugins = data.plugins || [];
|
|
95
95
|
if (plugins.length === 0) throw new Error(`${rel}: no plugins[] to bump`);
|
|
96
96
|
const befores = plugins.map((p) => `${p.name}=${p.version}`);
|
|
97
|
-
|
|
97
|
+
const tag = `v${version}`;
|
|
98
|
+
const refUpdates = [];
|
|
99
|
+
for (const plugin of plugins) {
|
|
100
|
+
plugin.version = version;
|
|
101
|
+
// Pin the source to the release tag for git/github sources so
|
|
102
|
+
// `/plugin install` serves a fixed, reproducible tag instead of tracking
|
|
103
|
+
// moving default-branch HEAD. Bumped atomically with the version so the
|
|
104
|
+
// ref can never go stale (the earlier flagged follow-up). Guard: only
|
|
105
|
+
// git/github object sources carry a ref -- npm/local sources must not.
|
|
106
|
+
const src = plugin.source;
|
|
107
|
+
if (src && typeof src === 'object' &&
|
|
108
|
+
(src.source === 'github' || src.source === 'git')) {
|
|
109
|
+
src.ref = tag;
|
|
110
|
+
refUpdates.push(`${plugin.name}.source.ref=${tag}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
98
113
|
writeText(rel, JSON.stringify(data, null, 2) + '\n');
|
|
99
|
-
|
|
114
|
+
const refNote = refUpdates.length ? ` (${refUpdates.join(', ')})` : '';
|
|
115
|
+
return `${rel}: [${befores.join(', ')}] -> all ${version}${refNote}`;
|
|
100
116
|
}
|
|
101
117
|
|
|
102
118
|
function bumpPyproject(rel, version) {
|
|
@@ -129,7 +129,7 @@ The package ships a single `gaia` binary (`bin/gaia.js`) that dispatches to Pyth
|
|
|
129
129
|
|
|
130
130
|
## 2. Plugin Packaging
|
|
131
131
|
|
|
132
|
-
Gaia ships as a **single, unified** plugin named `gaia`. There is **no `dist/` bundle** -- the npm package root (`@jaguilar87/gaia`) IS the plugin. The root `.claude-plugin/plugin.json` (
|
|
132
|
+
Gaia ships as a **single, unified** plugin named `gaia`. There is **no `dist/` bundle** -- the npm package root (`@jaguilar87/gaia`) IS the plugin. The root `.claude-plugin/plugin.json` (metadata only -- no inline hooks) and `hooks/hooks.json` (the canonical location Claude Code loads hooks from) are generated from `build/gaia.manifest.json` at pack time (`prepack` -> `generate:plugin-root`) and tracked in git. One bundle carries all hooks, modules, agents, skills, tools, and config, and every install runs the full orchestrator surface.
|
|
133
133
|
|
|
134
134
|
### Security Tiers (quick reference)
|
|
135
135
|
|
|
@@ -148,7 +148,7 @@ Enforcement: `blocked_commands.py` (permanent deny) + `mutative_verbs.py` (nonce
|
|
|
148
148
|
|
|
149
149
|
### Build
|
|
150
150
|
|
|
151
|
-
There is **no `dist/` bundle** -- the package root IS the plugin. `scripts/build-plugin.py gaia --manifests-only --output-dir .` (run via `npm run generate:plugin-root`) reads `build/gaia.manifest.json`, resolves `"all"` fields to concrete file lists, and regenerates `.claude-plugin/plugin.json` (inline hooks) + `hooks/hooks.json` in place at the package root.
|
|
151
|
+
There is **no `dist/` bundle** -- the package root IS the plugin. `scripts/build-plugin.py gaia --manifests-only --output-dir .` (run via `npm run generate:plugin-root`) reads `build/gaia.manifest.json`, resolves `"all"` fields to concrete file lists, and regenerates `.claude-plugin/plugin.json` (metadata only -- no inline hooks) + `hooks/hooks.json` (the canonical location Claude Code loads hooks from) in place at the package root.
|
|
152
152
|
|
|
153
153
|
### Publish
|
|
154
154
|
|
|
@@ -16,10 +16,10 @@ The norm for getting Gaia onto a machine and into the registry, organized as thr
|
|
|
16
16
|
|
|
17
17
|
Gaia ships as a **single** plugin named `gaia` (`scripts/build-plugin.py` has `VALID_PLUGINS = ("gaia",)`). **The package root IS the plugin, and it is the same tree as the git repo root** -- there is no `dist/` bundle. That single tree reaches a workspace through **two distribution channels**, and a change can pass on one while breaking the other:
|
|
18
18
|
|
|
19
|
-
- **plugin surface (git marketplace source)** -- Claude Code consumes the **git repository directly**: `.claude-plugin/marketplace.json` advertises the `gaia` plugin with `{"source": "github", "repo": "metraton/gaia"}`, so `/plugin marketplace add` + `/plugin install` make CC **clone the repo** into its plugin cache. **This is the channel that loads Gaia's agents, skills, and hooks in Claude Code.** The git source was adopted (commit `a43ef22`) precisely because the prior `source: npm` entry loaded **0 skills** on `/plugin install` -- a confirmed CC gap -- while the identical tree loads all 32 skills via a git/local source. CC
|
|
19
|
+
- **plugin surface (git marketplace source)** -- Claude Code consumes the **git repository directly**: `.claude-plugin/marketplace.json` advertises the `gaia` plugin with `{"source": "github", "repo": "metraton/gaia"}`, so `/plugin marketplace add` + `/plugin install` make CC **clone the repo** into its plugin cache. **This is the channel that loads Gaia's agents, skills, and hooks in Claude Code.** The git source was adopted (commit `a43ef22`) precisely because the prior `source: npm` entry loaded **0 skills** on `/plugin install` -- a confirmed CC gap -- while the identical tree loads all 32 skills via a git/local source. CC loads hooks from the repo root's `hooks/hooks.json` (the standard plugin convention) -- it does **not** use `settings.local.json` for hooks, and the root `.claude-plugin/plugin.json` is **metadata only** (no inline `hooks` block). Hooks are declared in exactly ONE place because CC reads both a plugin.json inline block AND `hooks/hooks.json`; declaring them in both double-registered every hook and fired every event twice (fixed in `a1b1245`). Both `.claude-plugin/plugin.json` and `hooks/hooks.json` are **generated from the manifest** (`prepack` / `generate:plugin-root`) and tracked in git, so the cloned tree already carries them. Git sources support version pinning by ref/sha; otherwise version resolution falls back to the marketplace entry's `version` field (still bumped atomically by `release:prepare`, which touches `plugin.version`, not `source`).
|
|
20
20
|
- **CLI / npm surface (npm package `@jaguilar87/gaia`)** -- `npm|pnpm install @jaguilar87/gaia` (and `gaia dev`) install the npm package; this is what provides the `gaia` **CLI** and the workspace wiring. `gaia install` then wires the workspace: it symlinks `.claude/{agents,tools,hooks,config,skills}` (plus a `CHANGELOG.md` link) to the installed package and merges hook events into `settings.local.json`. The DB is bootstrapped **lazily on first `gaia` CLI use** (`_ensure_db_bootstrapped` in `bin/gaia`) -- there is **no npm `postinstall`** (removed so the install is non-invasive and works identically under npm and pnpm, which ignores lifecycle scripts by default). This surface reads hooks from the package root's generated `hooks/hooks.json` (via `merge_local_hooks` in `_install_helpers.py`), which is regenerated at pack time from the manifest.
|
|
21
21
|
|
|
22
|
-
The two channels ship from the **same source tree** (git repo root and npm package root are one and the same). That is why Layer 2 exists: the plugin surface can only be proven by materializing the plugin root -- packing the tarball, extracting it, and mounting the extracted root in a live Claude Code -- nothing else exercises the
|
|
22
|
+
The two channels ship from the **same source tree** (git repo root and npm package root are one and the same). That is why Layer 2 exists: the plugin surface can only be proven by materializing the plugin root -- packing the tarball, extracting it, and mounting the extracted root in a live Claude Code -- nothing else exercises the root `plugin.json` / `hooks.json` / CC plugin loader.
|
|
23
23
|
|
|
24
24
|
## The three intentions
|
|
25
25
|
|
|
@@ -55,7 +55,7 @@ gaia release check # add --functional for the opt-in live plugin pr
|
|
|
55
55
|
|
|
56
56
|
1. `pre-publish:validate` -- the version-drift gate (`validate-manifests` in `ci.yml`, via `bin/pre-publish-validate.js --validate-only`). This is what catches a `package.json` / `pyproject.toml` / `plugin.json` / `marketplace.json` desync before it ships.
|
|
57
57
|
2. `gaia:verify-install:local` -- packs (via the shared `_pack_helpers.pack_tarball`, the same primitive `gaia dev` uses) and installs into a throwaway sandbox (`bin/validate-sandbox.sh --target sandbox`). This proves the **npm surface** of exactly what a registry publish would ship. (This is `gaia-verify` mode `npm-sandbox`.)
|
|
58
|
-
3. **Plugin-surface dry-run** (`bin/plugin-dryrun.sh`) -- packs the tarball itself, extracts it to a throwaway temp dir (the package root IS the plugin), and runs a **headless, offline** gate: filesystem asserts (root
|
|
58
|
+
3. **Plugin-surface dry-run** (`bin/plugin-dryrun.sh`) -- packs the tarball itself, extracts it to a throwaway temp dir (the package root IS the plugin), and runs a **headless, offline** gate: filesystem asserts (root `plugin.json` with NO inline `hooks` block, `hooks/hooks.json`, `bin/gaia`, `agents/`, `skills/`, and NO `dist/`) plus `claude plugin validate`. It touches no real workspace and spawns no session. `--functional` forwards to the script's own opt-in live `claude --plugin-dir <temp> -p '...'` probe (needs Claude auth/tokens -- never implicit). **SKIPs** (not fails) when the `claude` binary is not on PATH. This is the only place the plugin surface is proven before a tag exists. (This is `gaia-verify` mode `plugin`.) See the plugin-surface principle below for why a green npm sandbox does not cover it.
|
|
59
59
|
4. `npm test` -- the L1 suite (the tests CI runs that reproduce locally).
|
|
60
60
|
|
|
61
61
|
A `check` run that reports `FAIL` on gate 1 has failed a *subset* of CI, not passed a stand-in for it; a `FAIL`/`SKIP` on gate 3 means the plugin surface was never run (SKIP only when `claude` is genuinely absent) -- both gaps surface only after publish, when the fix costs another release. For the raw npm-script forms each gate wraps (useful when diagnosing which gate failed), see `reference.md` -> "Layer 2 runbook".
|
|
@@ -73,7 +73,7 @@ gaia release publish [version] # add --dry-run to preview the seque
|
|
|
73
73
|
| Step | Action | Notes |
|
|
74
74
|
|------|--------|-------|
|
|
75
75
|
| **(a)** | Determine the version | Default to the next **patch**. If the change is major/minor, **confirm with the user** (`NEEDS_INPUT`) before proceeding -- never silently pick major/minor. Pass the confirmed version as `gaia release publish`'s argument, or let it default to `patch`. |
|
|
76
|
-
| **(b)** | `release:prepare <version>` -- `gaia release publish` step 1 | The atomic core: bumps the hand-owned version sources at once (`package.json`, `pyproject.toml`, `.claude-plugin/marketplace.json`, `CHANGELOG.md`), runs `generate:plugin-root` (regenerating the ROOT `.claude-plugin/plugin.json`
|
|
76
|
+
| **(b)** | `release:prepare <version>` -- `gaia release publish` step 1 | The atomic core: bumps the hand-owned version sources at once (`package.json`, `pyproject.toml`, `.claude-plugin/marketplace.json`, `CHANGELOG.md`), runs `generate:plugin-root` (regenerating the ROOT `.claude-plugin/plugin.json` (metadata only) + `hooks/hooks.json` from the manifest -- `plugin.json` version is inherited from `package.json`, not hand-bumped), then `pre-publish:validate`. Fails loud on any drift. No `dist/` bundle. This wraps `scripts/release-prepare.mjs` -- invoked by the flow, never run by hand. |
|
|
77
77
|
| **(c)** | Pre-flight that reproduces CI -- step 2 | `pre-publish:validate` already ran inside (b). Now `npm test` runs so the local gate matches CI before the tag exists. Layer 2 should already be green. |
|
|
78
78
|
| **(d)** | Commit -- step 3 | `git add` (the version-source paths only) + `git commit` -- local-safe, not T3. Idempotent: nothing-to-commit on a tree already at the target version is a PASS, not a failure. |
|
|
79
79
|
| **(e)** | Tag, **force-free** -- step 4 | A *new* annotated tag (`v<version>`); never moves an existing one. If the remote diverged, reconcile with **merge, not rebase** (rebase forces a tag move, hard-denied locally). See `reference.md` -> "Reconciling a diverged remote". |
|
|
@@ -103,7 +103,7 @@ A fresh install or an edit is invisible until Claude Code picks it up, and *how*
|
|
|
103
103
|
## Principles -- why the sequence is normed, not optional
|
|
104
104
|
|
|
105
105
|
- **The pre-flight reproduces what CI validates, not a subset of it.** When the local check skips a gate CI runs (`pre-publish:validate`), that gate's failures surface only *after* publishing, on the published tarball, where the only remedy is another release. That is exactly how a `pyproject.toml` drift shipped green-local and red-CI. Layer 2 step 1 and Layer 3 step (c) close the gap. See `reference.md` -> "The pre-flight reproduces what CI validates".
|
|
106
|
-
- **The plugin surface is only proven by packing the tarball and mounting the extracted root.** The npm sandbox exercises the symlink / `settings.local.json` path; it never touches the root
|
|
106
|
+
- **The plugin surface is only proven by packing the tarball and mounting the extracted root.** The npm sandbox exercises the symlink / `settings.local.json` path; it never touches the root `plugin.json` / `hooks.json` or CC's plugin loader. The tarball can be missing files, carry a broken `hooks.json`, or fail to expose `bin/gaia` on PATH -- and none of that shows until CC mounts it. Layer 2 step 3 (`gaia:plugin-dryrun` -- pack, extract, headless validate) is the only pre-tag check that runs the plugin surface. Skipping it means the plugin breaks silently in production.
|
|
107
107
|
- **Bump every version source in one step, never one at a time.** `pre-publish:validate` requires `package.json`, `pyproject.toml`, `.claude-plugin/plugin.json` (generated), `.claude-plugin/marketplace.json`, and the `CHANGELOG.md` top header to agree. `release:prepare` writes the hand-owned sources from one target version and regenerates `plugin.json` (version inherited from `package.json`), so a hand-desync is impossible. See `scripts/release-prepare.mjs`.
|
|
108
108
|
- **Tag force-free; reconcile with merge, never rebase.** `publish.yml` no longer commits artifacts back to `main` (read-only checkout), so the remote does not auto-advance on release. But if the remote ever diverges, rebasing rewrites hashes and forces a tag move (`git tag -f` / `--force`), hard-denied by local hooks (`git_destructive` in `blocked_commands.py`, exit 2, not approvable). Merge preserves hashes and tags; a new release gets a *new* tag, never a moved one. See `reference.md` -> "Reconciling a diverged remote".
|
|
109
109
|
- **A release ends at an installed, validated version -- not at the tag.** Pushing the tag only starts `publish.yml`. The intention is not satisfied until the workflow reaches its outcome, npm serves the new version, and it installs cleanly into the target (Layer 3 step (h)).
|
|
@@ -117,7 +117,7 @@ A fresh install or an edit is invisible until Claude Code picks it up, and *how*
|
|
|
117
117
|
- **Bumping version sources one at a time** -- desyncs a source by hand; `pre-publish:validate` rejects the tree and a forgotten file ships if the check is skipped. Always go through `gaia release publish` (which invokes `release:prepare`), never a hand-edit.
|
|
118
118
|
- **Rebase to reconcile a diverged remote** -- forces a tag move, hard-denied locally. Merge instead.
|
|
119
119
|
- **Single-surface testing** -- a change can pass the npm sandbox and break the plugin mount, or vice versa. Layer 2 runs both surfaces for a reason.
|
|
120
|
-
- **Stale root manifests** -- editing `build/gaia.manifest.json` or a hook entry without regenerating means the tarball ships
|
|
120
|
+
- **Stale root manifests** -- editing `build/gaia.manifest.json` or a hook entry without regenerating means the tarball ships a stale `hooks/hooks.json`. `prepack` regenerates at every `npm pack`, and `release:prepare` regenerates via `generate:plugin-root`; the dry-run packs fresh, so a stale manifest surfaces there.
|
|
121
121
|
- **Blanket "restart Claude Code"** -- npm/pnpm hooks reload automatically; only the plugin surface needs `/reload-plugins`, and only a slash-command change needs a full restart. See "Reloading a change".
|
|
122
122
|
- **Assuming a `postinstall` ran** -- there is none. The DB is bootstrapped lazily on first `gaia` CLI use; under pnpm a lifecycle hook would never fire anyway. If the DB is missing, run any `gaia` command (or `gaia install`), not "re-run postinstall".
|
|
123
123
|
- **Local npm publish** -- bypasses the pipeline's pack + validate + sandbox gate.
|
|
@@ -37,7 +37,7 @@ gaia install --workspace <TARGET>
|
|
|
37
37
|
cd /home/jorge/ws/me/gaia
|
|
38
38
|
npm run gaia:install-local -- --workspace <TARGET>
|
|
39
39
|
```
|
|
40
|
-
`gaia:install-local` runs `npm pack` (whose `prepack` regenerates the root
|
|
40
|
+
`gaia:install-local` runs `npm pack` (whose `prepack` regenerates the root `plugin.json` (metadata only) + `hooks/hooks.json`) + `validate-sandbox.sh --target local`. `gaia dev --mode pack` auto-detects npm vs pnpm from the target workspace, so it covers this case too; use the raw npm script only when diagnosing.
|
|
41
41
|
|
|
42
42
|
**fresh (wipe install metadata first):**
|
|
43
43
|
Append `--fresh` to the `validate-sandbox.sh` form, or manually clear `node_modules/ package.json package-lock.json` (or the pnpm equivalents) in `<TARGET>` before reinstalling. Use when a prior install left state you want gone.
|
|
@@ -101,7 +101,7 @@ Sandbox path prints on exit; inspect `.claude/`, rerun checks, then `rm -rf` man
|
|
|
101
101
|
```
|
|
102
102
|
npm run gaia:plugin-dryrun # pack -> temp extract -> headless validate -> trap cleanup
|
|
103
103
|
```
|
|
104
|
-
`bin/plugin-dryrun.sh` packs the exact npm tarball (its `prepack` regenerates the root
|
|
104
|
+
`bin/plugin-dryrun.sh` packs the exact npm tarball (its `prepack` regenerates the root `plugin.json` (metadata only) + `hooks/hooks.json`), extracts it to a throwaway temp dir (the package root IS the plugin), and runs a headless, offline gate: filesystem asserts (root `plugin.json` present with NO inline `hooks` block, `hooks/hooks.json` with every entry point resolving, `bin/gaia`, `agents/`, `skills/`, and NO `dist/`) + `claude plugin validate`. It touches no real workspace and spawns no session; both temps are removed by an EXIT trap. `gaia release check` SKIPs (not fails) this gate when `claude` is not on PATH.
|
|
105
105
|
|
|
106
106
|
For an optional live functional probe (needs Claude auth/tokens -- opt-in, never implicit):
|
|
107
107
|
```
|
|
@@ -139,7 +139,7 @@ gaia release publish --dry-run [version] # preview the six-step sequence, exe
|
|
|
139
139
|
1. Layer 2 (pre-release) must be green first -- run `gaia release check`.
|
|
140
140
|
2. **`release:prepare <version>`** (`gaia release publish` step 1) -- the atomic bump. This wraps `scripts/release-prepare.mjs`, invoked by the flow, **never run by the user by hand**. In one command it:
|
|
141
141
|
- writes `<version>` to the hand-owned sources at once -- `package.json`, `pyproject.toml`, `.claude-plugin/marketplace.json` (top-level `version`), and the `CHANGELOG.md` top header (inserts a dated stub above the current top if absent -- edit its body before release);
|
|
142
|
-
- runs `npm run generate:plugin-root` to regenerate the ROOT `.claude-plugin/plugin.json` (inline hooks) + `hooks/hooks.json` from the manifest -- `plugin.json`'s version is inherited from `package.json` (`from:package.json`), so it is NOT hand-bumped. No `dist/` bundle;
|
|
142
|
+
- runs `npm run generate:plugin-root` to regenerate the ROOT `.claude-plugin/plugin.json` (metadata only -- no inline hooks) + `hooks/hooks.json` from the manifest -- `plugin.json`'s version is inherited from `package.json` (`from:package.json`), so it is NOT hand-bumped. No `dist/` bundle;
|
|
143
143
|
- runs `npm run pre-publish:validate` and fails loud on any drift.
|
|
144
144
|
|
|
145
145
|
This replaces hand-bumping one file at a time. The two real escapes a hand-bump leaves are a `pyproject.toml` left behind on a prior version (caught only by `pre-publish:validate`) and a `marketplace.json` that still advertises the old top-level version. `release:prepare` makes the desync impossible because all hand-owned sources are written from one target version and `plugin.json` is generated from it. For a bare semver: `5.0.5` for stable, `5.1.0-rc.1` for RC (no leading `v` -- the tag adds it). Idempotent: re-running with the same version is a no-op bump that re-validates.
|
|
@@ -177,7 +177,7 @@ Symptoms encountered in real install sessions, with the root cause and the fix.
|
|
|
177
177
|
| `.claude/` not wired after install | `gaia install` not run, or it exited non-zero | `cat ~/.gaia/last-install-error.json` (written by `gaia install` on failure). Re-run `gaia install --workspace <path>`. If it persists, file a bug. |
|
|
178
178
|
| `gaia doctor` walks up to the user `.claude/` instead of the workspace | Workspace not initialized (`.claude/` missing or no `plugin-registry.json`) | Re-run `gaia install --workspace <path>`. |
|
|
179
179
|
| DB missing / `no such table` on first use | Lazy bootstrap did not run (e.g. `gaia` never invoked yet) | Run any `gaia` command (it triggers `_ensure_db_bootstrapped`), or `gaia install` for the full seed. There is no postinstall to "re-run". |
|
|
180
|
-
| Plugin mounts but hooks never fire | Generated
|
|
180
|
+
| Plugin mounts but hooks never fire | Generated `hooks/hooks.json` broken at the package root, or CC did not reload | Regenerate (`npm run generate:plugin-root`), re-run `npm run gaia:plugin-dryrun`, and `/reload-plugins`. Inspect the root `hooks/hooks.json` (the canonical hook source); `.claude-plugin/plugin.json` is metadata only and must NOT carry an inline `hooks` block. |
|
|
181
181
|
| `bootstrap exited 1: table projects has no column named identity` | Schema/bootstrap drift (old seed SQL) | Update to a build whose seed matches the current schema. |
|
|
182
182
|
| `Permission denied` invoking a hook `.py` | Exec bit lost on cross-platform checkout | Hooks are invoked via `python3 <script>` (see `build-plugin.py` `generate_hooks_json`), so the exec bit should not matter; if it does, update to a build that uses the `python3 <script>` invoker. |
|
|
183
183
|
| Agent says "no conozco Gaia" or "developer agent does not exist" | `settings.local.json` missing or mis-wired (npm surface) | Re-run `gaia install`. In plugin surface, `/reload-plugins`. |
|
|
@@ -205,7 +205,7 @@ A green pre-flight only protects the release if it runs the same gates CI runs.
|
|
|
205
205
|
**Pre-publish gate (Layer 2):**
|
|
206
206
|
- `pytest tests/` green (or `npm test` for the L1 subset).
|
|
207
207
|
- `npm run pre-publish:validate` green locally -- the version-drift gate (`validate-manifests` in `ci.yml`).
|
|
208
|
-
- `npm pack --dry-run` confirms `scripts/bootstrap_database.sh`, `.claude-plugin/plugin.json` (
|
|
208
|
+
- `npm pack --dry-run` confirms `scripts/bootstrap_database.sh`, `.claude-plugin/plugin.json` (metadata only), and `hooks/hooks.json` (the canonical hook source) are included in the tarball, and that `dist/` is NOT.
|
|
209
209
|
- `npm run gaia:verify-install:local` green (npm surface).
|
|
210
210
|
- `npm run gaia:plugin-dryrun` green (plugin surface -- pack + extract + headless validate).
|
|
211
211
|
|
|
@@ -214,7 +214,7 @@ A green pre-flight only protects the release if it runs the same gates CI runs.
|
|
|
214
214
|
The workflow at `.github/workflows/publish.yml` runs on every GitHub Release event (read-only checkout -- it neither commits nor pushes). It:
|
|
215
215
|
- Checks out the exact tagged commit.
|
|
216
216
|
- Installs deps with `npm ci`.
|
|
217
|
-
- Packs the tarball with `npm pack` -- `prepack` (clean + `generate:plugin-root`) regenerates the root
|
|
217
|
+
- Packs the tarball with `npm pack` -- `prepack` (clean + `generate:plugin-root`) regenerates the root `plugin.json` (metadata only) + `hooks/hooks.json`, so the tarball root is a valid plugin root (the same tree the git plugin source serves). No `dist/` bundle is built or committed back.
|
|
218
218
|
- Runs `npm run pre-publish:validate` (after pack, so it sees the fresh root manifests).
|
|
219
219
|
- Auto-detects npm tag from the version string: `*-rc.*` -> `rc`, `*-beta.*` -> `beta`, `*-alpha.*` -> `alpha`, else -> `latest`.
|
|
220
220
|
- Runs the sandbox validation harness against the packed tarball (the gate).
|