graphban-cli 0.7.1__tar.gz → 0.8.0__tar.gz
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.
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/PKG-INFO +2 -2
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/README.md +1 -1
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/pyproject.toml +22 -1
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/src/gban/client.py +5 -1
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/src/gban/setup.py +35 -18
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/src/gban/skills/graphban-delegation/SKILL.md +10 -1
- graphban_cli-0.8.0/src/gban/skills/graphban-orient/SKILL.md +31 -0
- graphban_cli-0.8.0/src/gban/skills/graphban-prd/SKILL.md +40 -0
- graphban_cli-0.8.0/src/gban/skills/graphban-review/SKILL.md +21 -0
- graphban_cli-0.8.0/src/gban/skills/graphban-scan/SKILL.md +23 -0
- graphban_cli-0.8.0/src/gban/skills/graphban-work/SKILL.md +29 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/src/gban/skills/watch-wave/SKILL.md +10 -2
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/src/gban/stackmap.py +2 -1
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/src/gban/swamp.py +1 -1
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/tests/test_session.py +41 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/tests/test_setup.py +10 -3
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/.gitignore +0 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/LICENSE +0 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/src/gban/__init__.py +0 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/src/gban/cli.py +0 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/src/gban/config.py +0 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/src/gban/data/stack_sdks.toml +0 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/src/gban/doctor.py +0 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/src/gban/gitignore.py +0 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/tests/test_acts.py +0 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/tests/test_doctor.py +0 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/tests/test_login_projects.py +0 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/tests/test_packaging.py +0 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/tests/test_stackmap.py +0 -0
- {graphban_cli-0.7.1 → graphban_cli-0.8.0}/tests/test_swamp.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: graphban-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: gban — a Graphban client for the human at a terminal
|
|
5
5
|
Project-URL: Homepage, https://github.com/asc-me/graphban
|
|
6
6
|
Project-URL: Repository, https://github.com/asc-me/graphban
|
|
@@ -130,7 +130,7 @@ project is not a mistake anybody notices quickly.
|
|
|
130
130
|
`setup` mints a project-scoped credential, writes the `graphban` and `gbfleet` MCP entries
|
|
131
131
|
into every parent harness that would actually read them, stores that key next to the login
|
|
132
132
|
session (a `gb_sk_…`, never the refresh token), installs the supervisor if it is missing,
|
|
133
|
-
drops the
|
|
133
|
+
drops the shipped skills into `.claude/skills/` and `.agents/skills/`, and then **verifies** rather than
|
|
134
134
|
asserting: it asks the new credential what it can actually see. Restart the session that will
|
|
135
135
|
call `delegate`/`spawn` afterwards — MCP servers are read at startup.
|
|
136
136
|
|
|
@@ -115,7 +115,7 @@ project is not a mistake anybody notices quickly.
|
|
|
115
115
|
`setup` mints a project-scoped credential, writes the `graphban` and `gbfleet` MCP entries
|
|
116
116
|
into every parent harness that would actually read them, stores that key next to the login
|
|
117
117
|
session (a `gb_sk_…`, never the refresh token), installs the supervisor if it is missing,
|
|
118
|
-
drops the
|
|
118
|
+
drops the shipped skills into `.claude/skills/` and `.agents/skills/`, and then **verifies** rather than
|
|
119
119
|
asserting: it asks the new credential what it can actually see. Restart the session that will
|
|
120
120
|
call `delegate`/`spawn` afterwards — MCP servers are read at startup.
|
|
121
121
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "graphban-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "gban — a Graphban client for the human at a terminal"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -43,3 +43,24 @@ packages = ["src/gban"]
|
|
|
43
43
|
[tool.pytest.ini_options]
|
|
44
44
|
testpaths = ["tests"]
|
|
45
45
|
pythonpath = ["src", "tests"]
|
|
46
|
+
|
|
47
|
+
# Read by `scripts/scan_ci.py` (`bandit -c pyproject.toml -r .`, GRPH-1110). Any other finding
|
|
48
|
+
# is fixed or carries a line-level `# nosec <id>` with its reason.
|
|
49
|
+
[tool.bandit]
|
|
50
|
+
exclude_dirs = [
|
|
51
|
+
# Test code asserts by design (B101) and fakes credentials. It is not in the wheel.
|
|
52
|
+
"tests",
|
|
53
|
+
# A local virtualenv is third-party code, audited by pip-audit, not ours to lint.
|
|
54
|
+
".venv",
|
|
55
|
+
]
|
|
56
|
+
skips = [
|
|
57
|
+
# B404 (import subprocess): this is a developer CLI whose job is driving git, gh and the
|
|
58
|
+
# user's own tools. Importing subprocess is the feature, not a smell.
|
|
59
|
+
"B404",
|
|
60
|
+
# B603 (subprocess without shell=True): fires on EVERY argv-list call, which is the safe
|
|
61
|
+
# form. B602 (shell=True) stays on, and that is the injection check that matters.
|
|
62
|
+
"B603",
|
|
63
|
+
# B607 (partial executable path): `git`/`gh` are resolved from the user's own PATH on
|
|
64
|
+
# their own machine on purpose; hardcoding /usr/bin/git breaks every Homebrew install.
|
|
65
|
+
"B607",
|
|
66
|
+
]
|
|
@@ -85,11 +85,15 @@ class Client:
|
|
|
85
85
|
|
|
86
86
|
# ---- transport ---------------------------------------------------------------------
|
|
87
87
|
def _open(self, method: str, path: str, body: dict | None, headers: dict) -> tuple[int, dict]:
|
|
88
|
+
if not self.url.startswith(("http://", "https://")):
|
|
89
|
+
# urlopen would read `file:` and friends too, and send the credential with it.
|
|
90
|
+
raise Unreachable(f"could not reach {self.url}: not an http(s) URL")
|
|
88
91
|
data = json.dumps(body).encode() if body is not None else None
|
|
89
92
|
request = urllib.request.Request(self.url + path, data=data, method=method,
|
|
90
93
|
headers={"Content-Type": "application/json", **headers})
|
|
91
94
|
try:
|
|
92
|
-
|
|
95
|
+
# bandit B310: the scheme is checked to be http(s) above.
|
|
96
|
+
with urllib.request.urlopen(request, timeout=TIMEOUT) as response: # nosec B310
|
|
93
97
|
raw = response.read().decode() or "{}"
|
|
94
98
|
return response.status, (json.loads(raw) if raw.strip() else {})
|
|
95
99
|
except urllib.error.HTTPError as exc:
|
|
@@ -585,13 +585,19 @@ def supervisor(install: bool) -> list[dict]:
|
|
|
585
585
|
# `backend/app/services/artifacts.py` maps the `skill` tier to `.claude/skills/{slug}/SKILL.md`,
|
|
586
586
|
# `file_additive`, frontmatter with `name` and `description` followed by numbered steps. A skill
|
|
587
587
|
# written here in a different shape would be one the inventory scan reports and the learning loop
|
|
588
|
-
# cannot reason about.
|
|
588
|
+
# cannot reason about. That copy is what Claude, Grok, Cursor, and gbagent read.
|
|
589
|
+
#
|
|
590
|
+
# Qwen 0.23, agy, and dsh do not read `.claude/skills`. They read `<repo>/.agents/skills`
|
|
591
|
+
# (Qwen also reads `.qwen/skills`; one of the two is enough). The same bytes go there.
|
|
592
|
+
# gbagent walks both trees and keeps one copy per `name`.
|
|
589
593
|
#
|
|
590
594
|
# Every directory under `skills/` that holds a SKILL.md is installed. Naming only
|
|
591
595
|
# `graphban-delegation` here is how a second skill (`watch-wave`) would exist in the
|
|
592
596
|
# wheel and never reach a checkout.
|
|
593
597
|
|
|
594
598
|
SKILL_SLUG = "graphban-delegation"
|
|
599
|
+
#: Inventory path first. The second is the directory the other harnesses share.
|
|
600
|
+
SKILL_DEST_DIRS = (".claude/skills", ".agents/skills")
|
|
595
601
|
|
|
596
602
|
|
|
597
603
|
def skills_root() -> Path:
|
|
@@ -613,20 +619,28 @@ def shipped_slugs() -> list[str]:
|
|
|
613
619
|
)
|
|
614
620
|
|
|
615
621
|
|
|
616
|
-
def
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
622
|
+
def skill_dests(repo: Path, slug: str) -> list[Path]:
|
|
623
|
+
return [repo / rel / slug / "SKILL.md" for rel in SKILL_DEST_DIRS]
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def install_skill(repo: Path, slug: str = SKILL_SLUG) -> list[tuple[str, Path]]:
|
|
627
|
+
"""One `(status, path)` per harness directory. Never overwrites: a person may have edited
|
|
628
|
+
theirs, and a setup command that silently replaced it would take that edit away without
|
|
629
|
+
saying so. A dest that differs does not block the other directories."""
|
|
620
630
|
body = skill_source(slug).read_text(encoding="utf-8")
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
631
|
+
out = []
|
|
632
|
+
for dest in skill_dests(repo, slug):
|
|
633
|
+
if dest.exists():
|
|
634
|
+
out.append(("same" if dest.read_text(encoding="utf-8") == body else "differs", dest))
|
|
635
|
+
continue
|
|
636
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
637
|
+
dest.write_text(body, encoding="utf-8")
|
|
638
|
+
out.append(("written", dest))
|
|
639
|
+
return out
|
|
626
640
|
|
|
627
641
|
|
|
628
642
|
def skill(repo: Path) -> list[dict]:
|
|
629
|
-
"""Teach the agent the
|
|
643
|
+
"""Teach the agent the procedure, in every directory a fleet harness reads."""
|
|
630
644
|
slugs = shipped_slugs()
|
|
631
645
|
if not slugs:
|
|
632
646
|
return [_line("local", UNKNOWN, "skill",
|
|
@@ -634,17 +648,20 @@ def skill(repo: Path) -> list[dict]:
|
|
|
634
648
|
lines = []
|
|
635
649
|
for slug in slugs:
|
|
636
650
|
try:
|
|
637
|
-
|
|
651
|
+
outcomes = install_skill(repo, slug)
|
|
638
652
|
except OSError as exc:
|
|
639
653
|
lines.append(_line("local", UNKNOWN, "skill",
|
|
640
654
|
f"could not write {slug}: {exc}"))
|
|
641
655
|
continue
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
656
|
+
current = []
|
|
657
|
+
for status, dest in outcomes:
|
|
658
|
+
if status == "differs":
|
|
659
|
+
lines.append(_line("local", UNKNOWN, "skill",
|
|
660
|
+
f"{dest} exists and differs from the one shipped here — left alone"))
|
|
661
|
+
else:
|
|
662
|
+
current.append(f"{dest}" + ("" if status == "written" else " (already current)"))
|
|
663
|
+
if current:
|
|
664
|
+
lines.append(_line("local", PASS, "skill", "; ".join(current)))
|
|
648
665
|
return lines
|
|
649
666
|
|
|
650
667
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: graphban-delegation
|
|
3
|
-
description: Hand a Graphban work item to a child agent running on a cheaper model, and enable delegation on a project that does not have it yet. Use when asked to delegate, fan out, spawn a worker, or run a wave.
|
|
3
|
+
description: Hand a Graphban work item to a child agent running on a cheaper model, and enable delegation on a project that does not have it yet. Use when asked to delegate, fan out, spawn a worker, mint seats, retire a wave, or run a wave.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Delegating a Graphban item
|
|
@@ -186,6 +186,15 @@ chose the item, wrote the brief and picked the tier; review exists to be a secon
|
|
|
186
186
|
So a delegated item needs a second agent to review it. Plan for that, or the work stops at
|
|
187
187
|
`review` with nobody entitled to clear it.
|
|
188
188
|
|
|
189
|
+
## Before a wave of seats
|
|
190
|
+
|
|
191
|
+
`delegate(seat=true)` is one item. Several seats is `mint_enrolment`.
|
|
192
|
+
|
|
193
|
+
- `propose_allocation` first. It names workers and which cluster each would take. Nothing is assigned until `assign_role`.
|
|
194
|
+
- `collision_clusters(holds=true)` before claiming. `predicted: true` means the grouping used inferred areas.
|
|
195
|
+
- `mint_enrolment(review_only=true)` when that seat may review and must not take build work. The child still registers as `worker`. One seat per agent: two agents on one seat cannot review each other.
|
|
196
|
+
- `retire_wave` revokes the seats you minted and releases what they hold. It does not stop the processes. `agents_still_running` are still building against dead seats.
|
|
197
|
+
|
|
189
198
|
## When not to use this
|
|
190
199
|
|
|
191
200
|
For a whole backlog, ask the person to run `gbfleet until` instead. It runs the wave with no
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graphban-orient
|
|
3
|
+
description: >
|
|
4
|
+
Read a Graphban-tracked repo through the code graph before grep or a wide
|
|
5
|
+
file read. Use when starting work, finding where code lives, or before
|
|
6
|
+
search_code, code_neighbors, get_code_map, search_memory, or a filesystem
|
|
7
|
+
crawl. Use when the user runs /graphban-orient.
|
|
8
|
+
user-invocable: true
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Orient
|
|
12
|
+
|
|
13
|
+
Ask the graph before you read the tree. One graph call replaces a crawl.
|
|
14
|
+
|
|
15
|
+
1. `get_context` once per session. Keep the project id, `missing_tiers`, and gitops. A later call does not refresh a manifest that was fetched at connect.
|
|
16
|
+
2. Unset gitops (`source` unmeasured, or `control` `linked_unreachable`) is unmeasured. Leave the base branch, the branch pattern, and the release doc alone until a field has a source.
|
|
17
|
+
3. `search_code(query, top_k=5)` before grep.
|
|
18
|
+
4. `code_neighbors(path)` before opening a file to learn what calls it or what it calls.
|
|
19
|
+
5. `search_memory(query, top_k=3)` once per task. Leave `include_candidates` off. A candidate is not a published fact. Writing a shard is `graphban-work`.
|
|
20
|
+
6. `related_work(id)` once you have an item id.
|
|
21
|
+
7. Open a file when you have a path and a question.
|
|
22
|
+
|
|
23
|
+
List reads stay on the default `fields=lean`. `search_items` and `get_backlog` return id, title, and status. The one full item read is `get_item_details`, for the item you took. Claiming is `graphban-work`.
|
|
24
|
+
|
|
25
|
+
`get_code_map` takes a `kind` and a small `limit`. With neither, the reply is up to 200 nodes. Check `truncated` when you do call it.
|
|
26
|
+
|
|
27
|
+
Looking around uses the reads above. `describe_code`, `link_code`, and `unlink_code` write the graph.
|
|
28
|
+
|
|
29
|
+
A spec question is `graphban-prd`. `get_prd` is for editing a spec, not for finding a file.
|
|
30
|
+
|
|
31
|
+
`missing_tiers` on `get_context` means those tools were left out of the prompt. The tier decides what is listed. Scope and role decide what a call may do.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graphban-prd
|
|
3
|
+
description: >
|
|
4
|
+
Author, grill, decompose, or close a Graphban PRD. Use when writing a spec,
|
|
5
|
+
running a grill, answer_grill, update_prd, decompose_prd, request_rebaseline,
|
|
6
|
+
close_prd, or submit_verdict, or when the user runs /graphban-prd.
|
|
7
|
+
user-invocable: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# PRDs
|
|
11
|
+
|
|
12
|
+
Approval is the grill finishing. `update_prd` cannot set `approved`.
|
|
13
|
+
|
|
14
|
+
Read with one call. `get_prd` when you will edit. `prd_coverage` for gaps: `shaped: false` is not a clean pass, `gaps` means no tasks, `empty_sections` means no substance. `prd_acceptance` with one `view` for delivery. `completeness` is the view that lists baselined sections with nothing delivered.
|
|
15
|
+
|
|
16
|
+
## Grill
|
|
17
|
+
|
|
18
|
+
`grill_prd` returns the next questions and records nothing. Ask the author. `answer_grill` with their words. `graded: false` means the grader was not asked and this answer was not judged. Call `grill_prd` again while `outstanding` remains. `complete` is the grill finishing, which is what reaches approved.
|
|
19
|
+
|
|
20
|
+
Recording an answer with `update_prd` replaces markdown and leaves the PRD in `draft`, with no turns and no dimensions.
|
|
21
|
+
|
|
22
|
+
## Edit
|
|
23
|
+
|
|
24
|
+
`update_prd(section=<the ## heading>, body=<that section>)` rewrites one section. A whole-body replace needs `base_hash` from `get_prd`. A moved hash is a refusal. `status` on this call is `draft` or `review`.
|
|
25
|
+
|
|
26
|
+
## Decompose
|
|
27
|
+
|
|
28
|
+
`decompose_prd` with `create` false first. One proposed task per uncovered section. The task carries the section's framing, so the worker does not also read the PRD. Framing sections are tasks only when `include_prose` is true. `create` true files them once the grill has approved the PRD.
|
|
29
|
+
|
|
30
|
+
## Rebaseline
|
|
31
|
+
|
|
32
|
+
`request_rebaseline(reason_type, reason)` in your own words. `reason_type` is `learning`, `scope-change`, or `correction`. It reopens the grill. The current baseline governs until a new one is earned. Adding a section is a new PRD. A closed PRD is refused.
|
|
33
|
+
|
|
34
|
+
## Close
|
|
35
|
+
|
|
36
|
+
`close_prd` is terminal. Every section that `completeness` says has nothing delivered appears once in `dispositions`, as `promoted` (`promote_to` `item` or `prd`) or `deferred` with a reason. After close, further work is a new PRD.
|
|
37
|
+
|
|
38
|
+
## Verdict
|
|
39
|
+
|
|
40
|
+
`submit_verdict` citations are `{kind, ref}` and must resolve. `kind` is `code` (a code-graph path), `intent` (a baseline section — what an absence finding cites), or `evidence` (an item key). A citation that does not resolve is rejected. Signing work you claimed is flagged.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graphban-review
|
|
3
|
+
description: >
|
|
4
|
+
Review a Graphban item you did not build: claim_review, sign_off, or bounce.
|
|
5
|
+
Use when reviewing, signing off, or bouncing an item, or when the user runs
|
|
6
|
+
/graphban-review.
|
|
7
|
+
user-invocable: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Review
|
|
11
|
+
|
|
12
|
+
Review is a second person. `claim_review` refuses the builder.
|
|
13
|
+
|
|
14
|
+
1. `claim_review`. `claimed: false` means the item is yours, or it needs `gh` or `human-only` you did not declare. Stop on that item.
|
|
15
|
+
2. The diff is the `branch` on the claim. Read that branch. A review does not orient the whole repo.
|
|
16
|
+
3. `sign_off` needs `evidence` and the `commit` SHA you reviewed. It refuses the builder, and it refuses the delegation's `delegated_by`. Above the effort bar it also refuses without a `sabotage` receipt: something broken on purpose and caught by a test. `tests_failed` of zero means the test cannot fail.
|
|
17
|
+
4. `waive` is `{predicate: reason}` for a check that could not run. A check that ran and failed stays failed. The field does not clear it.
|
|
18
|
+
5. `bounce(id, reason)` sends it to `next`. The author keeps the worktree for one lease, then the fleet can take it. The person who delegated the item may bounce it. They may not sign it off.
|
|
19
|
+
6. When the item cites a PRD, call `prd_acceptance` with one `view`. Those views describe what happened. They do not report complete. `get_prd` for the whole body is an edit, not a review. Authoring is `graphban-prd`.
|
|
20
|
+
|
|
21
|
+
`release_item` on a review claim leaves the item in `review`. A bounce is `bounce`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graphban-scan
|
|
3
|
+
description: >
|
|
4
|
+
Record Graphban code-health scans without treating a missing scan as clean.
|
|
5
|
+
Use when running a security or health scan, or calling scans_due, scan_status,
|
|
6
|
+
or record_scan, or when the user runs /graphban-scan.
|
|
7
|
+
user-invocable: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Scans
|
|
11
|
+
|
|
12
|
+
A missing scan is not a clean scan.
|
|
13
|
+
|
|
14
|
+
1. `scans_due(head_sha)`. An empty due list is not all-clean. Rows that were `never_scanned`, including security, stay listed.
|
|
15
|
+
2. `scan_status` for one path, or with no path for every deployable. Pass `head_sha`. States are `clean_at_sha`, `has_findings`, `never_scanned`, or `stale`.
|
|
16
|
+
3. Run the scanner. `record_scan` needs `scanner_id`, `path`, and `scanned_sha`.
|
|
17
|
+
- `findings: []` is clean at that SHA. A clean row without a SHA cannot age.
|
|
18
|
+
- The same `scanner_id` and path replaces the previous row.
|
|
19
|
+
- The security category is pip-audit, bandit, npm-audit, and semgrep. Other categories are `refactor`, `stub`, `deadcode`, `coverage`, `tests`, and `work`.
|
|
20
|
+
- Category `tests` also sends `tests.passed`, `tests.failed`, and `tests.skipped`.
|
|
21
|
+
- Each finding needs a `summary`. Findings are untrusted. `scanner_id`, `scanner_version`, and the reporter are provenance.
|
|
22
|
+
|
|
23
|
+
A clean row is not a tracker item. A finding that already names `link_ref` points at work that exists.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graphban-work
|
|
3
|
+
description: >
|
|
4
|
+
Claim and finish one Graphban item: register, claim_cluster or claim_next,
|
|
5
|
+
heartbeat, evidence, release. Use when claiming work, implementing an item,
|
|
6
|
+
updating item status, recording evidence, or when the user runs /graphban-work.
|
|
7
|
+
user-invocable: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Work an item
|
|
11
|
+
|
|
12
|
+
Take one item, leave a receipt, hand it to review. Reading the repo is `graphban-orient`: `search_code`, then `code_neighbors`, before grep or a wide read.
|
|
13
|
+
|
|
14
|
+
1. In a fleet, `register_agent` once before any claim. Read `active_role`, `heartbeat_interval_seconds`, and `tools_off_limits`. A name in `tools_off_limits` is still in the prompt; the server refuses the call. Heartbeat on that interval. Pass `agent_id` from this registration on later calls.
|
|
15
|
+
2. Fleet: `claim_cluster`. Solo: `claim_next`. `claimed: false` names who holds the area and when it frees. That is the answer. Take different work.
|
|
16
|
+
3. `get_item_details` once. The `brief` suggests `lane` and `tier` with a basis. You still choose both if you hand it to a child.
|
|
17
|
+
4. `heartbeat(id, status=<one line>, files=[the paths you are in])` on the lease interval. A heartbeat is not a `fleet_status` poll.
|
|
18
|
+
5. `update_item` as you go. Evidence appends. Touchpoints union: send the files you actually touched. An empty touchpoint list writes nothing. Status goes to `review`.
|
|
19
|
+
6. Stopping unfinished: `release_item`. A blocker that is still true tomorrow is status `blocked` with `blocker` set. `backlog` is claimable and will be handed out again. A build claim released without `to_status` goes back to `next`.
|
|
20
|
+
|
|
21
|
+
Signing off is `graphban-review`. The builder does not set `done`.
|
|
22
|
+
|
|
23
|
+
Memory, when the next agent cannot see the lesson in the diff:
|
|
24
|
+
|
|
25
|
+
- `search_memory` first, as in `graphban-orient`.
|
|
26
|
+
- `add_memory`. Read the returned `status`. `candidate` does not appear in later search. A near-duplicate is rejected.
|
|
27
|
+
- `publish_memory` asks a judge. `kept: false` is a normal result.
|
|
28
|
+
|
|
29
|
+
Handing the item to a child is `graphban-delegation`. That is a `delegate`, not a second claim.
|
|
@@ -30,8 +30,16 @@ Items: ID1, ID2, …
|
|
|
30
30
|
Children if known: AGENT_IDS
|
|
31
31
|
PRs if known: #N
|
|
32
32
|
|
|
33
|
-
Call gbfleet__ps
|
|
34
|
-
|
|
33
|
+
Call gbfleet__ps and graphban__fleet_status(view="live").
|
|
34
|
+
|
|
35
|
+
Read each watched item from agents[].holdings (id, status, phase). Live
|
|
36
|
+
omits offline agents and drops the bounced flag.
|
|
37
|
+
|
|
38
|
+
For a watched id that is in no holding, one graphban__search_items call
|
|
39
|
+
with query set to that id and fields=lean. Not get_item_details.
|
|
40
|
+
|
|
41
|
+
Call graphban__get_item_details for a single id only when its status or
|
|
42
|
+
phase changed since the last fire, or when you need the bounce reason.
|
|
35
43
|
|
|
36
44
|
Report <=12 lines: running vs stopped, each item's status/claim/PR, quiet
|
|
37
45
|
(silent_for / quiet), bounce, or expired-nothing-claimed.
|
|
@@ -42,7 +42,8 @@ _SKIP_DIRS = frozenset({
|
|
|
42
42
|
".next", "coverage", "vendor", ".gbfleet", ".hg", ".svn",
|
|
43
43
|
})
|
|
44
44
|
_MAX_BYTES = 1_000_000
|
|
45
|
-
|
|
45
|
+
# bandit B104: hostnames to recognise in a URL, nothing binds to them.
|
|
46
|
+
_LOCAL_HOSTS = frozenset({"localhost", "127.0.0.1", "::1", "0.0.0.0", "host.docker.internal"}) # nosec B104
|
|
46
47
|
#: Where a local host lands when no table row or single infra node of its kind claims it.
|
|
47
48
|
#: One node for "this machine", whatever spelling the URL used: `host.docker.internal` is
|
|
48
49
|
#: Docker's name for the same host `localhost` names, not a service the app calls.
|
|
@@ -35,7 +35,7 @@ from gban.doctor import FAIL, PASS, UNKNOWN, _line
|
|
|
35
35
|
#: What `docs/swamp.md` §5 names. Changing either renames somebody's existing wiring, so they
|
|
36
36
|
#: are constants rather than options.
|
|
37
37
|
VAULT = "secrets"
|
|
38
|
-
SECRET_KEY = "graphban-api-key"
|
|
38
|
+
SECRET_KEY = "graphban-api-key" # nosec B105 — the vault entry's NAME, not its value
|
|
39
39
|
VAULT_TYPE = "local_encryption"
|
|
40
40
|
|
|
41
41
|
#: The adapter repository, a SIBLING of the checkout — `docs/swamp.md` says "sibling, not this
|
|
@@ -154,6 +154,47 @@ def test_an_unreachable_server_is_a_different_exit_code_from_an_expired_session(
|
|
|
154
154
|
assert "could not reach" in capsys.readouterr().err
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
@pytest.mark.parametrize("url", ["file:///etc/passwd", "gb.example.com", "ftp://gb.example.com"])
|
|
158
|
+
def test_a_url_that_is_not_http_is_refused_before_anything_is_opened(url, monkeypatch):
|
|
159
|
+
"""GRPH-1112. urlopen reads `file:` too, and would send the credential along. Sabotage:
|
|
160
|
+
delete the scheme check and urlopen is reached, so this fails."""
|
|
161
|
+
from gban.client import Client
|
|
162
|
+
|
|
163
|
+
def opened(*_args, **_kwargs):
|
|
164
|
+
raise AssertionError("urlopen was reached")
|
|
165
|
+
|
|
166
|
+
monkeypatch.setattr("gban.client.urllib.request.urlopen", opened)
|
|
167
|
+
with pytest.raises(Unreachable, match="not an http\\(s\\) URL"):
|
|
168
|
+
Client(url, api_key="KEY").call("GET", "/api/whoami")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@pytest.mark.parametrize("url", ["http://127.0.0.1:8000", "https://gb.example.com/"])
|
|
172
|
+
def test_an_http_or_https_server_url_still_reaches_urlopen(url, monkeypatch):
|
|
173
|
+
"""GRPH-1112. The check must not refuse a configured server URL gban is meant to use."""
|
|
174
|
+
from gban.client import Client
|
|
175
|
+
seen = []
|
|
176
|
+
|
|
177
|
+
class _Response:
|
|
178
|
+
status = 200
|
|
179
|
+
|
|
180
|
+
def __enter__(self):
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
def __exit__(self, *_exc):
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def read(self):
|
|
187
|
+
return b'{"ok": true}'
|
|
188
|
+
|
|
189
|
+
def opened(request, timeout):
|
|
190
|
+
seen.append(request.full_url)
|
|
191
|
+
return _Response()
|
|
192
|
+
|
|
193
|
+
monkeypatch.setattr("gban.client.urllib.request.urlopen", opened)
|
|
194
|
+
assert Client(url, api_key="KEY").call("GET", "/api/whoami") == {"ok": True}
|
|
195
|
+
assert seen == [url.rstrip("/") + "/api/whoami"]
|
|
196
|
+
|
|
197
|
+
|
|
157
198
|
def test_a_refusal_is_printed_in_the_servers_own_words_with_its_hint(home, monkeypatch, capsys):
|
|
158
199
|
"""4 / D8. Sabotage: re-word the refusal and this fails — the client would then hold a
|
|
159
200
|
second copy of a rule the server already enforces."""
|
|
@@ -515,6 +515,8 @@ def test_setup_installs_the_skill_where_the_product_says_skills_live(tmp_path, w
|
|
|
515
515
|
dest = repo / ".claude" / "skills" / "graphban-delegation" / "SKILL.md"
|
|
516
516
|
assert dest.exists()
|
|
517
517
|
assert dest.read_text().startswith("---\nname: graphban-delegation\n")
|
|
518
|
+
other = repo / ".agents" / "skills" / "graphban-delegation" / "SKILL.md"
|
|
519
|
+
assert other.read_text() == dest.read_text()
|
|
518
520
|
# THE CALL: skipping `skill()` in run() would leave this dest missing the
|
|
519
521
|
# watcher paragraph while skill_source() tests still passed.
|
|
520
522
|
body = dest.read_text()
|
|
@@ -533,9 +535,11 @@ def test_setup_installs_every_shipped_skill(tmp_path, wired):
|
|
|
533
535
|
assert "graphban-delegation" in slugs
|
|
534
536
|
assert "watch-wave" in slugs
|
|
535
537
|
for slug in slugs:
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
538
|
+
body = setup_mod.skill_source(slug).read_text()
|
|
539
|
+
for rel in setup_mod.SKILL_DEST_DIRS:
|
|
540
|
+
dest = repo / rel / slug / "SKILL.md"
|
|
541
|
+
assert dest.exists(), dest
|
|
542
|
+
assert dest.read_text() == body
|
|
539
543
|
assert sum(1 for l in lines if l.get("name") == "skill" and l.get("status") == "PASS") == len(slugs)
|
|
540
544
|
|
|
541
545
|
|
|
@@ -552,6 +556,9 @@ def test_an_edited_skill_is_left_alone(tmp_path, wired):
|
|
|
552
556
|
|
|
553
557
|
assert dest.read_text() == "mine\n"
|
|
554
558
|
assert any("left alone" in l["detail"] for l in lines)
|
|
559
|
+
# The edited copy stays. The directory a different harness reads is still installed.
|
|
560
|
+
agents = repo / ".agents" / "skills" / "graphban-delegation" / "SKILL.md"
|
|
561
|
+
assert agents.read_text() == setup_mod.skill_source("graphban-delegation").read_text()
|
|
555
562
|
# Editing one skill does not skip the others.
|
|
556
563
|
other = repo / ".claude" / "skills" / "watch-wave" / "SKILL.md"
|
|
557
564
|
assert other.exists()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|