lean-sdd 0.1.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.
- lean_sdd-0.1.0/.agents/.env.example +15 -0
- lean_sdd-0.1.0/.agents/README.md +139 -0
- lean_sdd-0.1.0/.agents/github-issue-contract.md +83 -0
- lean_sdd-0.1.0/.agents/github-mcp-contract.md +152 -0
- lean_sdd-0.1.0/.agents/lib/github-identity.sh +50 -0
- lean_sdd-0.1.0/.agents/lib/github-issues.sh +94 -0
- lean_sdd-0.1.0/.agents/lib/github-mcp.sh +52 -0
- lean_sdd-0.1.0/.agents/lib/github-pr.sh +250 -0
- lean_sdd-0.1.0/.agents/lib/mcp.sh +340 -0
- lean_sdd-0.1.0/.agents/skills/create-issue/SKILL.md +58 -0
- lean_sdd-0.1.0/.agents/skills/create-issue/scripts/create-issue.sh +21 -0
- lean_sdd-0.1.0/.agents/skills/finalize-implementation/SKILL.md +119 -0
- lean_sdd-0.1.0/.agents/skills/finalize-implementation/references/phase-contracts.md +163 -0
- lean_sdd-0.1.0/.agents/skills/finalize-implementation/scripts/merge-implementation.sh +68 -0
- lean_sdd-0.1.0/.agents/skills/finalize-implementation/scripts/publish-implementation.sh +48 -0
- lean_sdd-0.1.0/.agents/skills/get-issue/SKILL.md +52 -0
- lean_sdd-0.1.0/.agents/skills/get-issue/scripts/get-issue.sh +7 -0
- lean_sdd-0.1.0/.agents/skills/git-change-workflow/SKILL.md +64 -0
- lean_sdd-0.1.0/.agents/skills/git-change-workflow/scripts/prepare-change-branch.sh +63 -0
- lean_sdd-0.1.0/.agents/skills/list-repositories/SKILL.md +55 -0
- lean_sdd-0.1.0/.agents/skills/list-repositories/scripts/list-repositories.sh +63 -0
- lean_sdd-0.1.0/.agents/skills/update-issue/SKILL.md +157 -0
- lean_sdd-0.1.0/.agents/skills/update-issue/scripts/close-issue.sh +20 -0
- lean_sdd-0.1.0/.agents/skills/update-issue/scripts/comment-issue.sh +24 -0
- lean_sdd-0.1.0/.gitignore +55 -0
- lean_sdd-0.1.0/AGENTS.md +206 -0
- lean_sdd-0.1.0/CODING_STANDARDS.md +160 -0
- lean_sdd-0.1.0/CONSTITUTION.md +179 -0
- lean_sdd-0.1.0/LICENSE +21 -0
- lean_sdd-0.1.0/PKG-INFO +630 -0
- lean_sdd-0.1.0/README.md +621 -0
- lean_sdd-0.1.0/pyproject.toml +107 -0
- lean_sdd-0.1.0/src/lean_sdd/__init__.py +1 -0
- lean_sdd-0.1.0/src/lean_sdd/__main__.py +6 -0
- lean_sdd-0.1.0/src/lean_sdd/cli.py +65 -0
- lean_sdd-0.1.0/src/lean_sdd/doctor.py +150 -0
- lean_sdd-0.1.0/src/lean_sdd/installer.py +108 -0
- lean_sdd-0.1.0/src/lean_sdd/template_overrides/gitignore +5 -0
- lean_sdd-0.1.0/src/lean_sdd/template_overrides/readiness.md +28 -0
- lean_sdd-0.1.0/src/lean_sdd/templates.py +52 -0
- lean_sdd-0.1.0/tests/test_distribution.py +133 -0
- lean_sdd-0.1.0/tests/test_doctor.py +71 -0
- lean_sdd-0.1.0/tests/test_installer.py +130 -0
- lean_sdd-0.1.0/tests/test_skill_invocations.py +176 -0
- lean_sdd-0.1.0/uv.lock +307 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copy to .agents/.env locally; never commit the populated file.
|
|
2
|
+
# Skill invocations use: uv run --no-project --env-file .agents/.env -- bash ...
|
|
3
|
+
# uv parses dotenv data; do not source this file or put shell commands in it.
|
|
4
|
+
# Optional export prefixes are supported. Inherited environment values win.
|
|
5
|
+
export GITHUB_MCP_URL="https://api.githubcopilot.com/mcp/"
|
|
6
|
+
# Add your token privately here, or supply GITHUB_TOKEN from a secret manager.
|
|
7
|
+
# export GITHUB_TOKEN=''
|
|
8
|
+
export MCP_TOOLSETS=""
|
|
9
|
+
export MCP_PROTOCOL_VERSION="2025-06-18"
|
|
10
|
+
export MCP_CONNECT_TIMEOUT=10
|
|
11
|
+
export MCP_TIMEOUT=30
|
|
12
|
+
# Empty MCP_TOOLSETS uses endpoint defaults; verify current deployed capabilities.
|
|
13
|
+
# Git SSH/HTTPS credentials are separate from the MCP token.
|
|
14
|
+
# OWNER, REPO, WORKING_BRANCH, and issue/PR identities are explicit workflow
|
|
15
|
+
# arguments. Bind them in the calling shell after selection/readback, not here.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Lean-SDD workflow setup
|
|
2
|
+
|
|
3
|
+
The complete orchestration is in [AGENTS.md](../AGENTS.md). Engineering rules
|
|
4
|
+
live in [CONSTITUTION.md](../CONSTITUTION.md) and
|
|
5
|
+
[CODING_STANDARDS.md](../CODING_STANDARDS.md). Read these before implementation.
|
|
6
|
+
|
|
7
|
+
## Layout and prerequisites
|
|
8
|
+
|
|
9
|
+
Paths are relative to the adopting project root. Six skills under `skills/`
|
|
10
|
+
provide Bash entry points; shared helpers live in `lib/`. Scripts perform
|
|
11
|
+
individual operations, while the agent owns sequencing and human approval.
|
|
12
|
+
|
|
13
|
+
The workflow requires uv, Bash 4 or newer, Git, curl, jq, and standard shell tools
|
|
14
|
+
including awk, grep, and mktemp. On systems shipping older Bash, install a
|
|
15
|
+
compatible version and make it available as `bash`. Git uses separate SSH/HTTPS
|
|
16
|
+
credentials. uv loads the local environment file for skill invocations, even
|
|
17
|
+
when the application uses another language. Python projects also supply their
|
|
18
|
+
declared validation tools; the Bash skills have no Python package dependencies.
|
|
19
|
+
|
|
20
|
+
Supply an already provisioned GitHub repository and explicitly select its
|
|
21
|
+
owner/repository and existing working/base branch. Both effective origin fetch
|
|
22
|
+
and push URLs must identify that repository. No base defaults to `main`.
|
|
23
|
+
Provisioning and remote replacement are outside this workflow.
|
|
24
|
+
|
|
25
|
+
`lean-sdd doctor` performs local diagnostics without loading `.env`, contacting
|
|
26
|
+
GitHub, running project code, or proving readiness. A missing credential is
|
|
27
|
+
reported as a warning so installation can be inspected before credentials are
|
|
28
|
+
configured. Local warnings still need resolution where relevant to live use.
|
|
29
|
+
|
|
30
|
+
## Environment and capability verification
|
|
31
|
+
|
|
32
|
+
Prepare an untracked `.agents/.env` from [.env.example](.env.example) without
|
|
33
|
+
overwriting an existing file. Add `GITHUB_TOKEN` privately, or provide it through
|
|
34
|
+
your environment/secret manager. The installer never creates or copies secrets.
|
|
35
|
+
In a new worktree, provision this local file separately; untracked files do not
|
|
36
|
+
arrive with a checkout. Keep it even when all settings are supplied externally,
|
|
37
|
+
because the explicit `--env-file` path must exist.
|
|
38
|
+
|
|
39
|
+
Invoke every skill from the project root with:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv run --no-project --env-file .agents/.env -- \
|
|
43
|
+
bash .agents/skills/get-issue/scripts/get-issue.sh \
|
|
44
|
+
"$OWNER" "$REPO" "$ISSUE_NUMBER"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This example requires the actual retained issue identity, bound in the calling
|
|
48
|
+
shell. uv loads dotenv data into the child process without executing it as shell
|
|
49
|
+
code. The Bash scripts themselves do not auto-load configuration. `--no-project`
|
|
50
|
+
prevents application project discovery/synchronization for these Bash commands.
|
|
51
|
+
Existing environment variables take precedence over matching `.env` entries;
|
|
52
|
+
remove a stale exported value explicitly if you intend to use the file's value.
|
|
53
|
+
Do not source `.env`, print it, enable shell tracing, or place tokens in arguments.
|
|
54
|
+
|
|
55
|
+
The calling shell expands `"$OWNER"`, paths, and other arguments **before** uv
|
|
56
|
+
loads `.env`. Keep repository/base selection and retained issue/PR state out of
|
|
57
|
+
that file and bind them explicitly before invocation. Loading secrets for one
|
|
58
|
+
command does not export them into subsequent Codex shell commands; use the
|
|
59
|
+
prefix on every invocation. A uv failure stops dependent work without bypassing
|
|
60
|
+
the loader or retrying a write.
|
|
61
|
+
|
|
62
|
+
Store result JSON in a private, durable directory outside the worktree; use
|
|
63
|
+
`umask 077` and parse JSON with jq, never source or evaluate it.
|
|
64
|
+
|
|
65
|
+
Before live operations, verify the deployed complete schemas, both phase
|
|
66
|
+
contracts, and identity through the actual Bash libraries. Bind `AGENT_DIR` to
|
|
67
|
+
this project's absolute `.agents` path and `EVIDENCE_DIR` to an existing private
|
|
68
|
+
directory outside the worktree in the calling shell. This example performs MCP
|
|
69
|
+
reads only and passes those paths as data into a single uv-managed Bash process:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv run --no-project --env-file .agents/.env -- \
|
|
73
|
+
bash -s -- "$AGENT_DIR" "$EVIDENCE_DIR" <<'BASH'
|
|
74
|
+
set -euo pipefail
|
|
75
|
+
umask 077
|
|
76
|
+
AGENT_DIR=$1
|
|
77
|
+
EVIDENCE_DIR=$2
|
|
78
|
+
source "$AGENT_DIR/lib/github-mcp.sh"
|
|
79
|
+
source "$AGENT_DIR/lib/github-pr.sh"
|
|
80
|
+
mcp_connect "$GITHUB_MCP_URL" "$GITHUB_TOKEN" > "$EVIDENCE_DIR/initialize.json"
|
|
81
|
+
mcp_list_tools > "$EVIDENCE_DIR/tools.json"
|
|
82
|
+
github_mcp_check_capabilities "$EVIDENCE_DIR/tools.json"
|
|
83
|
+
c_phase=publication
|
|
84
|
+
c_capabilities "$EVIDENCE_DIR/tools.json"
|
|
85
|
+
c_phase=merge
|
|
86
|
+
c_capabilities "$EVIDENCE_DIR/tools.json"
|
|
87
|
+
mcp_call get_me '{}' > "$EVIDENCE_DIR/identity.json"
|
|
88
|
+
BASH
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
To check the loaded configuration locally without printing values or contacting
|
|
92
|
+
GitHub, use the installed CLI through the same loader:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
uv run --no-project --env-file .agents/.env -- lean-sdd doctor .
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Plain `lean-sdd doctor` still inspects only its inherited environment; it does
|
|
99
|
+
not parse `.env` itself. Neither form proves credential validity or permission.
|
|
100
|
+
|
|
101
|
+
The foundation and phase checks require the actual tools and fields consumed by
|
|
102
|
+
the scripts, including `expectedHeadSha` protection and merge-commit support.
|
|
103
|
+
Unsupported capabilities stop dependent work without a direct API or CLI
|
|
104
|
+
fallback. Identity access alone does not establish write, merge, or Git push
|
|
105
|
+
permission. Do not publish personal identity or session evidence.
|
|
106
|
+
|
|
107
|
+
## Using the workflow
|
|
108
|
+
|
|
109
|
+
Read the corresponding `SKILL.md` before each operation and require exit 0 before
|
|
110
|
+
advancing. `AGENTS.md` defines three distinct approval gates:
|
|
111
|
+
|
|
112
|
+
1. Create and verify the full issue; stop for implementation approval. Retrieve
|
|
113
|
+
it again after approval, recheck the engineering rules, and prepare its branch.
|
|
114
|
+
2. Implement, validate, commit, and record the implementation checkpoint; stop
|
|
115
|
+
for publication approval.
|
|
116
|
+
3. Publish and verify the PR, retain the complete successful JSON result, and
|
|
117
|
+
record the PR checkpoint; stop for separate merge approval.
|
|
118
|
+
4. Merge only the retained approved source, verify remote results and local base
|
|
119
|
+
synchronization, record finalization, then separately close/read back the issue.
|
|
120
|
+
|
|
121
|
+
A failed operation stops dependent work. Preserve known remote effects and
|
|
122
|
+
uncertainty, following the failure checkpoint rules in `AGENTS.md`. Do not
|
|
123
|
+
blindly retry uncertain writes or substitute new observations for approved state.
|
|
124
|
+
|
|
125
|
+
## Validation and readiness
|
|
126
|
+
|
|
127
|
+
The adopting Python project supplies its own source, tests, `pyproject.toml`,
|
|
128
|
+
and `uv.lock`. Required commands are defined in `AGENTS.md`: pytest, Ruff linting
|
|
129
|
+
and formatting, and mypy through `uv run`. The installer does not create an
|
|
130
|
+
application scaffold or execute those commands in the target project.
|
|
131
|
+
|
|
132
|
+
Verify current server capabilities and repository permissions before live use.
|
|
133
|
+
Installer tests and local diagnostics do not establish those permissions.
|
|
134
|
+
|
|
135
|
+
Operational references:
|
|
136
|
+
|
|
137
|
+
- [MCP transport contract](github-mcp-contract.md)
|
|
138
|
+
- [Issue and branch contract](github-issue-contract.md)
|
|
139
|
+
- [Publication and merge contracts](skills/finalize-implementation/references/phase-contracts.md)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# GitHub issue and branch contract
|
|
2
|
+
|
|
3
|
+
## Inputs and entry points
|
|
4
|
+
|
|
5
|
+
Paths below are relative to the adopting project root. Every
|
|
6
|
+
entry point is Bash, exits nonzero on failure with concise stderr, and emits one
|
|
7
|
+
JSON object on stdout only for success. Require uv for environment loading,
|
|
8
|
+
Bash, jq, and curl for MCP, and Git for branch preparation. From the project root,
|
|
9
|
+
prefix each entry point below with
|
|
10
|
+
`uv run --no-project --env-file .agents/.env -- bash .agents/skills/`.
|
|
11
|
+
uv loads `GITHUB_MCP_URL` and `GITHUB_TOKEN` from the untracked environment file,
|
|
12
|
+
preserving values already exported. Bind explicit arguments before invoking uv;
|
|
13
|
+
do not source `.env` or returned data as arbitrary code. See
|
|
14
|
+
[environment setup](README.md#environment-and-capability-verification).
|
|
15
|
+
The shared transport owns MCP sessions, requests, timeouts, and cleanup. Issue
|
|
16
|
+
scripts use owning-shell calls with output redirected to private temporary files.
|
|
17
|
+
|
|
18
|
+
| Operation | Invocation after the uv/Bash prefix above | Success contract |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| Discovery | `list-repositories/scripts/list-repositories.sh "$QUERY"` | query, normalized repositories, total_count, pages, incomplete_results/reasons |
|
|
21
|
+
| Create | `create-issue/scripts/create-issue.sh "$OWNER" "$REPO" "$TITLE" "$BODY"` | full normalized issue and returned database issue_id |
|
|
22
|
+
| Read | `get-issue/scripts/get-issue.sh "$OWNER" "$REPO" "$ISSUE_NUMBER"` | full normalized issue |
|
|
23
|
+
| Comment | `update-issue/scripts/comment-issue.sh "$OWNER" "$REPO" "$ISSUE_NUMBER" "$COMMENT"` | explicit issue identity, returned comment_id/comment_url |
|
|
24
|
+
| Close | `update-issue/scripts/close-issue.sh "$OWNER" "$REPO" "$ISSUE_NUMBER"` | explicit issue identity, returned issue_url, verified state=closed |
|
|
25
|
+
| Branch | `git-change-workflow/scripts/prepare-change-branch.sh "$OWNER" "$REPO" "$ISSUE_NUMBER" "$WORKING_BRANCH"` | owner, repo, issue_number, issue_reference, BRANCH, WORKING_BRANCH, starting SOURCE_COMMIT |
|
|
26
|
+
|
|
27
|
+
A normalized issue has `owner`, `repo`, positive numeric `issue_number`, returned
|
|
28
|
+
`issue_url`, complete `title`/`body`, observed `state`, optional observed
|
|
29
|
+
`updated_at`, `issue_reference=owner/repo#number`, and `BRANCH=gh-number`.
|
|
30
|
+
Numbers must be canonical positive decimal integers of at most 15 digits, within
|
|
31
|
+
jq's exact numeric range. Database issue/comment IDs remain strings. Missing or
|
|
32
|
+
null bodies fail; an actual empty string is preserved. GitHub URL identity is
|
|
33
|
+
checked case-insensitively against the explicitly selected repository.
|
|
34
|
+
|
|
35
|
+
`lib/github-identity.sh` contains lexical identity and canonical Git remote
|
|
36
|
+
validation. `lib/github-issues.sh` contains the narrow shared MCP domain decoder
|
|
37
|
+
and issue identity checks. Structured content must match the expected type
|
|
38
|
+
(object by default; pull-request helpers request arrays for PR/review lists).
|
|
39
|
+
Accompanying text, if present, must decode to that same value. Otherwise require
|
|
40
|
+
exactly one text JSON value of that type. Ambiguous representations are errors.
|
|
41
|
+
|
|
42
|
+
Discovery calls `get_me` then `search_repositories` with the exact caller query,
|
|
43
|
+
`minimal_output=false`, `perPage=100`, and successive pages. GitHub documents a
|
|
44
|
+
[1,000-result search boundary](https://github.com/github/docs/blob/main/content/rest/search/search.md).
|
|
45
|
+
Repeated results, changed totals, premature endings, server incomplete flags,
|
|
46
|
+
and that boundary are explicit reasons for incompleteness. Empty results are
|
|
47
|
+
valid. Repository records preserve owner/name/URL and optional visibility,
|
|
48
|
+
private, and archived metadata; they never assert write access or select a repo.
|
|
49
|
+
|
|
50
|
+
Branch preparation checks both effective fetch and push destinations using
|
|
51
|
+
`git remote get-url --all origin` and `git remote get-url --push --all origin`,
|
|
52
|
+
including Git's URL rewriting. Exactly one supported canonical GitHub URL each
|
|
53
|
+
must match the selected repository. SSH host aliases and local remotes are not
|
|
54
|
+
identity proof. The script never rewrites remotes or injects MCP credentials.
|
|
55
|
+
It requires attached HEAD, writable Git metadata, a clean worktree (including
|
|
56
|
+
untracked files), an existing local explicit base, and an absent issue branch.
|
|
57
|
+
It synchronizes only with `pull --ff-only`, rejects a local-ahead result, then
|
|
58
|
+
creates the branch at the fetched commit. Failures preserve and report actual
|
|
59
|
+
state, including when switching or fetching already happened. Git transport
|
|
60
|
+
errors are summarized because raw errors can expose credential-bearing URLs.
|
|
61
|
+
|
|
62
|
+
## Workflow gates
|
|
63
|
+
|
|
64
|
+
- After creation, stop for implementation approval; read the current issue anew
|
|
65
|
+
and recheck the constitution before branch preparation. Preserve the full
|
|
66
|
+
current plan and explicit owner/repo/number throughout.
|
|
67
|
+
- Use `update-issue/SKILL.md` templates for implementation, pull-request,
|
|
68
|
+
finalization, and failure checkpoints, populated only with actual facts.
|
|
69
|
+
- Implementation checkpoint success precedes separate publication approval.
|
|
70
|
+
Successful publication and PR checkpoint precede separate merge approval.
|
|
71
|
+
Retain actual publication values, including the full implementation
|
|
72
|
+
source SHA and explicit working branch, across the merge gate. The branch
|
|
73
|
+
preparation SOURCE_COMMIT is only its starting commit.
|
|
74
|
+
- The merge script must use the schema-verified `expectedHeadSha` for the
|
|
75
|
+
approved merge and verify the remote merge and local synchronization before
|
|
76
|
+
finalization.
|
|
77
|
+
- Use non-closing `owner/repo#number` references in commits/PR descriptions.
|
|
78
|
+
Closure remains a separate `update-issue` operation, invoked only after
|
|
79
|
+
the finalization comment succeeds. A confirmed update alone is insufficient;
|
|
80
|
+
targeted readback must confirm the same issue closed.
|
|
81
|
+
- A failed comment stops without recursive audit. Unconfirmed closure stops
|
|
82
|
+
without assuming open/closed or adding a failure comment. Unknown create/write
|
|
83
|
+
outcomes must be reconciled, never automatically retried.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# GitHub MCP transport contract
|
|
2
|
+
|
|
3
|
+
## Required server capabilities
|
|
4
|
+
|
|
5
|
+
Verify the deployed tool schemas and authenticated identity before live
|
|
6
|
+
operations. Follow [setup](README.md) for verification commands. Input schemas
|
|
7
|
+
alone do not establish response fields or repository permissions.
|
|
8
|
+
|
|
9
|
+
| Required operation | Advertised tool and input contract |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Repository discovery | `search_repositories`: `query`, `page`, `perPage` |
|
|
12
|
+
| Authenticated identity | `get_me`: empty arguments |
|
|
13
|
+
| Issue create/update | `issue_write`: `method` enum includes `create`, `update`; `owner`, `repo`, `issue_number`, `title`, `body` |
|
|
14
|
+
| Issue read | `issue_read`: `method=get`, `owner`, `repo`, `issue_number` |
|
|
15
|
+
| Issue close | `issue_write`: `method=update`, `state=closed`, `state_reason=completed`; verify afterward with `issue_read` |
|
|
16
|
+
| Audit comment | `add_issue_comment`: `owner`, `repo`, `issue_number`, `body` (do not use the reaction alternatives) |
|
|
17
|
+
| PR create | `create_pull_request`: `owner`, `repo`, `title`, `body`, `head`, `base` |
|
|
18
|
+
| PR read | `pull_request_read`: `method=get`, `owner`, `repo`, `pullNumber` |
|
|
19
|
+
| PR list | `list_pull_requests`: `owner`, `repo`, `head`, `base`, `state`, `page`, `perPage` |
|
|
20
|
+
| PR merge | `merge_pull_request`: `owner`, `repo`, `pullNumber`, `merge_method=merge`, `expectedHeadSha` |
|
|
21
|
+
|
|
22
|
+
`issue_write.method` advertises `create` and `update`. `issue_read.method`
|
|
23
|
+
advertises `get`, `get_comments`, `get_sub_issues`, `get_parent`, `get_labels`.
|
|
24
|
+
`pull_request_read.method` advertises `get`, `get_diff`, `get_status`,
|
|
25
|
+
`get_files`, `get_commits`, `get_review_comments`, `get_reviews`,
|
|
26
|
+
`get_comments`, `get_check_runs`.
|
|
27
|
+
|
|
28
|
+
`expectedHeadSha` is a string property in the actual merge input schema.
|
|
29
|
+
Although the server marks it optional, **the merge script must require the
|
|
30
|
+
retained approved SOURCE_COMMIT and always send it**. A preceding read cannot
|
|
31
|
+
replace that atomic expected-head safeguard.
|
|
32
|
+
|
|
33
|
+
## Transport interface
|
|
34
|
+
|
|
35
|
+
Source `lib/mcp.sh` once in the shell that owns the session. Public functions
|
|
36
|
+
return one JSON success object on stdout, concise diagnostics on stderr, and a
|
|
37
|
+
nonzero status on failure. Capture their stdout with redirection; do not put
|
|
38
|
+
public calls in command substitutions, pipelines, or background subshells.
|
|
39
|
+
The library checks the owning Bash process and rejects that misuse.
|
|
40
|
+
|
|
41
|
+
Bind `output_dir` to an existing private directory in the calling shell, then
|
|
42
|
+
load configuration into the Bash process that will own the MCP session:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv run --no-project --env-file .agents/.env -- \
|
|
46
|
+
bash -s -- "$output_dir" <<'BASH'
|
|
47
|
+
set -euo pipefail
|
|
48
|
+
output_dir=$1
|
|
49
|
+
source .agents/lib/mcp.sh
|
|
50
|
+
source .agents/lib/github-mcp.sh
|
|
51
|
+
umask 077
|
|
52
|
+
mcp_connect "$GITHUB_MCP_URL" "$GITHUB_TOKEN" > "$output_dir/initialize.json"
|
|
53
|
+
mcp_list_tools > "$output_dir/tools.json"
|
|
54
|
+
github_mcp_check_capabilities "$output_dir/tools.json"
|
|
55
|
+
mcp_call get_me '{}' > "$output_dir/identity.json"
|
|
56
|
+
# Reading completed output with $(jq ...) is safe: it changes no session state.
|
|
57
|
+
BASH
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- `mcp_connect <endpoint> <token>` resets connection state, initializes, verifies
|
|
61
|
+
the negotiated version and server metadata/tools capability, sends the
|
|
62
|
+
initialized notification, then returns the complete initialize result.
|
|
63
|
+
- `mcp_list_tools` follows every cursor and returns `{"tools":[...]}` only after
|
|
64
|
+
complete discovery. Repeated cursors, duplicate names, malformed pages and
|
|
65
|
+
more than 100 pages fail; no partial list is emitted.
|
|
66
|
+
- `mcp_call <tool-name> <arguments-json>` accepts one JSON object, returns the
|
|
67
|
+
**complete MCP tool result**, and does not parse text into domain data.
|
|
68
|
+
- `github_mcp_check_capabilities <tools-json-file>` checks the narrow input
|
|
69
|
+
contract above. Its success object explicitly says write access is unverified.
|
|
70
|
+
Missing tools, methods or fields block dependent operations. The integrated
|
|
71
|
+
setup also runs `c_capabilities` for publication and merge from
|
|
72
|
+
`lib/github-pr.sh` to check PR diagnostics/pagination and `get_commit`.
|
|
73
|
+
|
|
74
|
+
IDs are allocated before the HTTP subprocess runs, in the owning shell:
|
|
75
|
+
initialize uses 1; subsequent requests use 2, 3, etc. Notifications have no ID.
|
|
76
|
+
Each connection owns its optional session ID and negotiated protocol.
|
|
77
|
+
|
|
78
|
+
The supported protocol versions are `2025-06-18` (default) and `2025-03-26`.
|
|
79
|
+
An unknown or absent negotiated version is an error. The implementation follows
|
|
80
|
+
[MCP Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports)
|
|
81
|
+
and [initialization](https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle).
|
|
82
|
+
It supports plain JSON and terminated SSE message/default events, including
|
|
83
|
+
multiline data and CRLF. It validates one JSON-RPC envelope per message,
|
|
84
|
+
correlates responses, ignores unrelated events/notifications, and rejects
|
|
85
|
+
malformed/incomplete messages, duplicate matching responses, or server requests
|
|
86
|
+
for capabilities it does not advertise. It does not implement stream replay,
|
|
87
|
+
background listening, sampling, elicitation, or a general MCP SDK.
|
|
88
|
+
|
|
89
|
+
Requests use only the supplied HTTPS endpoint. Redirects and retries are
|
|
90
|
+
disabled, and curl's configuration file is disabled. `MCP_CONNECT_TIMEOUT`
|
|
91
|
+
(default 10) and `MCP_TIMEOUT` (default 30) accept integer seconds from 1 to 300.
|
|
92
|
+
`MCP_TOOLSETS`, when nonempty, is sent as `X-MCP-Toolsets`; any selection must
|
|
93
|
+
be verified again rather than assumed available. The verified configuration
|
|
94
|
+
uses the endpoint defaults. A GitHub MCP hostname is not a direct REST API.
|
|
95
|
+
|
|
96
|
+
Each HTTP subprocess owns a mode-0700 temporary directory and mode-0600
|
|
97
|
+
credential/payload files. Success, failure and handled interrupts clean those
|
|
98
|
+
files without replacing caller traps. As with any process, SIGKILL cannot run
|
|
99
|
+
cleanup. Callers must keep shell tracing off and protect their own success
|
|
100
|
+
output files, which can contain issue text. Tokens are not placed in curl's
|
|
101
|
+
argument vector; raw error bodies and curl diagnostics are never printed.
|
|
102
|
+
|
|
103
|
+
Network/HTTP, protocol, session, parsing and application errors propagate.
|
|
104
|
+
Failures invalidate the usable connection. An HTTP 404 requires an explicit
|
|
105
|
+
new connection before any later request. A write whose response is lost has
|
|
106
|
+
an **unconfirmed outcome**: no automatic retry or replay is attempted. The
|
|
107
|
+
error identifies the method/tool and request ID, without arguments or server
|
|
108
|
+
body. The orchestrator must stop and reconcile under its explicit policy.
|
|
109
|
+
|
|
110
|
+
## Shared domain contracts
|
|
111
|
+
|
|
112
|
+
These are **required normalized outputs**, not invented GitHub response schemas.
|
|
113
|
+
The issue and pull-request helpers map observed MCP results, including text
|
|
114
|
+
JSON or structured content, and must retain evidence for any newly consumed
|
|
115
|
+
server fields. Parse JSON as data with jq; never source or eval it. GitHub
|
|
116
|
+
service operations use only the configured official GitHub MCP server.
|
|
117
|
+
|
|
118
|
+
Identity inputs are explicit `owner`, `repo` and a positive integer
|
|
119
|
+
`issue_number`. Retain the returned issue URL. The human reference is
|
|
120
|
+
`owner/repo#number`; the issue branch is `gh-<issue-number>`. GitHub opaque IDs,
|
|
121
|
+
issue numbers and PR numbers are distinct. Never infer the repository from a
|
|
122
|
+
search hit, the issue from a branch, or `WORKING_BRANCH` from a default.
|
|
123
|
+
|
|
124
|
+
| Result | Fields the domain implementation must supply and validate |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| Issue | `owner`, `repo`, `issue_number`, `issue_url`, complete `title`, complete `body`, observed `state` |
|
|
127
|
+
| Comment | `owner`, `repo`, `issue_number`, returned `comment_id` |
|
|
128
|
+
| Discovery | `repositories` with returned identities/URLs; actual pagination and incomplete-result indication; never silently present partial results as complete |
|
|
129
|
+
| Publication | `owner`, `repo`, `issue_number`, `issue_url`, `PR_ID` (PR number), `PR_URL`, `SOURCE_COMMIT`, `BRANCH`, `WORKING_BRANCH` |
|
|
130
|
+
| Finalization | retained publication identity, verified `merged` result, actual `merge_commit`, actual local synchronization result and working-branch commit |
|
|
131
|
+
| Closure | explicit repository/issue identity and observed `state=closed`; otherwise an explicit unconfirmed outcome with nonzero exit |
|
|
132
|
+
|
|
133
|
+
Validate actual identities against requested/retained inputs, required field
|
|
134
|
+
presence and types, positive integer numbers, full specification text, returned
|
|
135
|
+
URLs and observed state before emitting success. Absent fields, `isError`,
|
|
136
|
+
wrong identity, malformed data or ambiguous output cannot become success.
|
|
137
|
+
Transport success alone does not establish domain success. Preserve approved
|
|
138
|
+
publication values literally across approval gates. Finalization must verify
|
|
139
|
+
both the remote merge and the actual local synchronization outcome; successful
|
|
140
|
+
write submission is insufficient to claim merge or closure.
|
|
141
|
+
|
|
142
|
+
## Validation and deployment prerequisites
|
|
143
|
+
|
|
144
|
+
Run `uv run pytest`, `uv run ruff check .`, `uv run ruff format --check .`,
|
|
145
|
+
`uv run mypy .`, and `bash -n` on the two library scripts. Production never
|
|
146
|
+
loads the fixtures or Python test code.
|
|
147
|
+
|
|
148
|
+
Remaining deployment prerequisites: explicitly selected GitHub repository,
|
|
149
|
+
appropriate token/policy access to that repository, separately working Git
|
|
150
|
+
SSH/HTTPS credentials, the applicable approvals and
|
|
151
|
+
verification of live operations. Repository provisioning and remote replacement
|
|
152
|
+
are outside this workflow.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared lexical identity checks. No network, Git, or shell evaluation of data.
|
|
3
|
+
github_error() {
|
|
4
|
+
printf 'ERROR: GitHub %s\n' "$1" >&2
|
|
5
|
+
return 1
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
github_repository_input() {
|
|
9
|
+
[[ $# == 2 && "$1" =~ ^[A-Za-z0-9][A-Za-z0-9-]{0,38}$ && "$1" != *- && "$1" != *--* &&
|
|
10
|
+
"$2" =~ ^[A-Za-z0-9_.-]{1,100}$ && "$2" != . && "$2" != .. ]] || {
|
|
11
|
+
github_error 'explicit valid owner and repository are required'; return 1;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
github_issue_input() {
|
|
16
|
+
[[ $# == 3 ]] || { github_error 'owner, repository and issue number are required'; return 1; }
|
|
17
|
+
github_repository_input "$1" "$2" || return 1
|
|
18
|
+
# Bound integers to exact jq/JSON numeric representation; no leading zeros.
|
|
19
|
+
[[ "$3" =~ ^[1-9][0-9]{0,14}$ ]] || {
|
|
20
|
+
github_error 'issue number must be a positive integer of at most 15 digits'; return 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
github_text_input() {
|
|
25
|
+
[[ $# == 1 && "$1" =~ [^[:space:]] ]] || {
|
|
26
|
+
github_error 'nonempty meaningful text is required'; return 1;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Output canonical owner/repo only. Credential-bearing URLs are never printed.
|
|
31
|
+
# get-url callers must supply Git's expanded effective URL, not raw config.
|
|
32
|
+
github_remote_identity() {
|
|
33
|
+
local url="$1" path='' authority=''
|
|
34
|
+
case "$url" in
|
|
35
|
+
https://*)
|
|
36
|
+
path="${url#https://}"; authority="${path%%/*}"
|
|
37
|
+
[[ "${authority##*@}" == github.com ]] || return 1
|
|
38
|
+
[[ "$path" == */* ]] || return 1
|
|
39
|
+
path="${path#*/}"
|
|
40
|
+
;;
|
|
41
|
+
git@github.com:*) path="${url#git@github.com:}" ;;
|
|
42
|
+
ssh://git@github.com/*) path="${url#ssh://git@github.com/}" ;;
|
|
43
|
+
ssh://git@github.com:22/*) path="${url#ssh://git@github.com:22/}" ;;
|
|
44
|
+
*) return 1 ;;
|
|
45
|
+
esac
|
|
46
|
+
path="${path%.git}"
|
|
47
|
+
[[ "$path" == */* && "${path#*/}" != */* ]] || return 1
|
|
48
|
+
github_repository_input "${path%%/*}" "${path#*/}" 2>/dev/null || return 1
|
|
49
|
+
printf '%s\n' "${path,,}"
|
|
50
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Helpers for the B executable entry points. Each entry point owns this scratch
|
|
3
|
+
# directory and EXIT trap; A's HTTP library preserves these caller traps.
|
|
4
|
+
source "$(dirname -- "${BASH_SOURCE[0]}")/github-identity.sh"
|
|
5
|
+
source "$(dirname -- "${BASH_SOURCE[0]}")/mcp.sh"
|
|
6
|
+
|
|
7
|
+
_GITHUB_SCRATCH=''
|
|
8
|
+
|
|
9
|
+
github_connect() {
|
|
10
|
+
[[ -n "${GITHUB_MCP_URL:-}" && -n "${GITHUB_TOKEN:-}" ]] || {
|
|
11
|
+
github_error 'GITHUB_MCP_URL and GITHUB_TOKEN are required'; return 1;
|
|
12
|
+
}
|
|
13
|
+
umask 077
|
|
14
|
+
_GITHUB_SCRATCH=$(mktemp -d "${TMPDIR:-/tmp}/lean-sdd-github.XXXXXXXX") || return 1
|
|
15
|
+
trap 'rm -rf -- "$_GITHUB_SCRATCH"' EXIT
|
|
16
|
+
trap 'exit 130' INT
|
|
17
|
+
trap 'exit 143' TERM
|
|
18
|
+
trap 'exit 129' HUP
|
|
19
|
+
mcp_connect "$GITHUB_MCP_URL" "$GITHUB_TOKEN" > "$_GITHUB_SCRATCH/initialize.json"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Decode exactly one value of the requested object/array type (object default).
|
|
23
|
+
# Conflicting structured/text output is ambiguous; C consumes PR/review arrays.
|
|
24
|
+
github_call() {
|
|
25
|
+
local result_type="${3:-object}"
|
|
26
|
+
[[ "$result_type" == object || "$result_type" == array ]] || return 2
|
|
27
|
+
mcp_call "$1" "$2" > "$_GITHUB_SCRATCH/result.json" || return 1
|
|
28
|
+
if ! jq -e --arg expected_type "$result_type" '
|
|
29
|
+
def text_object:
|
|
30
|
+
if (.content | length) != 1 or .content[0].type != "text" then error("content")
|
|
31
|
+
else .content[0].text | fromjson | if type == $expected_type then . else error("type") end end;
|
|
32
|
+
if has("structuredContent") then
|
|
33
|
+
.structuredContent as $data |
|
|
34
|
+
if ($data | type) != $expected_type then error("type")
|
|
35
|
+
elif (.content | length) == 0 then $data
|
|
36
|
+
elif text_object == $data then $data else error("conflict") end
|
|
37
|
+
else text_object end
|
|
38
|
+
' "$_GITHUB_SCRATCH/result.json" > "$_GITHUB_SCRATCH/data.json" 2>/dev/null; then
|
|
39
|
+
github_error 'malformed or ambiguous tool data'
|
|
40
|
+
return 1
|
|
41
|
+
fi
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Full issue_read shape, verified through MCP. A missing/null body is not an
|
|
45
|
+
# empty body: callers must not invent an authoritative specification.
|
|
46
|
+
github_normalize_issue() {
|
|
47
|
+
local owner="$1" repo="$2" number="$3"
|
|
48
|
+
if ! jq -ce --arg owner "$owner" --arg repo "$repo" --argjson number "$number" '
|
|
49
|
+
select(.number == $number and (.number | type == "number" and floor == . and . > 0)) |
|
|
50
|
+
select(.html_url | type == "string") |
|
|
51
|
+
select((.html_url | ascii_downcase) == ("https://github.com/" + $owner + "/" + $repo + "/issues/" + ($number|tostring) | ascii_downcase)) |
|
|
52
|
+
select((has("pull_request")|not) and ((has("__typename")|not) or .__typename == "Issue")) |
|
|
53
|
+
select((.title | type == "string" and test("\\S")) and (.body | type == "string")) |
|
|
54
|
+
select(.state == "open" or .state == "closed") |
|
|
55
|
+
select((has("repository_url")|not) or
|
|
56
|
+
(.repository_url | ascii_downcase) == ("https://api.github.com/repos/" + $owner + "/" + $repo | ascii_downcase)) |
|
|
57
|
+
select((has("updated_at")|not) or (.updated_at | type == "string" and length > 0)) |
|
|
58
|
+
{owner:$owner,repo:$repo,issue_number:.number,issue_url:.html_url,title,body,state,
|
|
59
|
+
issue_reference:($owner + "/" + $repo + "#" + ($number|tostring)),BRANCH:("gh-" + ($number|tostring))}
|
|
60
|
+
' "$_GITHUB_SCRATCH/data.json" > "$_GITHUB_SCRATCH/issue.json" 2>/dev/null; then
|
|
61
|
+
github_error 'issue identity, entity type, specification or state is invalid'
|
|
62
|
+
return 1
|
|
63
|
+
fi
|
|
64
|
+
# Preserve optional observed modification metadata; never infer a timestamp.
|
|
65
|
+
jq --slurpfile raw "$_GITHUB_SCRATCH/data.json" '
|
|
66
|
+
if $raw[0] | has("updated_at") then . + {updated_at:$raw[0].updated_at} else . end
|
|
67
|
+
' "$_GITHUB_SCRATCH/issue.json"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
github_read_issue() {
|
|
71
|
+
local arguments=''
|
|
72
|
+
arguments=$(jq -cn --arg owner "$1" --arg repo "$2" --argjson number "$3" \
|
|
73
|
+
'{owner:$owner,repo:$repo,issue_number:$number,method:"get"}') || return 1
|
|
74
|
+
github_call issue_read "$arguments" || return 1
|
|
75
|
+
github_normalize_issue "$1" "$2" "$3"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Write results currently return id (database ID as text) and url, not an issue
|
|
79
|
+
# number. Extract the number only from a returned, repository-validated issue URL.
|
|
80
|
+
github_write_identity() {
|
|
81
|
+
local owner="$1" repo="$2" expected="${3:-}"
|
|
82
|
+
if ! jq -ce --arg owner "$owner" --arg repo "$repo" --arg expected "$expected" '
|
|
83
|
+
select((.id | type == "string" and test("^[1-9][0-9]*$")) and (.url | type == "string")) |
|
|
84
|
+
select((has("pull_request")|not) and ((has("__typename")|not) or .__typename == "Issue")) |
|
|
85
|
+
. as $raw | .url | capture("^https://github.com/(?<owner>[A-Za-z0-9-]+)/(?<repo>[A-Za-z0-9_.-]+)/issues/(?<number>[1-9][0-9]{0,14})$") |
|
|
86
|
+
select((.owner|ascii_downcase) == ($owner|ascii_downcase) and (.repo|ascii_downcase) == ($repo|ascii_downcase)) |
|
|
87
|
+
select($expected == "" or .number == $expected) |
|
|
88
|
+
select(($raw|has("number")|not) or $raw.number == (.number|tonumber)) |
|
|
89
|
+
{owner:$owner,repo:$repo,issue_number:(.number|tonumber),issue_url:$raw.url,issue_id:$raw.id}
|
|
90
|
+
' "$_GITHUB_SCRATCH/data.json" 2>/dev/null; then
|
|
91
|
+
github_error 'write returned no reliable issue identity; outcome unconfirmed; do not retry'
|
|
92
|
+
return 1
|
|
93
|
+
fi
|
|
94
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Validate the narrow B/C capability contract against actual mcp_list_tools JSON.
|
|
3
|
+
# Does not establish permissions, call tools, normalize domain data, or load tests.
|
|
4
|
+
# Usage: github_mcp_check_capabilities <tools-json-file>
|
|
5
|
+
|
|
6
|
+
github_mcp_check_capabilities() {
|
|
7
|
+
[[ $# == 1 && -r "$1" ]] || {
|
|
8
|
+
printf 'ERROR: GitHub MCP capability check requires a readable tools JSON file\n' >&2
|
|
9
|
+
return 2
|
|
10
|
+
}
|
|
11
|
+
local missing=''
|
|
12
|
+
if ! missing=$(jq -ers '
|
|
13
|
+
if length != 1 or (.[0].tools | type) != "array" then error("invalid") else .[0] end |
|
|
14
|
+
.tools as $tools |
|
|
15
|
+
def schema($name):
|
|
16
|
+
[$tools[] | select(.name == $name)] |
|
|
17
|
+
if length == 1 then .[0].inputSchema else null end;
|
|
18
|
+
def fields($name; $fields):
|
|
19
|
+
schema($name) as $schema |
|
|
20
|
+
if $schema.type != "object" then [$name + ": missing/duplicate tool or invalid schema"]
|
|
21
|
+
else [$fields | to_entries[] | . as $field |
|
|
22
|
+
select($schema.properties[$field.key].type != $field.value) |
|
|
23
|
+
$name + "." + $field.key + ": missing/incompatible field"] end;
|
|
24
|
+
def enum($name; $field; $values):
|
|
25
|
+
(schema($name).properties[$field].enum // []) as $actual |
|
|
26
|
+
[$values[] | select(. as $value | $actual | index($value) == null) |
|
|
27
|
+
$name + "." + $field + ": missing enum " + .];
|
|
28
|
+
[fields("get_me"; {}),
|
|
29
|
+
fields("search_repositories"; {query:"string",page:"number",perPage:"number"}),
|
|
30
|
+
fields("issue_write"; {owner:"string",repo:"string",method:"string",issue_number:"number",title:"string",body:"string",state:"string",state_reason:"string"}),
|
|
31
|
+
fields("issue_read"; {owner:"string",repo:"string",method:"string",issue_number:"number"}),
|
|
32
|
+
fields("add_issue_comment"; {owner:"string",repo:"string",issue_number:"number",body:"string"}),
|
|
33
|
+
fields("create_pull_request"; {owner:"string",repo:"string",title:"string",body:"string",head:"string",base:"string"}),
|
|
34
|
+
fields("pull_request_read"; {owner:"string",repo:"string",method:"string",pullNumber:"number"}),
|
|
35
|
+
fields("list_pull_requests"; {owner:"string",repo:"string",head:"string",base:"string",state:"string",page:"number",perPage:"number"}),
|
|
36
|
+
fields("merge_pull_request"; {owner:"string",repo:"string",pullNumber:"number",expectedHeadSha:"string",merge_method:"string"}),
|
|
37
|
+
enum("issue_write"; "method"; ["create","update"]),
|
|
38
|
+
enum("issue_write"; "state"; ["open","closed"]),
|
|
39
|
+
enum("issue_write"; "state_reason"; ["completed"]),
|
|
40
|
+
enum("issue_read"; "method"; ["get"]),
|
|
41
|
+
enum("pull_request_read"; "method"; ["get"]),
|
|
42
|
+
enum("merge_pull_request"; "merge_method"; ["merge"])] | add | join("; ")
|
|
43
|
+
' "$1" 2>/dev/null); then
|
|
44
|
+
printf 'ERROR: malformed GitHub MCP capability data\n' >&2
|
|
45
|
+
return 1
|
|
46
|
+
fi
|
|
47
|
+
if [[ -n "$missing" ]]; then
|
|
48
|
+
printf 'ERROR: GitHub MCP capability unavailable: %s; dependent B/C/D blocked\n' "$missing" >&2
|
|
49
|
+
return 1
|
|
50
|
+
fi
|
|
51
|
+
printf '{"schema_verified":true,"write_access_verified":false}\n'
|
|
52
|
+
}
|