diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aeba5c..c60b728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Two-way GitHub Sync (0021-two-way-github-sync) - Automated Witness Capture (0020-automated-witness-capture) - Config Management (0019-config-management) - Method Cli (0001-method-cli) @@ -24,8 +25,8 @@ - Ship Sync Automation (0018-ship-sync-automation) ### Fixed -- Resolved review feedback on PR #5: revised release runbook bullets for - clarity, enforced phase heading order in tests, and clarified +- Resolved review feedback on PR #5: revised release runbook bullets for + clarity, enforced phase heading order in tests, and clarified commitment and signpost boundedness invariants. ## Unreleased diff --git a/README.md b/README.md index 4a6b532..b98b5cc 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ A backlog, a loop, and honest bookkeeping. ### Stances **The agent and the human sit at the same table.** They see different -things. Both are named in every design. Both must agree before work +things. Both are named in every design as abstract roles (e.g., +"Repository Operator", "System Architect"). Both must agree before work ships. **Default to building the agent surface first** - it is the foundation @@ -157,7 +158,7 @@ The backlog file is removed. Work does not live in two places. ### Commitment -Pull it and you own it - "you" meaning the named sponsors (human and +Pull it and you own it - "you" meaning the named sponsor roles (human and agent) in the design doc. It does not go back. - **Finish** - hill met. diff --git a/docs/BEARING.md b/docs/BEARING.md index 39a53b4..7580609 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -14,9 +14,9 @@ Current priority: pull TBD to continue the system's maturity. ## What just shipped? +- `0021-two-way-github-sync`: Two-way GitHub Sync - `0020-automated-witness-capture`: Automated Witness Capture - `0019-config-management`: Config Management -- `0018-ship-sync-automation`: Ship Sync Automation ## What feels wrong? diff --git a/docs/VISION.md b/docs/VISION.md index 37386e8..7812128 100644 --- a/docs/VISION.md +++ b/docs/VISION.md @@ -1,10 +1,10 @@ --- title: "METHOD - Executive Summary" -generated_at: 2026-04-04T20:20:00-07:00 +generated_at: 2026-04-04T22:35:00-07:00 generator: "manual synthesis following Executive Summary Protocol (Cycle 0013)" -generated_from_commit: "644e40a9205213ba4d3db5b233c7042ea1ba687e" +generated_from_commit: "921706b9fc9fd5c7cf78331bdd0f3f91013ea558" provenance_level: artifact_history -witness_ref: docs/method/retro/0020-automated-witness-capture/witness/verification.md +witness_ref: docs/method/retro/0021-two-way-github-sync/witness/verification.md source_files: - README.md - CHANGELOG.md @@ -32,6 +32,7 @@ source_files: - docs/design/0018-ship-sync-automation/ship-sync-automation.md - docs/design/0019-config-management/config-management.md - docs/design/0020-automated-witness-capture/automated-witness-capture.md + - docs/design/0021-two-way-github-sync/two-way-github-sync.md --- # METHOD - Executive Summary @@ -50,7 +51,7 @@ state of the system without replacing the underlying files. ## Current state METHOD has evolved from pure doctrine into a formal, programmable system. -Twenty cycles are already closed: +Twenty-one cycles are already closed: - **CLI Foundations (0001-0004, 0007):** Established the CLI, witness conventions, and separated the module structure. @@ -58,8 +59,8 @@ Twenty cycles are already closed: - **Maturity (0008-0011, 0016, 0019):** Formalized releases, metadata contracts, extracted a clean API, adopted System-Style JS, and implemented a formal configuration system. -- **Connectivity (0012, 0014):** Implemented an MCP server and a GitHub - Issue synchronization adapter. +- **Connectivity (0012, 0014, 0021):** Implemented an MCP server and full + two-way GitHub Issue synchronization. - **Workflow (0013, 0015, 0017-0018, 0020):** Formalized the Executive Summary Protocol, Git branch doctrine, Behavior Spikes, Ship Sync automation, and Automated Witness Capture. @@ -80,7 +81,7 @@ The repo is organized under two legends: Covers cycle discipline, backlog movement, adapters (GitHub, MCP), and named patterns (spikes, workflow). - **Active:** None. -- **Up-next:** `PROCESS_two-way-github-sync`. +- **Up-next:** `PROCESS_i18n-string-extraction`. ### SYNTH Covers repo self-description, signposts, and provenance level. @@ -93,8 +94,8 @@ Covers repo self-description, signposts, and provenance level. - None. ### Up-next -- **PROCESS_two-way-github-sync:** Support syncing comments and labels - back to the filesystem backlog. +- **PROCESS_i18n-string-extraction:** Extract hardcoded English strings into + a centralized messages file. ### Inbox - None. diff --git a/docs/design/0009-generated-signpost-provenance/generated-signpost-provenance.md b/docs/design/0009-generated-signpost-provenance/generated-signpost-provenance.md index 8c42a76..8fcf643 100644 --- a/docs/design/0009-generated-signpost-provenance/generated-signpost-provenance.md +++ b/docs/design/0009-generated-signpost-provenance/generated-signpost-provenance.md @@ -8,8 +8,8 @@ Source backlog item: `docs/method/backlog/asap/SYNTH_generated-signpost-provenan ## Sponsors -- Human: @james -- Agent: @gemini-cli +- Human: Backlog Operator +- Agent: Sync Automator ## Hill diff --git a/docs/design/0010-yaml-frontmatter-schema/yaml-frontmatter-schema.md b/docs/design/0010-yaml-frontmatter-schema/yaml-frontmatter-schema.md index 10cd437..7ccc6fa 100644 --- a/docs/design/0010-yaml-frontmatter-schema/yaml-frontmatter-schema.md +++ b/docs/design/0010-yaml-frontmatter-schema/yaml-frontmatter-schema.md @@ -8,8 +8,8 @@ Source backlog item: `docs/method/backlog/inbox/PROCESS_yaml-frontmatter-schema. ## Sponsors -- Human: @james -- Agent: @gemini-cli +- Human: Backlog Operator +- Agent: Sync Automator ## Hill diff --git a/docs/design/0011-library-api-surface/library-api-surface.md b/docs/design/0011-library-api-surface/library-api-surface.md index 8c7e73b..215e331 100644 --- a/docs/design/0011-library-api-surface/library-api-surface.md +++ b/docs/design/0011-library-api-surface/library-api-surface.md @@ -10,8 +10,8 @@ Legend: PROCESS ## Sponsors -- Human: @james -- Agent: @gemini-cli +- Human: Backlog Operator +- Agent: Sync Automator ## Hill diff --git a/docs/design/0012-mcp-server/mcp-server.md b/docs/design/0012-mcp-server/mcp-server.md index c6d3015..956ad85 100644 --- a/docs/design/0012-mcp-server/mcp-server.md +++ b/docs/design/0012-mcp-server/mcp-server.md @@ -10,8 +10,8 @@ Legend: PROCESS ## Sponsors -- Human: @james -- Agent: @gemini-cli +- Human: Backlog Operator +- Agent: Sync Automator ## Hill diff --git a/docs/design/0013-executive-summary-protocol/executive-summary-protocol.md b/docs/design/0013-executive-summary-protocol/executive-summary-protocol.md index 526825e..308b72a 100644 --- a/docs/design/0013-executive-summary-protocol/executive-summary-protocol.md +++ b/docs/design/0013-executive-summary-protocol/executive-summary-protocol.md @@ -10,8 +10,8 @@ Legend: SYNTH ## Sponsors -- Human: @james -- Agent: @gemini-cli +- Human: Backlog Operator +- Agent: Sync Automator ## Hill diff --git a/docs/design/0014-github-issue-adapter/github-issue-adapter.md b/docs/design/0014-github-issue-adapter/github-issue-adapter.md index d7048ad..b2f67d8 100644 --- a/docs/design/0014-github-issue-adapter/github-issue-adapter.md +++ b/docs/design/0014-github-issue-adapter/github-issue-adapter.md @@ -10,8 +10,8 @@ Legend: PROCESS ## Sponsors -- Human: @james -- Agent: @gemini-cli +- Human: Backlog Operator +- Agent: Sync Automator ## Hill diff --git a/docs/design/0015-git-branch-workflow-policy/git-branch-workflow-policy.md b/docs/design/0015-git-branch-workflow-policy/git-branch-workflow-policy.md index 067e1e7..9af1382 100644 --- a/docs/design/0015-git-branch-workflow-policy/git-branch-workflow-policy.md +++ b/docs/design/0015-git-branch-workflow-policy/git-branch-workflow-policy.md @@ -10,8 +10,8 @@ Legend: PROCESS ## Sponsors -- Human: @james -- Agent: @gemini-cli +- Human: Backlog Operator +- Agent: Sync Automator ## Hill diff --git a/docs/design/0016-system-style-javascript-adoption/system-style-javascript-adoption.md b/docs/design/0016-system-style-javascript-adoption/system-style-javascript-adoption.md index b3a59fe..255d456 100644 --- a/docs/design/0016-system-style-javascript-adoption/system-style-javascript-adoption.md +++ b/docs/design/0016-system-style-javascript-adoption/system-style-javascript-adoption.md @@ -10,8 +10,8 @@ Legend: PROCESS ## Sponsors -- Human: @james -- Agent: @gemini-cli +- Human: Backlog Operator +- Agent: Sync Automator ## Hill diff --git a/docs/design/0017-behavior-spike-convention/behavior-spike-convention.md b/docs/design/0017-behavior-spike-convention/behavior-spike-convention.md index c90b5cc..5b7ffd2 100644 --- a/docs/design/0017-behavior-spike-convention/behavior-spike-convention.md +++ b/docs/design/0017-behavior-spike-convention/behavior-spike-convention.md @@ -10,8 +10,8 @@ Legend: PROCESS ## Sponsors -- Human: @james -- Agent: @gemini-cli +- Human: Backlog Operator +- Agent: Sync Automator ## Hill diff --git a/docs/design/0018-ship-sync-automation/ship-sync-automation.md b/docs/design/0018-ship-sync-automation/ship-sync-automation.md index 66169b4..7ffb1ba 100644 --- a/docs/design/0018-ship-sync-automation/ship-sync-automation.md +++ b/docs/design/0018-ship-sync-automation/ship-sync-automation.md @@ -10,8 +10,8 @@ Legend: PROCESS ## Sponsors -- Human: @james -- Agent: @gemini-cli +- Human: Backlog Operator +- Agent: Sync Automator ## Hill diff --git a/docs/design/0019-config-management/config-management.md b/docs/design/0019-config-management/config-management.md index a0e7f3c..1d791d2 100644 --- a/docs/design/0019-config-management/config-management.md +++ b/docs/design/0019-config-management/config-management.md @@ -10,8 +10,8 @@ Legend: PROCESS ## Sponsors -- Human: @james -- Agent: @gemini-cli +- Human: Backlog Operator +- Agent: Sync Automator ## Hill diff --git a/docs/design/0020-automated-witness-capture/automated-witness-capture.md b/docs/design/0020-automated-witness-capture/automated-witness-capture.md index d30b941..1049d9e 100644 --- a/docs/design/0020-automated-witness-capture/automated-witness-capture.md +++ b/docs/design/0020-automated-witness-capture/automated-witness-capture.md @@ -10,8 +10,8 @@ Legend: SYNTH ## Sponsors -- Human: @james -- Agent: @gemini-cli +- Human: Backlog Operator +- Agent: Sync Automator ## Hill diff --git a/docs/design/0021-two-way-github-sync/two-way-github-sync.md b/docs/design/0021-two-way-github-sync/two-way-github-sync.md new file mode 100644 index 0000000..1854dad --- /dev/null +++ b/docs/design/0021-two-way-github-sync/two-way-github-sync.md @@ -0,0 +1,79 @@ +--- +title: "Two-way GitHub Sync" +legend: PROCESS +--- + +# Two-way GitHub Sync + +Source backlog item: `docs/method/backlog/up-next/PROCESS_two-way-github-sync.md` +Legend: PROCESS + +## Sponsors + +- Human: Backlog Operator +- Agent: Sync Automator + +## Hill + +Extend the GitHub adapter to support full two-way synchronization between +the local filesystem and GitHub Issues. The filesystem remains the +authority for local content; the adapter will: +1. **Push**: Update existing GitHub issues if the local title or body has + changed since the last sync. (This is the default action for + `sync github`). +2. **Pull**: Update local backlog items with remote labels, status + (Open/Closed), and top-level comments to keep the local context rich. + +If both `--push` and `--pull` are provided, they run sequentially: +local changes are pushed first, then remote updates are pulled. + +## Playback Questions + +### Human + +- [ ] `method sync github --push` (or default) updates the title and + description of an existing GitHub issue if the local file changes. +- [ ] `method sync github --pull` updates local backlog files with data + from GitHub (labels, status, comments). +- [ ] `method sync github --push --pull` runs both operations + sequentially (Push then Pull). +- [ ] Local files reflect GitHub status (e.g., if an issue is closed on + GitHub, the local file is updated or moved). + +### Agent + +- [ ] `GitHubAdapter.pushBacklog()` and `GitHubAdapter.pullBacklog()` are + implemented and tested with mocks. +- [ ] `tests/github-adapter.test.ts` proves that both remote-to-local and + local-to-remote updates work correctly. + +## Accessibility and Assistive Reading + +- Linear truth / reduced-complexity posture: Syncing remote comments + locally ensures the full context of an item is available in a single + linear markdown file. +- Non-visual or alternate-reading expectations: Same as one-way sync. + +## Localization and Directionality + +- Locale / wording / formatting assumptions: Standard English for synced + content headers. + +## Agent Inspectability and Explainability + +- What must be explicit and deterministic for agents: The mapping of + GitHub states to local lane movements must be deterministic. +- What must be attributable, evidenced, or governed: The source of the + synced data (GitHub) must be clear. + +## Non-goals + +- [ ] Real-time sync (this remains a manual command-triggered move). +- [ ] Conflicts resolution (filesystem wins for title/body content on + push; metadata like labels and comments are enriched on pull). + +## Backlog Context + +Implement two-way synchronization for the GitHub adapter, allowing +labels, comments, and issue status to sync back from GitHub to the local +filesystem backlog. diff --git a/docs/invariants/sponsor-abstractness.md b/docs/invariants/sponsor-abstractness.md new file mode 100644 index 0000000..61de96f --- /dev/null +++ b/docs/invariants/sponsor-abstractness.md @@ -0,0 +1,26 @@ +--- +title: "Invariant: Sponsor Abstractness" +--- + +## What must remain true? + +Sponsors named in design documents are abstract roles, not specific +individuals or agent instances. + +## Why does it matter? + +METHOD is a coordination protocol between two seats at the table: the +Human and the Agent. Naming literal people (e.g., "@james") or literal +agents (e.g., "@gemini-cli") creates a brittle, person-dependent history. +Roles (e.g., "Repository Operator", "Code Hardener", "Protocol Designer") +describe *who would care* about the feature and *what perspective* they +bring, which remains true regardless of who is currently sitting in the seat. + +## How do you check? + +- Design documents name sponsors as roles (e.g., "Human: System Architect"). +- No literal personal names or specific agent brand names are used in + the `Sponsors` section. +- The roles named are descriptive of the interests being represented + in the cycle. +- This is enforced by the automated docs test (`tests/docs.test.ts`). diff --git a/docs/method/backlog/asap/PROCESS_branch-naming-consistency.md b/docs/method/backlog/asap/PROCESS_branch-naming-consistency.md new file mode 100644 index 0000000..27efc15 --- /dev/null +++ b/docs/method/backlog/asap/PROCESS_branch-naming-consistency.md @@ -0,0 +1,27 @@ +--- +title: "Branch Naming Consistency" +legend: PROCESS +lane: asap +--- + +# Unify branch naming rules across METHOD docs + +METHOD currently names cycle branches inconsistently. + +Examples in the docs point in different directions: +- `docs/method/process.md` says cycle work must happen on + `cycles/` +- the same document's branch naming section says cycle branches use + `####-slug` + +That should be one rule, not two. + +Why this matters: +- branch naming is part of METHOD's coordination surface +- conflicting examples create unnecessary drift across repos +- agents and humans should not have to guess which rule is canonical + +Deliverable: +- choose one branch naming rule for cycle branches +- update `README.md` and `docs/method/process.md` to match +- include one clear example and remove contradictory wording diff --git a/docs/method/backlog/asap/PROCESS_red-phase-playback-coverage.md b/docs/method/backlog/asap/PROCESS_red-phase-playback-coverage.md new file mode 100644 index 0000000..2d4a976 --- /dev/null +++ b/docs/method/backlog/asap/PROCESS_red-phase-playback-coverage.md @@ -0,0 +1,32 @@ +--- +title: "RED Phase Playback Coverage" +legend: PROCESS +lane: asap +--- + +# RED should explicitly cover playback questions and test-shape breadth + +METHOD says RED tests are the executable spec and that playback +questions become specs. That is directionally correct, but it still +allows a weak RED phase where only the happy path is captured. + +The README should state this explicitly: + +- RED must cover the playback questions +- RED should also cover, where relevant to the hill: + - golden path + - failure modes + - edge cases + - stress / fuzz behavior +- If one of those categories is not relevant, the design doc or test + file should say so explicitly + +Why: +- It makes RED usable as both witness scaffold and regression suite +- It prevents narrow happy-path RED from masquerading as executable spec +- It makes the quality bar legible before GREEN starts + +Likely touch points: +- `README.md` RED step +- Possibly `process.md` if the repo wants a more operational version of + the same rule diff --git a/docs/method/backlog/asap/PROCESS_repo-lane-conformance.md b/docs/method/backlog/asap/PROCESS_repo-lane-conformance.md new file mode 100644 index 0000000..43be26d --- /dev/null +++ b/docs/method/backlog/asap/PROCESS_repo-lane-conformance.md @@ -0,0 +1,26 @@ +--- +title: "Repo Lane Conformance" +legend: PROCESS +lane: asap +--- + +# METHOD repos should either contain declared backlog lanes or say they are optional + +METHOD defines backlog lanes in `README.md`, but the repo itself can +drift from that declared shape. This repo did not contain `asap/` until +real work needed it. + +That mismatch is small, but it weakens METHOD's claim that the +filesystem is the coordination layer. + +Question to settle: +- are declared lanes required repo structure? +- or are they optional until first use? + +Either answer is fine, but the method should be explicit. + +Deliverable: +- choose the rule +- update docs to match +- optionally add a lightweight repo-conformance check or checklist for + METHOD repos so missing lanes/signposts are caught early diff --git a/docs/method/backlog/bad-code/PROCESS_async-exec-refactor.md b/docs/method/backlog/bad-code/PROCESS_async-exec-refactor.md new file mode 100644 index 0000000..034caa0 --- /dev/null +++ b/docs/method/backlog/bad-code/PROCESS_async-exec-refactor.md @@ -0,0 +1,24 @@ +--- +title: "Async Exec Refactor" +legend: PROCESS +--- + +# Async Exec Refactor + +The `Workspace.execCommand` method currently uses synchronous `execSync`, +which blocks the event loop and makes the CLI feel stuttery during +witness capture. + +## Success Criteria + +- Replace `execSync` with an asynchronous implementation (e.g., using + `child_process.spawn` or `exec` wrapped in Promises). +- Preserve existing behavior for `METHOD_TEST` inputs. +- Match current stdout/stderr output format exactly on both success + and error. +- Support a configurable timeout and cooperative cancellation (accepting + an `AbortSignal` or timeout ms). +- Preserve exit-code semantics and ensure identical error messages are + thrown. +- Add unit/integration tests verifying the async API shape (returning + `{ stdout, stderr, code }`) and timeout cancellation behavior. diff --git a/docs/method/backlog/inbox/PROCESS_i18n-string-extraction.md b/docs/method/backlog/inbox/PROCESS_i18n-string-extraction.md new file mode 100644 index 0000000..b081188 --- /dev/null +++ b/docs/method/backlog/inbox/PROCESS_i18n-string-extraction.md @@ -0,0 +1,10 @@ +--- +title: "i18n String Extraction" +legend: PROCESS +--- + +# i18n String Extraction + +Extract all hardcoded English user-facing strings from the CLI and +renderers into a centralized messages file (e.g., src/messages.ts) to +support future localization and satisfy the repo's i18n posture. diff --git a/docs/method/backlog/inbox/PROCESS_interactive-scaffolder.md b/docs/method/backlog/inbox/PROCESS_interactive-scaffolder.md new file mode 100644 index 0000000..626707b --- /dev/null +++ b/docs/method/backlog/inbox/PROCESS_interactive-scaffolder.md @@ -0,0 +1,10 @@ +--- +title: "Interactive Scaffolder" +legend: PROCESS +--- + +# Interactive Scaffolder + +Implement an interactive wizard for 'method pull' that prompts for +sponsor roles, accessibility posture, and initial playback questions to +ensure design docs are high-quality from the start. diff --git a/docs/method/backlog/inbox/PROCESS_multi-forge-adapter.md b/docs/method/backlog/inbox/PROCESS_multi-forge-adapter.md new file mode 100644 index 0000000..9e8503e --- /dev/null +++ b/docs/method/backlog/inbox/PROCESS_multi-forge-adapter.md @@ -0,0 +1,10 @@ +--- +title: "Multi-Forge Adapter" +legend: PROCESS +--- + +# Multi-Forge Adapter + +Generalize the synchronization strategy to support multiple forges +(GitLab, Bitbucket) through a common ForgeAdapter interface, reducing +the GitHub-specific coupling in the core. diff --git a/docs/method/backlog/inbox/PROCESS_semantic-drift-detector.md b/docs/method/backlog/inbox/PROCESS_semantic-drift-detector.md new file mode 100644 index 0000000..4f606d5 --- /dev/null +++ b/docs/method/backlog/inbox/PROCESS_semantic-drift-detector.md @@ -0,0 +1,24 @@ +--- +title: "Semantic Drift Detector" +legend: PROCESS +--- + +# Semantic Drift Detector + +Extend `method drift` to use LLM-based semantic matching for cases where +test descriptions don't exactly match playback questions but are +conceptually identical. + +## Matching Rules + +- **Automatic Match**: Cosine similarity >= 0.80 or LLM confidence >= 0.9. +- **Human Review**: Similarity between 0.65 and 0.80 triggers a "Near Miss" + hint requiring manual confirmation. +- **Non-Match**: Marked as drift if confidence is below 0.65. + +## Fallback Behavior + +- If the LLM call fails or times out, fall back to the existing lexical + normalized matching. +- Explicitly log failure modes: `low_confidence`, `timeout`, + `ambiguous_multi_intent`. diff --git a/docs/method/backlog/up-next/PROCESS_two-way-github-sync.md b/docs/method/backlog/up-next/PROCESS_two-way-github-sync.md deleted file mode 100644 index 5bc0592..0000000 --- a/docs/method/backlog/up-next/PROCESS_two-way-github-sync.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Two-way GitHub Sync" -legend: PROCESS ---- - -# Two-way GitHub Sync - -Implement two-way synchronization for the GitHub adapter, allowing -labels, comments, and issue status to sync back from GitHub to the local -filesystem backlog. diff --git a/docs/method/legends/PROCESS.md b/docs/method/legends/PROCESS.md index dc2a393..a2bf9c5 100644 --- a/docs/method/legends/PROCESS.md +++ b/docs/method/legends/PROCESS.md @@ -11,6 +11,8 @@ system. outcome. - [commitment-integrity](../../invariants/commitment-integrity.md) — pulled work does not go back to the backlog. +- [sponsor-abstractness](../../invariants/sponsor-abstractness.md) — + sponsors are abstract roles, not literal people or agents. ## What it covers diff --git a/docs/method/process.md b/docs/method/process.md index 0fc3500..c148ed5 100644 --- a/docs/method/process.md +++ b/docs/method/process.md @@ -26,8 +26,10 @@ METHOD cycles run as a calm pull-design-test-playback-close-review-ship-sync loo ## Default Loop 1. Pull an item from the backlog into `docs/design//`. -2. Write the design with both human and agent sponsors named, plus the - accessibility, localization, and agent-inspectability contract. +2. Write the design with both human and agent sponsors named as + abstract roles (e.g., "System Architect", "Workflow Automator"), + plus the accessibility, localization, and agent-inspectability + contract. 3. Write failing tests from the playback questions. 4. Make the tests pass. 5. Produce a reproducible playback witness, including reduced/ diff --git a/docs/method/retro/0001-method-cli/witness/verification.md b/docs/method/retro/0001-method-cli/witness/verification.md index 8870559..6506bf8 100644 --- a/docs/method/retro/0001-method-cli/witness/verification.md +++ b/docs/method/retro/0001-method-cli/witness/verification.md @@ -12,7 +12,7 @@ $ npm test > method@0.1.0 test > vitest run --config vitest.config.ts - RUN v4.1.2 /Users/james/git/method + RUN v4.1.2 ./method Test Files 1 passed (1) Tests 7 passed (7) @@ -38,7 +38,7 @@ $ npm run method -- status > method@0.1.0 method > tsx src/cli.ts status -METHOD Status /Users/james/git/method +METHOD Status ./method --- Backlog --- inbox 0 - diff --git a/docs/method/retro/0002-playback-witness-convention/witness/verification.md b/docs/method/retro/0002-playback-witness-convention/witness/verification.md index 4ca6a9d..edb61c5 100644 --- a/docs/method/retro/0002-playback-witness-convention/witness/verification.md +++ b/docs/method/retro/0002-playback-witness-convention/witness/verification.md @@ -14,7 +14,7 @@ $ npm run method -- status > method@0.1.0 method > tsx src/cli.ts status -METHOD Status /Users/james/git/method +METHOD Status ./method --- Backlog --- inbox 0 - @@ -55,7 +55,7 @@ $ npm run method -- status > method@0.1.0 method > tsx src/cli.ts status -METHOD Status /Users/james/git/method +METHOD Status ./method --- Backlog --- inbox 0 - diff --git a/docs/method/retro/0009-generated-signpost-provenance/witness/verification.md b/docs/method/retro/0009-generated-signpost-provenance/witness/verification.md index 52817b5..38ea93a 100644 --- a/docs/method/retro/0009-generated-signpost-provenance/witness/verification.md +++ b/docs/method/retro/0009-generated-signpost-provenance/witness/verification.md @@ -13,7 +13,7 @@ repo state. > vitest run --config vitest.config.ts - RUN v4.1.2 /Users/james/git/method + RUN v4.1.2 ./method Test Files 2 passed (2) diff --git a/docs/method/retro/0010-yaml-frontmatter-schema/witness/verification.md b/docs/method/retro/0010-yaml-frontmatter-schema/witness/verification.md index f54c323..8a231a3 100644 --- a/docs/method/retro/0010-yaml-frontmatter-schema/witness/verification.md +++ b/docs/method/retro/0010-yaml-frontmatter-schema/witness/verification.md @@ -15,7 +15,7 @@ standardized YAML frontmatter contract, with automated enforcement in > vitest run --config vitest.config.ts - RUN v4.1.2 /Users/james/git/method + RUN v4.1.2 ./method Test Files 2 passed (2) diff --git a/docs/method/retro/0011-library-api-surface/witness/verification.md b/docs/method/retro/0011-library-api-surface/witness/verification.md index 3c41753..e40d9eb 100644 --- a/docs/method/retro/0011-library-api-surface/witness/verification.md +++ b/docs/method/retro/0011-library-api-surface/witness/verification.md @@ -15,7 +15,7 @@ logic. > vitest run --config vitest.config.ts - RUN v4.1.2 /Users/james/git/method + RUN v4.1.2 ./method Test Files 3 passed (3) diff --git a/docs/method/retro/0012-mcp-server/witness/verification.md b/docs/method/retro/0012-mcp-server/witness/verification.md index 934339a..51bc3ce 100644 --- a/docs/method/retro/0012-mcp-server/witness/verification.md +++ b/docs/method/retro/0012-mcp-server/witness/verification.md @@ -13,7 +13,7 @@ This witness proves that the `method` MCP server has been implemented and expose > vitest run --config vitest.config.ts - RUN v4.1.2 /Users/james/git/method + RUN v4.1.2 ./method Test Files 4 passed (4) diff --git a/docs/method/retro/0013-executive-summary-protocol/witness/verification.md b/docs/method/retro/0013-executive-summary-protocol/witness/verification.md index af73572..03f27e3 100644 --- a/docs/method/retro/0013-executive-summary-protocol/witness/verification.md +++ b/docs/method/retro/0013-executive-summary-protocol/witness/verification.md @@ -15,7 +15,7 @@ current synthesis state. > vitest run --config vitest.config.ts - RUN v4.1.2 /Users/james/git/method + RUN v4.1.2 ./method Test Files 4 passed (4) diff --git a/docs/method/retro/0014-github-issue-adapter/witness/verification.md b/docs/method/retro/0014-github-issue-adapter/witness/verification.md index 7992747..280813d 100644 --- a/docs/method/retro/0014-github-issue-adapter/witness/verification.md +++ b/docs/method/retro/0014-github-issue-adapter/witness/verification.md @@ -15,7 +15,7 @@ in the markdown frontmatter. > vitest run --config vitest.config.ts - RUN v4.1.2 /Users/james/git/method + RUN v4.1.2 ./method Test Files 5 passed (5) diff --git a/docs/method/retro/0015-git-branch-workflow-policy/witness/verification.md b/docs/method/retro/0015-git-branch-workflow-policy/witness/verification.md index ab5ade0..c5fd1e2 100644 --- a/docs/method/retro/0015-git-branch-workflow-policy/witness/verification.md +++ b/docs/method/retro/0015-git-branch-workflow-policy/witness/verification.md @@ -15,7 +15,7 @@ test suite. > vitest run --config vitest.config.ts - RUN v4.1.2 /Users/james/git/method + RUN v4.1.2 ./method Test Files 5 passed (5) diff --git a/docs/method/retro/0016-system-style-javascript-adoption/system-style-javascript-adoption.md b/docs/method/retro/0016-system-style-javascript-adoption/system-style-javascript-adoption.md index 00330c6..02d9bb4 100644 --- a/docs/method/retro/0016-system-style-javascript-adoption/system-style-javascript-adoption.md +++ b/docs/method/retro/0016-system-style-javascript-adoption/system-style-javascript-adoption.md @@ -28,9 +28,9 @@ core domain remains portable. ## New Debt -- Existing calls to domain models (like `Workspace.status`) are not yet - explicitly parsing the return values through schemas, though the models - now use the schemas for definition. A follow-up cycle should enforce +- Existing calls to domain models (like `Workspace.status`) are not yet + explicitly parsing the return values through schemas, though the models + now use the schemas for definition. A follow-up cycle should enforce "validation at the point of entry/exit" more rigorously. ## Cool Ideas diff --git a/docs/method/retro/0016-system-style-javascript-adoption/witness/verification.md b/docs/method/retro/0016-system-style-javascript-adoption/witness/verification.md index 3d100b7..a6036f0 100644 --- a/docs/method/retro/0016-system-style-javascript-adoption/witness/verification.md +++ b/docs/method/retro/0016-system-style-javascript-adoption/witness/verification.md @@ -14,7 +14,7 @@ adopted as repo doctrine and enforced in the core domain models. > vitest run --config vitest.config.ts - RUN v4.1.2 /Users/james/git/method + RUN v4.1.2 ./method Test Files 6 passed (6) diff --git a/docs/method/retro/0017-behavior-spike-convention/behavior-spike-convention.md b/docs/method/retro/0017-behavior-spike-convention/behavior-spike-convention.md index f7123a2..172f72b 100644 --- a/docs/method/retro/0017-behavior-spike-convention/behavior-spike-convention.md +++ b/docs/method/retro/0017-behavior-spike-convention/behavior-spike-convention.md @@ -13,9 +13,9 @@ Drift check: yes ## Summary This cycle formalized the "Behavior Spike" convention in METHOD. We added -a new section to the process doc defining the 4-phase lifecycle for -temporary implementations: Capture, Execute, Witness, and Retire. This -doctrine ensures that "failing fast" or "buying clarity" is an honest, +a new section to the process doc defining the 4-phase lifecycle for +temporary implementations: Capture, Execute, Witness, and Retire. This +doctrine ensures that "failing fast" or "buying clarity" is an honest, documented part of the repo's history rather than a silent graveyard move. ## Playback Witness @@ -32,7 +32,7 @@ documented part of the repo's history rather than a silent graveyard move. ## Cool Ideas -- Add a `method spike` command to automate the creation of `SPIKE_` +- Add a `method spike` command to automate the creation of `SPIKE_` prefixed backlog items. ## Backlog Maintenance diff --git a/docs/method/retro/0017-behavior-spike-convention/witness/verification.md b/docs/method/retro/0017-behavior-spike-convention/witness/verification.md index 6fe0566..296edb1 100644 --- a/docs/method/retro/0017-behavior-spike-convention/witness/verification.md +++ b/docs/method/retro/0017-behavior-spike-convention/witness/verification.md @@ -15,7 +15,7 @@ test suite. > vitest run --config vitest.config.ts - RUN v4.1.2 /Users/james/git/method + RUN v4.1.2 ./method Test Files 6 passed (6) diff --git a/docs/method/retro/0018-ship-sync-automation/ship-sync-automation.md b/docs/method/retro/0018-ship-sync-automation/ship-sync-automation.md index c07bf62..fec7dd4 100644 --- a/docs/method/retro/0018-ship-sync-automation/ship-sync-automation.md +++ b/docs/method/retro/0018-ship-sync-automation/ship-sync-automation.md @@ -13,10 +13,10 @@ Drift check: yes ## Summary This cycle successfully automated the "Ship Sync Maneuver" through a new -`method sync ship` command. The command identifies closed cycles not +`method sync ship` command. The command identifies closed cycles not yet present in `CHANGELOG.md`, appends them to the "Unreleased" section, -and completely refreshes `docs/BEARING.md` with the latest ships and -backlog priorities. This reduces manual bookkeeping and ensures the +and completely refreshes `docs/BEARING.md` with the latest ships and +backlog priorities. This reduces manual bookkeeping and ensures the repo's signposts stay honest as the system matures. ## Playback Witness @@ -29,8 +29,8 @@ repo's signposts stay honest as the system matures. ## New Debt -- `renderBearing` currently uses a hardcoded template for the "What - feels wrong?" section; this could be moved to a configuration file or +- `renderBearing` currently uses a hardcoded template for the "What + feels wrong?" section; this could be moved to a configuration file or extracted from the existing `BEARING.md` in a future cycle. ## Cool Ideas diff --git a/docs/method/retro/0018-ship-sync-automation/witness/verification.md b/docs/method/retro/0018-ship-sync-automation/witness/verification.md index aec979e..fba39ed 100644 --- a/docs/method/retro/0018-ship-sync-automation/witness/verification.md +++ b/docs/method/retro/0018-ship-sync-automation/witness/verification.md @@ -15,7 +15,7 @@ automates the Ship Sync maneuver by updating `CHANGELOG.md` and > vitest run --config vitest.config.ts - RUN v4.1.2 /Users/james/git/method + RUN v4.1.2 ./method Test Files 7 passed (7) diff --git a/docs/method/retro/0019-config-management/config-management.md b/docs/method/retro/0019-config-management/config-management.md index 4cd4349..7d1a392 100644 --- a/docs/method/retro/0019-config-management/config-management.md +++ b/docs/method/retro/0019-config-management/config-management.md @@ -12,11 +12,11 @@ Drift check: yes ## Summary -This cycle delivered a formal configuration system for METHOD. The system -loads settings from a `.method.json` file in the repo root, validates them -via a Zod schema in `src/config.ts`, and integrates them into the -`Workspace` context. Environment variables continue to serve as high-priority -overrides. This shift standardizes how credentials and repo-local +This cycle delivered a formal configuration system for METHOD. The system +loads settings from a `.method.json` file in the repo root, validates them +via a Zod schema in `src/config.ts`, and integrates them into the +`Workspace` context. Environment variables continue to serve as high-priority +overrides. This shift standardizes how credentials and repo-local constants are managed across different environments. ## Playback Witness diff --git a/docs/method/retro/0019-config-management/witness/verification.md b/docs/method/retro/0019-config-management/witness/verification.md index 6054968..5b1fbf2 100644 --- a/docs/method/retro/0019-config-management/witness/verification.md +++ b/docs/method/retro/0019-config-management/witness/verification.md @@ -4,7 +4,7 @@ title: "Verification Witness for Cycle 0019" # Verification Witness for Cycle 0019 -This witness proves that the formal configuration system for METHOD has +This witness proves that the formal configuration system for METHOD has been implemented, validated, and integrated into the workspace. ## Test Results @@ -14,7 +14,7 @@ been implemented, validated, and integrated into the workspace. > vitest run --config vitest.config.ts - RUN v4.1.2 /Users/james/git/method + RUN v4.1.2 ./method Test Files 8 passed (8) diff --git a/docs/method/retro/0020-automated-witness-capture/automated-witness-capture.md b/docs/method/retro/0020-automated-witness-capture/automated-witness-capture.md index 5e9dcd7..6ebe050 100644 --- a/docs/method/retro/0020-automated-witness-capture/automated-witness-capture.md +++ b/docs/method/retro/0020-automated-witness-capture/automated-witness-capture.md @@ -12,11 +12,11 @@ Drift check: yes ## Summary -This cycle delivered the first phase of automated evidence capture for -METHOD. The `Workspace.closeCycle` method now automatically orchestrates -the execution of `npm test` and `method drift`, piping their outputs -into a standardized `verification.md` artifact. This ensures that -every closed cycle carries verifiable proof of its claims without +This cycle delivered the first phase of automated evidence capture for +METHOD. The `Workspace.closeCycle` method now automatically orchestrates +the execution of `npm test` and `method drift`, piping their outputs +into a standardized `verification.md` artifact. This ensures that +every closed cycle carries verifiable proof of its claims without manual operator effort. ## Playback Witness @@ -29,13 +29,13 @@ manual operator effort. ## New Debt -- The `execCommand` helper is currently synchronous and simple; it - could be improved to handle more complex terminal formatting (ANSI +- The `execCommand` helper is currently synchronous and simple; it + could be improved to handle more complex terminal formatting (ANSI stripping) or asynchronous execution in the future. ## Cool Ideas -- Support capturing specific files or directory structures as part of +- Support capturing specific files or directory structures as part of the witness (e.g., `witness_files` in design). - Automate screenshot capture for visual cycles. diff --git a/docs/method/retro/0020-automated-witness-capture/witness/verification.md b/docs/method/retro/0020-automated-witness-capture/witness/verification.md index 55b6f59..a215645 100644 --- a/docs/method/retro/0020-automated-witness-capture/witness/verification.md +++ b/docs/method/retro/0020-automated-witness-capture/witness/verification.md @@ -14,7 +14,7 @@ behavior and adheres to the repo invariants. > vitest run --config vitest.config.ts - RUN v4.1.2 /Users/james/git/method + RUN v4.1.2 ./method Test Files 9 passed (9) diff --git a/docs/method/retro/0021-two-way-github-sync/two-way-github-sync.md b/docs/method/retro/0021-two-way-github-sync/two-way-github-sync.md new file mode 100644 index 0000000..c0ca961 --- /dev/null +++ b/docs/method/retro/0021-two-way-github-sync/two-way-github-sync.md @@ -0,0 +1,45 @@ +--- +title: "Two-way GitHub Sync" +outcome: hill-met +drift_check: yes +--- + +# Two-way GitHub Sync Retro + +Design: `docs/design/0021-two-way-github-sync/two-way-github-sync.md` +Outcome: hill-met +Drift check: yes + +## Summary + +This cycle delivered full two-way synchronization for the GitHub adapter. +The `GitHubAdapter` now supports both `push` (updating remote issues +from local changes) and `pull` (updating local docs with remote labels, +comments, and status). The `method sync github` command was enhanced +with `--push` and `--pull` flags, and the MCP server now exposes a +`method_sync_github` tool. This ensures the filesystem remains the +authority while benefiting from the rich context of the GitHub forge. + +## Playback Witness + +- [Verification Witness](./witness/verification.md) + +## Drift + +- None recorded. + +## New Debt + +- Comment synchronization is additive-only and uses a simple string + check to avoid duplicates; it does not track individual comment IDs. + +## Cool Ideas + +- Support deleting local items if the remote issue is deleted. +- Sync GitHub milestones to backlog lanes or tags. + +## Backlog Maintenance + +- [x] Inbox processed +- [x] Priorities reviewed +- [x] Dead work buried or merged diff --git a/docs/method/retro/0021-two-way-github-sync/witness/verification.md b/docs/method/retro/0021-two-way-github-sync/witness/verification.md new file mode 100644 index 0000000..29380b3 --- /dev/null +++ b/docs/method/retro/0021-two-way-github-sync/witness/verification.md @@ -0,0 +1,36 @@ +--- +title: "Verification Witness for Cycle 21" +--- + +# Verification Witness for Cycle 21 + +This witness proves that `Two-way GitHub Sync` now carries the required +behavior and adheres to the repo invariants. + +## Test Results + +```text +> method@0.2.0 test +> vitest run --config vitest.config.ts + + + RUN v4.1.2 ./method + + + Test Files 9 passed (9) + Tests 104 passed (104) + Start at 22:32:53 + Duration 594ms (transform 710ms, setup 0ms, import 1.48s, tests 395ms, environment 1ms) +``` + +## Drift Results + +```text +No playback-question drift found. +Scanned 1 active cycle, 5 playback questions, 116 test descriptions. +Search basis: exact normalized match in tests/**/*.test.* and tests/**/*.spec.* descriptions. +``` + +## Manual Verification + +- [x] Automated capture completed successfully. diff --git a/src/adapters/github.ts b/src/adapters/github.ts index 1eee8cb..8fb205d 100644 --- a/src/adapters/github.ts +++ b/src/adapters/github.ts @@ -1,16 +1,20 @@ import { resolve } from 'node:path'; import { readBody, readHeading, type Workspace } from '../index.js'; +import { type WorkspaceStatus } from '../domain.js'; export interface GitHubIssue { id: number; number: number; url: string; + state: 'open' | 'closed'; + labels: string[]; } export interface GitHubSyncResult { path: string; issue?: GitHubIssue; skipped: boolean; + action: 'create' | 'push' | 'pull' | 'skip'; error?: string; } @@ -32,11 +36,34 @@ export class GitHubAdapter { this.repo = options.repo; } - async syncBacklog(): Promise { + async pushBacklog(): Promise { const status = this.workspace.status(); const results: GitHubSyncResult[] = []; - const allItems = [ + const allItems = this.getAllBacklogItems(status); + + for (const item of allItems) { + results.push(await this.pushItem(item.path)); + } + + return results; + } + + async pullBacklog(): Promise { + const status = this.workspace.status(); + const results: GitHubSyncResult[] = []; + + const allItems = this.getAllBacklogItems(status); + + for (const item of allItems) { + results.push(await this.pullItem(item.path)); + } + + return results; + } + + private getAllBacklogItems(status: WorkspaceStatus) { + return [ ...status.backlog.inbox, ...status.backlog.asap, ...status.backlog['up-next'], @@ -44,67 +71,150 @@ export class GitHubAdapter { ...status.backlog['bad-code'], ...status.backlog.root, ]; + } - for (const item of allItems) { - results.push(await this.syncItem(item.path)); - } + async pushItem(relativePath: string): Promise { + try { + const frontmatter = this.workspace.readFrontmatter(relativePath); + const fullPath = resolve(this.workspace.root, relativePath); + const title = readHeading(fullPath); + let body = readBody(fullPath); - return results; + // Strip local GitHub Comments block if present to avoid mirroring them back + const commentHeader = '## GitHub Comments'; + if (body.includes(commentHeader)) { + body = body.split(commentHeader)[0]?.trim() ?? body; + } + + if (frontmatter.github_issue_id === undefined || !/^\d+$/u.test(frontmatter.github_issue_id)) { + // Fallback to creation if ID is missing or invalid + const issue = await this.createIssue(title, body); + this.workspace.updateFrontmatter(relativePath, { + github_issue_id: String(issue.number), + github_issue_url: issue.url, + }); + return { path: relativePath, issue, skipped: false, action: 'create' }; + } + + // Update existing issue + const issueNumber = Number.parseInt(frontmatter.github_issue_id, 10); + const issue = await this.updateIssue(issueNumber, title, body); + return { path: relativePath, issue, skipped: false, action: 'push' }; + } catch (error: unknown) { + return { + path: relativePath, + skipped: false, + action: 'push', + error: error instanceof Error ? error.message : String(error), + }; + } } - async syncItem(relativePath: string): Promise { + async pullItem(relativePath: string): Promise { try { const frontmatter = this.workspace.readFrontmatter(relativePath); - if (frontmatter.github_issue_id !== undefined) { - return { path: relativePath, skipped: true }; + if (frontmatter.github_issue_id === undefined || !/^\d+$/u.test(frontmatter.github_issue_id)) { + return { path: relativePath, skipped: true, action: 'skip' }; } - const fullPath = resolve(this.workspace.root, relativePath); - const title = readHeading(fullPath); - const body = readBody(fullPath); + const issueNumber = Number.parseInt(frontmatter.github_issue_id, 10); + const remoteIssue = await this.fetchIssue(issueNumber); + const remoteComments = await this.fetchComments(issueNumber); - const issue = await this.createIssue(title, body); - + // Update local frontmatter this.workspace.updateFrontmatter(relativePath, { - github_issue_id: String(issue.number), - github_issue_url: issue.url, + github_issue_url: remoteIssue.url, + github_labels: remoteIssue.labels.join(','), }); - return { path: relativePath, issue, skipped: false }; + // Update local body with comments if any + if (remoteComments.length > 0) { + const fullPath = resolve(this.workspace.root, relativePath); + const localBody = readBody(fullPath); + const commentSection = '\n\n## GitHub Comments\n\n' + remoteComments.map(c => `**@${c.user}**: ${c.body}`).join('\n\n---\n\n'); + + // Simple avoid-duplication check + if (!localBody.includes('## GitHub Comments')) { + this.workspace.updateBody(relativePath, localBody + commentSection); + } + } + + // Handle closed status + if (remoteIssue.state === 'closed') { + const currentLane = relativePath.split('/')[3] || 'root'; + if (currentLane !== 'graveyard') { + this.workspace.moveBacklogItem(relativePath, 'graveyard'); + } + } + + return { path: relativePath, issue: remoteIssue, skipped: false, action: 'pull' }; } catch (error: unknown) { return { path: relativePath, skipped: false, + action: 'pull', error: error instanceof Error ? error.message : String(error), }; } } private async createIssue(title: string, body: string): Promise { - const response = await fetch( - `https://api.github.com/repos/${this.owner}/${this.repo}/issues`, - { - method: 'POST', - headers: { - Authorization: `Bearer ${this.token}`, - Accept: 'application/vnd.github+json', - 'X-GitHub-Api-Version': '2022-11-28', - 'User-Agent': 'METHOD-CLI', - }, - body: JSON.stringify({ title, body }), - } - ); + const data = await this.ghFetch(`/repos/${this.owner}/${this.repo}/issues`, { + method: 'POST', + body: JSON.stringify({ title, body }), + }); + return this.mapIssue(data); + } + + private async updateIssue(number: number, title: string, body: string): Promise { + const data = await this.ghFetch(`/repos/${this.owner}/${this.repo}/issues/${number}`, { + method: 'PATCH', + body: JSON.stringify({ title, body }), + }); + return this.mapIssue(data); + } + + private async fetchIssue(number: number): Promise { + const data = await this.ghFetch(`/repos/${this.owner}/${this.repo}/issues/${number}`); + return this.mapIssue(data); + } + + private async fetchComments(number: number): Promise<{ user: string; body: string }[]> { + const data = (await this.ghFetch(`/repos/${this.owner}/${this.repo}/issues/${number}/comments`)) as any[]; + return data.map((c: any) => ({ + user: c.user.login, + body: c.body, + })); + } + + private async ghFetch(endpoint: string, options: RequestInit = {}): Promise { + const url = endpoint.startsWith('http') ? endpoint : `https://api.github.com${endpoint}`; + const response = await fetch(url, { + ...options, + headers: { + Authorization: `Bearer ${this.token}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'METHOD-CLI', + ...options.headers, + }, + }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`GitHub API error: ${response.status} ${response.statusText}\n${errorBody}`); } - const data = (await response.json()) as any; + return response.json(); + } + + private mapIssue(data: any): GitHubIssue { return { id: data.id, number: data.number, url: data.html_url, + state: data.state, + labels: data.labels.map((l: any) => l.name), }; } } diff --git a/src/cli-args.ts b/src/cli-args.ts index acb66d5..efdc4be 100644 --- a/src/cli-args.ts +++ b/src/cli-args.ts @@ -10,7 +10,8 @@ export type ParsedCommand = | { command: 'drift'; cycle?: string } | { command: 'status' } | { command: 'mcp' } - | { command: 'sync'; adapter: 'github' | 'ship' }; + | { command: 'sync'; adapter: 'github'; push?: boolean; pull?: boolean } + | { command: 'sync'; adapter: 'ship' }; export function parseCliArgs(argv: readonly string[]): ParsedCommand { const [command, ...rest] = argv; @@ -51,10 +52,26 @@ export function parseCliArgs(argv: readonly string[]): ParsedCommand { } return { command: 'mcp' }; case 'sync': - if (rest[0] !== 'github' && rest[0] !== 'ship') { - throw new MethodError('Usage: method sync github|ship'); + if (rest[0] === 'github') { + const flags = rest.slice(1); + const allowedFlags = ['--push', '--pull']; + for (const flag of flags) { + if (!allowedFlags.includes(flag)) { + throw new MethodError(`Unknown sync github option: ${flag}\n\n${usage('sync')}`); + } + } + const push = flags.includes('--push'); + const pull = flags.includes('--pull'); + const finalPush = push || (!push && !pull); + return { command: 'sync', adapter: 'github', push: finalPush, pull }; } - return { command: 'sync', adapter: rest[0] }; + if (rest[0] === 'ship') { + if (rest.length > 1) { + throw new MethodError(`\`sync ship\` does not take any arguments.\n\n${usage('sync')}`); + } + return { command: 'sync', adapter: 'ship' }; + } + throw new MethodError(`Usage: method sync github|ship\n\n${usage('sync')}`); default: throw new MethodError(`Unknown command: ${command}`); } @@ -90,7 +107,15 @@ export function usage(topic?: string): string { } if (topic === 'sync') { - return 'Usage: method sync github|ship\n\nSynchronize the backlog with GitHub Issues or perform a Ship Sync.'; + return [ + 'Usage: method sync github|ship [options]', + '', + 'GitHub Options:', + ' --push Update GitHub issues with local changes (default).', + ' --pull Update local backlog with GitHub changes (labels, comments, status).', + '', + 'Perform Ship Sync or synchronize the backlog with GitHub Issues.', + ].join('\n'); } return [ @@ -104,7 +129,8 @@ export function usage(topic?: string): string { ' drift [cycle] Check active cycle playback questions against tests.', ' status Show backlog, active cycles, and legend health.', ' mcp Start the MCP server over stdio.', - ' sync github|ship Sync backlog with GitHub Issues or perform Ship Sync.', + ' sync github [--push|--pull] Sync backlog with GitHub Issues.', + ' sync ship Perform Ship Sync.', '', 'Run `method help ` for command-specific usage.', ].join('\n'); diff --git a/src/cli.ts b/src/cli.ts index e81a820..99688c9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -109,15 +109,35 @@ export async function runCli( repo: repo!, }); - const results = await adapter.syncBacklog(); - for (const result of results) { - if (result.skipped) { - continue; + if (!parsed.push && !parsed.pull) { + stderr.write(`${alert('No sync direction specified. Use --push and/or --pull.', { variant: 'error', ctx })}\n`); + return 1; + } + + if (parsed.push) { + const results = await adapter.pushBacklog(); + for (const result of results) { + if (result.skipped) continue; + if (result.error) { + stderr.write(`${alert(`Error pushing ${result.path}: ${result.error}`, { variant: 'error', ctx })}\n`); + } else if (result.action === 'create' || result.action === 'push') { + const issueLabel = result.issue?.number ?? ''; + const verb = result.action === 'create' ? 'Created' : 'Updated'; + stdout.write(`${alert(`${verb} GitHub Issue #${issueLabel} for ${result.path}`, { variant: 'success', ctx })}\n`); + } } - if (result.error) { - stderr.write(`${alert(`Error syncing ${result.path}: ${result.error}`, { variant: 'error', ctx })}\n`); - } else if (result.issue) { - stdout.write(`${alert(`Synced ${result.path} to GitHub Issue #${result.issue.number}`, { variant: 'success', ctx })}\n`); + } + + if (parsed.pull) { + const results = await adapter.pullBacklog(); + for (const result of results) { + if (result.skipped) continue; + if (result.error) { + stderr.write(`${alert(`Error pulling ${result.path}: ${result.error}`, { variant: 'error', ctx })}\n`); + } else if (result.action === 'pull') { + const issueLabel = result.issue?.number ?? ''; + stdout.write(`${alert(`Pulled remote changes from GitHub Issue #${issueLabel} into ${result.path}`, { variant: 'success', ctx })}\n`); + } } } return 0; diff --git a/src/index.ts b/src/index.ts index df188d0..153097d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,11 +3,12 @@ import { mkdirSync, readdirSync, readFileSync, + renameSync, unlinkSync, writeFileSync, } from 'node:fs'; import { execSync } from 'node:child_process'; -import { dirname, relative, resolve } from 'node:path'; +import { basename, dirname, relative, resolve } from 'node:path'; import { BACKLOG_DIR, type BacklogItem, @@ -319,6 +320,56 @@ export class Workspace { return result; } + updateBody(path: string, newBody: string): void { + const fullPath = resolve(this.root, path); + const content = readFileSync(fullPath, 'utf8'); + let frontmatter = ''; + + if (content.startsWith('---\n')) { + const end = content.indexOf('\n---\n', 4); + if (end !== -1) { + frontmatter = content.slice(0, end + 5); + } + } + + const title = readHeading(fullPath); + const newContent = frontmatter + ? `${frontmatter}\n# ${title}\n\n${newBody.trim()}\n` + : `# ${title}\n\n${newBody.trim()}\n`; + writeFileSync(fullPath, newContent, 'utf8'); + } + + moveBacklogItem(path: string, targetLane: Lane | 'graveyard'): string { + const fullPath = resolve(this.root, path); + if (!existsSync(fullPath)) { + throw new MethodError(`Backlog item not found: ${path}`); + } + + const fileName = basename(fullPath); + let targetDir: string; + if (targetLane === 'graveyard') { + targetDir = resolve(this.root, 'docs/method/graveyard'); + } else if (targetLane === 'root') { + targetDir = resolve(this.root, BACKLOG_DIR); + } else { + targetDir = resolve(this.root, BACKLOG_DIR, targetLane); + } + + mkdirSync(targetDir, { recursive: true }); + const targetPath = resolve(targetDir, fileName); + + if (fullPath === targetPath) { + return relative(this.root, targetPath); + } + + if (existsSync(targetPath)) { + throw new MethodError(`Destination already exists: ${relative(this.root, targetPath)}`); + } + + renameSync(fullPath, targetPath); + return relative(this.root, targetPath); + } + openCycles(): Cycle[] { return this.allCycles().filter((cycle) => existsSync(cycle.designDoc) && !existsSync(cycle.retroDoc)); } @@ -708,7 +759,7 @@ export function readBody(path: string): string { } } - const lines = body.split(/\r?\n/u); + const lines = body.trim().split(/\r?\n/u); const bodyLines = lines[0]?.startsWith('# ') ? lines.slice(1) : lines; const result = bodyLines.join('\n').trim(); return result.length > 0 ? result : 'TBD'; @@ -741,10 +792,6 @@ function titleCase(value: string): string { .join(' '); } -function basename(path: string): string { - return path.split('/').at(-1) ?? path; -} - function fileStem(path: string): string { const name = basename(path); return name.endsWith('.md') ? name.slice(0, -3) : name; diff --git a/src/mcp.ts b/src/mcp.ts index 78e8c5d..afeea06 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -1,10 +1,8 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from '@modelcontextprotocol/sdk/types.js'; +import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { relative } from 'node:path'; import { Workspace } from './index.js'; +import { GitHubAdapter } from './adapters/github.js'; import type { Outcome } from './domain.js'; export function createMcpServer(cwd: string = process.cwd()) { @@ -75,6 +73,17 @@ export function createMcpServer(cwd: string = process.cwd()) { description: 'Perform the Ship Sync maneuver (update CHANGELOG.md and BEARING.md)', inputSchema: { type: 'object', properties: {} }, }, + { + name: 'method_sync_github', + description: 'Synchronize backlog with GitHub Issues', + inputSchema: { + type: 'object', + properties: { + push: { type: 'boolean', description: 'Update GitHub issues with local changes (default: true)' }, + pull: { type: 'boolean', description: 'Update local backlog with GitHub changes' }, + }, + }, + }, { name: 'method_capture_witness', description: 'Automate terminal evidence capture for a cycle', @@ -135,6 +144,67 @@ export function createMcpServer(cwd: string = process.cwd()) { return { content: [{ type: 'text', text }] }; } + if (request.params.name === 'method_sync_github') { + const args = request.params.arguments as { push?: boolean; pull?: boolean } | undefined; + const push = args?.push || (!args?.push && !args?.pull); + const pull = args?.pull || false; + + const token = workspace.config.github_token; + const repoFull = workspace.config.github_repo; + + if (!token) { + throw new Error('GitHub token missing in .method.json or environment.'); + } + if (!repoFull || !repoFull.includes('/')) { + throw new Error('GitHub repo invalid; must be owner/repo in .method.json or environment.'); + } + + const [owner, repo] = repoFull.split('/'); + const adapter = new GitHubAdapter({ + workspace, + token, + owner: owner!, + repo: repo!, + }); + + const log: string[] = []; + let hasError = false; + + if (push) { + const results = await adapter.pushBacklog(); + for (const r of results) { + if (r.skipped) continue; + if (r.error) { + log.push(`Error pushing ${r.path}: ${r.error}`); + hasError = true; + } else { + const issueLabel = r.issue?.number ?? ''; + const verb = r.action === 'create' ? 'Created' : 'Updated'; + log.push(`${verb} GitHub Issue #${issueLabel} for ${r.path}`); + } + } + } + + if (pull) { + const results = await adapter.pullBacklog(); + for (const r of results) { + if (r.skipped) continue; + if (r.error) { + log.push(`Error pulling ${r.path}: ${r.error}`); + hasError = true; + } else { + const issueLabel = r.issue?.number ?? ''; + log.push(`Pulled remote changes from GitHub Issue #${issueLabel} into ${r.path}`); + } + } + } + + return { + content: [{ type: 'text', text: log.join('\n') || 'No changes.' }], + isError: hasError, + }; + } + if (request.params.name === 'method_capture_witness') { const args = request.params.arguments as { cycle?: string } | undefined; const path = workspace.captureWitness(args?.cycle); diff --git a/tests/docs.test.ts b/tests/docs.test.ts index 9e86b9d..972ca84 100644 --- a/tests/docs.test.ts +++ b/tests/docs.test.ts @@ -140,6 +140,37 @@ describe('METHOD docs', () => { } }); + it('enforces sponsor abstractness in design documents', () => { + const designs = walkMarkdownFiles('docs/design'); + const literalNames = /^(Gemini|Claude|James|Bard|GPT|GPT-\d+|@.*)$/i; + const singleCapitalizedWord = /^[A-Z][a-z]+$/u; + + for (const designPath of designs) { + const content = readRepoFile(designPath); + if (!content.includes('## Sponsors')) continue; + + const sponsorsMatch = /## Sponsors\n\n- Human: (?[\s\S]*?)\n- Agent: (?[\s\S]*?)(?=\n\n##|$)/u.exec(content); + + expect(sponsorsMatch, `${designPath} has a ## Sponsors heading but does not match the expected format: +- Human: Role +- Agent: Role`).not.toBeNull(); + + if (sponsorsMatch?.groups !== undefined) { + const human = (sponsorsMatch.groups.human ?? '').trim(); + const agent = (sponsorsMatch.groups.agent ?? '').trim(); + + for (const [label, name] of [['human', human], ['agent', agent]]) { + expect(name, `${designPath} ${label} sponsor should not be TBD`).not.toBe('TBD'); + expect(name, `${designPath} ${label} sponsor should not be a literal name: ${name}`).not.toMatch(literalNames); + + if (!name.includes(' ') && !name.includes('\n')) { + expect(name, `${designPath} ${label} sponsor should be a descriptive role, not a single name: ${name}`).not.toMatch(singleCapitalizedWord); + } + } + } + } + }); + it('keeps this repo inbox aligned with the current legend split', () => { const untaggedItems = inboxItemNames().filter((entry) => !/^(PROCESS|SYNTH)_/u.test(entry)); @@ -189,9 +220,10 @@ describe('METHOD docs', () => { }); it('sanitizes personal absolute paths from committed verification witnesses', () => { - const readmeRevisionVerification = readRepoFile('docs/method/retro/0003-readme-revision/witness/verification.md'); - const visionRefreshVerification = readRepoFile('docs/method/retro/0004-readme-and-vision-refresh/witness/verification.md'); - const driftDetectorVerification = readRepoFile('docs/method/retro/0005-drift-detector/witness/verification.md'); + const witnesses = walkMarkdownFiles('docs/method/retro') + .filter((relativePath) => relativePath.includes('/witness/verification.md')); + + expect(witnesses.length, 'expected at least one verification witness').toBeGreaterThan(0); const personalPathPatterns = [ '/Users/', @@ -201,10 +233,11 @@ describe('METHOD docs', () => { 'C:\\Users\\', ]; - for (const pattern of personalPathPatterns) { - expect(readmeRevisionVerification).not.toContain(pattern); - expect(visionRefreshVerification).not.toContain(pattern); - expect(driftDetectorVerification).not.toContain(pattern); + for (const witnessPath of witnesses) { + const content = readRepoFile(witnessPath); + for (const pattern of personalPathPatterns) { + expect(content, `${witnessPath} contains personal path: ${pattern}`).not.toContain(pattern); + } } }); @@ -321,6 +354,7 @@ describe('METHOD docs', () => { 'lane', 'github_issue_id', 'github_issue_url', + 'github_labels', ]; const docs = walkMarkdownFiles('docs'); @@ -479,9 +513,9 @@ describe('METHOD docs', () => { expect(vision, 'generator should name the cycle that produced the summary').toContain('0009-generated-signpost-provenance'); }); - it('`docs/VISION.md` summary is accurate for the current closed-cycle state (cycles 0001-0020).', () => { + it('`docs/VISION.md` summary is accurate for the current closed-cycle state (cycles 0001-0021).', () => { const vision = readRepoFile('docs/VISION.md'); - expect(vision).toContain('Twenty cycles are already closed:'); + expect(vision).toContain('Twenty-one cycles are already closed:'); expect(vision).toContain('0005-drift-detector'); expect(vision).toContain('0006-ci-gates'); expect(vision).toContain('0007-cli-module-split'); @@ -493,6 +527,7 @@ describe('METHOD docs', () => { expect(vision).toContain('0018-ship-sync-automation'); expect(vision).toContain('0019-config-management'); expect(vision).toContain('0020-automated-witness-capture'); + expect(vision).toContain('0021-two-way-github-sync'); }); it('`docs.test.ts` validates that `docs/VISION.md` frontmatter contains all mandatory fields (`generated_at`, `generator`, `generated_from_commit`, `provenance_level`, `witness_ref`, `source_files`).', () => { diff --git a/tests/github-adapter.test.ts b/tests/github-adapter.test.ts index b2850be..97a0262 100644 --- a/tests/github-adapter.test.ts +++ b/tests/github-adapter.test.ts @@ -1,8 +1,8 @@ -import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { initWorkspace, Workspace } from '../src/index.js'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { initWorkspace, Workspace, readBody } from '../src/index.js'; import { GitHubAdapter } from '../src/adapters/github.js'; const tempRoots: string[] = []; @@ -16,43 +16,31 @@ afterEach(() => { }); function createTempRoot(): string { - const root = mkdtempSync(join(tmpdir(), 'method-github-')); + const root = mkdtempSync(join(tmpdir(), 'method-github-two-way-')); tempRoots.push(root); return root; } -describe('GitHub Adapter', () => { - it('A new `method sync github` command (or tool) identifies backlog items missing a GitHub issue and creates them.', () => { - // Verified by the syncBacklog test below. - }); - - it('The created GitHub issue contains the title and body from the backlog markdown file.', () => { - // Verified by the syncBacklog test below. - }); - - it('The markdown file is updated with the `github_issue_id` in its frontmatter.', () => { - // Verified by the syncBacklog test below. - }); - - it('`src/index.ts` (or a new module) provides a synchronization interface.', () => { - expect(GitHubAdapter).toBeDefined(); - }); - - it('`tests/github-adapter.test.ts` proves that the sync logic correctly identifies "missing" issues and calls the GitHub API (mocked) to create them.', async () => { +describe('GitHub Adapter Two-way Sync', () => { + it('`method sync github --push` (or default) updates the title and description of an existing GitHub issue if the local file changes.', async () => { const root = createTempRoot(); initWorkspace(root); const workspace = new Workspace(root); - // Create a backlog item - const itemPath = workspace.captureIdea('Test idea for GitHub', 'PROTO', 'GitHub Sync'); - - // Mock fetch + // Create item with existing ID + workspace.captureIdea('Local Title', 'FEAT', 'My Item'); + const itemPath = 'docs/method/backlog/inbox/FEAT_my-item.md'; + workspace.updateFrontmatter(itemPath, { github_issue_id: '42' }); + + // Mock GitHub PATCH const mockResponse = { ok: true, json: async () => ({ id: 12345, number: 42, html_url: 'https://github.com/owner/repo/issues/42', + state: 'open', + labels: [], }), }; const fetchSpy = vi.fn().mockResolvedValue(mockResponse); @@ -65,31 +53,53 @@ describe('GitHub Adapter', () => { repo: 'repo', }); - const results = await adapter.syncBacklog(); + const results = await adapter.pushBacklog(); expect(results.length).toBe(1); - expect(results[0].skipped).toBe(false); - expect(results[0].issue?.number).toBe(42); + expect(results[0].action).toBe('push'); expect(fetchSpy).toHaveBeenCalled(); - - // Verify frontmatter was updated - const frontmatter = workspace.readFrontmatter(results[0].path); - expect(frontmatter.github_issue_id).toBe('42'); - expect(frontmatter.github_issue_url).toBe('https://github.com/owner/repo/issues/42'); + const [url, options] = fetchSpy.mock.calls[0]; + expect(url).toContain('/issues/42'); + expect(options.method).toBe('PATCH'); + expect(JSON.parse(options.body)).toEqual({ + title: 'My Item', + body: 'Local Title', + }); }); - it('The sync logic correctly handles existing `github_issue_id` fields by skipping creation.', async () => { + it('`method sync github --pull` updates local backlog files with data from GitHub (labels, status, comments).', async () => { const root = createTempRoot(); initWorkspace(root); const workspace = new Workspace(root); - // Create a backlog item with frontmatter already set - const itemPath = workspace.captureIdea('Existing idea', 'PROTO', 'Existing'); - workspace.updateFrontmatter('docs/method/backlog/inbox/PROTO_existing.md', { - github_issue_id: '100', + // Create item with existing ID + workspace.captureIdea('Local Title', 'FEAT', 'My Item'); + const itemPath = 'docs/method/backlog/inbox/FEAT_my-item.md'; + workspace.updateFrontmatter(itemPath, { github_issue_id: '42' }); + + // Mock GitHub GETs (Issue and Comments) + const fetchSpy = vi.fn().mockImplementation((url) => { + if (url.endsWith('/issues/42')) { + return Promise.resolve({ + ok: true, + json: async () => ({ + number: 42, + html_url: 'https://github.com/owner/repo/issues/42', + state: 'open', + labels: [{ name: 'bug' }, { name: 'priority' }], + }), + }); + } + if (url.endsWith('/comments')) { + return Promise.resolve({ + ok: true, + json: async () => [ + { user: { login: 'user1' }, body: 'First comment' }, + ], + }); + } + return Promise.reject(new Error('Unknown URL')); }); - - const fetchSpy = vi.fn(); vi.stubGlobal('fetch', fetchSpy); const adapter = new GitHubAdapter({ @@ -99,10 +109,65 @@ describe('GitHub Adapter', () => { repo: 'repo', }); - const results = await adapter.syncBacklog(); + const results = await adapter.pullBacklog(); expect(results.length).toBe(1); - expect(results[0].skipped).toBe(true); - expect(fetchSpy).not.toHaveBeenCalled(); + expect(results[0].action).toBe('pull'); + + // Verify local updates + const frontmatter = workspace.readFrontmatter(itemPath); + expect(frontmatter.github_labels).toBe('bug,priority'); + + const body = readBody(join(root, itemPath)); + expect(body).toContain('## GitHub Comments'); + expect(body).toContain('**@user1**: First comment'); + }); + + it('Local files reflect GitHub status (e.g., if an issue is closed on GitHub, the local file is updated or moved).', async () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + // Create item with existing ID + workspace.captureIdea('Closed Item', 'FEAT', 'Closed'); + const itemPath = 'docs/method/backlog/inbox/FEAT_closed.md'; + workspace.updateFrontmatter(itemPath, { github_issue_id: '99' }); + + // Mock GitHub GETs (Closed state) + const fetchSpy = vi.fn().mockImplementation((url) => { + if (url.endsWith('/issues/99')) { + return Promise.resolve({ + ok: true, + json: async () => ({ + number: 99, + html_url: 'https://github.com/owner/repo/issues/99', + state: 'closed', + labels: [], + }), + }); + } + if (url.endsWith('/comments')) { + return Promise.resolve({ ok: true, json: async () => [] }); + } + return Promise.reject(new Error('Unknown URL')); + }); + vi.stubGlobal('fetch', fetchSpy); + + const adapter = new GitHubAdapter({ + workspace, + token: 'fake-token', + owner: 'owner', + repo: 'repo', + }); + + await adapter.pullBacklog(); + + // Verify file was moved to graveyard + const graveyardPath = join(root, 'docs/method/graveyard/FEAT_closed.md'); + const inboxPath = join(root, 'docs/method/backlog/inbox/FEAT_closed.md'); + + expect(existsSync(graveyardPath), 'file should exist in graveyard').toBe(true); + expect(existsSync(inboxPath), 'file should no longer exist in inbox').toBe(false); + expect(readFileSync(graveyardPath, 'utf8')).toContain('Closed Item'); }); }); diff --git a/tests/witness.test.ts b/tests/witness.test.ts index 0207238..60203ca 100644 --- a/tests/witness.test.ts +++ b/tests/witness.test.ts @@ -31,11 +31,11 @@ describe('Automated Witness Capture', () => { }); it('`method close` (or a sub-command) automatically generates a `verification.md` with real test and CLI results.', () => { - // Verified by the internal call in closeCycle and the captureWitness test. + // Verified by the internal call in closeCycle and the captureWitness test below. }); it('The generated witness matches the actual state of the repository at close.', () => { - // Verified by the captureWitness test. + // Verified by the captureWitness test below. }); it('`tests/witness.test.ts` proves that the automated capture correctly pipes terminal output and test results into the witness markdown.', async () => {