@bradheitmann/odin-sentinel 0.4.12 → 0.5.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/.claude-plugin/marketplace.json +1 -1
- package/README.md +24 -17
- package/dist/src/harness-pacing/index.d.ts +10 -0
- package/dist/src/harness-pacing/index.js +11 -0
- package/dist/src/harness-pacing/index.js.map +1 -0
- package/dist/src/harness-pacing/recommend.d.ts +28 -0
- package/dist/src/harness-pacing/recommend.js +74 -0
- package/dist/src/harness-pacing/recommend.js.map +1 -0
- package/dist/src/harness-pacing/schema.d.ts +28 -0
- package/dist/src/harness-pacing/schema.js +2 -0
- package/dist/src/harness-pacing/schema.js.map +1 -0
- package/dist/src/harness-pacing/storage.d.ts +32 -0
- package/dist/src/harness-pacing/storage.js +74 -0
- package/dist/src/harness-pacing/storage.js.map +1 -0
- package/dist/src/mcp/server.js +29 -2
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/odin-watch/backends/cmux.d.ts +6 -0
- package/dist/src/odin-watch/backends/cmux.js +39 -0
- package/dist/src/odin-watch/backends/cmux.js.map +1 -0
- package/dist/src/odin-watch/backends/tmux.d.ts +6 -0
- package/dist/src/odin-watch/backends/tmux.js +40 -0
- package/dist/src/odin-watch/backends/tmux.js.map +1 -0
- package/dist/src/odin-watch/classifier.d.ts +27 -0
- package/dist/src/odin-watch/classifier.js +182 -0
- package/dist/src/odin-watch/classifier.js.map +1 -0
- package/dist/src/odin-watch/index.d.ts +2 -0
- package/dist/src/odin-watch/index.js +200 -0
- package/dist/src/odin-watch/index.js.map +1 -0
- package/dist/src/odin-watch/snapshotter.d.ts +11 -0
- package/dist/src/odin-watch/snapshotter.js +2 -0
- package/dist/src/odin-watch/snapshotter.js.map +1 -0
- package/dist/src/odin-watch/writers.d.ts +8 -0
- package/dist/src/odin-watch/writers.js +27 -0
- package/dist/src/odin-watch/writers.js.map +1 -0
- package/dist/src/protocol/index.d.ts +3 -1
- package/dist/src/protocol/index.js +4 -1
- package/dist/src/protocol/index.js.map +1 -1
- package/dist/src/protocol/repository.d.ts +14 -0
- package/dist/src/protocol/repository.js +25 -1
- package/dist/src/protocol/repository.js.map +1 -1
- package/dist/src/protocol/schemas.d.ts +144 -0
- package/dist/src/protocol/schemas.js +23 -0
- package/dist/src/protocol/schemas.js.map +1 -1
- package/dist/src/protocol/service.d.ts +19 -2
- package/dist/src/protocol/service.js +89 -3
- package/dist/src/protocol/service.js.map +1 -1
- package/dist/src/protocol/surface-layout.d.ts +20 -0
- package/dist/src/protocol/surface-layout.js +20 -0
- package/dist/src/protocol/surface-layout.js.map +1 -1
- package/dist/src/protocol/version.d.ts +2 -2
- package/dist/src/protocol/version.js +2 -2
- package/dist/src/protocol/version.js.map +1 -1
- package/dist/src/utils/execFileNoThrow.d.ts +5 -0
- package/dist/src/utils/execFileNoThrow.js +18 -0
- package/dist/src/utils/execFileNoThrow.js.map +1 -0
- package/docs/adapters/cmux-adapter.md +168 -0
- package/docs/adapters/herdr-adapter.md +150 -0
- package/docs/adapters/minimux-adapter.md +152 -0
- package/docs/adapters/plain-terminal.md +80 -0
- package/docs/adapters/tmux-adapter.md +150 -0
- package/docs/guides/quick-start.md +7 -7
- package/docs/guides/quickstart-prompts.md +4 -4
- package/docs/lattice/odin-lattice-design.md +555 -0
- package/docs/reference/distribution.md +11 -5
- package/docs/reference/public-surface-audit.md +3 -3
- package/package.json +7 -5
- package/plugins/odin-scp/.claude-plugin/plugin.json +2 -2
- package/plugins/odin-scp/README.md +6 -6
- package/plugins/odin-scp/skills/odin-scp/CHANGELOG.md +12 -0
- package/plugins/odin-scp/skills/odin-scp/SKILL.md +196 -3
- package/plugins/odin-scp/skills/odin-scp/references/canonical-introduction-prompt.md +0 -2
- package/protocol/SCP.md +2 -2
- package/protocol/bootstrap-skill.md +196 -3
- package/protocol/closeout.yaml +1 -1
- package/protocol/delegation.yaml +1 -1
- package/protocol/mission-frontrun/droids-scrutiny-feature-reviewer.md +70 -0
- package/protocol/mission-frontrun/orchestrator-contract.md +70 -0
- package/protocol/mission-frontrun/scrutiny-feature-reviewer-contract.md +73 -0
- package/protocol/mission-frontrun/scrutiny-validator-contract.md +77 -0
- package/protocol/mission-frontrun/worker-contract.md +66 -0
- package/protocol/model-profiles.yaml +8 -1
- package/protocol/receipts/boot-receipt.yaml +13 -0
- package/protocol/role-cards/dev-worker.md +74 -0
- package/protocol/role-cards/exec-asst.md +83 -0
- package/protocol/role-cards/exec-pm.md +66 -0
- package/protocol/role-cards/qa-worker.md +71 -0
- package/protocol/role-cards/team-pm.md +67 -0
- package/protocol/roles.yaml +1 -1
- package/protocol/skill-references/canonical-introduction-prompt.md +0 -2
- package/protocol/topology.yaml +1 -1
- package/scripts/audit/public-surface.mjs +27 -2
- package/scripts/audit/verify-pack.mjs +121 -5
package/protocol/roles.yaml
CHANGED
|
@@ -12,7 +12,6 @@ Use the `odin-scp` skill if available. Also read local project authority files w
|
|
|
12
12
|
- CLAUDE.md
|
|
13
13
|
- config/constitutional/constitutional-agent.md
|
|
14
14
|
- project-local governance or constitution files declared by the repository
|
|
15
|
-
- docs/handoffs/
|
|
16
15
|
- .odin/handoffs/
|
|
17
16
|
- .odin/audit/
|
|
18
17
|
|
|
@@ -41,7 +40,6 @@ Phase 0 - live preflight:
|
|
|
41
40
|
- git rev-parse HEAD
|
|
42
41
|
- git rev-parse @{u}, if upstream exists
|
|
43
42
|
2. Discover handoffs and audit state:
|
|
44
|
-
- docs/handoffs/
|
|
45
43
|
- .odin/handoffs/
|
|
46
44
|
- .odin/audit/
|
|
47
45
|
3. If no handoff exists, treat the repo as a fresh SCP bootstrap.
|
package/protocol/topology.yaml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
|
|
@@ -18,6 +18,7 @@ const PUBLIC_ROOTS = [
|
|
|
18
18
|
];
|
|
19
19
|
|
|
20
20
|
const EXCLUDED_PREFIXES = [".git/", "dist/", "node_modules/", "project/" + "planning" + "/", "." + "edge-" + "agentic" + "/local/", "tests/"];
|
|
21
|
+
export const FORBIDDEN_PUBLIC_PREFIXES = ["docs/handoffs/"];
|
|
21
22
|
|
|
22
23
|
function walk(dir) {
|
|
23
24
|
return readdirSync(dir).flatMap((entry) => {
|
|
@@ -47,6 +48,7 @@ function filesToAudit() {
|
|
|
47
48
|
return `${tracked}\n${untracked}`
|
|
48
49
|
.split("\n")
|
|
49
50
|
.filter(Boolean)
|
|
51
|
+
.filter((file) => existsSync(file))
|
|
50
52
|
.filter((file) => file !== "pnpm-lock.yaml")
|
|
51
53
|
.filter(isPublicAuditFile);
|
|
52
54
|
} catch {
|
|
@@ -54,6 +56,13 @@ function filesToAudit() {
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
function forbiddenPublicPathFindings() {
|
|
60
|
+
return FORBIDDEN_PUBLIC_PREFIXES.flatMap((prefix) => {
|
|
61
|
+
if (!existsSync(prefix)) return [];
|
|
62
|
+
return walk(prefix).map((file) => `${file}: internal handoff files must not exist under public docs`);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
const BUNDLED_DOC = new Set([
|
|
58
67
|
"README.md",
|
|
59
68
|
"docs/guides/quickstart-prompts.md",
|
|
@@ -78,13 +87,20 @@ const forbidden = [
|
|
|
78
87
|
"i"
|
|
79
88
|
)
|
|
80
89
|
},
|
|
81
|
-
{ name: "secret-looking assignment", pattern: /(api[_-]?key|secret|token|password)\s*[:=]\s*["'][^"']+["']/i }
|
|
90
|
+
{ name: "secret-looking quoted assignment", pattern: /(api[_-]?key|secret|token|password)\s*[:=]\s*["'][^"']+["']/i },
|
|
91
|
+
{ name: "secret-looking unquoted assignment", pattern: /(api[_-]?key|secret|token|password)\s*[:=]\s*[A-Za-z0-9._~+/=-]{16,}/i },
|
|
92
|
+
{ name: "bearer token literal", pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
|
|
93
|
+
{ name: "URI credential literal", pattern: /[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s:@]+@/i }
|
|
82
94
|
];
|
|
83
95
|
|
|
84
96
|
export function auditPublicSurface(fileTextByPath) {
|
|
85
97
|
const findings = [];
|
|
86
98
|
for (const [file, text] of Object.entries(fileTextByPath)) {
|
|
87
99
|
if (!isPublicAuditFile(file)) continue;
|
|
100
|
+
if (FORBIDDEN_PUBLIC_PREFIXES.some((prefix) => file.startsWith(prefix))) {
|
|
101
|
+
findings.push(`${file}: internal handoff files must not exist under public docs`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
88
104
|
for (const rule of forbidden) {
|
|
89
105
|
if (rule.exemptFiles?.has(file)) continue;
|
|
90
106
|
if (rule.pattern.test(text)) findings.push(`${file}: ${rule.name}`);
|
|
@@ -95,6 +111,15 @@ export function auditPublicSurface(fileTextByPath) {
|
|
|
95
111
|
|
|
96
112
|
export function main() {
|
|
97
113
|
const publicFiles = filesToAudit();
|
|
114
|
+
const forbiddenPublicFiles = publicFiles.filter((file) => FORBIDDEN_PUBLIC_PREFIXES.some((prefix) => file.startsWith(prefix)));
|
|
115
|
+
const forbiddenPathFindings = forbiddenPublicPathFindings();
|
|
116
|
+
if (forbiddenPublicFiles.length > 0) {
|
|
117
|
+
throw new Error(`Public surface audit failed: internal handoff files are public:\n${forbiddenPublicFiles.join("\n")}`);
|
|
118
|
+
}
|
|
119
|
+
if (forbiddenPathFindings.length > 0) {
|
|
120
|
+
throw new Error(`Public surface audit failed:\n${forbiddenPathFindings.join("\n")}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
98
123
|
const findings = auditPublicSurface(Object.fromEntries(publicFiles.map((file) => [file, readFileSync(file, "utf8")])));
|
|
99
124
|
|
|
100
125
|
if (findings.length > 0) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
4
5
|
|
|
5
6
|
export const MINIMUM_COMPATIBLE_CHILD_MCP_VERSION = "0.4.5";
|
|
@@ -17,7 +18,17 @@ const requiredProtocolFiles = [
|
|
|
17
18
|
"protocol/skill-references/boot-receipt-examples.md",
|
|
18
19
|
"protocol/skill-references/canonical-introduction-prompt.md",
|
|
19
20
|
"protocol/skill-references/harness-skill-targets.md",
|
|
20
|
-
"protocol/skill-references/team-bootstrap-runbook.md"
|
|
21
|
+
"protocol/skill-references/team-bootstrap-runbook.md",
|
|
22
|
+
"protocol/role-cards/exec-pm.md",
|
|
23
|
+
"protocol/role-cards/team-pm.md",
|
|
24
|
+
"protocol/role-cards/dev-worker.md",
|
|
25
|
+
"protocol/role-cards/qa-worker.md",
|
|
26
|
+
"protocol/role-cards/exec-asst.md",
|
|
27
|
+
"protocol/mission-frontrun/orchestrator-contract.md",
|
|
28
|
+
"protocol/mission-frontrun/worker-contract.md",
|
|
29
|
+
"protocol/mission-frontrun/scrutiny-validator-contract.md",
|
|
30
|
+
"protocol/mission-frontrun/scrutiny-feature-reviewer-contract.md",
|
|
31
|
+
"protocol/mission-frontrun/droids-scrutiny-feature-reviewer.md"
|
|
21
32
|
];
|
|
22
33
|
|
|
23
34
|
const requiredTemplateFiles = [
|
|
@@ -44,6 +55,12 @@ export const requiredPackageFiles = [
|
|
|
44
55
|
"docs/reference/cost-and-privacy.md",
|
|
45
56
|
"docs/reference/distribution.md",
|
|
46
57
|
"docs/reference/public-surface-audit.md",
|
|
58
|
+
"docs/lattice/odin-lattice-design.md",
|
|
59
|
+
"docs/adapters/cmux-adapter.md",
|
|
60
|
+
"docs/adapters/tmux-adapter.md",
|
|
61
|
+
"docs/adapters/minimux-adapter.md",
|
|
62
|
+
"docs/adapters/herdr-adapter.md",
|
|
63
|
+
"docs/adapters/plain-terminal.md",
|
|
47
64
|
...requiredProtocolFiles,
|
|
48
65
|
"plugins/odin-scp/.claude-plugin/plugin.json",
|
|
49
66
|
"plugins/odin-scp/skills/odin-scp/SKILL.md",
|
|
@@ -58,6 +75,9 @@ export const requiredPackageFiles = [
|
|
|
58
75
|
...requiredTemplateFiles,
|
|
59
76
|
"scripts/audit/public-surface.mjs",
|
|
60
77
|
"scripts/audit/verify-pack.mjs",
|
|
78
|
+
"scripts/protocol/install-activation-hooks.mjs",
|
|
79
|
+
"scripts/protocol/verify-governed-context.mjs",
|
|
80
|
+
"scripts/protocol/verify-instruction-read.mjs",
|
|
61
81
|
"AGENTS.md",
|
|
62
82
|
"CLAUDE.md",
|
|
63
83
|
"README.md",
|
|
@@ -82,14 +102,20 @@ const protocolResourceVersionLockedFiles = new Set([
|
|
|
82
102
|
"protocol/topology.yaml"
|
|
83
103
|
]);
|
|
84
104
|
|
|
85
|
-
const forbiddenPackagePrefixes = ["project/" + "planning" + "/", "." + "edge-" + "agentic" + "/local/"];
|
|
105
|
+
const forbiddenPackagePrefixes = ["docs/handoffs/", "project/" + "planning" + "/", "." + "edge-" + "agentic" + "/local/"];
|
|
106
|
+
const AUDIT_SCRIPT_EXEMPTIONS = new Set(["scripts/audit/public-surface.mjs", "scripts/audit/verify-pack.mjs"]);
|
|
107
|
+
const INTERNAL_HANDOFF_REFERENCE_EXEMPTIONS = new Set([...AUDIT_SCRIPT_EXEMPTIONS, "docs/reference/distribution.md"]);
|
|
86
108
|
const forbiddenPackagedContentRules = [
|
|
87
109
|
{ name: "local evidence path", pattern: new RegExp(`\\.${"edge-" + "agentic"}/local`, "i") },
|
|
88
110
|
{ name: "local ODIN audit path", pattern: /\.odin\/local\//i },
|
|
89
111
|
{ name: "private planning path", pattern: new RegExp(`project/${"planning"}/`, "i") },
|
|
112
|
+
{ name: "internal handoff path reference", pattern: /docs\/handoffs\//i, exemptFiles: INTERNAL_HANDOFF_REFERENCE_EXEMPTIONS },
|
|
90
113
|
{ name: "macOS home path", pattern: new RegExp(`/${"Users"}/[A-Za-z0-9._-]+/`) },
|
|
91
114
|
{ name: "Linux home path", pattern: /\/home\/[A-Za-z0-9._-]+\// },
|
|
92
|
-
{ name: "secret-looking assignment", pattern: /(api[_-]?key|secret|token|password)\s*[:=]\s*["'][^"']+["']/i }
|
|
115
|
+
{ name: "secret-looking quoted assignment", pattern: /(api[_-]?key|secret|token|password)\s*[:=]\s*["'][^"']+["']/i },
|
|
116
|
+
{ name: "secret-looking unquoted assignment", pattern: /(api[_-]?key|secret|token|password)\s*[:=]\s*[A-Za-z0-9._~+/=-]{16,}/i },
|
|
117
|
+
{ name: "bearer token literal", pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
|
|
118
|
+
{ name: "URI credential literal", pattern: /[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s:@]+@/i }
|
|
93
119
|
];
|
|
94
120
|
|
|
95
121
|
function asPathSet(paths) {
|
|
@@ -104,6 +130,9 @@ export function validatePackageMetadata(packageJson) {
|
|
|
104
130
|
if (!packageJson.license) errors.push("package.json missing license");
|
|
105
131
|
if (!packageJson.engines?.node) errors.push("package.json missing engines.node");
|
|
106
132
|
if (!Array.isArray(packageJson.files) || packageJson.files.length === 0) errors.push("package.json missing files allowlist");
|
|
133
|
+
if (packageJson.scripts?.prepublishOnly !== "pnpm run validate") {
|
|
134
|
+
errors.push("package.json prepublishOnly must run pnpm run validate");
|
|
135
|
+
}
|
|
107
136
|
for (const file of [".claude-plugin", "docs", "plugins", "protocol", "templates", "AGENTS.md", "CLAUDE.md", "README.md", "LICENSE"]) {
|
|
108
137
|
if (!packageJson.files?.includes(file)) errors.push(`package.json files allowlist missing ${file}`);
|
|
109
138
|
}
|
|
@@ -121,6 +150,30 @@ export function validatePackageMetadata(packageJson) {
|
|
|
121
150
|
return errors;
|
|
122
151
|
}
|
|
123
152
|
|
|
153
|
+
function walkFiles(dir) {
|
|
154
|
+
if (!existsSync(dir)) return [];
|
|
155
|
+
return readdirSync(dir).flatMap((entry) => {
|
|
156
|
+
const path = join(dir, entry);
|
|
157
|
+
return statSync(path).isDirectory() ? walkFiles(path) : [path];
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function expectedGeneratedDistFiles() {
|
|
162
|
+
const expected = new Set();
|
|
163
|
+
for (const file of walkFiles("src")) {
|
|
164
|
+
if (!file.endsWith(".ts") || file.endsWith(".d.ts")) continue;
|
|
165
|
+
const jsFile = file.replace(/^src\//, "dist/src/").replace(/\.ts$/, ".js");
|
|
166
|
+
expected.add(jsFile);
|
|
167
|
+
expected.add(`${jsFile}.map`);
|
|
168
|
+
expected.add(jsFile.replace(/\.js$/, ".d.ts"));
|
|
169
|
+
}
|
|
170
|
+
return expected;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function allowedGeneratedDistFiles() {
|
|
174
|
+
return new Set([...requiredPackageFiles.filter((file) => file.startsWith("dist/")), ...expectedGeneratedDistFiles()]);
|
|
175
|
+
}
|
|
176
|
+
|
|
124
177
|
export function validatePackFileList(pathsInput) {
|
|
125
178
|
const paths = asPathSet(pathsInput);
|
|
126
179
|
const errors = [];
|
|
@@ -133,6 +186,10 @@ export function validatePackFileList(pathsInput) {
|
|
|
133
186
|
const privatePaths = Array.from(paths).filter((file) => forbiddenPackagePrefixes.some((prefix) => file.startsWith(prefix)));
|
|
134
187
|
if (privatePaths.length > 0) errors.push(`Package includes private local paths: ${privatePaths.join(", ")}`);
|
|
135
188
|
|
|
189
|
+
const allowed = new Set([...requiredPackageFiles, ...allowedGeneratedDistFiles()]);
|
|
190
|
+
const unexpected = Array.from(paths).filter((file) => !allowed.has(file));
|
|
191
|
+
if (unexpected.length > 0) errors.push(`Package includes unexpected files: ${unexpected.join(", ")}`);
|
|
192
|
+
|
|
136
193
|
return errors;
|
|
137
194
|
}
|
|
138
195
|
|
|
@@ -140,6 +197,7 @@ export function validatePackFileContents(fileTextByPath) {
|
|
|
140
197
|
const findings = [];
|
|
141
198
|
for (const [file, text] of Object.entries(fileTextByPath)) {
|
|
142
199
|
for (const rule of forbiddenPackagedContentRules) {
|
|
200
|
+
if (rule.exemptFiles?.has(file)) continue;
|
|
143
201
|
if (rule.pattern.test(text)) findings.push(`${file}: ${rule.name}`);
|
|
144
202
|
}
|
|
145
203
|
}
|
|
@@ -201,6 +259,56 @@ export function findStaleVersionReferences(fileTextByPath, currentVersion, minim
|
|
|
201
259
|
return findings;
|
|
202
260
|
}
|
|
203
261
|
|
|
262
|
+
export function findUnpinnedInstallReferences(fileTextByPath, currentVersion) {
|
|
263
|
+
const findings = [];
|
|
264
|
+
const packageName = "@bradheitmann/odin-sentinel";
|
|
265
|
+
const pinned = `${packageName}@${currentVersion}`;
|
|
266
|
+
const commandMarkers = [
|
|
267
|
+
"pnpm",
|
|
268
|
+
"npx",
|
|
269
|
+
"npm",
|
|
270
|
+
"claude mcp",
|
|
271
|
+
"--package",
|
|
272
|
+
"installurl",
|
|
273
|
+
"\"args\"",
|
|
274
|
+
"args =",
|
|
275
|
+
"command"
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
for (const [file, text] of Object.entries(fileTextByPath)) {
|
|
279
|
+
if (AUDIT_SCRIPT_EXEMPTIONS.has(file)) continue;
|
|
280
|
+
const lines = text.split("\n");
|
|
281
|
+
for (const [index, line] of lines.entries()) {
|
|
282
|
+
if (!line.includes(packageName)) continue;
|
|
283
|
+
const windowText = lines.slice(Math.max(0, index - 2), Math.min(lines.length, index + 3)).join(" ");
|
|
284
|
+
const lowerWindow = windowText.toLowerCase();
|
|
285
|
+
if (!commandMarkers.some((marker) => lowerWindow.includes(marker))) continue;
|
|
286
|
+
if (windowText.includes(pinned)) continue;
|
|
287
|
+
findings.push(`${file}:${index + 1}: install command must pin ${pinned}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return findings;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function validateRuntimeVersionConstants(versionText, currentVersion, minimumCompatibleVersion = MINIMUM_COMPATIBLE_CHILD_MCP_VERSION, file = "src/protocol/version.ts") {
|
|
295
|
+
const errors = [];
|
|
296
|
+
const required = [
|
|
297
|
+
[`PROTOCOL_SCHEMA_VERSION`, currentVersion],
|
|
298
|
+
[`PUBLIC_LATEST_VERSION`, currentVersion],
|
|
299
|
+
[`MINIMUM_COMPATIBLE_MCP_VERSION`, minimumCompatibleVersion]
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
for (const [constant, expected] of required) {
|
|
303
|
+
const pattern = new RegExp(`\\b${constant}\\s*=\\s*["']${expected.replaceAll(".", "\\.")}["']`);
|
|
304
|
+
if (!pattern.test(versionText)) {
|
|
305
|
+
errors.push(`${file}: ${constant} must be ${expected}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return errors;
|
|
310
|
+
}
|
|
311
|
+
|
|
204
312
|
export function validatePublicProtocolSync({ scpText, bootstrapText, currentVersion, minimumCompatibleVersion = MINIMUM_COMPATIBLE_CHILD_MCP_VERSION }) {
|
|
205
313
|
const errors = [];
|
|
206
314
|
const requiredMarkers = [
|
|
@@ -244,9 +352,13 @@ export function validatePluginSync({ pluginManifestText, pluginSkillText, plugin
|
|
|
244
352
|
} else {
|
|
245
353
|
if (server.command !== "pnpm") errors.push("Claude plugin odin-sentinel server must use pnpm");
|
|
246
354
|
const args = Array.isArray(server.args) ? server.args : [];
|
|
247
|
-
|
|
355
|
+
const expectedArgs = ["dlx", "--package", `@bradheitmann/odin-sentinel@${currentVersion}`, "odin-sentinel-mcp"];
|
|
356
|
+
for (const requiredArg of expectedArgs) {
|
|
248
357
|
if (!args.includes(requiredArg)) errors.push(`Claude plugin odin-sentinel args missing ${requiredArg}`);
|
|
249
358
|
}
|
|
359
|
+
if (JSON.stringify(args) !== JSON.stringify(expectedArgs)) {
|
|
360
|
+
errors.push(`Claude plugin odin-sentinel args must exactly equal ${JSON.stringify(expectedArgs)}`);
|
|
361
|
+
}
|
|
250
362
|
}
|
|
251
363
|
|
|
252
364
|
for (const marker of [`SCP_PUBLIC_VERSION: ${currentVersion}`, `MIN_COMPATIBLE_CHILD_MCP: ${minimumCompatibleVersion}`]) {
|
|
@@ -331,6 +443,7 @@ function readPublicVersionFiles() {
|
|
|
331
443
|
"docs/reference/client-compatibility.md",
|
|
332
444
|
"docs/reference/distribution.md",
|
|
333
445
|
"docs/reference/public-surface-audit.md",
|
|
446
|
+
"src/protocol/version.ts",
|
|
334
447
|
".claude-plugin/marketplace.json",
|
|
335
448
|
"protocol/SCP.md",
|
|
336
449
|
"protocol/bootstrap-skill.md",
|
|
@@ -363,6 +476,9 @@ export function runVerifyPack({ pack, packageJson, publicVersionFiles, costPriva
|
|
|
363
476
|
...validatePackFileContents(packFileTexts),
|
|
364
477
|
...validatePackagedProtocolVersions(packFileTexts, packageJson.version),
|
|
365
478
|
...findStaleVersionReferences(publicVersionFiles, packageJson.version),
|
|
479
|
+
...findUnpinnedInstallReferences(packFileTexts, packageJson.version),
|
|
480
|
+
...validateRuntimeVersionConstants(publicVersionFiles["src/protocol/version.ts"], packageJson.version),
|
|
481
|
+
...validateRuntimeVersionConstants(packFileTexts["dist/src/protocol/version.js"] ?? "", packageJson.version, MINIMUM_COMPATIBLE_CHILD_MCP_VERSION, "dist/src/protocol/version.js"),
|
|
366
482
|
...validatePublicProtocolSync({
|
|
367
483
|
scpText: publicVersionFiles["protocol/SCP.md"],
|
|
368
484
|
bootstrapText: publicVersionFiles["protocol/bootstrap-skill.md"],
|