@julioborges/gantry 1.0.1 → 1.0.4

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.
@@ -185,7 +185,7 @@ def main() -> int:
185
185
  print(f" ERROR: {error}")
186
186
  if errors:
187
187
  return 1
188
- return 0 if selected or parked else 2
188
+ return 0 if selected or parked or all(i.done for i in issues.values()) else 2
189
189
 
190
190
 
191
191
  if __name__ == "__main__":
@@ -36,7 +36,7 @@ sys.path.insert(0, str(Path(__file__).parent))
36
36
  import runlog # noqa: E402
37
37
 
38
38
  DECISION_EVENTS = {"PreToolUse", "tool.execute.before"}
39
- SUBAGENT_START_EVENTS = {"SubagentStart"}
39
+ SUBAGENT_START_EVENTS = {"SubagentStart", "invoke_subagent"}
40
40
  SUBAGENT_STOP_EVENTS = {"SubagentStop"}
41
41
  COMPACTION_EVENTS = {"PreCompact", "session.compacted"}
42
42
 
@@ -67,15 +67,16 @@ EVENT_REQUIRED_CONCEPTS = {
67
67
  CONCEPT_FIELD_BY_HARNESS = {
68
68
  "claude-code": {"tool_name": "tool_name", "tool_input": "tool_input", "session_id": "session_id"},
69
69
  "opencode": {"tool_name": "tool", "tool_input": "args", "session_id": "sessionID"},
70
+ "antigravity": {"tool_name": "tool_name", "tool_input": "tool_input", "session_id": "session_id"},
70
71
  }
71
72
  CONCEPT_ALIASES = {
72
- "tool_name": ("tool_name", "tool", "toolName"),
73
- "tool_input": ("tool_input", "args", "toolInput", "input"),
73
+ "tool_name": ("tool_name", "tool", "toolName", "name"),
74
+ "tool_input": ("tool_input", "args", "toolInput", "input", "parameters", "arguments"),
74
75
  "session_id": ("session_id", "sessionID", "sessionId"),
75
76
  }
76
77
 
77
- MODIFYING_TOOLS = {"edit", "write", "multiedit", "notebookedit", "applypatch", "patch"}
78
- BASH_TOOLS = {"bash", "shell", "exec"}
78
+ MODIFYING_TOOLS = {"edit", "write", "multiedit", "notebookedit", "applypatch", "patch", "writetofile", "replacefilecontent"}
79
+ BASH_TOOLS = {"bash", "shell", "exec", "runcommand"}
79
80
 
80
81
  ISSUE_FILE_RE = re.compile(r"^\d{2,}-[a-z0-9-]+\.md$")
81
82
  STATUS_LINE_RE = re.compile(r"(?m)^Status:\s*(\S+)")
@@ -134,13 +135,21 @@ def tool_input(payload: dict) -> dict:
134
135
 
135
136
 
136
137
  def extract_path(payload: dict, arguments: dict) -> str | None:
137
- value = _first(arguments, ("file_path", "filePath", "path", "filename")) or _first(payload, ("file_path", "path"))
138
+ value = (
139
+ _first(arguments, ("file_path", "filePath", "path", "filename", "TargetFile", "target_file", "targetFile"))
140
+ or _first(payload, ("file_path", "path", "TargetFile", "target_file", "targetFile"))
141
+ )
138
142
  return str(value) if value else None
139
143
 
140
144
 
141
145
  def extract_text(arguments: dict) -> str:
142
146
  parts: list[str] = []
143
- for key in ("old_string", "oldString", "new_string", "newString", "content", "text"):
147
+ for key in (
148
+ "old_string", "oldString", "new_string", "newString", "content", "text",
149
+ "CodeContent", "code_content", "codeContent",
150
+ "TargetContent", "target_content", "targetContent",
151
+ "ReplacementContent", "replacement_content", "replacementContent",
152
+ ):
144
153
  value = arguments.get(key)
145
154
  if isinstance(value, str):
146
155
  parts.append(value)
@@ -158,7 +167,11 @@ def extract_text(arguments: dict) -> str:
158
167
  def extract_new_text(arguments: dict) -> str:
159
168
  """The resulting text a modifying tool would write -- never the text it replaces."""
160
169
  parts: list[str] = []
161
- for key in ("new_string", "newString", "content", "text"):
170
+ for key in (
171
+ "new_string", "newString", "content", "text",
172
+ "CodeContent", "code_content", "codeContent",
173
+ "ReplacementContent", "replacement_content", "replacementContent",
174
+ ):
162
175
  value = arguments.get(key)
163
176
  if isinstance(value, str):
164
177
  parts.append(value)
@@ -182,18 +195,22 @@ def is_draft_safe(new_text: str) -> bool:
182
195
 
183
196
 
184
197
  def extract_command(arguments: dict) -> str | None:
185
- value = _first(arguments, ("command", "cmd"))
198
+ value = _first(arguments, ("command", "cmd", "CommandLine", "command_line", "commandLine"))
186
199
  return str(value) if value else None
187
200
 
188
201
 
189
202
  def payload_cwd(payload: dict) -> str | None:
190
203
  """The harness-reported working directory for this call, if the payload carries one.
191
204
 
192
- Claude Code spells it `cwd`, OpenCode spells it `directory`. Neither is a decision
193
- concept the capability files gate on -- it is only ever used to pick *where* to look
194
- (a git worktree, an Issue path), never to grant or withhold authority on its own.
205
+ Claude Code spells it `cwd`, OpenCode spells it `directory`, Antigravity spells it `Cwd`
206
+ in run_command tool_input. Neither is a decision concept the capability files gate on --
207
+ it is only ever used to pick *where* to look (a git worktree, an Issue path), never to grant
208
+ or withhold authority on its own.
195
209
  """
196
- value = _first(payload, ("cwd", "directory"))
210
+ value = _first(payload, ("cwd", "directory", "Cwd"))
211
+ if not value and isinstance(payload, dict):
212
+ args = tool_input(payload)
213
+ value = _first(args, ("cwd", "directory", "Cwd"))
197
214
  return str(value) if value else None
198
215
 
199
216
 
@@ -218,23 +235,23 @@ def apply_edits(original: str, arguments: dict) -> str | None:
218
235
  """
219
236
  edits = arguments.get("edits")
220
237
  if not isinstance(edits, list):
221
- old = arguments.get("old_string", arguments.get("oldString"))
222
- new = arguments.get("new_string", arguments.get("newString"))
238
+ old = _first(arguments, ("old_string", "oldString", "TargetContent", "target_content", "targetContent"))
239
+ new = _first(arguments, ("new_string", "newString", "ReplacementContent", "replacement_content", "replacementContent"))
223
240
  if not isinstance(old, str) or not isinstance(new, str):
224
241
  return None
225
- replace_all = arguments.get("replace_all", arguments.get("replaceAll"))
242
+ replace_all = arguments.get("replace_all", arguments.get("replaceAll", arguments.get("AllowMultiple", arguments.get("allow_multiple", arguments.get("allowMultiple")))))
226
243
  edits = [{"old_string": old, "new_string": new, "replace_all": replace_all}]
227
244
  text = original
228
245
  for edit in edits:
229
246
  if not isinstance(edit, dict):
230
247
  return None
231
- old = edit.get("old_string", edit.get("oldString"))
232
- new = edit.get("new_string", edit.get("newString"))
248
+ old = _first(edit, ("old_string", "oldString", "TargetContent", "target_content", "targetContent"))
249
+ new = _first(edit, ("new_string", "newString", "ReplacementContent", "replacement_content", "replacementContent"))
233
250
  if not isinstance(old, str) or not isinstance(new, str):
234
251
  return None
235
252
  if old not in text:
236
253
  return None
237
- replace_all = bool(edit.get("replace_all", edit.get("replaceAll")))
254
+ replace_all = bool(edit.get("replace_all", edit.get("replaceAll", edit.get("AllowMultiple", edit.get("allow_multiple", edit.get("allowMultiple"))))))
238
255
  count = -1 if replace_all else 1
239
256
  text = text.replace(old, new, count)
240
257
  return text
@@ -258,7 +275,7 @@ def decide(payload: dict, cwd: Path) -> Decision:
258
275
  if ISSUE_FILE_RE.match(basename):
259
276
  target = Path(path)
260
277
  target = target if target.is_absolute() else cwd / target
261
- if normalized_name in {"write", "multiedit"}:
278
+ if normalized_name in {"write", "multiedit", "writetofile"}:
262
279
  # The draft exemption only ever applies to *creating* a new Issue file.
263
280
  # Once the target exists, its Status/checkbox fields are already under
264
281
  # protection, and the new content must fall through to the same
@@ -266,7 +283,7 @@ def decide(payload: dict, cwd: Path) -> Decision:
266
283
  # never exempted just because the new content, read alone, looks draft-safe.
267
284
  if not target.exists() and is_draft_safe(extract_new_text(arguments)):
268
285
  return Decision(True)
269
- if normalized_name in {"edit", "multiedit"} and target.exists():
286
+ if normalized_name in {"edit", "multiedit", "replacefilecontent"} and target.exists():
270
287
  # Compare the whole file before/after applying the edit in memory, so a
271
288
  # value-only edit (e.g. 'ready-for-agent' -> 'done', or '[ ]' -> '[x]'
272
289
  # without the '- ' scaffolding) is caught even though neither its
@@ -449,7 +466,14 @@ def handle_subagent_event(payload: dict, args: argparse.Namespace, event: str) -
449
466
  return
450
467
  role = None
451
468
  if isinstance(payload, dict):
452
- role = _first(payload, ("role", "subagent_type", "agent_type", "subagentType", "agentType", "description"))
469
+ role = _first(payload, ("role", "subagent_type", "agent_type", "subagentType", "agentType", "description", "Role", "TypeName"))
470
+ if not role:
471
+ args_input = tool_input(payload)
472
+ subagents = args_input.get("Subagents") or args_input.get("subagents")
473
+ if isinstance(subagents, list) and subagents:
474
+ role = _first(subagents[0], ("Role", "role", "TypeName", "typeName"))
475
+ if not role:
476
+ role = _first(args_input, ("Role", "role", "TypeName", "typeName", "description"))
453
477
  record(
454
478
  cwd,
455
479
  root,
@@ -463,6 +487,36 @@ def handle_subagent_event(payload: dict, args: argparse.Namespace, event: str) -
463
487
  )
464
488
 
465
489
 
490
+ def record_invoke_subagent_event(payload: dict, args: argparse.Namespace, cwd: Path, event: str) -> None:
491
+ args_input = tool_input(payload)
492
+ subagents = args_input.get("Subagents") or args_input.get("subagents")
493
+ roles = []
494
+ if isinstance(subagents, list):
495
+ for sub in subagents:
496
+ if isinstance(sub, dict):
497
+ r = _first(sub, ("Role", "role", "TypeName", "typeName", "type_name", "description"))
498
+ if r:
499
+ roles.append(str(r))
500
+ if not roles:
501
+ r = _first(args_input, ("Role", "role", "TypeName", "typeName", "description"))
502
+ if r:
503
+ roles.append(str(r))
504
+ run_id, root = resolve_run(payload, args.run_id, args.state_root, cwd)
505
+ if run_id:
506
+ for role_name in (roles or ["unknown"]):
507
+ record(
508
+ cwd,
509
+ root,
510
+ run_id,
511
+ {
512
+ "ts": now_iso(),
513
+ "run": run_id,
514
+ "event": event,
515
+ "data": {"role": role_name},
516
+ },
517
+ )
518
+
519
+
466
520
  def handle_compaction_event(payload: dict, args: argparse.Namespace) -> None:
467
521
  cwd = Path(args.cwd).resolve()
468
522
  run_id, root = resolve_run(payload if isinstance(payload, dict) else {}, args.run_id, args.state_root, cwd)
@@ -539,14 +593,26 @@ def main() -> int:
539
593
  if args.event in DECISION_EVENTS:
540
594
  decision = handle_decision_event(payload, args)
541
595
  if decision.allow:
596
+ name = tool_name(payload)
597
+ normalized_name = re.sub(r"[^a-z]", "", name)
598
+ if normalized_name == "invokesubagent":
599
+ record_invoke_subagent_event(payload, args, cwd, "subagent.started")
542
600
  return allow()
543
601
  message = f"deny: {decision.rule} {decision.path}"
544
602
  if args.json:
545
- print(json.dumps({"decision": "deny", "rule": decision.rule, "path": decision.path}, separators=(",", ":")))
603
+ print(json.dumps({"decision": "deny", "rule": decision.rule, "path": decision.path, "reason": message}, separators=(",", ":")))
604
+ return 0
546
605
  else:
547
606
  print(message)
548
- print(message, file=sys.stderr)
549
- return 2
607
+ print(message, file=sys.stderr)
608
+ return 2
609
+
610
+ if args.event == "PostToolUse":
611
+ name = tool_name(payload)
612
+ normalized_name = re.sub(r"[^a-z]", "", name)
613
+ if normalized_name == "invokesubagent":
614
+ record_invoke_subagent_event(payload, args, cwd, "subagent.stopped")
615
+ return allow()
550
616
 
551
617
  if args.event in SUBAGENT_START_EVENTS:
552
618
  handle_subagent_event(payload, args, "subagent.started")
@@ -29,14 +29,17 @@ EVENTS = {
29
29
  "policy.changed",
30
30
  "issue.done",
31
31
  "issue.blocked",
32
+ "issue.paused",
32
33
  "refutation",
33
34
  "review.finding",
35
+ "role.selected",
36
+ "role.changed",
34
37
  }
35
38
  RUN_EVENTS = {"run.started", "run.resumed", "run.cancelled", "run.finished"}
36
39
  ROUND_EVENTS = {"round.started", "round.finished"}
37
40
  PHASE_EVENTS = {"phase.started", "phase.finished"}
38
41
  SUBAGENT_EVENTS = {"subagent.started", "subagent.stopped"}
39
- ISSUE_EVENTS = {"issue.done", "issue.blocked", "refutation", "review.finding"}
42
+ ISSUE_EVENTS = {"issue.done", "issue.blocked", "issue.paused", "refutation", "review.finding", "role.selected", "role.changed"}
40
43
  FINISHED_EVENTS = {"run.cancelled", "run.finished"}
41
44
  UNIT_ID_RE = re.compile(r"^[0-9a-f]{12}$")
42
45
  RUN_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]*$")
@@ -277,6 +280,29 @@ def validate_event(payload: object) -> dict:
277
280
  if not isinstance(data, dict) or "policyHash" not in data:
278
281
  raise EventError("policy.changed requires data.policyHash")
279
282
  require_string(data["policyHash"], "data.policyHash")
283
+ elif event == "issue.paused":
284
+ if "issue" not in payload:
285
+ raise EventError("issue.paused requires issue")
286
+ if not isinstance(data, dict) or not {"role", "reason"} <= set(data):
287
+ raise EventError("issue.paused requires data.role and data.reason")
288
+ require_string(data["role"], "data.role")
289
+ require_string(data["reason"], "data.reason")
290
+ elif event == "role.selected":
291
+ if "issue" not in payload:
292
+ raise EventError("role.selected requires issue")
293
+ if not isinstance(data, dict) or not {"role", "requested", "effective"} <= set(data):
294
+ raise EventError("role.selected requires data.role, data.requested, and data.effective")
295
+ require_string(data["role"], "data.role")
296
+ if not isinstance(data["requested"], dict) or not isinstance(data["effective"], dict):
297
+ raise EventError("data.requested and data.effective must be objects")
298
+ elif event == "role.changed":
299
+ if "issue" not in payload:
300
+ raise EventError("role.changed requires issue")
301
+ if not isinstance(data, dict) or not {"role", "requested", "effective"} <= set(data):
302
+ raise EventError("role.changed requires data.role, data.requested, and data.effective")
303
+ require_string(data["role"], "data.role")
304
+ if not isinstance(data["requested"], dict) or not isinstance(data["effective"], dict):
305
+ raise EventError("data.requested and data.effective must be objects")
280
306
  elif event in ISSUE_EVENTS and "issue" not in payload:
281
307
  raise EventError(f"{event} requires issue")
282
308
  return payload
@@ -339,6 +365,17 @@ def inflight(root: Path, unit: str) -> list[dict]:
339
365
  "phase": event["phase"],
340
366
  "worktree": event.get("data", {}).get("worktree"),
341
367
  }
368
+ elif event["event"] == "issue.paused":
369
+ if event["issue"] in active:
370
+ if event.get("data", {}).get("worktree"):
371
+ active[event["issue"]]["worktree"] = event["data"]["worktree"]
372
+ else:
373
+ active[event["issue"]] = {
374
+ "run": event["run"],
375
+ "issue": event["issue"],
376
+ "phase": event.get("phase") or "Critic",
377
+ "worktree": event.get("data", {}).get("worktree"),
378
+ }
342
379
  elif event["event"] == "phase.finished":
343
380
  active.pop(event["issue"], None)
344
381
  elif event["event"] in {"issue.done", "issue.blocked"}:
@@ -2,6 +2,8 @@
2
2
  """Conversational setup wizard for Gantry policy and hooks."""
3
3
  import argparse
4
4
  import json
5
+ import os
6
+ import shutil
5
7
  import sys
6
8
  from pathlib import Path
7
9
 
@@ -111,6 +113,43 @@ def main() -> None:
111
113
  indented_new_hooks = new_hooks_text.replace('\n', '\n' + ' ' * base_indent)
112
114
  settings_path.write_text(content[:start_idx] + indented_new_hooks + content[end_idx:], encoding="utf-8")
113
115
 
116
+ # Antigravity hook wiring: when Antigravity is detected, generate or merge .agents/hooks.json
117
+ if (repo_root / ".agents").exists() or shutil.which("agy") or os.environ.get("ANTIGRAVITY_PROJECT_DIR") or os.environ.get("GEMINI_CLI"):
118
+ ag_hook_frag_path = Path(__file__).resolve().parents[1] / "hooks" / "antigravity.hooks.json"
119
+ if ag_hook_frag_path.exists():
120
+ ag_hook_frag = json.loads(ag_hook_frag_path.read_text(encoding="utf-8"))
121
+ else:
122
+ guard_cmd = 'python3 ".agents/skills/gantry/scripts/guard.py" PreToolUse --json'
123
+ ag_hook_frag = {
124
+ "hooks": {
125
+ "PreToolUse": [
126
+ {
127
+ "matcher": "*",
128
+ "hooks": [
129
+ {
130
+ "type": "command",
131
+ "command": guard_cmd,
132
+ }
133
+ ]
134
+ }
135
+ ]
136
+ }
137
+ }
138
+ agents_dir = repo_root / ".agents"
139
+ agents_dir.mkdir(parents=True, exist_ok=True)
140
+ hooks_json_path = agents_dir / "hooks.json"
141
+ if hooks_json_path.exists():
142
+ try:
143
+ existing_hooks = json.loads(hooks_json_path.read_text(encoding="utf-8"))
144
+ if not isinstance(existing_hooks, dict):
145
+ existing_hooks = {}
146
+ except Exception:
147
+ existing_hooks = {}
148
+ merged_hooks = merge_dicts(existing_hooks, ag_hook_frag)
149
+ hooks_json_path.write_text(json.dumps(merged_hooks, indent=2) + "\n", encoding="utf-8")
150
+ else:
151
+ hooks_json_path.write_text(json.dumps(ag_hook_frag, indent=2) + "\n", encoding="utf-8")
152
+
114
153
  agents_path = repo_root / "AGENTS.md"
115
154
  content = agents_path.read_text(encoding="utf-8") if agents_path.exists() else ""
116
155
  begin_marker = "<!-- gantry:begin -->"
@@ -16,15 +16,28 @@ You are the sole conversational writer of repository policy.
16
16
  - Enables recording by default.
17
17
  - Asks before denial hooks.
18
18
 
19
- 3. **Constraints**:
19
+ 3. **Caveman Lite Option**:
20
+ - Offer Caveman lite as a recommended, explicitly confirmed repository preference (`"caveman": true`).
21
+ - Explain conversational scope (messages and summaries only; specs, issues, code, contracts, and exact errors retain full detail), user-managed installation, fallback to normal behavior, and variable savings.
22
+ - If declined or skipped, resolve to disabled (`"caveman": false`).
23
+ - When confirmed and Caveman is not installed, guide user with host-harness installation command (`npx skills add caveman` for Claude Code; clone into `~/.gemini/config/skills/caveman` or `.agents/skills/caveman` for Antigravity; `.agents/skills/caveman` for OpenCode/Codex). Gantry runs no installer and changes no global agent configuration.
24
+ - After user reports installation, verify host-harness discovery and readability using `python3 .agents/skills/gantry/scripts/caveman.py check --harness <harness>`.
25
+
26
+ 4. **Role Execution Defaults & Antigravity**:
27
+ - Persist sparse repository role execution defaults under `execution.roles` in `.gantry/config.json`.
28
+ - Each role selection contains `harness`, `model` and optional `effort`.
29
+ - When Antigravity is detected (`.agents/` directory or `agy` CLI on PATH), `setup.py` generates or merges `.agents/hooks.json` to wire `PreToolUse` to `guard.py PreToolUse --json`.
30
+ - Unrelated repository policy, hook settings and Caveman opt-in are preserved during merges.
31
+
32
+ 5. **Constraints**:
20
33
  - Creates no engine, database, MCP service, or automatic cleanup.
21
34
  - All setup-generated policy, prompts, and marked content must be English.
22
35
 
23
- 4. **Applying the Policy**:
36
+ 6. **Applying the Policy**:
24
37
  Once the operator confirms the settings, construct the JSON configuration and pipe it to `setup.py`:
25
38
 
26
39
  ```bash
27
40
  python3 .agents/skills/gantry/scripts/setup.py --config '{...}'
28
41
  ```
29
42
 
30
- The `setup.py` script renders the full proposed `.gantry/config.json` before writing, supports merge, overwrite, and abort for an existing policy, handles idempotent merging of the Claude Code hook fragment into `.claude/settings.json`, and adds or replaces only the marked Gantry section in `AGENTS.md`. Do not modify these files directly.
43
+ The `setup.py` script renders the full proposed `.gantry/config.json` before writing, supports merge, overwrite, and abort for an existing policy, handles idempotent merging of the Claude Code hook fragment into `.claude/settings.json`, configures `.agents/hooks.json` when Antigravity is detected, and adds or replaces only the marked Gantry section in `AGENTS.md`. Do not modify these files directly.
package/README.md CHANGED
@@ -12,6 +12,8 @@ harness.**
12
12
  [![Agent Skills](https://img.shields.io/badge/Agent-Skills-orange.svg)](#installation)
13
13
  [![Status: early development](https://img.shields.io/badge/status-early%20development-yellow.svg)](#project-status)
14
14
 
15
+ ![Gantry crane](assets/gantry.png)
16
+
15
17
  Gantry is an open source, harness-neutral skill pack for agentic software
16
18
  development. Deterministic scripts decide which Issues are ready and whether
17
19
  checks pass. Fresh agents implement, review, and challenge each delivery. You
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@julioborges/gantry",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "Install the Gantry harness-neutral agentic SDLC skill pack.",
5
5
  "type": "commonjs",
6
6
  "license": "Apache-2.0",
@@ -10,15 +10,27 @@
10
10
  },
11
11
  "homepage": "https://github.com/JulioBorges/gantry#readme",
12
12
  "bugs": "https://github.com/JulioBorges/gantry/issues",
13
- "keywords": ["agent-skills", "gantry", "sdlc", "claude-code", "codex", "opencode"],
14
- "engines": {"node": ">=22.20.0"},
15
- "bin": {"gantry": "bin/gantry.mjs"},
13
+ "keywords": [
14
+ "agent-skills",
15
+ "gantry",
16
+ "sdlc",
17
+ "claude-code",
18
+ "codex",
19
+ "opencode"
20
+ ],
21
+ "engines": {
22
+ "node": ">=22.20.0"
23
+ },
24
+ "bin": {
25
+ "gantry": "bin/gantry.mjs"
26
+ },
16
27
  "files": [
17
28
  "bin/gantry.mjs",
18
29
  "scripts/ensure-npm-author.mjs",
19
30
  ".agents/skills/gantry",
20
31
  ".agents/skills/gantry-setup",
21
32
  ".agents/skills/gantry-dashboard",
33
+ "assets/gantry.png",
22
34
  "README.md",
23
35
  "LICENSE",
24
36
  "!**/__pycache__/**",
@@ -31,6 +43,11 @@
31
43
  "pack:check": "npm run test:package",
32
44
  "prepublishOnly": "node scripts/ensure-npm-author.mjs && npm test && npm run test:package"
33
45
  },
34
- "dependencies": {"skills": "1.5.26"},
35
- "publishConfig": {"access": "public", "registry": "https://registry.npmjs.org/"}
46
+ "dependencies": {
47
+ "skills": "1.5.26"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public",
51
+ "registry": "https://registry.npmjs.org/"
52
+ }
36
53
  }