@julioborges/gantry 1.0.1 → 1.0.3
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/.agents/skills/gantry/SKILL.md +32 -1
- package/.agents/skills/gantry/capabilities/antigravity.json +15 -0
- package/.agents/skills/gantry/hooks/antigravity.hooks.json +26 -0
- package/.agents/skills/gantry/reference/plan-workflow.md +52 -14
- package/.agents/skills/gantry/reference/round-workflow.md +277 -11
- package/.agents/skills/gantry/scripts/budget.py +49 -7
- package/.agents/skills/gantry/scripts/caveman.py +243 -0
- package/.agents/skills/gantry/scripts/common.py +2 -0
- package/.agents/skills/gantry/scripts/discovery.py +262 -0
- package/.agents/skills/gantry/scripts/execution.py +797 -0
- package/.agents/skills/gantry/scripts/frontier.py +1 -1
- package/.agents/skills/gantry/scripts/guard.py +91 -25
- package/.agents/skills/gantry/scripts/runlog.py +38 -1
- package/.agents/skills/gantry/scripts/setup.py +39 -0
- package/.agents/skills/gantry-setup/SKILL.md +16 -3
- package/package.json +22 -6
|
@@ -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 =
|
|
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 (
|
|
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 (
|
|
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
|
|
193
|
-
concept the capability files gate on --
|
|
194
|
-
(a git worktree, an Issue path), never to grant
|
|
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
|
|
222
|
-
new = arguments
|
|
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
|
|
232
|
-
new = edit
|
|
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
|
-
|
|
549
|
-
|
|
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. **
|
|
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
|
-
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@julioborges/gantry",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Install the Gantry harness-neutral agentic SDLC skill pack.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -10,9 +10,20 @@
|
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://github.com/JulioBorges/gantry#readme",
|
|
12
12
|
"bugs": "https://github.com/JulioBorges/gantry/issues",
|
|
13
|
-
"keywords": [
|
|
14
|
-
|
|
15
|
-
|
|
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",
|
|
@@ -31,6 +42,11 @@
|
|
|
31
42
|
"pack:check": "npm run test:package",
|
|
32
43
|
"prepublishOnly": "node scripts/ensure-npm-author.mjs && npm test && npm run test:package"
|
|
33
44
|
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"skills": "1.5.26"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"registry": "https://registry.npmjs.org/"
|
|
51
|
+
}
|
|
36
52
|
}
|