@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.
- package/.agents/skills/gantry/SKILL.md +166 -0
- package/.agents/skills/gantry/capabilities/claude-code.json +15 -0
- package/.agents/skills/gantry/capabilities/codex.json +14 -0
- package/.agents/skills/gantry/capabilities/opencode.json +15 -0
- package/.agents/skills/gantry/dashboard/static/app.js +100 -0
- package/.agents/skills/gantry/dashboard/static/index.html +16 -0
- package/.agents/skills/gantry/dashboard/static/style.css +74 -0
- package/.agents/skills/gantry/hooks/claude-code.settings.json +56 -0
- package/.agents/skills/gantry/hooks/codex.hooks.json +4 -0
- package/.agents/skills/gantry/hooks/git/pre-commit +77 -0
- package/.agents/skills/gantry/hooks/git/pre-push +123 -0
- package/.agents/skills/gantry/hooks/git/skipscan.py +88 -0
- package/.agents/skills/gantry/hooks/opencode.plugin.js +44 -0
- package/.agents/skills/gantry/reference/plan-workflow.md +383 -0
- package/.agents/skills/gantry/reference/round-workflow.md +755 -0
- package/.agents/skills/gantry/schemas/critic.json +93 -0
- package/.agents/skills/gantry/schemas/implementer.json +52 -0
- package/.agents/skills/gantry/schemas/learner.json +35 -0
- package/.agents/skills/gantry/schemas/plan-critic.json +39 -0
- package/.agents/skills/gantry/schemas/planner.json +64 -0
- package/.agents/skills/gantry/schemas/requirement-critic.json +48 -0
- package/.agents/skills/gantry/schemas/reviewer.json +52 -0
- package/.agents/skills/gantry/scripts/acceptance.py +66 -0
- package/.agents/skills/gantry/scripts/budget.py +162 -0
- package/.agents/skills/gantry/scripts/cleanup.py +186 -0
- package/.agents/skills/gantry/scripts/common.py +361 -0
- package/.agents/skills/gantry/scripts/dashboard.py +233 -0
- package/.agents/skills/gantry/scripts/frontier.py +192 -0
- package/.agents/skills/gantry/scripts/gates.py +401 -0
- package/.agents/skills/gantry/scripts/guard.py +568 -0
- package/.agents/skills/gantry/scripts/learner.py +99 -0
- package/.agents/skills/gantry/scripts/result.py +104 -0
- package/.agents/skills/gantry/scripts/roadmap.py +212 -0
- package/.agents/skills/gantry/scripts/runlog.py +491 -0
- package/.agents/skills/gantry/scripts/setup.py +139 -0
- package/.agents/skills/gantry/scripts/spec.py +252 -0
- package/.agents/skills/gantry/templates/issue.md +32 -0
- package/.agents/skills/gantry/templates/prd.md +26 -0
- package/.agents/skills/gantry/templates/spec.md +48 -0
- package/.agents/skills/gantry-dashboard/SKILL.md +55 -0
- package/.agents/skills/gantry-setup/SKILL.md +30 -0
- package/LICENSE +201 -0
- package/README.md +437 -0
- package/bin/gantry.mjs +45 -0
- package/package.json +36 -0
- 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())
|