@sdd-method/maintain-cli 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +1 -1
  3. package/dist/lib/agent-harness.js +17 -8
  4. package/dist/lib/briefing/index.js +1 -1
  5. package/dist/lib/briefing/render.js +17 -8
  6. package/dist/lib/catalogue/aggregate.js +6 -6
  7. package/dist/lib/catalogue/aggregation-manifest-loader.js +4 -4
  8. package/dist/lib/catalogue/build.js +1 -1
  9. package/dist/lib/catalogue/builders/c4-components.js +1 -1
  10. package/dist/lib/catalogue/builders/c4-containers.js +1 -1
  11. package/dist/lib/catalogue/builders/c4-l1-candidates.js +1 -1
  12. package/dist/lib/catalogue/builders/container-dependency.js +1 -1
  13. package/dist/lib/catalogue/builders/data-store-mapping.js +1 -1
  14. package/dist/lib/catalogue/builders/screen-tree.js +1 -1
  15. package/dist/lib/catalogue/contract-path-resolution.js +2 -2
  16. package/dist/lib/catalogue/dedupe-keyed-tables.js +0 -0
  17. package/dist/lib/catalogue/freshness.js +4 -3
  18. package/dist/lib/catalogue/scores-coverage.js +1 -1
  19. package/dist/lib/catalogue/service-contract-aliases-loader.js +1 -1
  20. package/dist/lib/ci-binding/index.js +44 -5
  21. package/dist/lib/ci-binding/module.js +74 -2
  22. package/dist/lib/context-pull/index.js +79 -39
  23. package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
  24. package/dist/lib/hooks-seed/entries.js +55 -4
  25. package/dist/lib/hooks-seed/index.js +4 -7
  26. package/dist/lib/hooks-seed/merge.js +31 -0
  27. package/dist/lib/init/ci-workflow.js +26 -2
  28. package/dist/lib/lock-file.js +10 -0
  29. package/dist/lib/pack/builder.js +56 -4
  30. package/dist/lib/pack/index.js +1 -0
  31. package/dist/lib/pack/publish-transform.js +1 -1
  32. package/dist/lib/sync/briefing-merge.js +50 -34
  33. package/dist/lib/sync/diff.js +1 -1
  34. package/dist/lib/sync/index.js +46 -72
  35. package/dist/lib/sync/settings-merge.js +71 -163
  36. package/dist/lib/validate/plan.js +6 -3
  37. package/dist/maintain-program.js +6 -3
  38. package/dist/maintain-version.json +2 -2
  39. package/dist/verbs/catalogue-check.js +28 -0
  40. package/dist/verbs/context.js +1 -1
  41. package/dist/verbs/pack-build.js +22 -0
  42. package/dist/verbs/sync.js +17 -0
  43. package/dist/verbs/validate.js +6 -1
  44. package/dist/verify-program.js +2 -2
  45. package/dist/verify-version.json +2 -2
  46. package/package.json +2 -6
  47. package/dist/lib/hooks-seed/copilot.js +0 -167
  48. package/dist/lib/hooks-seed/lifecycle-mapping.js +0 -28
  49. package/dist/lib/mcp-seed/entries.js +0 -14
  50. package/dist/lib/mcp-seed/index.js +0 -28
  51. package/dist/lib/mcp-seed/merge.js +0 -99
  52. package/dist/lib/mcp-seed/types.js +0 -13
  53. package/dist/lib/sync/copilot-hooks-merge.js +0 -108
@@ -1,33 +1,33 @@
1
1
  /**
2
- * `sdd-cli context pull` — write the code-repo briefing and MCP_SETUP.md into a
3
- * code repository from its upstream SDD (M-ADR 0128 §1; M-ADR 0150 §4.3–4.4).
2
+ * `sdd-cli context pull` — write the code-repo briefing into a code repository from its upstream
3
+ * SDD (M-ADR 0220 §4.5, re-homing M-ADR 0150 §4.4 Phase D).
4
4
  *
5
- * The successor to the code-repo half of `generate-claude-md.sh`, scoped by
6
- * M-ADR 0150 Phase D: no dynamic content is baked in; that is the SDD's MCP
7
- * server's job. A sibling verb to `generate-briefing`, not an extension of it:
5
+ * The successor to the code-repo half of `generate-claude-md.sh`. No dynamic content is baked
6
+ * in: the briefing points at the SDD's files. A sibling verb to `generate-briefing`, not an extension of it:
8
7
  * `generate-briefing` needs the target to be an SDD (marker, lock, synced
9
8
  * templates), and a code repository is none of those. They share the
10
9
  * placeholder substitution and the wrapper's marker-block merge.
11
10
  *
12
11
  * What it writes, and nothing else (M-ADR 0218 §4.3 item 9, a MAINTAIN verb):
13
12
  * agent-instructions.md whole file, method-managed
14
- * CLAUDE.md / AGENTS.md the method-managed block only; adopter content survives
15
- * MCP_SETUP.md whole file, method-managed
16
- * Templates are read from the SDD, which carries them via its baseline bundle.
13
+ * CLAUDE.md, AGENTS.md the method-managed block only; adopter content survives (AGENTS.md is
14
+ * the pointer, M-ADR 0220 §4.6, when the SDD's baseline carries it)
15
+ * Templates are read from the SDD, which carries them via its baseline bundle. MCP_SETUP.md is no
16
+ * longer written (M-ADR 0220 §4.5); a copy this verb wrote earlier is removed when it still
17
+ * matches the method's last render, and left with a notice when it does not.
17
18
  */
18
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
19
+ import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
19
20
  import { basename, dirname, join, resolve } from "node:path";
20
- import { AGENT_INSTRUCTIONS_REL_PATH, WRAPPER_BLOCK_BEGIN, WRAPPER_BLOCK_END, findUnresolvedPlaceholders, harnessWrapperOutputPath, resolveBriefingVariables, splitWrapperTemplate, } from "../briefing/index.js";
21
+ import { AGENT_INSTRUCTIONS_REL_PATH, AGENTS_POINTER, WRAPPER_BLOCK_BEGIN, WRAPPER_BLOCK_END, findUnresolvedPlaceholders, harnessWrapperOutputPath, resolveBriefingVariables, splitWrapperTemplate, } from "../briefing/index.js";
21
22
  import { readLockFile } from "../lock-file.js";
