@sdd-method/maintain-cli 0.1.0-rc.0 → 0.2.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.
package/README.md CHANGED
@@ -4,4 +4,4 @@
4
4
 
5
5
  Verbs: everything in `@sdd-method/verify-cli`, plus `sync`, `generate-briefing`, `context pull`, `catalogue build`, `catalogue aggregate`, `catalogue graph`, `catalogue diff`, `pack build`, `render screen`.
6
6
 
7
- Each writes only derived or method-managed content, and two runs on the same commit write the same bytes. Authoring verbs — `init`, `scaffold`, `plan`, `mcp`, `conformance` and the rest — are not present in it, and are in `@sdd-method/sdd-cli`. Invoking one fails with exit 1.
7
+ Each writes only derived or method-managed content, and two runs on the same commit write the same bytes. Authoring verbs — `init`, `scaffold`, `plan`, `conformance` and the rest — are not present in it, and are in `@sdd-method/sdd-cli`. Invoking one fails with exit 1.
@@ -1,25 +1,34 @@
1
1
  /**
2
2
  * Agent-harness identifiers — the value space of `--agent-harness` on
3
- * `sdd-cli init` and `sdd-cli sync` per ADR 0141 §4.6.
3
+ * `sdd-cli init`, `sdd-cli sync` and `context pull` per ADR 0141 §4.6.
4
4
  *
5
- * Claude Code is the reference implementation per ADR 0141 §4.4 and
6
- * the default. GitHub Copilot CLI is the second reference per
7
- * `docs/method/methods/hook-contracts.md`. Adopters using a different
8
- * harness implement the portable contracts themselves per ADR 0141 §4.5.
5
+ * Claude Code is the one supported harness (M-ADR 0220 §4.6) and the default. The GitHub Copilot
6
+ * CLI value is retired: passing it fails, naming that record. Every SDD and code repository also
7
+ * gets an AGENTS.md pointer to agent-instructions.md, with no flag; it is a pointer, not support
8
+ * for another harness. Adopters binding any other harness implement the portable contracts
9
+ * themselves (ADR 0141 §4.5).
9
10
  */
10
- export const AGENT_HARNESSES = ["claude-code", "github-copilot-cli"];
11
+ export const AGENT_HARNESSES = ["claude-code"];
11
12
  export const DEFAULT_AGENT_HARNESS = "claude-code";
13
+ /** Retired values, each with the record that retired it. */
14
+ export const RETIRED_AGENT_HARNESSES = {
15
+ "github-copilot-cli": "M-ADR 0220 §4.6",
16
+ };
12
17
  export function isAgentHarness(value) {
13
18
  return AGENT_HARNESSES.includes(value);
14
19
  }
15
20
  /**
16
21
  * Resolve an optional CLI input to a concrete harness, falling back to
17
- * the reference default when unset. Throws on an unsupported value so
18
- * the CLI can surface a clear error to the caller.
22
+ * the reference default when unset. Throws on an unsupported or retired
23
+ * value so the CLI can surface a clear error to the caller.
19
24
  */
