@rubytech/create-maxy 1.0.711 → 1.0.712
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/dist/index.js +38 -3
- package/package.json +2 -2
- package/payload/platform/plugins/linkedin-import/PLUGIN.md +1 -0
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +26 -5
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +53 -82
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md +42 -49
- package/payload/platform/plugins/memory/PLUGIN.md +1 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +48 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +33 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +229 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -0
- package/payload/platform/scripts/redact-install-logs.sh +85 -0
- package/payload/platform/scripts/setup.sh +20 -3
- package/payload/platform/scripts/verify-skill-tool-surface.sh +255 -0
- package/payload/platform/templates/specialists/agents/database-operator.md +6 -2
- package/payload/server/chunk-U5JPRUYZ.js +12298 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{graph-BNx6E7BH.js → graph-DJ7IfYHV.js} +12 -12
- package/payload/server/public/graph.html +1 -1
- package/payload/server/server.js +16 -9
|
@@ -86,12 +86,20 @@ else
|
|
|
86
86
|
# Configure Neo4j for local use
|
|
87
87
|
sudo sed -i 's/#server.default_listen_address=0.0.0.0/server.default_listen_address=127.0.0.1/' /etc/neo4j/neo4j.conf
|
|
88
88
|
|
|
89
|
-
# Generate a strong random password and store it
|
|
89
|
+
# Generate a strong random password and store it.
|
|
90
|
+
# Password handling block is set +x bracketed so even bash -x setup.sh
|
|
91
|
+
# cannot print the substituted secret. The password is written to
|
|
92
|
+
# platform/config/.neo4j-password (chmod 600) — the only readable source.
|
|
93
|
+
# set-initial-password reads the secret via $(cat ...) so the literal
|
|
94
|
+
# never appears on the parent shell's command line, and stdout is
|
|
95
|
+
# discarded so neo4j-admin's own echo cannot leak it either (Task 744).
|
|
96
|
+
{ set +x; } 2>/dev/null
|
|
90
97
|
NEO4J_GENERATED_PASSWORD=$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)
|
|
91
98
|
mkdir -p "$INSTALL_DIR/platform/config"
|
|
92
|
-
|
|
99
|
+
printf '%s' "$NEO4J_GENERATED_PASSWORD" > "$INSTALL_DIR/platform/config/.neo4j-password"
|
|
93
100
|
chmod 600 "$INSTALL_DIR/platform/config/.neo4j-password"
|
|
94
|
-
|
|
101
|
+
unset NEO4J_GENERATED_PASSWORD
|
|
102
|
+
sudo neo4j-admin dbms set-initial-password "$(cat "$INSTALL_DIR/platform/config/.neo4j-password")" >/dev/null 2>&1
|
|
95
103
|
|
|
96
104
|
# Start and enable
|
|
97
105
|
sudo systemctl enable neo4j
|
|
@@ -139,6 +147,15 @@ else
|
|
|
139
147
|
cd "$INSTALL_DIR"
|
|
140
148
|
fi
|
|
141
149
|
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
# 6.5. Redact install-log credential leaks (Task 744 — idempotent).
|
|
152
|
+
# Pre-fix logs may contain plaintext neo4j passwords; this script scrubs
|
|
153
|
+
# every install-*.log to "[REDACTED]". Safe on already-clean logs.
|
|
154
|
+
# ------------------------------------------------------------------
|
|
155
|
+
if [ -x "$INSTALL_DIR/platform/scripts/redact-install-logs.sh" ]; then
|
|
156
|
+
bash "$INSTALL_DIR/platform/scripts/redact-install-logs.sh" || true
|
|
157
|
+
fi
|
|
158
|
+
|
|
142
159
|
# ------------------------------------------------------------------
|
|
143
160
|
# 7. Install dependencies and build
|
|
144
161
|
# ------------------------------------------------------------------
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Pre-publish acceptance gate (Task 744).
|
|
3
|
+
#
|
|
4
|
+
# Statically intersects what each shipped skill *prescribes* (every
|
|
5
|
+
# backtick-quoted `mcp__<server>__<tool>` token in SKILL.md and references/*.md)
|
|
6
|
+
# against the dispatched specialist's frontmatter `tools:` list. Catches the
|
|
7
|
+
# class of bug where a skill prescribes a tool the specialist does not have,
|
|
8
|
+
# and where a skill prescribes a forbidden direct-execution path
|
|
9
|
+
# (`cypher-shell`, `neo4j-admin` invocations, raw-Cypher DML in prose).
|
|
10
|
+
#
|
|
11
|
+
# Wired into the root `packages/create-maxy/package.json` `prepublishOnly`
|
|
12
|
+
# script so a regression cannot reach npm publish without firing.
|
|
13
|
+
#
|
|
14
|
+
# One stdout line per (skill, specialist) pair:
|
|
15
|
+
# [verify] skill=<plugin>/<skill> specialist=<n> resolved=<n>/<m> forbidden=<n>
|
|
16
|
+
#
|
|
17
|
+
# Exit 0: every prescribed token resolves AND no forbidden tokens.
|
|
18
|
+
# Exit 1: any unresolved or any forbidden — stderr names the offending token.
|
|
19
|
+
#
|
|
20
|
+
# Skill→specialist mapping comes from PLUGIN.md frontmatter `specialist:` field.
|
|
21
|
+
# Plugins without that field are admin-owned (loaded by the admin agent
|
|
22
|
+
# directly via plugin-read); for those the gate only enforces the forbidden-
|
|
23
|
+
# token rule, since admin's tool surface is the union of all enabled plugins.
|
|
24
|
+
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
|
28
|
+
cd "$REPO_ROOT"
|
|
29
|
+
|
|
30
|
+
python3 - <<'PYEOF'
|
|
31
|
+
import os, re, sys
|
|
32
|
+
|
|
33
|
+
REPO_ROOT = os.getcwd()
|
|
34
|
+
PLUGINS_DIR = os.path.join(REPO_ROOT, "platform", "plugins")
|
|
35
|
+
SPECIALISTS_DIR = os.path.join(REPO_ROOT, "platform", "templates", "specialists", "agents")
|
|
36
|
+
|
|
37
|
+
# Per-skill specialist ownership for plugins where the plugin itself is
|
|
38
|
+
# multi-purpose. The PLUGIN.md `specialist:` field handles single-owner
|
|
39
|
+
# plugins (linkedin-import → database-operator). Mixed-use plugins like
|
|
40
|
+
# `memory` declare per-skill ownership here.
|
|
41
|
+
EXPLICIT_OWNERSHIP = {
|
|
42
|
+
# plugin/skill -> specialist
|
|
43
|
+
"memory/document-ingest": "database-operator",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Skills that are explicitly admin-owned (loaded via plugin-read by the admin
|
|
47
|
+
# agent itself, not delegated to a specialist). These get only the forbidden-
|
|
48
|
+
# token check since admin's effective tool set is the union of all enabled
|
|
49
|
+
# plugins.
|
|
50
|
+
ADMIN_OWNED_SKILLS = {
|
|
51
|
+
"memory/conversational-memory",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
TOKEN_RE = re.compile(r"`(mcp__[a-z][a-z0-9_-]*__[a-z][a-z0-9_-]*)`")
|
|
55
|
+
FENCED_BLOCK_RE = re.compile(r"```(?P<lang>[a-zA-Z]*)\n(?P<body>.*?)\n```", re.S)
|
|
56
|
+
PROSE_CYPHER_RE = re.compile(
|
|
57
|
+
r"`(?:MERGE|CREATE|DETACH\s+DELETE)\s+\(",
|
|
58
|
+
re.IGNORECASE,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def parse_frontmatter(path: str) -> dict | None:
|
|
63
|
+
"""Parse YAML-ish frontmatter without PyYAML — handles `key: value` and
|
|
64
|
+
`key:\n - item\n - item` shapes used in PLUGIN.md and specialist files."""
|
|
65
|
+
try:
|
|
66
|
+
text = open(path, encoding="utf-8").read()
|
|
67
|
+
except FileNotFoundError:
|
|
68
|
+
return None
|
|
69
|
+
m = re.match(r"^---\n(.*?)\n---", text, re.S)
|
|
70
|
+
if not m:
|
|
71
|
+
return None
|
|
72
|
+
block = m.group(1)
|
|
73
|
+
out: dict = {}
|
|
74
|
+
cur_key: str | None = None
|
|
75
|
+
cur_list: list[str] | None = None
|
|
76
|
+
for line in block.split("\n"):
|
|
77
|
+
if not line.strip():
|
|
78
|
+
continue
|
|
79
|
+
# List item under cur_key
|
|
80
|
+
if line.startswith(" - ") or line.startswith("- "):
|
|
81
|
+
if cur_list is None:
|
|
82
|
+
continue
|
|
83
|
+
cur_list.append(line.split("- ", 1)[1].strip())
|
|
84
|
+
continue
|
|
85
|
+
# Top-level key
|
|
86
|
+
m2 = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$", line)
|
|
87
|
+
if not m2:
|
|
88
|
+
continue
|
|
89
|
+
cur_key = m2.group(1)
|
|
90
|
+
rhs = m2.group(2).strip()
|
|
91
|
+
if rhs:
|
|
92
|
+
# inline value — strip surrounding quotes
|
|
93
|
+
if (rhs.startswith('"') and rhs.endswith('"')) or (
|
|
94
|
+
rhs.startswith("'") and rhs.endswith("'")
|
|
95
|
+
):
|
|
96
|
+
rhs = rhs[1:-1]
|
|
97
|
+
out[cur_key] = rhs
|
|
98
|
+
cur_list = None
|
|
99
|
+
else:
|
|
100
|
+
cur_list = []
|
|
101
|
+
out[cur_key] = cur_list
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def specialist_tools(specialist: str) -> set[str] | None:
|
|
106
|
+
fm = parse_frontmatter(os.path.join(SPECIALISTS_DIR, f"{specialist}.md"))
|
|
107
|
+
if fm is None:
|
|
108
|
+
return None
|
|
109
|
+
raw = fm.get("tools", "")
|
|
110
|
+
if isinstance(raw, list):
|
|
111
|
+
items = raw
|
|
112
|
+
else:
|
|
113
|
+
items = [t.strip() for t in str(raw).split(",")]
|
|
114
|
+
return {t for t in items if t}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def extract_prescribed_tokens(text: str) -> set[str]:
|
|
118
|
+
return set(TOKEN_RE.findall(text))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def extract_forbidden(text: str) -> list[tuple[str, str]]:
|
|
122
|
+
forbidden: list[tuple[str, str]] = []
|
|
123
|
+
|
|
124
|
+
# Forbidden invocations inside fenced shell blocks
|
|
125
|
+
for m in FENCED_BLOCK_RE.finditer(text):
|
|
126
|
+
lang = m.group("lang").lower()
|
|
127
|
+
body = m.group("body")
|
|
128
|
+
if lang in {"bash", "sh", "shell", "zsh"}:
|
|
129
|
+
if re.search(r"\bcypher-shell\b", body):
|
|
130
|
+
forbidden.append(("cypher-shell", "in fenced shell block"))
|
|
131
|
+
if re.search(r"\bneo4j-admin\s+dbms\b", body):
|
|
132
|
+
forbidden.append(("neo4j-admin", "in fenced shell block"))
|
|
133
|
+
|
|
134
|
+
# Strip fenced blocks for the prose-Cypher heuristic
|
|
135
|
+
prose = FENCED_BLOCK_RE.sub("", text)
|
|
136
|
+
if PROSE_CYPHER_RE.search(prose):
|
|
137
|
+
forbidden.append(("raw-cypher-dml", "in backtick-quoted prose"))
|
|
138
|
+
|
|
139
|
+
return forbidden
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def aggregate_skill_text(skill_dir: str) -> str:
|
|
143
|
+
out: list[str] = []
|
|
144
|
+
skill_md = os.path.join(skill_dir, "SKILL.md")
|
|
145
|
+
if not os.path.exists(skill_md):
|
|
146
|
+
return ""
|
|
147
|
+
out.append(open(skill_md, encoding="utf-8").read())
|
|
148
|
+
refs = os.path.join(skill_dir, "references")
|
|
149
|
+
if os.path.isdir(refs):
|
|
150
|
+
for name in sorted(os.listdir(refs)):
|
|
151
|
+
if name.endswith(".md"):
|
|
152
|
+
out.append(open(os.path.join(refs, name), encoding="utf-8").read())
|
|
153
|
+
return "\n".join(out)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main() -> int:
|
|
157
|
+
if not os.path.isdir(PLUGINS_DIR):
|
|
158
|
+
print(f"[verify] PLUGINS_DIR not found: {PLUGINS_DIR}", file=sys.stderr)
|
|
159
|
+
return 1
|
|
160
|
+
if not os.path.isdir(SPECIALISTS_DIR):
|
|
161
|
+
print(f"[verify] SPECIALISTS_DIR not found: {SPECIALISTS_DIR}", file=sys.stderr)
|
|
162
|
+
return 1
|
|
163
|
+
|
|
164
|
+
summary: list[str] = []
|
|
165
|
+
errors: list[str] = []
|
|
166
|
+
pairs_checked = 0
|
|
167
|
+
|
|
168
|
+
for plugin in sorted(os.listdir(PLUGINS_DIR)):
|
|
169
|
+
pdir = os.path.join(PLUGINS_DIR, plugin)
|
|
170
|
+
if not os.path.isdir(pdir):
|
|
171
|
+
continue
|
|
172
|
+
plugin_fm = parse_frontmatter(os.path.join(pdir, "PLUGIN.md")) or {}
|
|
173
|
+
plugin_specialist = plugin_fm.get("specialist")
|
|
174
|
+
|
|
175
|
+
skills_dir = os.path.join(pdir, "skills")
|
|
176
|
+
if not os.path.isdir(skills_dir):
|
|
177
|
+
continue
|
|
178
|
+
for skill_name in sorted(os.listdir(skills_dir)):
|
|
179
|
+
sdir = os.path.join(skills_dir, skill_name)
|
|
180
|
+
if not os.path.isdir(sdir):
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
text = aggregate_skill_text(sdir)
|
|
184
|
+
if not text:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
prescribed = extract_prescribed_tokens(text)
|
|
188
|
+
forbidden = extract_forbidden(text)
|
|
189
|
+
|
|
190
|
+
ownership_key = f"{plugin}/{skill_name}"
|
|
191
|
+
if ownership_key in ADMIN_OWNED_SKILLS:
|
|
192
|
+
specialist = None
|
|
193
|
+
else:
|
|
194
|
+
specialist = (
|
|
195
|
+
EXPLICIT_OWNERSHIP.get(ownership_key)
|
|
196
|
+
or plugin_specialist
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if specialist is None:
|
|
200
|
+
# Admin-owned: only enforce forbidden-token rule.
|
|
201
|
+
for tok, ctx in forbidden:
|
|
202
|
+
errors.append(
|
|
203
|
+
f"[verify] skill={ownership_key} specialist=admin "
|
|
204
|
+
f"FORBIDDEN token={tok} context=\"{ctx}\""
|
|
205
|
+
)
|
|
206
|
+
summary.append(
|
|
207
|
+
f"[verify] skill={ownership_key} specialist=admin (admin-owned) "
|
|
208
|
+
f"tokens={len(prescribed)} forbidden={len(forbidden)}"
|
|
209
|
+
)
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
tools = specialist_tools(specialist)
|
|
213
|
+
if tools is None:
|
|
214
|
+
errors.append(
|
|
215
|
+
f"[verify] skill={ownership_key} specialist={specialist} "
|
|
216
|
+
f"ERROR specialist frontmatter not parseable"
|
|
217
|
+
)
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
unresolved = sorted(prescribed - tools)
|
|
221
|
+
for tok in unresolved:
|
|
222
|
+
errors.append(
|
|
223
|
+
f"[verify] skill={ownership_key} specialist={specialist} "
|
|
224
|
+
f"unresolved={tok}"
|
|
225
|
+
)
|
|
226
|
+
for tok, ctx in forbidden:
|
|
227
|
+
errors.append(
|
|
228
|
+
f"[verify] skill={ownership_key} specialist={specialist} "
|
|
229
|
+
f"FORBIDDEN token={tok} context=\"{ctx}\""
|
|
230
|
+
)
|
|
231
|
+
summary.append(
|
|
232
|
+
f"[verify] skill={ownership_key} specialist={specialist} "
|
|
233
|
+
f"resolved={len(prescribed) - len(unresolved)}/{len(prescribed)} "
|
|
234
|
+
f"forbidden={len(forbidden)}"
|
|
235
|
+
)
|
|
236
|
+
pairs_checked += 1
|
|
237
|
+
|
|
238
|
+
for line in summary:
|
|
239
|
+
print(line)
|
|
240
|
+
|
|
241
|
+
if errors:
|
|
242
|
+
for line in errors:
|
|
243
|
+
print(line, file=sys.stderr)
|
|
244
|
+
print(
|
|
245
|
+
f"[verify] FAIL pairs_checked={pairs_checked} errors={len(errors)}",
|
|
246
|
+
file=sys.stderr,
|
|
247
|
+
)
|
|
248
|
+
return 1
|
|
249
|
+
|
|
250
|
+
print(f"[verify] OK pairs_checked={pairs_checked}")
|
|
251
|
+
return 0
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
sys.exit(main())
|
|
255
|
+
PYEOF
|
|
@@ -3,7 +3,7 @@ name: database-operator
|
|
|
3
3
|
description: "Document and archive ingestion and ad-hoc graph operations — running the universal `document-ingest` skill for any unstructured document (PDF, text, transcript, web page, audio, video) and per-source archive-import skills (LinkedIn Basic Data Export today; CRM-type seed archives as each plugin ships), plus operator-driven graph hygiene (prune orphans, deduplicate entities, add edges, normalise labels). Delegate when the operator uploads any document, drops an archive directory into chat, or asks for any graph operation that is not a routine per-turn write."
|
|
4
4
|
summary: "Ingests every unstructured document and external archive into your graph (LinkedIn today; other CRM sources in future) and handles ad-hoc graph tidy-ups on request. For example, when you upload a CV, a pricing guide, or a contract; when you drop a LinkedIn export folder into chat; or when you ask to prune orphan nodes, merge duplicate people, or add edges between entities."
|
|
5
5
|
model: claude-sonnet-4-6
|
|
6
|
-
tools: Read, Bash, Glob, Grep, mcp__graph__maxy-graph-read_neo4j_cypher, mcp__graph__maxy-graph-get_neo4j_schema, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memory-delete, mcp__memory__memory-search, mcp__memory__memory-rank, mcp__memory__memory-reindex, mcp__memory__memory-find-candidates, mcp__memory__memory-ingest, mcp__memory__memory-ingest-extract, mcp__memory__memory-ingest-web, mcp__memory__memory-classify, mcp__memory__graph-prune-denylist-list, mcp__memory__graph-prune-denylist-add, mcp__memory__graph-prune-denylist-remove, mcp__contacts__contact-create, mcp__contacts__contact-update, mcp__contacts__contact-lookup, mcp__contacts__contact-list, mcp__admin__file-attach, mcp__admin__plugin-read
|
|
6
|
+
tools: Read, Bash, Glob, Grep, mcp__graph__maxy-graph-read_neo4j_cypher, mcp__graph__maxy-graph-get_neo4j_schema, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memory-delete, mcp__memory__memory-search, mcp__memory__memory-rank, mcp__memory__memory-reindex, mcp__memory__memory-find-candidates, mcp__memory__memory-ingest, mcp__memory__memory-ingest-extract, mcp__memory__memory-ingest-web, mcp__memory__memory-classify, mcp__memory__memory-archive-write, mcp__memory__graph-prune-denylist-list, mcp__memory__graph-prune-denylist-add, mcp__memory__graph-prune-denylist-remove, mcp__contacts__contact-create, mcp__contacts__contact-update, mcp__contacts__contact-lookup, mcp__contacts__contact-list, mcp__admin__file-attach, mcp__admin__plugin-read
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Database Operator
|
|
@@ -12,7 +12,7 @@ You own document and archive ingestion and ad-hoc graph operations. You receive
|
|
|
12
12
|
|
|
13
13
|
## Prerogatives
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Four rules govern every turn. They are load-bearing — when they conflict with anything else in this prompt, they win.
|
|
16
16
|
|
|
17
17
|
**PRECISE.** Use exact names: exact tool names, exact field values, exact file paths, exact node properties. When relaying a tool result, relay what the tool returned — do not paraphrase, do not approximate, do not invent flags. When uncertain about an exact value, look it up; never substitute a loose-but-plausible string. *Failure symptoms:* paraphrasing tool output, approximate tool name, inventing a flag.
|
|
18
18
|
|
|
@@ -26,6 +26,10 @@ Three rules govern every turn. They are load-bearing — when they conflict with
|
|
|
26
26
|
|
|
27
27
|
A landfill graph defeats EVIDENCE-BASED: search returns noise, the agent re-writes the noise, the noise compounds. Compress on write; filter on read.
|
|
28
28
|
|
|
29
|
+
**LOUD-FAIL.** If a dispatched skill prescribes a tool not present in your live tool surface, or a credential not provided in your tool input, terminate with a structured blocker — never improvise via Bash, never search the filesystem for credentials, never construct a parallel write path. Return: `Skill <name> prescribes <tool/credential>; not available. Cannot proceed. Operator must <remediation>.` Identical doctrine to Task 740 classifier failure and Task 560 graph-MCP loud-fail. *Failure symptoms:* `cypher-shell` invocation, `find … neo4j` / `grep … NEO4J_PASSWORD` filesystem probes, `curl` against Neo4j HTTP endpoints, any Bash improvisation that recreates the missing tool's effect.
|
|
30
|
+
|
|
31
|
+
The pre-publish gate (`platform/scripts/verify-skill-tool-surface.sh`) statically asserts every shipped skill's prescribed `mcp__*` tokens resolve against your frontmatter `tools:` list, so a missing tool is a build error, not a production discovery. LOUD-FAIL is the runtime backstop when that gate is bypassed (e.g. operator-edited skill).
|
|
32
|
+
|
|
29
33
|
---
|
|
30
34
|
|
|
31
35
|
## Output contract
|