Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ on `gh`, `pr-fix` stops at its start and `pr-review` posts only a pull request's

| Server | Skills |
| --- | --- |
| GitHub | `work`, `pr-create`, `pr-review`, `pr-fix`, `ticket-draft`, `ticket-audit`, `revise`, `skill-feedback`; `work`'s ticket status too where the standard's tracker is GitHub's; `implement-change` when its source is a GitHub issue; `downstream`, for `ci <run>`; `onboard`, when connected, and its first rules; `rebase`, when connected, for the pull request's base |
| GitHub | `work`, `pr-create`, `pr-review`, `pr-fix`, `ticket-draft`, `ticket-audit`, `revise`, `skill-feedback`; `work`'s ticket status too where the standard's tracker is GitHub's; `implement-change` when its source is a GitHub issue; `downstream`, for `ci <run>`; `onboard`, when connected, the repository's issues and their labels for its tracker included, and its first rules; `rebase`, when connected, for the pull request's base |
| Jira | `ticket-draft`, `design-doc`, `architecture-doc` and `process-doc` (only the tickets you or a page name), `ticket-audit`, `revise` (with `measures`, for an epic it adds or a context entry reads, or for an issue `correct` cites); `pr-review`, for the ticket when no context page holds it; `work`, for a ticket's status when it has no context page here; `implement-change` when its source is a ticket; `onboard`, when connected, and its first rules |
| Confluence | `design-doc` (your notes, the area's pages, and the publish), `architecture-doc` (the system's page, the area's pages, and the publish), `process-doc` (your notes, the pages you point at, and the publish), `ticket-draft` (the year plan, and `investigate`), `revise` (the context refresh, or a page `correct` cites), `ticket-audit` (the year plan); `onboard`, when connected, and its first rules |
| Neither | `commit`, `rebase` with no GitHub server connected, `drawio`, `architecture-doc` and `process-doc` until they publish, `implement-change` from a file or a description, `test-suite-review`, `writing`, `quickstart`, `onboard` with no server connected, the repository alone |
Expand Down Expand Up @@ -772,7 +772,7 @@ session-start line or `work` says something unexpected, these files are what it
| `onboard/downstream.json` | `onboard`, from its read-only code search when GitHub is connected: the repositories that import or depend on this one's package, each with its count of files | `scripts/survey.py --downstream-from`, for the candidate's proposed `downstream` | Replaced at each survey |
| `onboard/profile.yaml` | `onboard`, through `scripts/survey.py --profile`: the candidate profile, every key it cannot fill reading `not yet recorded`, with downstream repositories and depth globs proposed for the person to trim, and `profile.sources.json` beside it, where each value came from | `sandbox/onboard_roundtrip.py`; the person | Replaced at each survey |
| `onboard/answers.json` | `onboard`, from the person's answers: each key they confirmed, trimmed or typed, in the form `scripts/survey.py --answers` takes | `scripts/survey.py --drop-in`; `sandbox/onboard_roundtrip.py`; the person | Replaced at each run that asks |
| `onboard/project.yaml` | `onboard`, for a new standard: the project's settings the person gave, the tracker's workflow and the internal domains, in the profile's form | `onboard`, at step 5, into the standard's `profile.yaml` on the person's yes | Replaced at each run that asks |
| `onboard/project.yaml` | `onboard`, for a new standard: the project's settings the person gave, the tracker's workflow (a Jira workflow as `jira:`, or GitHub Issues' labels as `issue_tracker:`, with a ticket pattern when one is recorded) and the internal domains, in the profile's form | `onboard`, at step 5, into the standard's `profile.yaml` on the person's yes | Replaced at each run that asks |
| `onboard/profile.dropin.yaml` | `onboard`, through `scripts/survey.py --drop-in --answers`: only the keys the survey filled and the person settled, so the team's values stand for the rest | `onboard`, at step 5, on the person's yes; the person, to place it; a sandbox variant in place of the hand-written profile | Replaced at each run that asks |
| `onboard/clone/<owner>__<repo>` | `onboard`, for a standard in another repository, in the main working tree's `.review/` whichever working tree it runs in: a clone of it, read-only until its step 5 writes this repository's settings there on a branch | `git config lasp-dev.standard`, which names it until the standard's pull request merges; `onboard`'s next run, which uses it as it stands | Kept; removed by the person once the pull request merges and the config is unset |
| `design/<slug>/` | `design-doc`, under the page ID or a topic slug: `notes.md`, `frame.md` (confirmed with `Confirmed:`), a `.drawio` file per diagram, `draft.md`, a `tickets/<n>.md` prompt per ticket to create; `page.md` in `revise` | `design-doc`, resuming from the frame and publishing `draft.md`; `ticket-draft`, given a prompt; the person, placing the diagrams | Not cleared by the tools |
Expand Down
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ merged pull request is its record. Owners are roles. Status is `planned`, `defer
| --- | --- | --- | --- | --- |
| Neutral examples in generic skills (#21) | `quickstart` still checks for the plugin this one replaced, and `using-curryer` still ships in the plugin, loaded only in curryer and the repositories that depend on it (#67). `scripts/coupling.json` allows the rest and does not scan the docs (#15). `using-curryer` leaves for curryer's own standard, or a domain plugin installed beside this one | plugin maintainer | before the public flip | planned |
| Forge, tracker and wiki as roles (#24) | Every skill speaks GitHub, Jira or Confluence directly, so a repository on GitLab or tracking work in GitHub Issues gets little of the workflow. One documented shape per role, the neutral thread JSON as the forge's, with the current skills as the first adapters; GitLab next, then GitHub Issues as a tracker | plugin maintainer | a team on GitLab or GitHub Issues adopts the plugin | idea |
| The skills on the ticket shape (#58) | The shape, the format and the profile's `issue_tracker:` are tracker-neutral (`docs/contracts.md`, "The ticket shape"; `standards/ticket-format.md`), but `onboard` still writes `jira:`. No ticket is kept as a file in the repository: a ticket is drafted once, in the format, for whichever tracker files it | plugin maintainer | with #59 | planned |
| GitHub Issues as a tracker (#59) | Most repositories outside LASP track work in GitHub Issues, through a server already connected for pull requests; it is the first tracker adapter after Jira. Reading an issue, `work` routing by its state and labels, `ticket-draft` drafting and filing one under an approved write plan with an allow-only gate, and `ticket-audit`'s `duplicates` and `pulls` sweeps closing issues through the same plan have landed. Left: onboard detecting GitHub Issues as the tracker, the project's branch convention and mapping its labels to the roles; no Projects fields, estimates or sub-issues | plugin maintainer | the ticket shape's profile and format land | planned |
| The skills on the ticket shape (#58) | The shape, the format and the profile's `issue_tracker:` are tracker-neutral (`docs/contracts.md`, "The ticket shape"; `standards/ticket-format.md`), but `onboard` still writes `jira:` for a Jira tracker. No ticket is kept as a file in the repository: a ticket is drafted once, in the format, for whichever tracker files it | plugin maintainer | with #59 | planned |
| GitHub Issues as a tracker (#59), what is left | GitHub Issues is the first tracker adapter after Jira; reading, routing, drafting and filing, the audit sweeps and onboard's detection have landed. Left: no Projects fields, estimates or sub-issues | plugin maintainer | a project that needs them | deferred |
| A GitLab forge adapter (#61) | A repository on GitLab gets local-only reviews; the adapter writes the thread shape (#60) through a GitLab server or `glab` | plugin maintainer | step 5 of #24's order, after #60 | planned |
| Bitbucket, other trackers, Markdown files as a wiki (#62) | No team has asked yet | plugin maintainer | a team asks | idea |

Expand Down
16 changes: 13 additions & 3 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,9 @@ page as a ticket and counts no gate, and `standard_file.sh` exits 1 (version 3).
and the shadow read are gone, with `.review/profile-shadow.log` and exit 3 (version 15).

Writers: `onboard`, on the person's yes at its step 5: the drop-in's settled keys and their derived gates, `public`
and, for a new standard, `.review/onboard/project.yaml` with a `decisions.md` holding none yet, into the standard's `profile.yaml` or,
and, for a new standard, `.review/onboard/project.yaml` (a Jira tracker as `jira:`, GitHub Issues as
`issue_tracker:` with `kind: github`, a `location` only for another repository's issues, and a
`ticket.pattern` only when one is recorded) with a `decisions.md` holding none yet, into the standard's `profile.yaml` or,
for a standard covering several repositories, its `profiles/<repo>.yaml`, with the pointer here; people, by hand from `standards/profile.example.yaml`;
the plugin, `standards/default-profile.yaml`; each repository's
maintainers, its `standards/profile.yaml`; and `profile.py`, through `survey.py --drop-in`, the
Expand Down Expand Up @@ -1325,7 +1327,7 @@ Pinned by: tests/test_check_static.py::test_the_update_writes_the_baseline

## The survey

Version: 10
Version: 11

Fields: `scripts/survey.py`'s printout, from the committed files of `--ref` (default `HEAD`)
only; a candidate file the checkout holds but git does not track is named on the `not committed`
Expand Down Expand Up @@ -1416,7 +1418,15 @@ as `where.sh` reads it or at a commit subject's start, a number followed by `-<n
date and no key (`onboard/WISER-2026-10-29`), and names the project when seen with two numbers or more,
or in a branch name, and with at least twice as many numbers as any other key so seen, so a history
where two projects are seen about as often names none; the commit habit counts the subjects that open with that
key. `--json` prints the same rows, their values whole, as a list of objects with `family`,
key. `GH-<n>` is GitHub's issue form and names no project. The `issue key` row, after `ticket
project`, reads GitHub issue keys in the same places (version 11): a branch name's `issue-<n>`,
`GH-<n>`, or `<n>` alone, each before `-<word>`, `_` or the name's end, so `2026-10-29-notes` is none,
and a subject opening with `#<n>` or `GH-<n>`, with the same date guard. Its value is
`branches: <form> <count>, … of <N>; subjects: <k> of <M>; pattern <p>`, `<p>` the ticket pattern the
branch forms imply as `where.sh` reads a GitHub tracker's branch: `(issue-)?\d+`, its default, for
`issue-<n>` and `<n>-words`; `GH-\d+` for `GH-<n>` alone; `mixed` for `GH-<n>` beside another
form; `none` with no branch form. No key holds it (`-`), and the candidate takes nothing from it,
since a GitHub tracker's pattern is only ever the one its standard records; `onboard` proposes it. `--json` prints the same rows, their values whole, as a list of objects with `family`,
`found_in`, `value`, `key`, `plugin_today`, `plugin_source`, `match` and `conflict`.

With `--profile <path>`, it also writes a candidate profile there, in the plugin's profile format
Expand Down
6 changes: 6 additions & 0 deletions sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ such a call then runs only when `hooks/issue-gate.sh` allows it, and the stub re
file, which each scenario's setup removes and its verify reads; nothing reaches GitHub. The stub's
tools carry github-mcp-server's names, arguments and output shapes.

The same pack's `tracker-gh-onboard` and `tracker-gh-onboard-mixed` run `onboard` with no
standard, each setup making its own history: one with no Jira key, where onboard reads the
issues and writes their labels as `issue_tracker:` under `.review/onboard/`, and one whose branch
names a Jira project beside the issues, where onboard asks which tracker holds the tickets. The
stub also answers onboard's `search_repositories` and `search_code`.

A project's own pack, which replays its
pull requests and tickets against its GitHub and Jira, read-only, is a file of the same form in a
directory beside this file, given with `--scenarios`, with a README that lists its scenarios and
Expand Down
14 changes: 9 additions & 5 deletions sandbox/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,9 @@ def holding(sandbox: Path, who: str, wait: bool):

def reset(sandbox: Path) -> None:
"""Both clones back to their pinned commits, out of any rebase or merge a run left paused, with untracked and ignored files gone but .venv kept,
origin fetching from the URL `sandbox.sh` gave it and kept as `sandbox.fetch`, and `main` and `origin/main` back at the `main`
of the bare repository under remotes/, the source's `main` when the sandbox was built, which a
scenario's setup may have moved."""
origin fetching from the URL `sandbox.sh` gave it and kept as `sandbox.fetch`, every `origin/*` back at the branches of the bare
repository under remotes/ and `main` at its `main`, the source's when the sandbox was built, and every other local branch
deleted, since a scenario's setup may have moved, made or deleted any of them."""
for name, (sha, branch) in pins(sandbox).items():
repo = sandbox / "repos" / name
fetch = subprocess.run(["git", "-C", str(repo), "config", "--get", "sandbox.fetch"], capture_output=True, text=True).stdout.strip()
Expand All @@ -361,8 +361,12 @@ def reset(sandbox: Path) -> None:
git(repo, "checkout", "-q", "-f", "-B", branch, sha)
git(repo, "reset", "-q", "--hard", sha)
git(repo, "clean", "-q", "-fdx", "-e", ".venv")
main = git(sandbox / "remotes" / f"{name}.git", "rev-parse", "--verify", "refs/heads/main")
git(repo, "update-ref", "refs/remotes/origin/main", main)
bare = sandbox / "remotes" / f"{name}.git"
git(repo, "fetch", "-q", "--prune", str(bare), "+refs/heads/*:refs/remotes/origin/*")
for other in git(repo, "for-each-ref", "refs/heads", "--format=%(refname:lstrip=2)").split():
if other not in (branch, "main"):
git(repo, "branch", "-q", "-D", other)
main = git(bare, "rev-parse", "--verify", "refs/heads/main")
if branch != "main":
git(repo, "update-ref", "refs/heads/main", main)

Expand Down
27 changes: 27 additions & 0 deletions sandbox/tracker-github/fixture/github-mcp-server-stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,9 +279,36 @@ def call(name: str, args: dict):
"updated_at": p["updated_at"],
"pull_request": {"html_url": url(args, "pull", p["number"]), "diff_url": url(args, "pull", p["number"]) + ".diff"}})
return {"incomplete_results": False, "items": [chosen(h, args.get("fields")) for h in hits], "total_count": len(hits)}
if name in SEARCHES:
return searched(name, args)
raise ValueError(f"unknown tool {name}")


