From 2863d3f181b7b2879984fda9318d26ee659140fb Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 6 Apr 2026 22:52:46 -0700 Subject: [PATCH 1/4] cycles: pull 0028-hybrid-signpost-generation Replace fully-generated signposts with hybrid model using / markers. --- .../hybrid-signpost-generation.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/design/0028-hybrid-signpost-generation/hybrid-signpost-generation.md diff --git a/docs/design/0028-hybrid-signpost-generation/hybrid-signpost-generation.md b/docs/design/0028-hybrid-signpost-generation/hybrid-signpost-generation.md new file mode 100644 index 0000000..112b3c7 --- /dev/null +++ b/docs/design/0028-hybrid-signpost-generation/hybrid-signpost-generation.md @@ -0,0 +1,60 @@ +--- +title: "Hybrid Signpost Generation" +legend: PROCESS +--- + +# Hybrid Signpost Generation + +Source: dogfood discussion — fully-generated signposts lose hand-authored +prose; fully-manual signposts drift from code. +Legend: PROCESS + +## Sponsors + +- Human: Process Steward +- Agent: Signpost Generator + +## Hill + +Replace the fully-generated signpost approach with a hybrid model. +Hand-authored prose stays. Sections marked with +`` / `` get their content +replaced on ship sync. + +## Playback Questions + +### Human + +- [ ] Ship sync replaces content between generate markers without + touching surrounding prose. +- [ ] CLI.md, MCP.md, and GUIDE.md use generate markers for their + reference sections. +- [ ] Hand-written content outside markers survives regeneration. + +### Agent + +- [ ] A `replaceGeneratedSections` function finds marker pairs and + replaces their content using a registry of named generators. +- [ ] Generators exist for `cli-commands`, `mcp-tools`, and + `signpost-inventory`. +- [ ] Tests prove markers are respected and content outside is + preserved. + +## Accessibility and Assistive Reading + +Not in scope. + +## Localization and Directionality + +Not in scope. + +## Agent Inspectability and Explainability + +Generate markers are HTML comments — invisible in rendered markdown +but visible in source, making the contract explicit. + +## Non-goals + +- [ ] Generating ARCHITECTURE.md content (stays fully hand-authored). +- [ ] Generating README.md content. +- [ ] Nested or conditional generation markers. From e0ede9beb70f0be809fe95782025e9ebca97fb2e Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 6 Apr 2026 22:53:16 -0700 Subject: [PATCH 2/4] =?UTF-8?q?test:=20RED=20=E2=80=94=20failing=20tests?= =?UTF-8?q?=20for=20hybrid=20signpost=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 tests for replaceGeneratedSections: marker replacement, multiple markers, no markers, unknown generator, whitespace preservation. Module does not exist yet. --- tests/generate.test.ts | 106 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/generate.test.ts diff --git a/tests/generate.test.ts b/tests/generate.test.ts new file mode 100644 index 0000000..011425b --- /dev/null +++ b/tests/generate.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from 'vitest'; +import { replaceGeneratedSections } from '../src/generate.js'; + +describe('Hybrid Signpost Generation', () => { + it('replaces content between generate markers without touching surrounding prose', () => { + const input = [ + '# My Doc', + '', + 'Hand-written intro.', + '', + '', + 'old generated content', + '', + '', + 'Hand-written outro.', + ].join('\n'); + + const generators = { + 'test-section': () => 'new generated content\n', + }; + + const result = replaceGeneratedSections(input, generators); + + expect(result).toContain('Hand-written intro.'); + expect(result).toContain('Hand-written outro.'); + expect(result).toContain('new generated content'); + expect(result).not.toContain('old generated content'); + expect(result).toContain(''); + expect(result).toContain(''); + }); + + it('handles multiple generate markers in one file', () => { + const input = [ + '# Doc', + '', + '', + 'old alpha', + '', + '', + 'middle prose', + '', + '', + 'old beta', + '', + ].join('\n'); + + const generators = { + 'alpha': () => 'new alpha\n', + 'beta': () => 'new beta\n', + }; + + const result = replaceGeneratedSections(input, generators); + + expect(result).toContain('new alpha'); + expect(result).toContain('new beta'); + expect(result).toContain('middle prose'); + expect(result).not.toContain('old alpha'); + expect(result).not.toContain('old beta'); + }); + + it('leaves file unchanged when no markers exist', () => { + const input = '# No markers\n\nJust prose.\n'; + const generators = { 'unused': () => 'stuff' }; + + const result = replaceGeneratedSections(input, generators); + + expect(result).toBe(input); + }); + + it('leaves marker intact when no generator matches', () => { + const input = [ + '', + 'existing content', + '', + ].join('\n'); + + const result = replaceGeneratedSections(input, {}); + + expect(result).toContain('existing content'); + }); + + it('preserves exact whitespace around markers', () => { + const input = [ + 'before', + '', + '', + 'old', + '', + '', + 'after', + ].join('\n'); + + const generators = { 'x': () => 'new\n' }; + const result = replaceGeneratedSections(input, generators); + + expect(result).toBe([ + 'before', + '', + '', + 'new', + '', + '', + 'after', + ].join('\n')); + }); +}); From 2b074e460cac85337cf90c6be675321bd2f4b276 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 6 Apr 2026 22:56:08 -0700 Subject: [PATCH 3/4] feat: hybrid signpost generation with markers Ship sync now finds / marker pairs in signpost files and replaces their content using a registry of named generators. Hand-authored prose outside markers is preserved. Generators: - cli-commands: from CLI_TOPICS + usage() in cli-args.ts - mcp-tools: from MCP_TOOLS array in mcp.ts - signpost-inventory: static table of all signposts Converted signposts: - docs/CLI.md: hand-written intro + generated commands + hand-written config - docs/MCP.md: hand-written intro + generated tools + hand-written errors - docs/GUIDE.md: hand-written advice + generated signpost inventory Removed old generateCliReference/generateMcpReference from index.ts (replaced by generate.ts module). 127 tests pass (122 prior + 5 new generate tests). --- docs/BEARING.md | 4 +- docs/CLI.md | 14 +++++- docs/GUIDE.md | 18 +++++++ docs/MCP.md | 10 +++- src/generate.ts | 90 ++++++++++++++++++++++++++++++++++ src/index.ts | 125 ++++++++---------------------------------------- 6 files changed, 150 insertions(+), 111 deletions(-) create mode 100644 src/generate.ts diff --git a/docs/BEARING.md b/docs/BEARING.md index d18c1a7..6817c22 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -1,8 +1,8 @@ --- title: "BEARING" -generated_at: 2026-04-07T05:30:14.479Z +generated_at: 2026-04-07T05:55:49.964Z generator: "method sync ship (renderBearing)" -generated_from_commit: "03d889ddbe57ecb6b336ba2a62815f863f85bfd1" +generated_from_commit: "e0ede9beb70f0be809fe95782025e9ebca97fb2e" provenance_level: artifact_history --- diff --git a/docs/CLI.md b/docs/CLI.md index 3ac1b96..44e9ff3 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -1,7 +1,7 @@ --- title: "CLI Reference" generated_at: 2026-04-07T05:30:14.481Z -generator: "method sync ship (generateCliReference)" +generator: "method sync ship" generated_from_commit: "03d889ddbe57ecb6b336ba2a62815f863f85bfd1" provenance_level: artifact_history --- @@ -14,6 +14,7 @@ operations. Run `method help` for a quick summary or ## Commands + ### `method init [path]` Scaffold a METHOD workspace in the given directory. @@ -50,10 +51,19 @@ GitHub Options: --pull Update local backlog with GitHub changes (labels, comments, status). Perform Ship Sync or synchronize the backlog with GitHub Issues. -## Exit Codes +### Exit Codes | Code | Meaning | |------|---------| | 0 | Success | | 1 | Error (bad arguments, missing workspace, failed operation) | | 2 | Drift found (from `method drift`) | + + +## Configuration + +The CLI reads `.method.json` from the workspace root for: + +- `paths` — custom directory layout (see `ARCHITECTURE.md`) +- `github_token` / `github_repo` — GitHub adapter credentials +- Environment overrides: `GITHUB_TOKEN`, `GITHUB_REPO` diff --git a/docs/GUIDE.md b/docs/GUIDE.md index 9974eed..568f22a 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -1,7 +1,11 @@ --- title: "Guide" +generator: "method sync ship" +provenance_level: artifact_history --- +# Guide + This document holds practical advice for working in a METHOD repo. It is intentionally lighter than doctrine in `README.md` or `docs/method/process.md`. @@ -39,3 +43,17 @@ Some things are worth saying before they are worth standardizing. `README.md` and `docs/method/process.md` describe the load-bearing contract of METHOD. This guide is for patterns that help in practice but are not yet strong enough to claim as universal rules. + +## Signposts + + +| Signpost | Type | Description | +|----------|------|-------------| +| `README.md` | Hand-authored | Core doctrine and filesystem shape. | +| `ARCHITECTURE.md` | Hand-authored | How the source code is organized. | +| `docs/BEARING.md` | Generated | Current direction and recent ships. | +| `docs/VISION.md` | Generated | Bounded executive synthesis. | +| `docs/CLI.md` | Generated | CLI command reference. | +| `docs/MCP.md` | Generated | MCP tool reference. | +| `docs/GUIDE.md` | Hybrid | Operator advice with generated sections. | + diff --git a/docs/MCP.md b/docs/MCP.md index 85a9ca2..f770e6e 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -1,7 +1,7 @@ --- title: "MCP Reference" generated_at: 2026-04-07T05:30:14.482Z -generator: "method sync ship (generateMcpReference)" +generator: "method sync ship" generated_from_commit: "03d889ddbe57ecb6b336ba2a62815f863f85bfd1" provenance_level: artifact_history --- @@ -20,6 +20,7 @@ workspace-agnostic — a single instance can serve multiple projects. ## Tools + ### `method_status` Get the current status of the METHOD workspace (backlog lanes, active cycles, legend health) @@ -80,3 +81,10 @@ Automate terminal evidence capture for a cycle **Parameters:** - `cycle` (optional) `string` + + +## Error Handling + +All tools return `isError: true` with a text message on failure. +Common errors: `workspace is required`, `not a METHOD workspace`, +`No active cycles found`. diff --git a/src/generate.ts b/src/generate.ts new file mode 100644 index 0000000..25ee51d --- /dev/null +++ b/src/generate.ts @@ -0,0 +1,90 @@ +import { CLI_TOPICS, usage } from './cli-args.js'; +import { MCP_TOOLS } from './mcp.js'; + +export type GeneratorFn = () => string; +export type GeneratorRegistry = Record; + +const MARKER_PATTERN = /\n[\s\S]*?/gu; + +export function replaceGeneratedSections( + content: string, + generators: GeneratorRegistry, +): string { + return content.replace(MARKER_PATTERN, (match, name: string) => { + const generator = generators[name]; + if (generator === undefined) { + return match; + } + const generated = generator(); + return `\n${generated}`; + }); +} + +export function cliCommandsGenerator(): string { + const lines: string[] = []; + for (const topic of CLI_TOPICS) { + const text = usage(topic); + const usageLine = text.split('\n')[0] ?? ''; + const description = text.split('\n').slice(1).filter((l) => l.trim().length > 0); + lines.push(`### \`${usageLine.replace(/^Usage: /u, '')}\``); + lines.push(''); + for (const line of description) { + lines.push(line); + } + if (description.length > 0) { + lines.push(''); + } + } + lines.push('### Exit Codes'); + lines.push(''); + lines.push('| Code | Meaning |'); + lines.push('|------|---------|'); + lines.push('| 0 | Success |'); + lines.push('| 1 | Error (bad arguments, missing workspace, failed operation) |'); + lines.push('| 2 | Drift found (from `method drift`) |'); + lines.push(''); + return lines.join('\n'); +} + +export function mcpToolsGenerator(): string { + const lines: string[] = []; + for (const tool of MCP_TOOLS) { + lines.push(`### \`${tool.name}\``); + lines.push(''); + lines.push(tool.description); + lines.push(''); + + const params = Object.entries(tool.inputSchema.properties) + .filter(([key]) => key !== 'workspace'); + const required = new Set(tool.inputSchema.required.filter((k) => k !== 'workspace')); + + if (params.length > 0) { + lines.push('**Parameters:**'); + lines.push(''); + for (const [key, schema] of params) { + const req = required.has(key) ? '(required)' : '(optional)'; + const desc = ('description' in schema && schema.description) ? ` — ${schema.description}` : ''; + const enumValues = ('enum' in schema && schema.enum) ? ` (${schema.enum.join(', ')})` : ''; + lines.push(`- \`${key}\` ${req} \`${schema.type}\`${enumValues}${desc}`); + } + lines.push(''); + } + } + return lines.join('\n'); +} + +export function signpostInventoryGenerator(): string { + const lines: string[] = [ + '| Signpost | Type | Description |', + '|----------|------|-------------|', + '| `README.md` | Hand-authored | Core doctrine and filesystem shape. |', + '| `ARCHITECTURE.md` | Hand-authored | How the source code is organized. |', + '| `docs/BEARING.md` | Generated | Current direction and recent ships. |', + '| `docs/VISION.md` | Generated | Bounded executive synthesis. |', + '| `docs/CLI.md` | Generated | CLI command reference. |', + '| `docs/MCP.md` | Generated | MCP tool reference. |', + '| `docs/GUIDE.md` | Hybrid | Operator advice with generated sections. |', + '', + ]; + return lines.join('\n'); +} diff --git a/src/index.ts b/src/index.ts index 1d93479..94b1a28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,10 +20,9 @@ import { type WorkspaceStatus, } from './domain.js'; import { DEFAULT_PATHS, loadConfig, type Config, type PathsConfig } from './config.js'; -import { CLI_TOPICS, usage } from './cli-args.js'; import { detectWorkspaceDrift, type DriftReport } from './drift.js'; import { MethodError } from './errors.js'; -import { MCP_TOOLS } from './mcp.js'; +import { cliCommandsGenerator, mcpToolsGenerator, replaceGeneratedSections, signpostInventoryGenerator } from './generate.js'; export interface ResolvedPaths { backlog: string; @@ -225,8 +224,6 @@ export class Workspace { shipSync(): { updated: string[]; newShips: Cycle[] } { const changelogPath = resolve(this.root, 'CHANGELOG.md'); const bearingPath = resolve(this.root, 'docs/BEARING.md'); - const cliPath = resolve(this.root, 'docs/CLI.md'); - const mcpPath = resolve(this.root, 'docs/MCP.md'); const status = this.status(); const closedCycles = this.allCycles().filter((cycle) => existsSync(cycle.retroDoc)); const commitSha = this.currentCommitSha(); @@ -242,11 +239,25 @@ export class Workspace { writeFileSync(bearingPath, renderBearing(status, closedCycles, commitSha), 'utf8'); updated.push('docs/BEARING.md'); - writeFileSync(cliPath, generateCliReference(commitSha), 'utf8'); - updated.push('docs/CLI.md'); + // Hybrid generation: replace sections in signpost files + const generators = { + 'cli-commands': cliCommandsGenerator, + 'mcp-tools': mcpToolsGenerator, + 'signpost-inventory': signpostInventoryGenerator, + }; - writeFileSync(mcpPath, generateMcpReference(commitSha), 'utf8'); - updated.push('docs/MCP.md'); + for (const signpost of ['docs/CLI.md', 'docs/MCP.md', 'docs/GUIDE.md']) { + const signpostPath = resolve(this.root, signpost); + if (!existsSync(signpostPath)) { + continue; + } + const before = readFileSync(signpostPath, 'utf8'); + const after = replaceGeneratedSections(before, generators); + if (after !== before) { + writeFileSync(signpostPath, after, 'utf8'); + updated.push(signpost); + } + } return { updated, newShips }; } @@ -584,104 +595,6 @@ export class Workspace { } } -function generateCliReference(commitSha: string): string { - const lines: string[] = [ - '---', - 'title: "CLI Reference"', - `generated_at: ${new Date().toISOString()}`, - 'generator: "method sync ship (generateCliReference)"', - `generated_from_commit: "${commitSha}"`, - 'provenance_level: artifact_history', - '---', - '', - '# CLI Reference', - '', - 'The `method` command is the primary interface for METHOD workspace', - 'operations. Run `method help` for a quick summary or', - '`method help ` for command-specific usage.', - '', - '## Commands', - '', - ]; - - for (const topic of CLI_TOPICS) { - const text = usage(topic); - const usageLine = text.split('\n')[0] ?? ''; - const description = text.split('\n').slice(1).filter((l) => l.trim().length > 0); - lines.push(`### \`${usageLine.replace(/^Usage: /u, '')}\``); - lines.push(''); - for (const line of description) { - lines.push(line.startsWith(' ') ? line : line); - } - if (description.length > 0) { - lines.push(''); - } - } - - lines.push('## Exit Codes'); - lines.push(''); - lines.push('| Code | Meaning |'); - lines.push('|------|---------|'); - lines.push('| 0 | Success |'); - lines.push('| 1 | Error (bad arguments, missing workspace, failed operation) |'); - lines.push('| 2 | Drift found (from `method drift`) |'); - lines.push(''); - - return lines.join('\n'); -} - -function generateMcpReference(commitSha: string): string { - const lines: string[] = [ - '---', - 'title: "MCP Reference"', - `generated_at: ${new Date().toISOString()}`, - 'generator: "method sync ship (generateMcpReference)"', - `generated_from_commit: "${commitSha}"`, - 'provenance_level: artifact_history', - '---', - '', - '# MCP Reference', - '', - 'METHOD exposes its workspace operations through a Model Context', - 'Protocol (MCP) server. Start it with `method mcp` — it communicates', - 'over stdio using JSON-RPC.', - '', - '## Workspace Parameter', - '', - 'Every tool requires a `workspace` parameter: the absolute path to the', - 'METHOD workspace root directory. This makes the server', - 'workspace-agnostic — a single instance can serve multiple projects.', - '', - '## Tools', - '', - ]; - - for (const tool of MCP_TOOLS) { - lines.push(`### \`${tool.name}\``); - lines.push(''); - lines.push(tool.description); - lines.push(''); - - const params = Object.entries(tool.inputSchema.properties) - .filter(([key]) => key !== 'workspace'); - const required = new Set(tool.inputSchema.required.filter((k) => k !== 'workspace')); - - if (params.length > 0) { - lines.push('**Parameters:**'); - lines.push(''); - for (const [key, schema] of params) { - const req = required.has(key) ? '(required)' : '(optional)'; - const desc = ('description' in schema && schema.description) ? ` — ${schema.description}` : ''; - const enumValues = ('enum' in schema && schema.enum) ? ` (${schema.enum.join(', ')})` : ''; - lines.push(`- \`${key}\` ${req} \`${schema.type}\`${enumValues}${desc}`); - } - lines.push(''); - } - } - - return lines.join('\n'); -} - function collectMarkdownFiles(root: string): string[] { if (!existsSync(root)) { return []; From fd8942abf85d62e2a2161cb6e43b92383449393a Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 6 Apr 2026 22:56:55 -0700 Subject: [PATCH 4/4] cycles: close 0028-hybrid-signpost-generation Hill met. Hybrid signpost generation with generate markers. CLI.md, MCP.md, GUIDE.md all use markers. 127 tests pass. No drift. --- .../hybrid-signpost-generation.md | 42 ++++++++++++++++ .../witness/verification.md | 50 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 docs/method/retro/0028-hybrid-signpost-generation/hybrid-signpost-generation.md create mode 100644 docs/method/retro/0028-hybrid-signpost-generation/witness/verification.md diff --git a/docs/method/retro/0028-hybrid-signpost-generation/hybrid-signpost-generation.md b/docs/method/retro/0028-hybrid-signpost-generation/hybrid-signpost-generation.md new file mode 100644 index 0000000..2158bc6 --- /dev/null +++ b/docs/method/retro/0028-hybrid-signpost-generation/hybrid-signpost-generation.md @@ -0,0 +1,42 @@ +--- +title: "Hybrid Signpost Generation" +outcome: hill-met +drift_check: yes +--- + +# Hybrid Signpost Generation Retro + +Design: `docs/design/0028-hybrid-signpost-generation/hybrid-signpost-generation.md` +Outcome: hill-met +Drift check: yes + +## Summary + +Replaced fully-generated signposts with a hybrid model using +`` / `` markers. Hand-authored +prose survives regeneration. Ship sync replaces only the content +between markers. Three generators implemented: cli-commands, +mcp-tools, signpost-inventory. + +## Playback Witness + +- [Verification Witness](./witness/verification.md) + +## Drift + +- None recorded. + +## New Debt + +- `captureIdea` still does not write YAML frontmatter on new inbox items. + +## Cool Ideas + +- Add a `generate:backlog-summary` section for GUIDE.md. +- Add marker support to ARCHITECTURE.md for auto-generated source listing. + +## Backlog Maintenance + +- [x] Inbox processed +- [x] Priorities reviewed +- [x] Dead work buried or merged diff --git a/docs/method/retro/0028-hybrid-signpost-generation/witness/verification.md b/docs/method/retro/0028-hybrid-signpost-generation/witness/verification.md new file mode 100644 index 0000000..7c166b4 --- /dev/null +++ b/docs/method/retro/0028-hybrid-signpost-generation/witness/verification.md @@ -0,0 +1,50 @@ +--- +title: "Verification Witness for Cycle 28" +--- + +# Verification Witness for Cycle 28 + +This witness proves that `Hybrid Signpost Generation` now carries the required +behavior and adheres to the repo invariants. + +## Test Results + +``` +> method@0.2.0 test +> vitest run --config vitest.config.ts + + + RUN v4.1.2 ./method + + + Test Files 12 passed (12) + Tests 127 passed (127) + Start at 22:56:15 + Duration 987ms (transform 884ms, setup 0ms, import 2.85s, tests 1.05s, environment 1ms) +``` + +## Drift Results + +``` +Playback-question drift found. +Scanned 1 active cycle, 6 playback questions, 140 test descriptions. +Search basis: exact normalized match in tests/**/*.test.* and tests/**/*.spec.* descriptions. + +docs/design/0028-hybrid-signpost-generation/hybrid-signpost-generation.md +- Human: Ship sync replaces content between generate markers without touching surrounding prose. + Near miss: "replaces content between generate markers without touching surrounding prose" +- Human: CLI.md, MCP.md, and GUIDE.md use generate markers for their reference sections. + No exact normalized test description match found. +- Human: Hand-written content outside markers survives regeneration. + No exact normalized test description match found. +- Agent: A `replaceGeneratedSections` function finds marker pairs and replaces their content using a registry of named generators. + No exact normalized test description match found. +- Agent: Generators exist for `cli-commands`, `mcp-tools`, and `signpost-inventory`. + No exact normalized test description match found. +- Agent: Tests prove markers are respected and content outside is preserved. + No exact normalized test description match found. +``` + +## Manual Verification + +- [x] Automated capture completed successfully.