22
23
  import { readRepoKind } from "../repo-kind.js";
23
24
  import { writeMarkedBlockFile } from "../sync/yaml-block-merge.js";
24
25
  import { defaultSearchRoots, resolveCodeRepoName, resolveSdd } from "./resolve.js";
26
+ import { RETIRED_MCP_SETUP_TEMPLATE } from "./retired-mcp-setup.js";
25
27
  export const CODE_REPO_BRIEFING_ROOT = "orchestration/templates/briefing/code-repo";
26
- export const MCP_SETUP_TEMPLATE = "orchestration/templates/mcp/code-repo/MCP_SETUP.md.hbs";
27
28
  export const MCP_SETUP_REL_PATH = "MCP_SETUP.md";
28
29
  const WRAPPER_TEMPLATE = {
29
30
  "claude-code": "claude-code/CLAUDE.md.hbs",
30
- "github-copilot-cli": "github-copilot-cli/AGENTS.md.hbs",
31
31
  };
32
32
  export async function runContextPull(opts, out = (line) => process.stdout.write(line)) {
33
33
  const codeRepo = resolve(opts.codeRepo);
@@ -73,21 +73,23 @@ export async function runContextPull(opts, out = (line) => process.stdout.write(
73
73
  repo_kind: sdd.repo_kind,
74
74
  baseline_version: sdd.baseline_version,
75
75
  pull_date: opts.pullDate ?? new Date().toISOString().slice(0, 10),
76
- // MCP_SETUP.md's own names, kept so the template the bash generator rendered still renders.
77
- upstream_sdd_name: basename(sddRoot),
78
- sdd_method_baseline_version: sdd.baseline_version,
79
76
  };
80
77
  const aiTemplate = join(sddRoot, CODE_REPO_BRIEFING_ROOT, "agent-instructions.md.hbs");
81
78
  const wrapperTemplate = join(sddRoot, CODE_REPO_BRIEFING_ROOT, WRAPPER_TEMPLATE[opts.agentHarness]);
82
- const mcpTemplate = join(sddRoot, MCP_SETUP_TEMPLATE);
83
- for (const t of [aiTemplate, wrapperTemplate, mcpTemplate]) {
79
+ for (const t of [aiTemplate, wrapperTemplate]) {
84
80
  if (!(await exists(t))) {
85
81
  out(fail(`${sddRoot} does not carry ${t.slice(sddRoot.length + 1)}: its method baseline predates context pull. Sync the SDD to a baseline that ships the code-repo templates.`));
86
82
  return { exitCode: 1 };
87
83
  }
88
84
  }
85
+ // The AGENTS.md pointer (M-ADR 0220 §4.6) arrived later than the other code-repo templates: an SDD
86
+ // whose baseline does not carry it yet gets no pointer, and everything else proceeds.
87
+ const pointerTemplate = join(sddRoot, CODE_REPO_BRIEFING_ROOT, AGENTS_POINTER.templateRel);
88
+ const wrapperSources = [[harnessWrapperOutputPath(opts.agentHarness), wrapperTemplate]];
89
+ if (await exists(pointerTemplate))
90
+ wrapperSources.push([AGENTS_POINTER.outputRel, pointerTemplate]);
89
91
  const rendered = {};
90
- for (const [rel, path] of [[AGENT_INSTRUCTIONS_REL_PATH, aiTemplate], [harnessWrapperOutputPath(opts.agentHarness), wrapperTemplate], [MCP_SETUP_REL_PATH, mcpTemplate]]) {
92
+ for (const [rel, path] of [[AGENT_INSTRUCTIONS_REL_PATH, aiTemplate], ...wrapperSources]) {
91
93
  const content = substituteAll(await readFile(path, "utf8"), vars);
92
94
  const unresolved = findUnresolvedPlaceholders(content);
93
95
  if (unresolved.length > 0) {
@@ -96,30 +98,37 @@ export async function runContextPull(opts, out = (line) => process.stdout.write(
96
98
  }
97
99
  rendered[rel] = content;
98
100
  }
99
- // The wrapper first: it is the only write that can be refused, and a refusal must leave nothing written.
100
- const wrapperRel = harnessWrapperOutputPath(opts.agentHarness);
101
- const wrapperPath = join(codeRepo, wrapperRel);
102
- const existing = (await exists(wrapperPath)) ? await readFile(wrapperPath, "utf8") : null;
103
- const parts = splitWrapperTemplate(rendered[wrapperRel]);
104
- const hasMarkers = existing !== null && existing.includes(WRAPPER_BLOCK_BEGIN) && existing.includes(WRAPPER_BLOCK_END);
105
- let wrapperChanged;
106
- if (existing !== null && !hasMarkers) {
107
- // No markers: a pre-briefing-layer file (the bash generator wrote these whole) or adopter-authored.
108
- if (!opts.allowOverwrite) {
109
- out(fail(`${wrapperRel} exists without method-managed markers, so it is treated as adopter-authored. Nothing was written. Re-run with --allow-overwrite to replace it with the rendered wrapper (move anything worth keeping below its markers afterwards).`));
101
+ // The wrappers first: they are the only writes that can be refused, and a refusal must leave
102
+ // nothing written, so every wrapper is checked before any is written.
103
+ const existingOf = {};
104
+ for (const [rel] of wrapperSources) {
105
+ const path = join(codeRepo, rel);
106
+ const existing = (await exists(path)) ? await readFile(path, "utf8") : null;
107
+ existingOf[rel] = existing;
108
+ const hasMarkers = existing !== null && existing.includes(WRAPPER_BLOCK_BEGIN) && existing.includes(WRAPPER_BLOCK_END);
109
+ if (existing !== null && !hasMarkers && !opts.allowOverwrite) {
110
+ // No markers: a pre-briefing-layer file (the bash generator wrote these whole) or adopter-authored.
111
+ out(fail(`${rel} exists without method-managed markers, so it is treated as adopter-authored. Nothing was written. Re-run with --allow-overwrite to replace it with the rendered wrapper (move anything worth keeping below its markers afterwards).`));
110
112
  return { exitCode: 1 };
111
113
  }
112
- await writeFile(wrapperPath, rendered[wrapperRel], "utf8");
113
- wrapperChanged = existing !== rendered[wrapperRel];
114
- }
115
- else {
116
- const merge = await writeMarkedBlockFile(wrapperPath, existing === null ? rendered[wrapperRel] : parts.block, { begin: WRAPPER_BLOCK_BEGIN, end: WRAPPER_BLOCK_END }, opts.allowOverwrite, existing === null ? undefined : { pre: parts.pre, post: parts.post });
117
- wrapperChanged = merge.changed;
118
114
  }
119
115
  const changed = [];
120
- if (wrapperChanged)
121
- changed.push(wrapperRel);
122
- for (const rel of [AGENT_INSTRUCTIONS_REL_PATH, MCP_SETUP_REL_PATH]) {
116
+ for (const [rel] of wrapperSources) {
117
+ const path = join(codeRepo, rel);
118
+ const existing = existingOf[rel] ?? null;
119
+ const hasMarkers = existing !== null && existing.includes(WRAPPER_BLOCK_BEGIN) && existing.includes(WRAPPER_BLOCK_END);
120
+ if (existing !== null && !hasMarkers) {
121
+ await writeFile(path, rendered[rel], "utf8");
122
+ if (existing !== rendered[rel])
123
+ changed.push(rel);
124
+ continue;
125
+ }
126
+ const parts = splitWrapperTemplate(rendered[rel]);
127
+ const merge = await writeMarkedBlockFile(path, existing === null ? rendered[rel] : parts.block, { begin: WRAPPER_BLOCK_BEGIN, end: WRAPPER_BLOCK_END }, opts.allowOverwrite, existing === null ? undefined : { pre: parts.pre, post: parts.post });
128
+ if (merge.changed)
129
+ changed.push(rel);
130
+ }
131
+ for (const rel of [AGENT_INSTRUCTIONS_REL_PATH]) {
123
132
  const path = join(codeRepo, rel);
124
133
  const before = (await exists(path)) ? await readFile(path, "utf8") : null;
125
134
  if (before !== rendered[rel]) {
@@ -128,10 +137,41 @@ export async function runContextPull(opts, out = (line) => process.stdout.write(
128
137
  changed.push(rel);
129
138
  }
130
139
  }
140
+ const mcpSetup = await retireMcpSetup(codeRepo);
131
141
  out(info(`Code repo "${repoName}" is specified by ${sdd.sdd_name} (${sdd.repo_kind}, ${sdd.baseline_version}) at ${sddRoot}.`));
132
- out(info(changed.length > 0 ? `Wrote ${changed.join(", ")}.` : `Already current: ${AGENT_INSTRUCTIONS_REL_PATH}, ${wrapperRel}, ${MCP_SETUP_REL_PATH}.`));
142
+ out(info(changed.length > 0 ? `Wrote ${changed.join(", ")}.` : `Already current: ${[AGENT_INSTRUCTIONS_REL_PATH, ...wrapperSources.map(([rel]) => rel)].join(", ")}.`));
143
+ if (mcpSetup === "removed")
144
+ out(info(`Removed ${MCP_SETUP_REL_PATH}: the method no longer ships it (M-ADR 0220), and this copy was unedited.`));
145
+ if (mcpSetup === "kept")
146
+ out(info(`${MCP_SETUP_REL_PATH} is no longer written (M-ADR 0220). This copy differs from the method's last render, so it was left; delete it if nothing in it is yours.`));
133
147
  return { exitCode: 0 };
134
148
  }
149
+ /**
150
+ * Remove an MCP_SETUP.md this verb wrote before M-ADR 0220, when it is byte-identical to the
151
+ * method's last template rendered with the values its own header records. Anything else is the
152
+ * adopter's now, and stays.
153
+ */
154
+ export async function retireMcpSetup(codeRepo) {
155
+ const path = join(codeRepo, MCP_SETUP_REL_PATH);
156
+ if (!(await exists(path)))
157
+ return "absent";
158
+ const text = await readFile(path, "utf8");
159
+ const repo = /^\* \*\*Emitted by:\*\* `sdd-cli context pull`, for code repo `([^`]*)`$/m.exec(text);
160
+ const upstream = /^\* \*\*Upstream SDD:\*\* `([^`]*)` at `([^`]*)`$/m.exec(text);
161
+ const baseline = /^\* \*\*Baseline version:\*\* `([^`]*)`$/m.exec(text);
162
+ if (!repo || !upstream || !baseline)
163
+ return "kept";
164
+ const render = substituteAll(RETIRED_MCP_SETUP_TEMPLATE, {
165
+ repo_name: repo[1],
166
+ upstream_sdd_name: upstream[1],
167
+ sdd_root: upstream[2],
168
+ sdd_method_baseline_version: baseline[1],
169
+ });
170
+ if (render !== text)
171
+ return "kept";
172
+ await rm(path);
173
+ return "removed";
174
+ }
135
175
  /** `{{name}}` substitution over an open variable set (the briefing renderer's is closed to four). */
136
176
  export function substituteAll(template, vars) {
137
177
  return template.replace(/\{\{\s*([a-z_][a-z0-9_]*)\s*\}\}/g, (full, key) => (key in vars ? vars[key] : full));
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The last MCP_SETUP.md template the method shipped (sdd-method 52bbc36), kept so `context pull`
3
+ * can tell a copy it wrote from one an adopter edited after M-ADR 0220 §4.5 retired the file.
4
+ * The SDD's own baseline stops carrying the template, so the comparison cannot read it from there.
5
+ */
6
+ export const RETIRED_MCP_SETUP_TEMPLATE = "# MCP_SETUP.md\n\n* **Source:** sdd-method-baseline\n* **Emitted by:** `sdd-cli context pull`, for code repo `{{repo_name}}`\n* **Upstream SDD:** `{{upstream_sdd_name}}` at `{{sdd_root}}`\n* **Baseline version:** `{{sdd_method_baseline_version}}`\n\nThis file is **method-shipped guidance for agents working in this code repo**. It tells the agent how to reach the upstream SDD's live context via MCP. Read it alongside `agent-instructions.md` at session start.\n\nPer M-ADR 0150 (MCP as the primary dynamic-context layer), the **primary dynamic-context surface** for this code repo is the upstream SDD's MCP server, not baked-in snapshot content. Prefer MCP queries over reading static files for anything that might have changed since the last `sdd-cli context pull`.\n\n## \u00a71 What MCP gives you here\n\nThis code repo (`{{repo_name}}`) consumes specifications from the upstream SDD `{{upstream_sdd_name}}`. The upstream SDD exposes a read-only MCP server (`sdd-mcp`) you can query for:\n\n* **Catalogue rows** \u2014 current capability inventory, service registry, contract list, feature tree, cross-SDD edges.\n* **Gate state** \u2014 lock-in recommendations, intent-quality scores, domain-architecture scores, contract-quality scores per capability.\n* **Plan progress** \u2014 active plans, phases shipped, deferred follow-ups.\n* **Spec lookups** \u2014 ADRs, capability specs, feature specs, traceability manifests.\n\nThe static briefing in this repo (`agent-instructions.md` and the harness wrapper) covers conventions and orientation that are stable across syncs. Anything that changes faster than syncs run \u2014 gate state, catalogue rows, lock-in status \u2014 belongs behind MCP.\n\n## \u00a72 Server identity\n\n* **Server name:** `sdd-mcp`\n* **Upstream SDD path:** `{{sdd_root}}`\n* **Transport:** stdio (per M-ADR 0128's three-MCP-server commitment)\n\n`{{sdd_root}}` is where the SDD was checked out when this file was written; adjust it if your checkout differs. The MCP server resolves the SDD root with this precedence (per sdd-cli):\n\n1. `SDD_REPO_PATH` environment variable.\n2. `--sdd-repo <path>` flag on `sdd-cli mcp serve`.\n3. A platform SDD serves itself: when the git root's own `.sdd-repo-kind` reads `platform-product`, that repository is the SDD, whatever its siblings carry (so a platform repo needs no `SDD_REPO_PATH`).\n4. Sibling-directory auto-discovery from the code repo.\n\n## \u00a73 Harness wiring\n\n### Claude Code (reference)\n\nAdd this fragment to `.claude/settings.json` in this code repo. If the SDD is checked out in a sibling directory, discovery finds it and the `--sdd-repo` argument can be dropped:\n\n```jsonc\n{\n \"mcpServers\": {\n \"sdd-mcp\": {\n \"command\": \"sdd-cli\",\n \"args\": [\"mcp\", \"serve\", \"--sdd-repo\", \"{{sdd_root}}\"]\n }\n }\n}\n```\n\nVerify the wiring: open this code repo in Claude Code; in a session, ask the agent \"list available MCP resources\". You should see `sdd://` URIs from `sdd-mcp`.\n\n### GitHub Copilot CLI\n\nCopilot CLI's MCP wiring uses `.github/copilot/mcp.json` (verify against the current Copilot CLI docs at the time of setup; this surface is evolving). Equivalent shape:\n\n```jsonc\n{\n \"servers\": {\n \"sdd-mcp\": {\n \"command\": \"sdd-cli\",\n \"args\": [\"mcp\", \"serve\", \"--sdd-repo\", \"{{sdd_root}}\"]\n }\n }\n}\n```\n\n### Other harnesses\n\nFor any other harness (OpenAI Codex CLI, OpenCode, a generic-stdio harness, or custom in-house): follow the adopter-responsibility flow per M-ADR 0141 \u00a74.5. Read the portable contract of each MCP resource (in the upstream SDD's `docs/method/methods/`) and instantiate against your harness's native MCP-client interface. Document the wiring in an adopter-side ADR or guide.\n\n## \u00a74 Resource surface \u2014 quick reference\n\nThe upstream `sdd-mcp` server exposes resources keyed by `sdd://` URIs. The most relevant for code-repo agents:\n\n| URI pattern | Returns | Typical use |\n|---|---|---|\n| `sdd://catalogue/{name}` | All rows from one catalogue (e.g. `services`, `capabilities`, `features`, `contracts`) | Discover what exists in the SDD |\n| `sdd://catalogue/metamodel` | The catalogue metamodel doc | Understand the entity / edge model |\n| `sdd://catalogue/schema` | Parsed schema (entities + columns + FK constraints) | Reason about data shape |\n| `sdd://gates/{gate}` | Current gate state per capability | Check whether your dependency has locked in |\n| `sdd://specs/{path}` | Spec content lookup | Read a specific feature / capability spec by ID |\n| `sdd://adrs/{id}` | ADR content + metadata | Cross-reference architectural decisions |\n\nNewly-added resource families ship via baseline sync; consult the upstream SDD's MCP setup docs for the current list.\n\nTools (callable via MCP `tools/call`): `find_producers`, `find_consumers`, `find_consumers_of_contract`, `find_dependencies`, `find_dependents`, `cascade_impact`, `query_edges_for_entity`, `find_orphan_rows`, `query_catalogue_coverage`, `find_products_by_category`, `find_related_specs`, `search_specs`, `list_adrs`, `query_gate_status`. Ask the server for `tools/list` for each tool's arguments.\n\n## \u00a75 Troubleshooting\n\n**`sdd-mcp` unreachable / \"Connection refused\"**\nThe MCP server is launched on-demand by the harness; if it isn't starting, check:\n1. `sdd-cli` is installed and on `$PATH` (`which sdd-cli` should resolve).\n2. The harness's MCP config exists and points at `sdd-cli mcp serve`.\n3. The `--sdd-repo` path (or the sibling the server discovers) is a real SDD root.\n\n**\"SDD root not found\" or \"manifest missing\"**\nThe SDD path doesn't resolve to a valid SDD. The path should contain `.sdd/sdd-manifest.yaml` (or the legacy `docs/method/foundations/` layout).\n\n**Version mismatch on resources**\nSome resource families require a minimum `sdd-cli` version. Check `npm list -g @sdd-method/sdd-cli`; upgrade via `npm install -g @sdd-method/sdd-cli@latest` if a resource returns \"unsupported by this server version\".\n\n**Permission denied**\nFor adopter-scoped resources (per M-ADR 0130), `platform-mcp` / `partner-mcp` enforce scope. If you're hitting permission errors, confirm you're querying the right MCP server for the resource (operator scope vs partner scope).\n\n## \u00a76 Fallback semantics \u2014 no silent stale-data\n\nIf `sdd-mcp` is unreachable for any reason, **the agent must report the error and continue with whatever static context exists in this code repo** \u2014 `CLAUDE.md` snapshot + the orientation in this file. **There is no silent fallback to stale baked-in dynamic state.** Per M-ADR 0128 \u00a71, falling back to stale data re-introduces exactly the problem MCP exists to fix.\n\nIf MCP-served dynamic context is unavailable, surface this back to the user explicitly. The user can choose to:\n\n* Proceed without dynamic context (accept the limitation).\n* Restart the MCP server / fix the connection.\n* Re-run `sdd-cli context pull` for a fresh static briefing, acknowledging it is still a point-in-time read.\n\n## Cross-references\n\n* Upstream SDD: `{{sdd_root}}`\n* M-ADR 0128 (MCP live-query infrastructure)\n* M-ADR 0150 (MCP as primary dynamic-context layer)\n* M-ADR 0130 (adopter MCP composition pattern \u2014 `platform-mcp` / `partner-mcp`)\n* M-ADR 0148 (the static-briefing layer this file complements)\n\n---\n\n<!-- This file is rewritten whole by sdd-cli context pull. Put repo-specific MCP guidance in the harness wrapper, below its method-managed markers. -->\n";
7
+ //# sourceMappingURL=retired-mcp-setup.js.map
@@ -5,6 +5,7 @@
5
5
  * merge path can identify it on refresh. Adopter-added hooks under
6
6
  * the same event survive because they lack the sentinel.
7
7
  */
8
+ import { EXACT_VERSION, getVerifyCliVersion } from "../package-version.js";
8
9
  import { METHOD_MANAGED_SENTINEL } from "./types.js";
9
10
  function withSentinel(command) {
10
11
  if (command.includes(METHOD_MANAGED_SENTINEL))
@@ -22,13 +23,63 @@ export function buildMethodGuardPreToolUse() {
22
23
  ],
23
24
  };
24
25
  }
25
- export function buildSessionStartValidate() {
26
+ const SESSION_START_VALIDATE_ARGS = ' validate --target-repo "${CLAUDE_PROJECT_DIR:-$(pwd)}" --validator adr-structure 2>&1 | head -20';
27
+ /** The verify persona's npm package, unless the lock names a rescoped one (sync --cli-package). */
28
+ export const DEFAULT_VERIFY_CLI_PACKAGE = "@sdd-method/verify-cli";
29
+ /** One npm package name, scoped or not, the shape `npm pack` and `npx` accept. */
30
+ export const NPM_PACKAGE_NAME = /^(@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
31
+ export const CLI_SOURCES = ["registry", "installed"];
32
+ export const DEFAULT_CLI_SOURCE = { source: "registry" };
33
+ function registryLauncher(version, pkg) {
34
+ return `npx --yes ${pkg}@${version}${SESSION_START_VALIDATE_ARGS}`;
35
+ }
36
+ function installedLauncher(version) {
37
+ return (`v=$(sdd-verify --version 2>/dev/null); [ "$v" = "${version}" ] || ` +
38
+ `echo "[method] sdd-verify \${v:-is not installed}; this SDD pins verify-cli ${version}: ` +
39
+ `pull your CLI release repository and run ./install.sh ${version}"; ` +
40
+ `if [ -n "$v" ]; then sdd-verify${SESSION_START_VALIDATE_ARGS}; fi`);
41
+ }
42
+ /**
43
+ * The session-start check runs the VERIFY persona's CLI (M-ADR 0218, 0220), not sdd-cli, which is
44
+ * the administrator's: validating is routine author work. It runs at the exact verify-cli version
45
+ * this sdd-cli releases with, the version the managed CI workflow installs, so a local pass comes
46
+ * from the same verifier CI uses.
47
+ */
48
+ export function sessionStartValidateCommand(verifyVersion = getVerifyCliVersion(), cli = DEFAULT_CLI_SOURCE) {
49
+ const body = cli.source === "installed"
50
+ ? installedLauncher(verifyVersion)
51
+ : registryLauncher(verifyVersion, cli.package ?? DEFAULT_VERIFY_CLI_PACKAGE);
52
+ return withSentinel(body);
53
+ }
54
+ const REGISTRY_FORM = /^npx --yes ((?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*)@(\S+) validate /;
55
+ const INSTALLED_FORM = /^v=\$\(sdd-verify --version 2>\/dev\/null\); \[ "\$v" = "([^"]+)" \]/;
56
+ /**
57
+ * Whether a session-start command is one this tool rendered, at any exact version and from either
58
+ * source: the sdd-cli form it wrote until the hook moved to verify-cli, the npx form under any
59
+ * package name, or the installed form. Sync replaces its own earlier render without counting it as
60
+ * drift, so a release that moves the pin, or a switch of source, needs no --allow-overwrite (the
61
+ * flag that also rewrites keys the adopter owns). The candidate is re-rendered from the version it
62
+ * names and must match exactly, so anything edited under the sentinel is still drift.
63
+ */
64
+ export function isOwnSessionStartRender(command) {
65
+ const body = command.replace(METHOD_MANAGED_SENTINEL, "").trimEnd();
66
+ if (body === `sdd-cli${SESSION_START_VALIDATE_ARGS}`)
67
+ return true;
68
+ const npx = REGISTRY_FORM.exec(body);
69
+ if (npx && EXACT_VERSION.test(npx[2]))
70
+ return body === registryLauncher(npx[2], npx[1]);
71
+ const installed = INSTALLED_FORM.exec(body);
72
+ if (installed && EXACT_VERSION.test(installed[1]))
73
+ return body === installedLauncher(installed[1]);
74
+ return false;
75
+ }
76
+ export function buildSessionStartValidate(cli = DEFAULT_CLI_SOURCE) {
26
77
  return {
27
78
  matcher: "startup",
28
79
  hooks: [
29
80
  {
30
81
  type: "command",
31
- command: withSentinel('sdd-cli validate --target-repo "${CLAUDE_PROJECT_DIR:-$(pwd)}" --validator adr-structure 2>&1 | head -20'),
82
+ command: sessionStartValidateCommand(getVerifyCliVersion(), cli),
32
83
  },
33
84
  ],
34
85
  };
@@ -37,10 +88,10 @@ export function buildSessionStartValidate() {
37
88
  * Full method-managed hooks block. When extending, add a new event
38
89
  * name to the returned object and a corresponding builder above.
39
90
  */
40
- export function buildMethodManagedHooks() {
91
+ export function buildMethodManagedHooks(cli = DEFAULT_CLI_SOURCE) {
41
92
  const hooks = {
42
93
  PreToolUse: [buildMethodGuardPreToolUse()],
43
- SessionStart: [buildSessionStartValidate()],
94
+ SessionStart: [buildSessionStartValidate(cli)],
44
95
  };
45
96
  return hooks;
46
97
  }
@@ -5,13 +5,10 @@
5
5
  * `.claude/settings.json`'s `hooks` key. Ownership is identified by
6
6
  * the `# sdd-method-managed:true` sentinel in each hook command.
7
7
  *
8
- * Integration: `src/lib/sync/settings-merge.ts` conditionally calls
9
- * `upsertMethodManagedHooks` after `upsertMethodManagedServers` when
10
- * the adopter opted into `--agent-harness claude-code` at init time.
8
+ * Integration: `src/lib/sync/settings-merge.ts` calls `upsertMethodManagedHooks`
9
+ * when the repo runs the claude-code harness (the default).
11
10
  */
12
11
  export { METHOD_MANAGED_SENTINEL, isMethodManagedMatcher } from "./types.js";
13
- export { buildMethodManagedHooks } from "./entries.js";
14
- export { detectHooksDrift, upsertMethodManagedHooks, } from "./merge.js";
15
- export { buildMethodManagedCopilotHooks, copilotHookFilePath, detectCopilotHookDrift, isMethodManagedEvent, upsertCopilotHookFile, } from "./copilot.js";
16
- export { nativeEventName, } from "./lifecycle-mapping.js";
12
+ export { buildMethodManagedHooks, CLI_SOURCES, DEFAULT_CLI_SOURCE, DEFAULT_VERIFY_CLI_PACKAGE, isOwnSessionStartRender, NPM_PACKAGE_NAME, sessionStartValidateCommand, } from "./entries.js";
13
+ export { detectHooksDrift, normaliseOwnHookRenders, upsertMethodManagedHooks, } from "./merge.js";
17
14
  //# sourceMappingURL=index.js.map
@@ -6,6 +6,7 @@
6
6
  * sync untouched.
7
7
  */
8
8
  import { METHOD_MANAGED_SENTINEL, isMethodManagedMatcher } from "./types.js";
9
+ import { isOwnSessionStartRender } from "./entries.js";
9
10
  /**
10
11
  * Strip the method-managed sentinel and trailing whitespace from a hook
11
12
  * command so two commands can be compared modulo ownership marker.
@@ -105,6 +106,36 @@ export function upsertMethodManagedHooks(existing, owned) {
105
106
  * from what we would write now. Returns the list of events where drift
106
107
  * is present.
107
108
  */
109
+ /**
110
+ * The existing settings with every method-managed session-start command this tool rendered at an
111
+ * earlier version swapped for the current render, for drift detection only. The tool's own older
112
+ * output is regenerable, never drift (the same rule settings-merge applies to an SDD_REPO_PATH it
113
+ * wrote). The raw settings still go to the upsert, which rewrites the older render.
114
+ */
115
+ export function normaliseOwnHookRenders(existing, owned) {
116
+ const current = existing["hooks"];
117
+ const ownedCommand = owned.SessionStart?.[0]?.hooks[0]?.command;
118
+ if (!isHooksObject(current) || ownedCommand === undefined)
119
+ return existing;
120
+ const block = current;
121
+ const sessionStart = block.SessionStart;
122
+ if (!sessionStart)
123
+ return existing;
124
+ return {
125
+ ...existing,
126
+ hooks: {
127
+ ...block,
128
+ SessionStart: sessionStart.map((m) => isMethodManagedMatcher(m)
129
+ ? {
130
+ ...m,
131
+ hooks: m.hooks.map((h) => typeof h.command === "string" && isOwnSessionStartRender(h.command)
132
+ ? { ...h, command: ownedCommand }
133
+ : h),
134
+ }
135
+ : m),
136
+ },
137
+ };
138
+ }
108
139
  export function detectHooksDrift(existing, owned) {
109
140
  const current = isHooksObject(existing["hooks"])
110
141
  ? existing["hooks"]
@@ -8,7 +8,7 @@
8
8
  * block-marker pair so the sync path can preserve adopter-authored
9
9
  * content outside the block.
10
10
  */
11
- import { DAGGER_VERSION, MODULE_CALL, MODULE_DIR } from "../ci-binding/module.js";
11
+ import { DAGGER_VERSION, MODULE_CALL, MODULE_DIR, REBUILD_FLAG, REBUILD_SCRIPT } from "../ci-binding/module.js";
12
12
  export const WORKFLOW_REL_PATH = ".github/workflows/method-validation.yml";
13
13
  export const BLOCK_BEGIN = "# method-managed: begin";
14
14
  export const BLOCK_END = "# method-managed: end";
@@ -56,6 +56,13 @@ function buildMethodManagedBlock(profile, runner) {
56
56
  "# released with the sdd-cli that last synced this repository. The catalogue",
57
57
  "# freshness step (@sdd-method/maintain-cli, pinned the same way) is advisory.",
58
58
  "#",
59
+ "# Catalogue rebuild (M-ADR 0220 §4.4), off until you configure it: set the",
60
+ "# repository variable SDD_CATALOGUE_APP_ID and the secret",
61
+ "# SDD_CATALOGUE_APP_PRIVATE_KEY for a GitHub App that can write contents, and",
62
+ "# the job rebuilds the catalogues and pushes them to the branch it is building",
63
+ "# (a pull request's own branch, never a fork's); the freshness step then gates.",
64
+ "# An App token, because pushes made with the default token start no workflow.",
65
+ "#",
59
66
  "# Run it locally (needs a container engine):",
60
67
  `# ${MODULE_CALL}`,
61
68
  "",
@@ -74,11 +81,28 @@ function buildMethodManagedBlock(profile, runner) {
74
81
  " name: Method Validation",
75
82
  ` runs-on: ${runsOn(runner)}`,
76
83
  " steps:",
84
+ " - name: Catalogue rebuild token",
85
+ " id: catalogue-token",
86
+ " if: vars.SDD_CATALOGUE_APP_ID != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)",
87
+ " uses: actions/create-github-app-token@v1",
88
+ " with:",
89
+ " app-id: ${{ vars.SDD_CATALOGUE_APP_ID }}",
90
+ " private-key: ${{ secrets.SDD_CATALOGUE_APP_PRIVATE_KEY }}",
77
91
  " - uses: actions/checkout@v4",
92
+ " with:",
93
+ // The branch itself when rebuilding, so the rebuild lands on it; otherwise the event's own ref.
94
+ " ref: ${{ steps.catalogue-token.outputs.token != '' && (github.head_ref || github.ref_name) || '' }}",
95
+ " persist-credentials: false",
78
96
  " - name: Install Dagger",
79
97
  ` run: curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR="$RUNNER_TEMP/dagger" DAGGER_VERSION=${DAGGER_VERSION} sh && echo "$RUNNER_TEMP/dagger" >> "$GITHUB_PATH"`,
98
+ " - name: Rebuild catalogues",
99
+ " if: steps.catalogue-token.outputs.token != ''",
100
+ " env:",
101
+ " SDD_REBUILD_TOKEN: ${{ steps.catalogue-token.outputs.token }}",
102
+ " SDD_REBUILD_BRANCH: ${{ github.head_ref || github.ref_name }}",
103
+ ` run: SDD_REBUILD_PUSH_URL="https://x-access-token:$SDD_REBUILD_TOKEN@\${GITHUB_SERVER_URL#https://}/$GITHUB_REPOSITORY.git" sh ${REBUILD_SCRIPT}`,
80
104
  " - name: Method Validation",
81
- ` run: ${MODULE_CALL}`,
105
+ ` run: ${MODULE_CALL} ${REBUILD_FLAG}`,
82
106
  BLOCK_END,
83
107
  ].join("\n");
84
108
  }
@@ -66,6 +66,8 @@ export function serialiseLockFile(lock) {
66
66
  ...(lock.ciRunner !== undefined ? [`ci_runner: "${lock.ciRunner}"`] : []),
67
67
  ...(lock.ciWorkflowBlockSha256 !== undefined ? [`ci_workflow_block_sha256: "${lock.ciWorkflowBlockSha256}"`] : []),
68
68
  ...(lock.ciModuleSha256 !== undefined ? [`ci_module_sha256: "${lock.ciModuleSha256}"`] : []),
69
+ ...(lock.cliSource !== undefined ? [`cli_source: ${lock.cliSource}`] : []),
70
+ ...(lock.cliPackage !== undefined ? [`cli_package: "${lock.cliPackage}"`] : []),
69
71
  "bundles:",
70
72
  ];
71
73
  for (const b of lock.bundles) {
@@ -117,6 +119,8 @@ function parseVersioned(raw) {
117
119
  let ciBinding;
118
120
  let ciRunner;
119
121
  let ciModuleSha256;
122
+ let cliSource;
123
+ let cliPackage;
120
124
  const bundles = [];
121
125
  let inBundles = false;
122
126
  let inPaths = false;
@@ -186,6 +190,10 @@ function parseVersioned(raw) {
186
190
  ciRunner = stripQuotes(value);
187
191
  else if (key === "ci_module_sha256")
188
192
  ciModuleSha256 = stripQuotes(value);
193
+ else if (key === "cli_source")
194
+ cliSource = stripQuotes(value);
195
+ else if (key === "cli_package")
196
+ cliPackage = stripQuotes(value);
189
197
  }
190
198
  continue;
191
199
  }
@@ -313,6 +321,8 @@ function parseVersioned(raw) {
313
321
  ...(ciRunner !== undefined ? { ciRunner } : {}),
314
322
  ...(ciWorkflowBlockSha256 !== undefined ? { ciWorkflowBlockSha256 } : {}),
315
323
  ...(ciModuleSha256 !== undefined ? { ciModuleSha256 } : {}),
324
+ ...(cliSource !== undefined ? { cliSource } : {}),
325
+ ...(cliPackage !== undefined ? { cliPackage } : {}),
316
326
  };
317
327
  }
318
328
  function migrateV1(raw, legacyBundleName) {
@@ -69,7 +69,9 @@ export async function buildPack(opts) {
69
69
  // 4. Checksums (over the published payload)
70
70
  const checksums = await computeChecksums(payload, publishedPaths);
71
71
  // 5. Manifest + managed_paths + checksums files
72
- const gitInfo = await readGitInfo(opts.producerRepo);
72
+ const gitInfo = opts.provenance
73
+ ? { commit: opts.provenance.commit, branch: opts.provenance.branch }
74
+ : await readGitInfo(opts.producerRepo);
73
75
  await writeFile(join(root, "MANAGED_PATHS.txt"), publishedPaths.join("\n") + "\n");
74
76
  await writeFile(join(root, "CHECKSUMS.sha256"), checksums);
75
77
  await writeFile(join(root, "BUNDLE_MANIFEST.yaml"), renderManifest({
@@ -79,12 +81,13 @@ export async function buildPack(opts) {
79
81
  managedPathCount: publishedPaths.length,
80
82
  sourceCommit: gitInfo.commit,
81
83
  sourceBranch: gitInfo.branch,
84
+ ...(opts.provenance && { sourceRepo: opts.provenance.repo, generatedAt: opts.provenance.generatedAt }),
82
85
  }));
83
86
  // 6. tar -czf
84
87
  await mkdir(outDir, { recursive: true });
85
88
  const tarballName = `${rootName}.tar.gz`;
86
89
  const tarballPath = join(outDir, tarballName);
87
- await execFile("tar", ["-czf", tarballPath, "-C", stage, rootName]);
90
+ await writeArchive(tarballPath, stage, rootName, await archiveEpoch(opts));
88
91
  // Also emit a sidecar checksum file for the tarball itself
89
92
  const tarballHash = await sha256File(tarballPath);
90
93
  const sidecar = `${tarballHash} ${tarballName}\n`;
@@ -210,8 +213,8 @@ function renderManifest(ctx) {
210
213
  // sdd-method/orchestration/templates/bundle/BUNDLE_MANIFEST.yaml.hbs.
211
214
  // We don't go through a Handlebars engine because that would be the
212
215
  // only template-rendered file in sdd-cli; everything else is direct.
213
- const generatedAt = new Date().toISOString();
214
- const sourceRepo = deriveSourceRepoName(ctx.producerRepo);
216
+ const generatedAt = ctx.generatedAt ?? new Date().toISOString();
217
+ const sourceRepo = ctx.sourceRepo ?? deriveSourceRepoName(ctx.producerRepo);
215
218
  const lines = [];
216
219
  lines.push(`bundle_name: "${ctx.packName}"`);
217
220
  lines.push(`bundle_version: "v${ctx.seed.version}"`);
@@ -256,6 +259,55 @@ function renderManifest(ctx) {
256
259
  }
257
260
  return lines.join("\n") + "\n";
258
261
  }
262
+ /**
263
+ * Member file times: the release record's time, else SOURCE_DATE_EPOCH, else the producer's HEAD
264
+ * commit time, else now (not a git tree, no record). Two cuts of one commit then agree.
265
+ */
266
+ async function archiveEpoch(opts) {
267
+ if (opts.provenance)
268
+ return Math.floor(Date.parse(opts.provenance.generatedAt) / 1000);
269
+ const env = process.env.SOURCE_DATE_EPOCH;
270
+ if (env && /^\d+$/.test(env))
271
+ return Number(env);
272
+ try {
273
+ const { stdout } = await execFile("git", ["-C", opts.producerRepo, "log", "-1", "--format=%ct", "HEAD"]);
274
+ if (/^\d+$/.test(stdout.trim()))
275
+ return Number(stdout.trim());
276
+ }
277
+ catch {
278
+ // Not a git working tree.
279
+ }
280
+ return Math.floor(Date.now() / 1000);
281
+ }
282
+ /**
283
+ * Write the tarball deterministically with GNU tar: sorted members, owner and group zeroed, modes
284
+ * normalised, fixed file times; gzip -n, so the header carries no name or time. Plain `tar -czf` stamped all of these from the machine, so a rebuild never matched.
285
+ * Without GNU tar (e.g. macOS's bsdtar) the archive is written the old way and is not reproducible.
286
+ */
287
+ async function writeArchive(tarballPath, stage, rootName, epoch) {
288
+ let gnu = false;
289
+ try {
290
+ gnu = (await execFile("tar", ["--version"])).stdout.includes("GNU tar");
291
+ }
292
+ catch {
293
+ gnu = false;
294
+ }
295
+ if (!gnu) {
296
+ await execFile("tar", ["-czf", tarballPath, "-C", stage, rootName]);
297
+ return;
298
+ }
299
+ const tarPath = `${tarballPath}.tar`;
300
+ try {
301
+ await execFile("tar", ["--sort=name", "--format=gnu", `--mtime=@${epoch}`, "--owner=0", "--group=0", "--numeric-owner",
302
+ "--mode=u+rwX,go+rX,go-w", "-cf", tarPath, "-C", stage, rootName], { env: { ...process.env, LC_ALL: "C" } });
303
+ // GNU gzip, not Node's zlib: Node bundles a zlib fork whose output can depend on the CPU, and
304
+ // the method's own bundle builder compresses with gzip too.
305
+ await execFile("sh", ["-c", 'gzip -n -9 -c "$1" > "$2"', "sh", tarPath, tarballPath]);
306
+ }
307
+ finally {
308
+ await rm(tarPath, { force: true });
309
+ }
310
+ }
259
311
  /**
260
312
  * Read git provenance from the producer working tree. Mirrors the
261
313
  * behaviour of sdd-method/orchestration/scripts/build-method-baseline-
@@ -53,6 +53,7 @@ export async function runPackBuild(opts, stdout = (s) => process.stdout.write(s)
53
53
  }),
54
54
  ...(publishTransform && { publishTransform: true }),
55
55
  ...(opts.originPrefix === true && { mAdrPrefix: true }),
56
+ ...(opts.provenance && { provenance: opts.provenance }),
56
57
  });
57
58
  }
58
59
  catch (err) {
@@ -48,7 +48,7 @@ const DENY_TEMPORAL = [
48
48
  ];
49
49
  const DENY_PRODUCER = [
50
50
  /sdd-method#/i,
51
- /sdd-method-gov/i,
51
+ /sdd-method-g[o]v/i, // the class keeps the literal name out of this file, which ships
52
52
  /sdd-method repo/i,
53
53
  /the method repo/i,
54
54
  /governance repo/i,