# The two searches onboarding makes before it reads the issues: `search_repositories`, which
# returns the repository its query's `repo:` names, public, on `main`, as the server's minimal
# output gives it, and `search_code`, which finds nothing, so no standard elsewhere covers the
# repository.
SEARCHES = {
"search_repositories": ("Find GitHub repositories by name, description, readme, topics, or other metadata.",
{"query": TEXT, "sort": TEXT, "order": TEXT, "page": NUMBER, "perPage": NUMBER, "minimal_output": {"type": "boolean"}},
["query"]),
"search_code": ("Fast and precise code search across ALL GitHub repositories using GitHub's native search engine.",
{"query": TEXT, "sort": TEXT, "order": TEXT, "page": NUMBER, "perPage": NUMBER}, ["query"]),
}
TOOLS.update(SEARCHES)


def searched(name: str, args: dict) -> dict:
named = re.search(r"\brepo:([^/\s]+)/(\S+)", str(args.get("query", "")))
if name == "search_code" or not named:
return {"total_count": 0, "incomplete_results": False, "items": []}
owner, repo = named.groups()
return {"total_count": 1, "incomplete_results": False,
"items": [{"id": 2001, "name": repo, "full_name": f"{owner}/{repo}", "html_url": f"https://github.com/{owner}/{repo}",
"private": False, "fork": False, "archived": False, "default_branch": "main", "open_issues_count": 2,
"created_at": "2026-01-05T10:00:00Z", "updated_at": "2026-02-11T08:00:00Z"}]}


def schema(props: dict, required: list) -> dict:
return {"type": "object", "properties": props, "required": required}

Expand Down
Loading
Loading