20
25
  export function resolveAgentHarness(input) {
21
26
  if (input === undefined)
22
27
  return DEFAULT_AGENT_HARNESS;
28
+ const retired = RETIRED_AGENT_HARNESSES[input];
29
+ if (retired !== undefined) {
30
+ throw new Error(`--agent-harness "${input}" is retired (${retired}): Claude Code is the one supported harness, and every repository gets an AGENTS.md pointer to agent-instructions.md without a flag. Supported: ${AGENT_HARNESSES.join(", ")}.`);
31
+ }
23
32
  if (!isAgentHarness(input)) {
24
33
  throw new Error(`Unsupported --agent-harness "${input}". Supported: ${AGENT_HARNESSES.join(", ")}.`);
25
34
  }
@@ -8,6 +8,6 @@
8
8
  * content below survives sync via the existing `writeMarkedBlockFile`
9
9
  * machinery.
10
10
  */
11
- export { AGENT_INSTRUCTIONS_REL_PATH, BRIEFING_TEMPLATE_ROOT, WRAPPER_BLOCK_BEGIN, WRAPPER_BLOCK_END, extractWrapperBlock, findUnresolvedPlaceholders, harnessWrapperOutputPath, renderAgentInstructions, renderHarnessWrapper, splitWrapperTemplate, substitute, } from "./render.js";
11
+ export { AGENT_INSTRUCTIONS_REL_PATH, BRIEFING_TEMPLATE_ROOT, WRAPPER_BLOCK_BEGIN, WRAPPER_BLOCK_END, extractWrapperBlock, findUnresolvedPlaceholders, AGENTS_POINTER, briefingWrappers, harnessWrapperOutputPath, renderAgentInstructions, renderHarnessWrapper, renderWrapper, splitWrapperTemplate, substitute, } from "./render.js";
12
12
  export { resolveBriefingVariables } from "./variables.js";
13
13
  //# sourceMappingURL=index.js.map
@@ -21,15 +21,22 @@ const HARNESS_WRAPPER_TEMPLATE = {
21
21
  templateRel: "claude-code/CLAUDE.md.hbs",
22
22
  outputRel: "CLAUDE.md",
23
23
  },
24
- "github-copilot-cli": {
25
- templateRel: "github-copilot-cli/AGENTS.md.hbs",
26
- outputRel: "AGENTS.md",
27
- },
28
24
  };
29
- /** Output filename per harness — e.g. `CLAUDE.md` or `AGENTS.md`. */
25
+ /** Output filename per harness — `CLAUDE.md`. */
30
26
  export function harnessWrapperOutputPath(harness) {
31
27
  return HARNESS_WRAPPER_TEMPLATE[harness].outputRel;
32
28
  }
29
+ /**
30
+ * The AGENTS.md pointer (M-ADR 0220 §4.6): a few lines directing any agent tool that reads a root
31
+ * AGENTS.md to agent-instructions.md. Written for every SDD and code repository with no flag, and
32
+ * rendered and refreshed exactly as the CLAUDE.md wrapper is. A pointer, not support for another
33
+ * harness. Relative to the briefing template root; code repositories read it under code-repo/.
34
+ */
35
+ export const AGENTS_POINTER = { templateRel: "AGENTS.md.hbs", outputRel: "AGENTS.md" };
36
+ /** Every marker-merged wrapper a repository gets: the harness's own, then the AGENTS.md pointer. */
37
+ export function briefingWrappers(harness) {
38
+ return [HARNESS_WRAPPER_TEMPLATE[harness], AGENTS_POINTER];
39
+ }
33
40
  /**
34
41
  * Marker pair the wrapper templates use to delimit the method-managed
35
42
  * region. The wrapper sync path slices this region out of the freshly
@@ -71,9 +78,11 @@ export async function renderAgentInstructions(targetRepo, vars) {
71
78
  return renderFromPath(templatePath, vars);
72
79
  }
73
80
  export async function renderHarnessWrapper(targetRepo, harness, vars) {
74
- const { templateRel } = HARNESS_WRAPPER_TEMPLATE[harness];
75
- const templatePath = join(targetRepo, BRIEFING_TEMPLATE_ROOT, templateRel);
76
- return renderFromPath(templatePath, vars);
81
+ return renderWrapper(targetRepo, HARNESS_WRAPPER_TEMPLATE[harness].templateRel, vars);
82
+ }
83
+ /** Render one wrapper template from the briefing root; null when the baseline does not ship it. */
84
+ export async function renderWrapper(targetRepo, templateRel, vars) {
85
+ return renderFromPath(join(targetRepo, BRIEFING_TEMPLATE_ROOT, templateRel), vars);
77
86
  }
