@julioborges/gantry 0.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.
Files changed (46) hide show
  1. package/.agents/skills/gantry/SKILL.md +166 -0
  2. package/.agents/skills/gantry/capabilities/claude-code.json +15 -0
  3. package/.agents/skills/gantry/capabilities/codex.json +14 -0
  4. package/.agents/skills/gantry/capabilities/opencode.json +15 -0
  5. package/.agents/skills/gantry/dashboard/static/app.js +100 -0
  6. package/.agents/skills/gantry/dashboard/static/index.html +16 -0
  7. package/.agents/skills/gantry/dashboard/static/style.css +74 -0
  8. package/.agents/skills/gantry/hooks/claude-code.settings.json +56 -0
  9. package/.agents/skills/gantry/hooks/codex.hooks.json +4 -0
  10. package/.agents/skills/gantry/hooks/git/pre-commit +77 -0
  11. package/.agents/skills/gantry/hooks/git/pre-push +123 -0
  12. package/.agents/skills/gantry/hooks/git/skipscan.py +88 -0
  13. package/.agents/skills/gantry/hooks/opencode.plugin.js +44 -0
  14. package/.agents/skills/gantry/reference/plan-workflow.md +383 -0
  15. package/.agents/skills/gantry/reference/round-workflow.md +755 -0
  16. package/.agents/skills/gantry/schemas/critic.json +93 -0
  17. package/.agents/skills/gantry/schemas/implementer.json +52 -0
  18. package/.agents/skills/gantry/schemas/learner.json +35 -0
  19. package/.agents/skills/gantry/schemas/plan-critic.json +39 -0
  20. package/.agents/skills/gantry/schemas/planner.json +64 -0
  21. package/.agents/skills/gantry/schemas/requirement-critic.json +48 -0
  22. package/.agents/skills/gantry/schemas/reviewer.json +52 -0
  23. package/.agents/skills/gantry/scripts/acceptance.py +66 -0
  24. package/.agents/skills/gantry/scripts/budget.py +162 -0
  25. package/.agents/skills/gantry/scripts/cleanup.py +186 -0
  26. package/.agents/skills/gantry/scripts/common.py +361 -0
  27. package/.agents/skills/gantry/scripts/dashboard.py +233 -0
  28. package/.agents/skills/gantry/scripts/frontier.py +192 -0
  29. package/.agents/skills/gantry/scripts/gates.py +401 -0
  30. package/.agents/skills/gantry/scripts/guard.py +568 -0
  31. package/.agents/skills/gantry/scripts/learner.py +99 -0
  32. package/.agents/skills/gantry/scripts/result.py +104 -0
  33. package/.agents/skills/gantry/scripts/roadmap.py +212 -0
  34. package/.agents/skills/gantry/scripts/runlog.py +491 -0
  35. package/.agents/skills/gantry/scripts/setup.py +139 -0
  36. package/.agents/skills/gantry/scripts/spec.py +252 -0
  37. package/.agents/skills/gantry/templates/issue.md +32 -0
  38. package/.agents/skills/gantry/templates/prd.md +26 -0
  39. package/.agents/skills/gantry/templates/spec.md +48 -0
  40. package/.agents/skills/gantry-dashboard/SKILL.md +55 -0
  41. package/.agents/skills/gantry-setup/SKILL.md +30 -0
  42. package/LICENSE +201 -0
  43. package/README.md +437 -0
  44. package/bin/gantry.mjs +45 -0
  45. package/package.json +36 -0
  46. package/scripts/ensure-npm-author.mjs +29 -0
