@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 +1 -1
- package/dist/lib/agent-harness.js +17 -8
- package/dist/lib/briefing/index.js +1 -1
- package/dist/lib/briefing/render.js +17 -8
- package/dist/lib/catalogue/aggregation-manifest-loader.js +4 -4
- package/dist/lib/catalogue/freshness.js +4 -3
- package/dist/lib/ci-binding/index.js +18 -7
- package/dist/lib/context-pull/index.js +79 -39
- package/dist/lib/context-pull/retired-mcp-setup.js +7 -0
- package/dist/lib/hooks-seed/entries.js +30 -1
- package/dist/lib/hooks-seed/index.js +4 -7
- package/dist/lib/hooks-seed/merge.js +31 -0
- package/dist/lib/init/ci-workflow.js +15 -6
- package/dist/lib/lock-file.js +5 -0
- package/dist/lib/sync/briefing-merge.js +50 -34
- package/dist/lib/sync/diff.js +1 -1
- package/dist/lib/sync/index.js +31 -72
- package/dist/lib/sync/settings-merge.js +70 -162
- package/dist/maintain-program.js +5 -2
- package/dist/maintain-version.json +2 -2
- package/dist/verbs/catalogue-check.js +28 -0
- package/dist/verbs/context.js +1 -1
- package/dist/verbs/sync.js +13 -1
- package/dist/verify-program.js +1 -1
- package/dist/verify-version.json +2 -2
- package/package.json +3 -3
- package/dist/lib/hooks-seed/copilot.js +0 -167
- package/dist/lib/hooks-seed/lifecycle-mapping.js +0 -28
- package/dist/lib/mcp-seed/entries.js +0 -14
- package/dist/lib/mcp-seed/index.js +0 -28
- package/dist/lib/mcp-seed/merge.js +0 -99
- package/dist/lib/mcp-seed/types.js +0 -13
- package/dist/lib/sync/copilot-hooks-merge.js +0 -108
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`, `
|
|
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
|
|
3
|
+
* `sdd-cli init`, `sdd-cli sync` and `context pull` per ADR 0141 §4.6.
|
|
4
4
|
*
|
|
5
|
-
* Claude Code is the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* harness
|
|
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"
|
|
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
|
|
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 —
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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.
|
|
119
|
-
`
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
|
3
|
-
*
|
|
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
|
|
6
|
-
*
|
|
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
|
|
15
|
-
*
|
|
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
|
|
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],
|
|
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
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
out(fail(`${
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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:
|
|
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`
|
|
9
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
75
|
+
` runs-on: ${runsOn(runner)}`,
|
|
67
76
|
" steps:",
|
|
68
77
|
" - uses: actions/checkout@v4",
|
|
69
78
|
" - name: Install Dagger",
|
package/dist/lib/lock-file.js
CHANGED
|
@@ -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
|
};
|