78
87
  /**
79
88
  * Split a fully-rendered wrapper template at the method-managed
@@ -1,8 +1,8 @@
1
1
  // Aggregation-manifest loader per
2
2
  // docs/method/methods/aggregation-manifest-schema.yaml.
3
3
  //
4
- // v1 supports `pull.kind: filesystem` only; other kinds (git-ref,
5
- // release-asset, mcp-resource) are schema-reserved and reject with a
4
+ // v1 supports `pull.kind: filesystem` only; the other kinds (git-ref,
5
+ // release-asset) are schema-reserved and reject with a
6
6
  // clear "deferred" error so adopters get unambiguous feedback.
7
7
  import { readFile } from "node:fs/promises";
8
8
  import { isAbsolute, relative, resolve } from "node:path";
@@ -115,8 +115,8 @@ export async function loadAggregationManifest(manifestPath, options) {
115
115
  const kind = s.pull.kind;
116
116
  if (kind !== "filesystem") {
117
117
  throw new Error(`aggregation manifest: sibling "${s.sdd_id}" uses pull.kind="${kind}"\n` +
118
- ` v1 supports filesystem only. Other kinds (git-ref, release-asset,\n` +
119
- ` mcp-resource) are schema-reserved and not yet implemented.\n` +
118
+ ` v1 supports filesystem only. The other kinds (git-ref, release-asset)\n` +
119
+ ` are schema-reserved and not yet implemented.\n` +
120
120
  ` See: docs/method/methods/aggregation-manifest-schema.yaml#pull_kinds`);
121
121
  }
122
122
  const path = s.pull.path;
@@ -39,6 +39,7 @@ export async function compareCatalogueDirs(producedDir, committedDir) {
39
39
  return stale;
40
40
  }
41
41
  export async function checkCataloguesFresh(sddPath, opts, out) {
42
+ const label = opts.label ?? "catalogue build --check";
42
43
  const tmp = await mkdtemp(join(tmpdir(), "sdd-catalogue-check-"));
43
44
  try {
44
45
  const built = await runCatalogueBuild({
@@ -48,16 +49,16 @@ export async function checkCataloguesFresh(sddPath, opts, out) {
48
49
  ...(opts.snapshotStore !== undefined ? { snapshotStore: opts.snapshotStore } : {}),
49
50
  }, () => { });
50
51
  if (built.exitCode !== 0) {
51
- out(`[FAIL] catalogue build --check: the build itself failed (exit ${built.exitCode}); run catalogue build to see why.\n`);
52
+ out(`[FAIL] ${label}: the build itself failed (exit ${built.exitCode}); run catalogue build to see why.\n`);
52
53
  return { exitCode: built.exitCode, stale: [] };
53
54
  }
54
55
  const produced = (await readdir(tmp)).sort();
55
56
  const stale = await compareCatalogueDirs(tmp, join(sddPath, "catalogues"));
56
57
  if (stale.length === 0) {
57
- out(`[PASS] catalogue build --check: ${produced.length} catalogue file(s) reproduce byte-identically from the committed specs.\n`);
58
+ out(`[PASS] ${label}: ${produced.length} catalogue file(s) reproduce byte-identically from the committed specs.\n`);
58
59
  return { exitCode: 0, stale };
59
60
  }
60
- out(`[FAIL] catalogue build --check: ${stale.length} catalogue file(s) are stale against the committed specs — run sdd-cli catalogue build and commit the result:\n`);
61
+ out(`[FAIL] ${label}: ${stale.length} catalogue file(s) are stale against the committed specs — catalogue build (sdd-maintain or sdd-cli) rebuilds them:\n`);
61
62
  for (const s of stale)
62
63
  out(` ${s}\n`);
63
64
  return { exitCode: 1, stale };
@@ -18,6 +18,16 @@ import { DAGGER_VERSION, MODULE_CALL, MODULE_DIR, moduleSha256, renderModuleFile
18
18
  export const CI_BINDINGS = ["github", "gitlab", "jenkins"];
19
19
  /** M-ADR 0219 §8.2 (ruled 2026-10-16): what every SDD has before it chooses. */
20
20
  export const DEFAULT_CI_BINDING = "github";
