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.
Files changed (45) hide show
  1. lean_sdd-0.1.0/.agents/.env.example +15 -0
  2. lean_sdd-0.1.0/.agents/README.md +139 -0
  3. lean_sdd-0.1.0/.agents/github-issue-contract.md +83 -0
  4. lean_sdd-0.1.0/.agents/github-mcp-contract.md +152 -0
  5. lean_sdd-0.1.0/.agents/lib/github-identity.sh +50 -0
  6. lean_sdd-0.1.0/.agents/lib/github-issues.sh +94 -0
  7. lean_sdd-0.1.0/.agents/lib/github-mcp.sh +52 -0
  8. lean_sdd-0.1.0/.agents/lib/github-pr.sh +250 -0
  9. lean_sdd-0.1.0/.agents/lib/mcp.sh +340 -0
  10. lean_sdd-0.1.0/.agents/skills/create-issue/SKILL.md +58 -0
  11. lean_sdd-0.1.0/.agents/skills/create-issue/scripts/create-issue.sh +21 -0
  12. lean_sdd-0.1.0/.agents/skills/finalize-implementation/SKILL.md +119 -0
  13. lean_sdd-0.1.0/.agents/skills/finalize-implementation/references/phase-contracts.md +163 -0
  14. lean_sdd-0.1.0/.agents/skills/finalize-implementation/scripts/merge-implementation.sh +68 -0
  15. lean_sdd-0.1.0/.agents/skills/finalize-implementation/scripts/publish-implementation.sh +48 -0
  16. lean_sdd-0.1.0/.agents/skills/get-issue/SKILL.md +52 -0
  17. lean_sdd-0.1.0/.agents/skills/get-issue/scripts/get-issue.sh +7 -0
  18. lean_sdd-0.1.0/.agents/skills/git-change-workflow/SKILL.md +64 -0
  19. lean_sdd-0.1.0/.agents/skills/git-change-workflow/scripts/prepare-change-branch.sh +63 -0
  20. lean_sdd-0.1.0/.agents/skills/list-repositories/SKILL.md +55 -0
  21. lean_sdd-0.1.0/.agents/skills/list-repositories/scripts/list-repositories.sh +63 -0
  22. lean_sdd-0.1.0/.agents/skills/update-issue/SKILL.md +157 -0
  23. lean_sdd-0.1.0/.agents/skills/update-issue/scripts/close-issue.sh +20 -0
  24. lean_sdd-0.1.0/.agents/skills/update-issue/scripts/comment-issue.sh +24 -0
  25. lean_sdd-0.1.0/.gitignore +55 -0
  26. lean_sdd-0.1.0/AGENTS.md +206 -0
  27. lean_sdd-0.1.0/CODING_STANDARDS.md +160 -0
  28. lean_sdd-0.1.0/CONSTITUTION.md +179 -0
  29. lean_sdd-0.1.0/LICENSE +21 -0
  30. lean_sdd-0.1.0/PKG-INFO +630 -0
  31. lean_sdd-0.1.0/README.md +621 -0
  32. lean_sdd-0.1.0/pyproject.toml +107 -0
  33. lean_sdd-0.1.0/src/lean_sdd/__init__.py +1 -0
  34. lean_sdd-0.1.0/src/lean_sdd/__main__.py +6 -0
  35. lean_sdd-0.1.0/src/lean_sdd/cli.py +65 -0
  36. lean_sdd-0.1.0/src/lean_sdd/doctor.py +150 -0
  37. lean_sdd-0.1.0/src/lean_sdd/installer.py +108 -0
  38. lean_sdd-0.1.0/src/lean_sdd/template_overrides/gitignore +5 -0
  39. lean_sdd-0.1.0/src/lean_sdd/template_overrides/readiness.md +28 -0
  40. lean_sdd-0.1.0/src/lean_sdd/templates.py +52 -0
  41. lean_sdd-0.1.0/tests/test_distribution.py +133 -0
  42. lean_sdd-0.1.0/tests/test_doctor.py +71 -0
  43. lean_sdd-0.1.0/tests/test_installer.py +130 -0
  44. lean_sdd-0.1.0/tests/test_skill_invocations.py +176 -0
  45. 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
+ }