@@ -0,0 +1,93 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "complete": {
5
+ "type": "boolean"
6
+ },
7
+ "criteria": {
8
+ "type": "array",
9
+ "items": {
10
+ "type": "object",
11
+ "properties": {
12
+ "index": {
13
+ "type": "integer"
14
+ },
15
+ "met": {
16
+ "type": "boolean"
17
+ },
18
+ "evidence": {
19
+ "type": "string"
20
+ }
21
+ },
22
+ "required": [
23
+ "index",
24
+ "met",
25
+ "evidence"
26
+ ],
27
+ "additionalProperties": false
28
+ }
29
+ },
30
+ "gatesVerdict": {
31
+ "type": "string",
32
+ "enum": [
33
+ "pass",
34
+ "fail",
35
+ "no_gates",
36
+ "not_run"
37
+ ]
38
+ },
39
+ "gateResult": {
40
+ "type": "object",
41
+ "properties": {
42
+ "verdict": {
43
+ "type": "string",
44
+ "enum": [
45
+ "pass",
46
+ "fail",
47
+ "no_gates",
48
+ "not_run"
49
+ ]
50
+ }
51
+ },
52
+ "required": [
53
+ "verdict"
54
+ ],
55
+ "additionalProperties": true
56
+ },
57
+ "gateFailures": {
58
+ "type": "array",
59
+ "items": {
60
+ "type": "string"
61
+ }
62
+ },
63
+ "refutations": {
64
+ "type": "array",
65
+ "items": {
66
+ "type": "string"
67
+ }
68
+ },
69
+ "requiredFixes": {
70
+ "type": "array",
71
+ "items": {
72
+ "type": "string"
73
+ }
74
+ },
75
+ "decisionsForOperator": {
76
+ "type": "array",
77
+ "items": {
78
+ "type": "string"
79
+ }
80
+ }
81
+ },
82
+ "required": [
83
+ "complete",
84
+ "criteria",
85
+ "gatesVerdict",
86
+ "gateResult",
87
+ "gateFailures",
88
+ "refutations",
89
+ "requiredFixes",
90
+ "decisionsForOperator"
91
+ ],
92
+ "additionalProperties": false
93
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "worktree": {
5
+ "type": "string"
6
+ },
7
+ "branch": {
8
+ "type": "string"
9
+ },
10
+ "commits": {
11
+ "type": "array",
12
+ "items": {
13
+ "type": "string"
14
+ }
15
+ },
16
+ "summary": {
17
+ "type": "string"
18
+ },
19
+ "testsAdded": {
20
+ "type": "array",
21
+ "items": {
22
+ "type": "string"
23
+ }
24
+ },
25
+ "gatesResult": {
26
+ "type": "string"
27
+ },
28
+ "decisions": {
29
+ "type": "array",
30
+ "items": {
31
+ "type": "string"
32
+ }
33
+ },
34
+ "blockers": {
35
+ "type": "array",
36
+ "items": {
37
+ "type": "string"
38
+ }
39
+ }
40
+ },
41
+ "required": [
42
+ "worktree",
43
+ "branch",
44
+ "commits",
45
+ "summary",
46
+ "testsAdded",
47
+ "gatesResult",
48
+ "decisions",
49
+ "blockers"
50
+ ],
51
+ "additionalProperties": false
52
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "candidates": {
5
+ "type": "array",
6
+ "items": {
7
+ "type": "object",
8
+ "properties": {
9
+ "lesson": {
10
+ "type": "string"
11
+ },
12
+ "evidence": {
13
+ "type": "array",
14
+ "items": {
15
+ "type": "string"
16
+ }
17
+ },
18
+ "target": {
19
+ "type": "string"
20
+ }
21
+ },
22
+ "required": [
23
+ "lesson",
24
+ "evidence",
25
+ "target"
26
+ ],
27
+ "additionalProperties": false
28
+ }
29
+ }
30
+ },
31
+ "required": [
32
+ "candidates"
33
+ ],
34
+ "additionalProperties": false
35
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "acceptable": {
5
+ "type": "boolean"
6
+ },
7
+ "problems": {
8
+ "type": "array",
9
+ "items": {
10
+ "type": "object",
11
+ "properties": {
12
+ "problem": {
13
+ "type": "string"
14
+ },
15
+ "fix": {
16
+ "type": "string"
17
+ }
18
+ },
19
+ "required": [
20
+ "problem",
21
+ "fix"
22
+ ],
23
+ "additionalProperties": false
24
+ }
25
+ },
26
+ "frontierErrors": {
27
+ "type": "array",
28
+ "items": {
29
+ "type": "string"
30
+ }
31
+ }
32
+ },
33
+ "required": [
34
+ "acceptable",
35
+ "problems",
36
+ "frontierErrors"
37
+ ],
38
+ "additionalProperties": false
39
+ }
@@ -0,0 +1,64 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "filesWritten": {
5
+ "type": "array",
6
+ "items": {
7
+ "type": "string"
8
+ }
9
+ },
10
+ "issues": {
11
+ "type": "array",
12
+ "items": {
13
+ "type": "object",
14
+ "properties": {
15
+ "ref": {
16
+ "type": "string"
17
+ },
18
+ "path": {
19
+ "type": "string"
20
+ },
21
+ "title": {
22
+ "type": "string"
23
+ },
24
+ "criteriaCount": {
25
+ "type": "integer"
26
+ },
27
+ "blockedBy": {
28
+ "type": "array",
29
+ "items": {
30
+ "type": "string"
31
+ }
32
+ }
33
+ },
34
+ "required": [
35
+ "ref",
36
+ "path",
37
+ "title",
38
+ "criteriaCount",
39
+ "blockedBy"
40
+ ],
41
+ "additionalProperties": false
42
+ }
43
+ },
44
+ "roadmapAdditions": {
45
+ "type": "array",
46
+ "items": {
47
+ "type": "string"
48
+ }
49
+ },
50
+ "openDecisions": {
51
+ "type": "array",
52
+ "items": {
53
+ "type": "string"
54
+ }
55
+ }
56
+ },
57
+ "required": [
58
+ "filesWritten",
59
+ "issues",
60
+ "roadmapAdditions",
61
+ "openDecisions"
62
+ ],
63
+ "additionalProperties": false
64
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "blocking": {
5
+ "type": "array",
6
+ "items": {
7
+ "type": "object",
8
+ "properties": {
9
+ "quote": {
10
+ "type": "string"
11
+ },
12
+ "reason": {
13
+ "type": "string"
14
+ }
15
+ },
16
+ "required": [
17
+ "quote",
18
+ "reason"
19
+ ],
20
+ "additionalProperties": false
21
+ }
22
+ },
23
+ "findings": {
24
+ "type": "array",
25
+ "items": {
26
+ "type": "object",
27
+ "properties": {
28
+ "quote": {
29
+ "type": "string"
30
+ },
31
+ "reason": {
32
+ "type": "string"
33
+ }
34
+ },
35
+ "required": [
36
+ "quote",
37
+ "reason"
38
+ ],
39
+ "additionalProperties": false
40
+ }
41
+ }
42
+ },
43
+ "required": [
44
+ "blocking",
45
+ "findings"
46
+ ],
47
+ "additionalProperties": false
48
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "blocking": {
5
+ "type": "array",
6
+ "items": {
7
+ "type": "object",
8
+ "properties": {
9
+ "finding": {
10
+ "type": "string"
11
+ },
12
+ "fix": {
13
+ "type": "string"
14
+ }
15
+ },
16
+ "required": [
17
+ "finding",
18
+ "fix"
19
+ ],
20
+ "additionalProperties": false
21
+ }
22
+ },
23
+ "nonBlocking": {
24
+ "type": "array",
25
+ "items": {
26
+ "type": "object",
27
+ "properties": {
28
+ "finding": {
29
+ "type": "string"
30
+ },
31
+ "fix": {
32
+ "type": "string"
33
+ }
34
+ },
35
+ "required": [
36
+ "finding",
37
+ "fix"
38
+ ],
39
+ "additionalProperties": false
40
+ }
41
+ },
42
+ "summary": {
43
+ "type": "string"
44
+ }
45
+ },
46
+ "required": [
47
+ "blocking",
48
+ "nonBlocking",
49
+ "summary"
50
+ ],
51
+ "additionalProperties": false
52
+ }
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+ """Extract or verify the acceptance criteria of one Issue."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ sys.path.insert(0, str(Path(__file__).parent))
11
+ from common import artifact_path, load_issues, load_policy_issues, parse_issue, repo_root, resolve_issue_arg, section # noqa: E402
12
+
13
+
14
+ def tick_all(path: Path) -> int:
15
+ """Tick every criterion; only roadmap.py invokes this state transition."""
16
+ issue = parse_issue(path)
17
+ lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
18
+ changed = 0
19
+ for criterion in issue.criteria:
20
+ old = lines[criterion.line_no - 1]
21
+ new = old.replace("- [ ]", "- [x]", 1)
22
+ if new != old:
23
+ lines[criterion.line_no - 1] = new
24
+ changed += 1
25
+ if changed:
26
+ path.write_text("".join(lines), encoding="utf-8")
27
+ return changed
28
+
29
+
30
+ def main() -> int:
31
+ parser = argparse.ArgumentParser(description=__doc__)
32
+ parser.add_argument("issue", help="spec#NN or an Issue path")
33
+ parser.add_argument("--scratch", help="legacy Issue root override")
34
+ parser.add_argument("--json", action="store_true")
35
+ parser.add_argument("--check", action="store_true", help="require done Status and every criterion ticked")
36
+ args = parser.parse_args()
37
+ root = repo_root()
38
+ issues = load_issues(root / args.scratch) if args.scratch else load_policy_issues(root)
39
+ issue = resolve_issue_arg(args.issue, root, issues)
40
+ if not issue:
41
+ print(f"issue not found: {args.issue}", file=sys.stderr)
42
+ return 2
43
+ text = issue.path.read_text(encoding="utf-8")
44
+ payload = {
45
+ **issue.to_dict(root),
46
+ "spec_path": str(artifact_path(root, "specs", issue.spec).relative_to(root)),
47
+ "criteria": [{"index": item.index, "text": item.text, "checked": item.checked} for item in issue.criteria],
48
+ "what_to_build": section(text, "What to build").strip(),
49
+ "notes": section(text, "Notes").strip(),
50
+ }
51
+ payload["complete"] = issue.done and bool(issue.criteria) and all(item.checked for item in issue.criteria)
52
+ if args.json:
53
+ print(json.dumps(payload, indent=2))
54
+ else:
55
+ print(f"{issue.ref} {issue.title}")
56
+ print(f" status: {issue.status} blocked by: {', '.join(issue.blocked_by) or 'none'}")
57
+ for item in issue.criteria:
58
+ print(f" [{'x' if item.checked else ' '}] {item.index}. {item.text}")
59
+ if args.check and not payload["complete"]:
60
+ print(f"NOT COMPLETE: status={issue.status}, ticked={payload['criteria_checked']}/{payload['criteria_total']}", file=sys.stderr)
61
+ return 1
62
+ return 0
63
+
64
+
65
+ if __name__ == "__main__":
66
+ sys.exit(main())
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env python3
2
+ """Estimate an Issue's initial context package against a declared model window."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import re
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from common import artifact_path, parse_issue, repo_root, resolve_policy, section
12
+
13
+ FILES_TO_READ_HEADING_RE = re.compile(r"^###\s+Files to read\s*$", re.MULTILINE)
14
+ LISTED_FILE_RE = re.compile(r"^\s*-\s+`([^`\r\n]+)`\s*$")
15
+
16
+
17
+ class ConfigurationError(ValueError):
18
+ """A policy, capability, or model declaration cannot be used safely."""
19
+
20
+
21
+ def relative_file(root: Path, value: str) -> Path | None:
22
+ """Resolve an explicitly named repository-relative regular file."""
23
+ candidate = Path(value.strip())
24
+ if not value or candidate.is_absolute() or ".." in candidate.parts:
25
+ return None
26
+ path = (root / candidate).resolve()
27
+ try:
28
+ path.relative_to(root)
29
+ except ValueError:
30
+ return None
31
+ return path if path.is_file() else None
32
+
33
+
34
+ def explicitly_listed_files(issue_path: Path, root: Path) -> list[Path]:
35
+ """Return files listed exactly under ``### Files to read`` in What to build."""
36
+ text = issue_path.read_text(encoding="utf-8")
37
+ what_to_build = section(text, "What to build")
38
+ heading = FILES_TO_READ_HEADING_RE.search(what_to_build)
39
+ if heading is None:
40
+ return []
41
+ listing = what_to_build[heading.end():]
42
+ next_heading = re.search(r"^#{1,3}\s+", listing, re.MULTILINE)
43
+ if next_heading:
44
+ listing = listing[:next_heading.start()]
45
+ paths: dict[Path, None] = {}
46
+ for line in listing.splitlines():
47
+ match = LISTED_FILE_RE.fullmatch(line)
48
+ path = relative_file(root, match.group(1)) if match else None
49
+ if path is not None:
50
+ paths[path] = None
51
+ return sorted(paths, key=lambda path: path.relative_to(root).as_posix())
52
+
53
+
54
+ def declared_models(capabilities_dir: Path) -> dict[str, tuple[str, int]]:
55
+ """Return model IDs and windows from the shipped capability declarations."""
56
+ models: dict[str, tuple[str, int]] = {}
57
+ for path in sorted(capabilities_dir.glob("*.json")):
58
+ try:
59
+ capability = json.loads(path.read_text(encoding="utf-8"))
60
+ except json.JSONDecodeError as exc:
61
+ raise ConfigurationError(f"{path} is not valid JSON: {exc}") from exc
62
+ declared = capability.get("models")
63
+ if not isinstance(declared, dict):
64
+ raise ConfigurationError(f"{path} must declare a models object")
65
+ for model, details in declared.items():
66
+ window = details.get("contextWindow") if isinstance(details, dict) else None
67
+ if not isinstance(model, str) or not isinstance(window, int) or isinstance(window, bool) or window <= 0:
68
+ raise ConfigurationError(f"{path} has an invalid contextWindow for {model!r}")
69
+ if set(details) != {"contextWindow"}:
70
+ raise ConfigurationError(f"{path} must map {model!r} only to contextWindow")
71
+ if model in models:
72
+ raise ConfigurationError(f"model {model!r} is declared by more than one capability")
73
+ models[model] = (path.stem, window)
74
+ if not models:
75
+ raise ConfigurationError(f"no capability declarations found in {capabilities_dir}")
76
+ return models
77
+
78
+
79
+ def estimate(issue_path: Path, model: str, root: Path, capabilities_dir: Path) -> dict:
80
+ """Build the deterministic initial-package estimate for one Issue."""
81
+ issue = parse_issue(issue_path)
82
+ spec_path = artifact_path(root, "specs", issue.spec).resolve()
83
+ if not spec_path.is_file():
84
+ raise ConfigurationError(f"parent Spec does not exist: {spec_path.relative_to(root)}")
85
+ models = declared_models(capabilities_dir)
86
+ if model not in models:
87
+ raise ConfigurationError(f"unknown model ID: {model!r}")
88
+ policy = resolve_policy(root)
89
+ share = policy.get("budget", {}).get("contextShare")
90
+ if not isinstance(share, (int, float)) or isinstance(share, bool) or not 0 < share <= 1:
91
+ raise ConfigurationError("budget.contextShare must be a number greater than 0 and at most 1")
92
+
93
+ paths: dict[Path, None] = {issue_path.resolve(): None, spec_path: None}
94
+ for path in explicitly_listed_files(issue_path, root):
95
+ paths[path] = None
96
+ ordered = [issue_path.resolve(), spec_path]
97
+ ordered.extend(sorted((path for path in paths if path not in {issue_path.resolve(), spec_path}),
98
+ key=lambda path: path.relative_to(root).as_posix()))
99
+ files = [
100
+ {"path": path.relative_to(root).as_posix(), "bytes": path.stat().st_size}
101
+ for path in ordered
102
+ ]
103
+ byte_count = sum(entry["bytes"] for entry in files)
104
+ harness, window = models[model]
105
+ estimated_tokens = byte_count / 4
106
+ budget_tokens = window * share
107
+ over_budget = estimated_tokens > budget_tokens
108
+ return {
109
+ "issue": issue.path.relative_to(root).as_posix(),
110
+ "spec": spec_path.relative_to(root).as_posix(),
111
+ "model": model,
112
+ "harness": harness,
113
+ "files": files,
114
+ "bytes": byte_count,
115
+ "estimatedTokens": estimated_tokens,
116
+ "contextWindow": window,
117
+ "contextShare": share,
118
+ "budgetTokens": budget_tokens,
119
+ "overBudget": over_budget,
120
+ "verdict": "over_budget" if over_budget else "within_budget",
121
+ }
122
+
123
+
124
+ def emit(payload: dict, as_json: bool) -> None:
125
+ if as_json:
126
+ print(json.dumps(payload, sort_keys=True))
127
+ return
128
+ if "error" in payload:
129
+ print(f"configuration error: {payload['error']}")
130
+ return
131
+ print(f"estimated tokens: {payload['estimatedTokens']}")
132
+ print(f"context window: {payload['contextWindow']}")
133
+ print(f"context share: {payload['contextShare']}")
134
+ print(f"budget tokens: {payload['budgetTokens']}")
135
+ print(f"verdict: {payload['verdict']}")
136
+
137
+
138
+ def main() -> int:
139
+ parser = argparse.ArgumentParser(description=__doc__)
140
+ parser.add_argument("issue", help="Issue path")
141
+ parser.add_argument("--model", required=True, help="exact model ID declared by a capability")
142
+ parser.add_argument("--cwd", default=".", help="repository or worktree to inspect")
143
+ parser.add_argument("--json", action="store_true", help="emit machine-readable output")
144
+ args = parser.parse_args()
145
+
146
+ issue_path = Path(args.issue).resolve()
147
+ root = repo_root(Path(args.cwd))
148
+ if not issue_path.is_file():
149
+ emit({"error": f"configuration error: Issue does not exist: {issue_path}"}, args.json)
150
+ return 1
151
+ capabilities = Path(__file__).resolve().parents[1] / "capabilities"
152
+ try:
153
+ payload = estimate(issue_path, args.model, root, capabilities)
154
+ except (ConfigurationError, ValueError) as exc:
155
+ emit({"error": f"configuration error: {exc}"}, args.json)
156
+ return 1
157
+ emit(payload, args.json)
158
+ return 1 if payload["overBudget"] else 0
159
+
160
+
161
+ if __name__ == "__main__":
162
+ sys.exit(main())