21
+ /**
22
+ * Runner labels (sync --ci-runner): where the host runs the job. One label is a word of letters,
23
+ * digits, '.', '_' or '-', so a label can never inject YAML or Groovy into a wrapper.
24
+ */
25
+ export const CI_RUNNER_DEFAULT = "default";
26
+ const RUNNER_LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
27
+ export function parseCiRunner(s) {
28
+ const labels = s.split(",").map((l) => l.trim());
29
+ return labels.length > 0 && labels.every((l) => RUNNER_LABEL.test(l)) ? labels : null;
30
+ }
21
31
  export function isCiBinding(s) {
22
32
  return CI_BINDINGS.includes(s);
23
33
  }
@@ -42,13 +52,14 @@ function header(comment, profile) {
42
52
  // GitLab: a Docker-in-Docker service, the shape GitLab documents for Dagger. The runner must allow
43
53
  // privileged containers; that is the adopter's infrastructure (M-ADR 0219 §4.5). No `stage:`, so
44
54
  // the job lands in the default `test` stage whatever stages the adopter declares around it.
45
- function gitlabBlock(profile) {
55
+ function gitlabBlock(profile, runner) {
46
56
  return [
47
57
  HASH.begin,
48
58
  ...header("#", profile),
49
59
  "",
50
60
  "method-validation:",
51
61
  " image: docker:27.3.1",
62
+ ...(runner !== undefined && runner.length > 0 ? [` tags: [${runner.join(", ")}]`] : []),
52
63
  " services:",
53
64
  " - docker:27.3.1-dind",
54
65
  " variables:",
@@ -70,13 +81,13 @@ function gitlabBlock(profile) {
70
81
  // Jenkins: a declarative pipeline at its own path, so it never collides with an adopter's
71
82
  // Jenkinsfile; the job's Script Path points at it. The agent must reach a Docker daemon. Dagger is
72
83
  // installed outside the workspace, so the binary is not uploaded with --source.
73
- function jenkinsBlock(profile) {
84
+ function jenkinsBlock(profile, runner) {
74
85
  return [
75
86
  SLASH.begin,
76
87
  ...header("//", profile),
77
88
  "",
78
89
  "pipeline {",
79
- " agent any",
90
+ runner !== undefined && runner.length > 0 ? ` agent { label '${runner.join(" && ")}' }` : " agent any",
80
91
  " stages {",
81
92
  " stage('Method Validation') {",
82
93
  " steps {",
@@ -100,13 +111,13 @@ export const HOST_WRAPPERS = {
100
111
  relPath: ".gitlab-ci.yml",
101
112
  markers: HASH,
102
113
  block: gitlabBlock,
103
- file: (p) => gitlabBlock(p) + "\n\n# Adopter-specific jobs go below, outside the method-managed markers.\n",
114
+ file: (p, r) => gitlabBlock(p, r) + "\n\n# Adopter-specific jobs go below, outside the method-managed markers.\n",
104
115
  },
105
116
  jenkins: {
106
117
  relPath: ".jenkins/method-validation.Jenkinsfile",
107
118
  markers: SLASH,
108
119
  block: jenkinsBlock,
109
- file: (p) => jenkinsBlock(p) + "\n",
120
+ file: (p, r) => jenkinsBlock(p, r) + "\n",
110
121
  },
111
122
  };
112
123
  /** sha256 of a host file's method-managed block (begin marker through end marker), or null without one. */
@@ -128,7 +139,7 @@ export async function syncCiBinding(o) {
128
139
  // 1. The host wrapper.
129
140
  const wrapperAbs = join(o.targetRepo, host.relPath);
130
141
  const existing = await tryRead(wrapperAbs);
131
- const block = host.block(o.profile);
142
+ const block = host.block(o.profile, o.runner);
132
143
  const drifted = existing !== null && detectBlockDrift(existing, block, host.markers);
133
144
  const recorded = sameBinding ? o.prevLock?.ciWorkflowBlockSha256 : undefined;
134
145
  const pristine = existing !== null && recorded !== undefined && blockSha256(existing, host.markers) === recorded;
@@ -159,7 +170,7 @@ export async function syncCiBinding(o) {
159
170
  if (o.checkOnly || failures.length > 0) {
160
171
  return { failures, log, wrapperSha: null, moduleSha: moduleSha256(onDisk) };
161
172
  }
162
- const written = await writeMarkedBlockFile(wrapperAbs, existing === null ? host.file(o.profile) : block, host.markers, o.allowOverwrite);
173
+ const written = await writeMarkedBlockFile(wrapperAbs, existing === null ? host.file(o.profile, o.runner) : block, host.markers, o.allowOverwrite);
163
174
  if (written.changed)
164
175
  log.push(`${existing === null ? "Wrote" : "Refreshed"} ${host.relPath}.`);
165
176
  if (!matches) {
@@ -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,41 @@ export function buildMethodGuardPreToolUse() {
22
23
  ],
23
24
  };
24
25
  }
26
+ const SESSION_START_VALIDATE_ARGS = ' validate --target-repo "${CLAUDE_PROJECT_DIR:-$(pwd)}" --validator adr-structure 2>&1 | head -20';
27
+ const VERIFY_CLI_NPX = /^npx --yes @sdd-method\/verify-cli@(\S+)$/;
28
+ /**
29
+ * The session-start check runs the VERIFY persona's CLI (M-ADR 0218, 0220), not sdd-cli, which is
30
+ * the administrator's: validating is routine author work. It runs through npx at the exact
31
+ * verify-cli version this sdd-cli releases with, the version the managed CI workflow installs, so a
32
+ * local pass comes from the same verifier CI uses and nothing needs installing globally.
33
+ */
34
+ export function sessionStartValidateCommand(verifyVersion = getVerifyCliVersion()) {
35
+ return withSentinel(`npx --yes @sdd-method/verify-cli@${verifyVersion}${SESSION_START_VALIDATE_ARGS}`);
36
+ }
37
+ /**
38
+ * Whether a session-start command is one this tool rendered, at any version: the sdd-cli form it
39
+ * wrote until the hook moved to verify-cli, or the npx form at any exact version. Sync replaces its
40
+ * own earlier render without counting it as drift; otherwise every release that moves the pinned
41
+ * version would refuse the settings sync until --allow-overwrite, the flag that also rewrites keys
42
+ * the adopter owns. Anything else under the sentinel (an edited command) is still drift.
43
+ */
44
+ export function isOwnSessionStartRender(command) {
45
+ const body = command.replace(METHOD_MANAGED_SENTINEL, "").trimEnd();
46
+ if (!body.endsWith(SESSION_START_VALIDATE_ARGS))
47
+ return false;
48
+ const launcher = body.slice(0, body.length - SESSION_START_VALIDATE_ARGS.length);
49
+ if (launcher === "sdd-cli")
50
+ return true;
51
+ const npx = VERIFY_CLI_NPX.exec(launcher);
52
+ return npx !== null && EXACT_VERSION.test(npx[1]);
53
+ }
25
54
  export function buildSessionStartValidate() {
26
55
  return {
27
56
  matcher: "startup",
28
57
  hooks: [
29
58
  {
30
59
  type: "command",
31
- command: withSentinel('sdd-cli validate --target-repo "${CLAUDE_PROJECT_DIR:-$(pwd)}" --validator adr-structure 2>&1 | head -20'),
60
+ command: sessionStartValidateCommand(),
32
61
  },
33
62
  ],
34
63
  };
@@ -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, isOwnSessionStartRender, 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"]
@@ -16,8 +16,8 @@ export const BLOCK_END = "# method-managed: end";
16
16
  * Full file content (block + adopter-extension footer). Used by
17
17
  * `sdd-cli init` to write a fresh workflow.
18
18
  */
19
- export function renderMethodValidationWorkflow(profile) {
20
- const methodBlock = buildMethodManagedBlock(profile);
19
+ export function renderMethodValidationWorkflow(profile, runner) {
20
+ const methodBlock = buildMethodManagedBlock(profile, runner);
21
21
  return methodBlock + "\n\n# Adopter-specific jobs go in a separate workflow file\n# (e.g. .github/workflows/adopter-validation.yml) — see\n# docs/method/methods/ci-baseline-guide.md.\n";
22
22
  }
23
23
  /**
@@ -26,10 +26,19 @@ export function renderMethodValidationWorkflow(profile) {
26
26
  * `writeMarkedBlockFile` so adopter content outside the markers is
27
27
  * preserved verbatim.
28
28
  */
29
- export function renderMethodManagedWorkflowBlock(profile) {
30
- return buildMethodManagedBlock(profile);
29
+ export function renderMethodManagedWorkflowBlock(profile, runner) {
30
+ return buildMethodManagedBlock(profile, runner);
31
31
  }
32
- function buildMethodManagedBlock(profile) {
32
+ /**
33
+ * `runs-on`: GitHub's hosted runner unless the SDD chose labels (sync --ci-runner), e.g. a
34
+ * self-hosted runner, where the Dagger engine and its cache persist between runs.
35
+ */
36
+ function runsOn(runner) {
37
+ if (runner === undefined || runner.length === 0)
38
+ return "ubuntu-latest";
39
+ return runner.length === 1 ? runner[0] : `[${runner.join(", ")}]`;
40
+ }
41
+ function buildMethodManagedBlock(profile, runner) {
33
42
  return [
34
43
  BLOCK_BEGIN,
35
44
  "# Method Validation per M-ADR 0129, run by the method's Dagger module (M-ADR 0219).",
@@ -63,7 +72,7 @@ function buildMethodManagedBlock(profile) {
63
72
  // ONE job: GitHub bills each job rounded UP to a whole minute (adopter measurement, 2026-10-12).
64
73
  " method-validation:",
65
74
  " name: Method Validation",
66
- " runs-on: ubuntu-latest",
75
+ ` runs-on: ${runsOn(runner)}`,
67
76
  " steps:",
68
77
  " - uses: actions/checkout@v4",
69
78
  " - name: Install Dagger",
@@ -63,6 +63,7 @@ export function serialiseLockFile(lock) {
63
63
  `method_repo: ${lock.methodRepo}`,
64
64
  `lock_version: ${lock.lockVersion}`,
65
65
  ...(lock.ciBinding !== undefined ? [`ci_binding: ${lock.ciBinding}`] : []),
66
+ ...(lock.ciRunner !== undefined ? [`ci_runner: "${lock.ciRunner}"`] : []),
66
67
  ...(lock.ciWorkflowBlockSha256 !== undefined ? [`ci_workflow_block_sha256: "${lock.ciWorkflowBlockSha256}"`] : []),
67
68
  ...(lock.ciModuleSha256 !== undefined ? [`ci_module_sha256: "${lock.ciModuleSha256}"`] : []),
68
69
  "bundles:",
@@ -114,6 +115,7 @@ function parseVersioned(raw) {
114
115
  let lockVersion;
115
116
  let ciWorkflowBlockSha256;
116
117
  let ciBinding;
118
+ let ciRunner;
117
119
  let ciModuleSha256;
118
120
  const bundles = [];
119
121
  let inBundles = false;
@@ -180,6 +182,8 @@ function parseVersioned(raw) {
180
182
  ciWorkflowBlockSha256 = stripQuotes(value);
181
183
  else if (key === "ci_binding")
182
184
  ciBinding = stripQuotes(value);
185
+ else if (key === "ci_runner")
186
+ ciRunner = stripQuotes(value);
183
187
  else if (key === "ci_module_sha256")
184
188
  ciModuleSha256 = stripQuotes(value);
185
189
  }
@@ -306,6 +310,7 @@ function parseVersioned(raw) {
306
310
  lockVersion: LOCK_VERSION,
307
311
  bundles,
308
312
  ...(ciBinding !== undefined ? { ciBinding } : {}),
313
+ ...(ciRunner !== undefined ? { ciRunner } : {}),
309
314
  ...(ciWorkflowBlockSha256 !== undefined ? { ciWorkflowBlockSha256 } : {}),
310
315
  ...(ciModuleSha256 !== undefined ? { ciModuleSha256 } : {}),
311
316
  };