@juzser/blacksmith 0.1.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/agents/auditor.md +146 -0
- package/.claude/agents/coder.md +164 -0
- package/.claude/agents/grader.md +110 -0
- package/.claude/agents/merger.md +107 -0
- package/.claude/agents/planner.md +92 -0
- package/.claude/agents/researcher.md +105 -0
- package/.claude/agents/reviewer.md +171 -0
- package/.claude/agents/scribe.md +72 -0
- package/.claude/agents/security-reviewer.md +130 -0
- package/.claude/agents/spec-reviewer.md +215 -0
- package/.claude/agents/tester.md +121 -0
- package/.claude/agents/uiux.md +126 -0
- package/.claude/agents/verifier.md +103 -0
- package/.claude/agents/wave-runner.md +74 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/factory/orchestrator/dist/actors.js +46 -0
- package/factory/orchestrator/dist/agents-registry.js +308 -0
- package/factory/orchestrator/dist/args.js +84 -0
- package/factory/orchestrator/dist/artifacts.js +111 -0
- package/factory/orchestrator/dist/attribution.js +160 -0
- package/factory/orchestrator/dist/audit.js +812 -0
- package/factory/orchestrator/dist/autonomy.js +151 -0
- package/factory/orchestrator/dist/budgetAlarm.js +366 -0
- package/factory/orchestrator/dist/budgets.js +196 -0
- package/factory/orchestrator/dist/claims.js +633 -0
- package/factory/orchestrator/dist/cli.js +3185 -0
- package/factory/orchestrator/dist/commit.js +152 -0
- package/factory/orchestrator/dist/coverage.js +188 -0
- package/factory/orchestrator/dist/crossFinding.js +479 -0
- package/factory/orchestrator/dist/crosscheck.js +396 -0
- package/factory/orchestrator/dist/daemon.js +707 -0
- package/factory/orchestrator/dist/db/projector.js +1317 -0
- package/factory/orchestrator/dist/db/queries.js +1714 -0
- package/factory/orchestrator/dist/db/schema.js +305 -0
- package/factory/orchestrator/dist/delegation.js +400 -0
- package/factory/orchestrator/dist/diffstat.js +164 -0
- package/factory/orchestrator/dist/dispatchAudit.js +275 -0
- package/factory/orchestrator/dist/dotenv.js +72 -0
- package/factory/orchestrator/dist/effort.js +202 -0
- package/factory/orchestrator/dist/epic.js +1018 -0
- package/factory/orchestrator/dist/epicWidth.js +238 -0
- package/factory/orchestrator/dist/errorIssues.js +249 -0
- package/factory/orchestrator/dist/errors.js +11 -0
- package/factory/orchestrator/dist/escalation.js +500 -0
- package/factory/orchestrator/dist/events.js +1015 -0
- package/factory/orchestrator/dist/findingAge.js +91 -0
- package/factory/orchestrator/dist/findingContext.js +103 -0
- package/factory/orchestrator/dist/findings.js +1156 -0
- package/factory/orchestrator/dist/gate.js +911 -0
- package/factory/orchestrator/dist/gh.js +146 -0
- package/factory/orchestrator/dist/git.js +87 -0
- package/factory/orchestrator/dist/goalCheck.js +380 -0
- package/factory/orchestrator/dist/graph.js +61 -0
- package/factory/orchestrator/dist/hookDecision.js +106 -0
- package/factory/orchestrator/dist/immutability.js +171 -0
- package/factory/orchestrator/dist/impact.js +280 -0
- package/factory/orchestrator/dist/integration.js +141 -0
- package/factory/orchestrator/dist/judgePreflight.js +155 -0
- package/factory/orchestrator/dist/judges.js +310 -0
- package/factory/orchestrator/dist/lessonAudit.js +399 -0
- package/factory/orchestrator/dist/lessons.js +1329 -0
- package/factory/orchestrator/dist/mcp.js +555 -0
- package/factory/orchestrator/dist/paths.js +82 -0
- package/factory/orchestrator/dist/plan.js +502 -0
- package/factory/orchestrator/dist/planQuorum.js +421 -0
- package/factory/orchestrator/dist/policy.js +1308 -0
- package/factory/orchestrator/dist/policyHook.js +28 -0
- package/factory/orchestrator/dist/preconditions.js +47 -0
- package/factory/orchestrator/dist/projects.js +134 -0
- package/factory/orchestrator/dist/prompts.js +57 -0
- package/factory/orchestrator/dist/provenance.js +201 -0
- package/factory/orchestrator/dist/providers/api-transport.js +117 -0
- package/factory/orchestrator/dist/providers/cli-transport.js +208 -0
- package/factory/orchestrator/dist/providers/index.js +54 -0
- package/factory/orchestrator/dist/providers/schema-validate.js +152 -0
- package/factory/orchestrator/dist/providers/types.js +49 -0
- package/factory/orchestrator/dist/queue.js +263 -0
- package/factory/orchestrator/dist/quorum.js +347 -0
- package/factory/orchestrator/dist/quorumEscalations.js +132 -0
- package/factory/orchestrator/dist/results.js +65 -0
- package/factory/orchestrator/dist/roadmap.js +437 -0
- package/factory/orchestrator/dist/runtime.js +86 -0
- package/factory/orchestrator/dist/sameMistakeKpi.js +243 -0
- package/factory/orchestrator/dist/sandbox.js +165 -0
- package/factory/orchestrator/dist/scaffold.js +373 -0
- package/factory/orchestrator/dist/scheduler.js +408 -0
- package/factory/orchestrator/dist/schemas.js +195 -0
- package/factory/orchestrator/dist/security.js +222 -0
- package/factory/orchestrator/dist/severity.js +293 -0
- package/factory/orchestrator/dist/spec.js +376 -0
- package/factory/orchestrator/dist/specChange.js +497 -0
- package/factory/orchestrator/dist/stack.js +324 -0
- package/factory/orchestrator/dist/symbols.js +966 -0
- package/factory/orchestrator/dist/taskEvents.js +399 -0
- package/factory/orchestrator/dist/taskId.js +126 -0
- package/factory/orchestrator/dist/taxonomy.js +113 -0
- package/factory/orchestrator/dist/testSelect.js +129 -0
- package/factory/orchestrator/dist/testerAudit.js +264 -0
- package/factory/orchestrator/dist/testgate.js +171 -0
- package/factory/orchestrator/dist/usage.js +874 -0
- package/factory/orchestrator/dist/waivers.js +228 -0
- package/factory/orchestrator/dist/waveBudget.js +223 -0
- package/factory/orchestrator/dist/waveConcurrency.js +250 -0
- package/factory/orchestrator/dist/waveNext.js +210 -0
- package/factory/orchestrator/dist/waveSchedule.js +146 -0
- package/factory/orchestrator/dist/worktree.js +207 -0
- package/factory/orchestrator/drizzle/0000_broken_rockslide.sql +183 -0
- package/factory/orchestrator/drizzle/0001_luxuriant_zaladane.sql +1 -0
- package/factory/orchestrator/drizzle/0002_yellow_wild_pack.sql +8 -0
- package/factory/orchestrator/drizzle/0003_damp_riptide.sql +12 -0
- package/factory/orchestrator/drizzle/0004_broad_lady_vermin.sql +1 -0
- package/factory/orchestrator/drizzle/0005_romantic_jocasta.sql +2 -0
- package/factory/orchestrator/drizzle/0006_bent_madelyne_pryor.sql +16 -0
- package/factory/orchestrator/drizzle/0007_overjoyed_goliath.sql +1 -0
- package/factory/orchestrator/drizzle/0008_smart_iron_patriot.sql +2 -0
- package/factory/orchestrator/drizzle/0009_agents_epic_id.sql +2 -0
- package/factory/orchestrator/drizzle/0010_milestone_kind.sql +1 -0
- package/factory/orchestrator/drizzle/meta/_journal.json +83 -0
- package/factory/policies/budgets.yml +262 -0
- package/factory/policies/crosscheck.yml +308 -0
- package/factory/policies/delegation.yml +44 -0
- package/factory/policies/effort.yml +142 -0
- package/factory/policies/guardrails.yml +306 -0
- package/factory/policies/lessons.md +239 -0
- package/factory/policies/scheduler.yml +90 -0
- package/factory/policies/sensitive-paths.yml +148 -0
- package/factory/policies/severity.yml +83 -0
- package/factory/policies/stack.yml +60 -0
- package/factory/policies/taxonomy.yml +340 -0
- package/factory/policies/worktree.yml +95 -0
- package/factory/scaffold/base/.github/workflows/ci.yml +34 -0
- package/factory/scaffold/base/.gitignore.tmpl +11 -0
- package/factory/scaffold/base/AGENTS.md +33 -0
- package/factory/scaffold/base/README.md +25 -0
- package/factory/scaffold/base/biome.json.tmpl +30 -0
- package/factory/scaffold/base/package.json +25 -0
- package/factory/scaffold/base/pnpm-workspace.yaml.tmpl +16 -0
- package/factory/scaffold/base/src/index.ts +4 -0
- package/factory/scaffold/base/test/index.test.ts +8 -0
- package/factory/scaffold/base/tsconfig.json +20 -0
- package/factory/scaffold/base/tsconfig.test.json +9 -0
- package/factory/scaffold/base/vitest.config.ts +29 -0
- package/factory/scaffold/mcp/mcp.manifest.json +22 -0
- package/factory/scaffold/mcp/package.fragment.json +9 -0
- package/factory/scaffold/mcp/src/mcp/README.md +63 -0
- package/factory/scaffold/mcp/src/mcp/guard.ts +110 -0
- package/factory/scaffold/mcp/src/mcp/redact.ts +69 -0
- package/factory/scaffold/mcp/src/mcp/server.ts +55 -0
- package/factory/scaffold/mcp/src/mcp/tools/health.ts +60 -0
- package/factory/scaffold/mcp/test/mcp/guard.test.ts +108 -0
- package/factory/scaffold/mcp/test/mcp/redact.test.ts +61 -0
- package/factory/scaffold/mcp/test/mcp/surface.test.ts +100 -0
- package/factory/scaffold/ui/index.html +12 -0
- package/factory/scaffold/ui/package.fragment.json +13 -0
- package/factory/scaffold/ui/src/App.vue +10 -0
- package/factory/scaffold/ui/src/main.ts +5 -0
- package/factory/scaffold/ui/src/shims.d.ts +11 -0
- package/factory/scaffold/ui/vite.config.ts +20 -0
- package/factory/scaffold/ui-tailwind/package.fragment.json +6 -0
- package/factory/scaffold/ui-tailwind/vite.config.ts +26 -0
- package/factory/specs/schema/event.schema.json +82 -0
- package/factory/specs/schema/finding-evidence.schema.json +45 -0
- package/factory/specs/schema/finding.schema.json +115 -0
- package/factory/specs/schema/grader-verdict.schema.json +50 -0
- package/factory/specs/schema/judge-verdict.schema.json +19 -0
- package/factory/specs/schema/lesson.schema.json +85 -0
- package/factory/specs/schema/mcp-manifest.schema.json +230 -0
- package/factory/specs/schema/result.schema.json +81 -0
- package/factory/specs/schema/spec-change-request.schema.json +46 -0
- package/factory/specs/schema/task-spec.schema.json +134 -0
- package/package.json +98 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
// MCP surface: the agent-callable face every project leaving the factory ships
|
|
2
|
+
// (docs/standards/mcp.md). Four jobs live here and nothing else:
|
|
3
|
+
//
|
|
4
|
+
// addMcpSurface() — layer factory/scaffold/mcp/ onto an ALREADY scaffolded
|
|
5
|
+
// project, and register the milestone that makes it due.
|
|
6
|
+
// checkManifest() — turn the numbered baseline in docs/standards/mcp.md into
|
|
7
|
+
// pass/fail over a declared manifest.
|
|
8
|
+
// runMcpCheck() — what `smith mcp check` prints: the manifest verdict plus
|
|
9
|
+
// the roadmap's own (MCP-M1/M2, "the surface is due").
|
|
10
|
+
// resolveMcpSurface()/mcpBlockers()
|
|
11
|
+
// — what the epic-close gate refuses on.
|
|
12
|
+
//
|
|
13
|
+
// checkManifest is deliberately pure and filesystem-free: `smith mcp check` and
|
|
14
|
+
// the epic-close gate both call it, and a gate that needs a working directory
|
|
15
|
+
// to render a verdict is a gate that behaves differently in CI than on a
|
|
16
|
+
// laptop. Rule ids (MCP-P1, MCP-T4, ...) are the contract with the doc — when
|
|
17
|
+
// one changes here, the doc's numbered list changes in the same commit.
|
|
18
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { SmithError } from './errors.js';
|
|
21
|
+
import { PROJECTS_DIR, ROADMAP_PATH, SCAFFOLD_DIR, WORKSPACES_DIR } from './paths.js';
|
|
22
|
+
import { loadRoadmap, ownsEpic, readRoadmapText, roadmapDeclaresId, } from './roadmap.js';
|
|
23
|
+
import { copyTemplateDir, mergePackageJson, PACKAGE_FRAGMENT_NAME, validateProjectName, } from './scaffold.js';
|
|
24
|
+
export class McpError extends SmithError {
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The wire revision the factory pins — chosen to be what the Tier-1 TypeScript
|
|
28
|
+
* SDK actually negotiates, not the newest revision on paper. Verified against
|
|
29
|
+
* @modelcontextprotocol/sdk@1.30.0 (the only published dist-tag as of
|
|
30
|
+
* 2026-08-07), whose LATEST_PROTOCOL_VERSION is '2025-11-25'. Pinning the
|
|
31
|
+
* newer MCP_TARGET_REVISION today would produce a standard nothing can build.
|
|
32
|
+
*/
|
|
33
|
+
export const MCP_PROTOCOL_REVISION = '2025-11-25';
|
|
34
|
+
/**
|
|
35
|
+
* Where the pin moves once the SDK ships it. 2026-07-28 removes the
|
|
36
|
+
* initialize/initialized handshake and the Mcp-Session-Id header outright, so
|
|
37
|
+
* the scaffold is written to depend on neither — see docs/standards/mcp.md
|
|
38
|
+
* "Forward compatibility" for the migration trigger.
|
|
39
|
+
*/
|
|
40
|
+
export const MCP_TARGET_REVISION = '2026-07-28';
|
|
41
|
+
/** Suffix of the milestone every project must close before its final one. */
|
|
42
|
+
export const MCP_MILESTONE_SUFFIX = 'mcp-surface';
|
|
43
|
+
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
44
|
+
const TOOL_NAME = /^[a-z][a-z0-9_]{2,63}$/;
|
|
45
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
46
|
+
const MUTATION_CLASSES = ['read-only', 'write', 'destructive', 'outbound'];
|
|
47
|
+
function isRecord(value) {
|
|
48
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
49
|
+
}
|
|
50
|
+
function isHttps(value) {
|
|
51
|
+
return typeof value === 'string' && value.startsWith('https://');
|
|
52
|
+
}
|
|
53
|
+
/** MCP-T1/T2/T3/T4/T5 — one tool entry against the tool-surface policy. */
|
|
54
|
+
function checkTool(tool, index, seen, violations) {
|
|
55
|
+
const at = `/tools/${index}`;
|
|
56
|
+
if (!isRecord(tool)) {
|
|
57
|
+
violations.push({ rule: 'MCP-T5', path: at, message: 'Tool entry must be an object.' });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const name = tool.name;
|
|
61
|
+
if (typeof name !== 'string' || !TOOL_NAME.test(name)) {
|
|
62
|
+
violations.push({
|
|
63
|
+
rule: 'MCP-T5',
|
|
64
|
+
path: at,
|
|
65
|
+
message: `Tool name must match ${TOOL_NAME.source}; got ${JSON.stringify(name)}.`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
else if (seen.has(name)) {
|
|
69
|
+
violations.push({ rule: 'MCP-T5', path: at, message: `Duplicate tool name "${name}".` });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
seen.add(name);
|
|
73
|
+
}
|
|
74
|
+
const mutation = tool.mutation;
|
|
75
|
+
if (typeof mutation !== 'string' || !MUTATION_CLASSES.includes(mutation)) {
|
|
76
|
+
violations.push({
|
|
77
|
+
rule: 'MCP-T2',
|
|
78
|
+
path: at,
|
|
79
|
+
message: `Tool must declare a mutation class (${MUTATION_CLASSES.join(' | ')}); got ${JSON.stringify(mutation)}.`,
|
|
80
|
+
});
|
|
81
|
+
return; // every remaining rule is relative to the class — nothing left to say
|
|
82
|
+
}
|
|
83
|
+
// Hints are read out of an unvalidated object on purpose: a missing hint is
|
|
84
|
+
// a policy violation reported under the rule its mutation class belongs to,
|
|
85
|
+
// never a silently-defaulted `false`.
|
|
86
|
+
const hints = isRecord(tool.annotations) ? tool.annotations : {};
|
|
87
|
+
const { readOnlyHint, destructiveHint, openWorldHint } = hints;
|
|
88
|
+
if (mutation === 'read-only') {
|
|
89
|
+
if (readOnlyHint !== true || destructiveHint !== false || openWorldHint !== false) {
|
|
90
|
+
violations.push({
|
|
91
|
+
rule: 'MCP-T1',
|
|
92
|
+
path: at,
|
|
93
|
+
message: 'A read-only tool must annotate readOnlyHint: true, destructiveHint: false, openWorldHint: false.',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (mutation === 'write' || mutation === 'destructive') {
|
|
98
|
+
if (readOnlyHint !== false) {
|
|
99
|
+
violations.push({
|
|
100
|
+
rule: 'MCP-T2',
|
|
101
|
+
path: at,
|
|
102
|
+
message: `A ${mutation} tool must annotate readOnlyHint: false.`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (mutation === 'destructive' && destructiveHint !== true) {
|
|
106
|
+
violations.push({
|
|
107
|
+
rule: 'MCP-T2',
|
|
108
|
+
path: at,
|
|
109
|
+
message: 'A destructive tool must annotate destructiveHint: true.',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (mutation === 'outbound' && openWorldHint !== true) {
|
|
114
|
+
violations.push({
|
|
115
|
+
rule: 'MCP-T3',
|
|
116
|
+
path: at,
|
|
117
|
+
message: 'An outbound tool must annotate openWorldHint: true.',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (mutation !== 'read-only') {
|
|
121
|
+
const approval = isRecord(tool.approval) ? tool.approval : undefined;
|
|
122
|
+
const signed = approval !== undefined &&
|
|
123
|
+
typeof approval.operator === 'string' &&
|
|
124
|
+
approval.operator.length > 0 &&
|
|
125
|
+
typeof approval.date === 'string' &&
|
|
126
|
+
ISO_DATE.test(approval.date) &&
|
|
127
|
+
typeof approval.milestone === 'string' &&
|
|
128
|
+
approval.milestone.length > 0;
|
|
129
|
+
if (!signed) {
|
|
130
|
+
violations.push({
|
|
131
|
+
rule: 'MCP-T4',
|
|
132
|
+
path: at,
|
|
133
|
+
message: `A ${mutation} tool needs approval: { operator, date (YYYY-MM-DD), milestone } recorded at the mcp surface milestone.`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** MCP-X1..X4 — transport declaration and, for the HTTP opt-in, its preconditions. */
|
|
139
|
+
function checkTransport(transport, violations) {
|
|
140
|
+
const at = '/transport';
|
|
141
|
+
if (!isRecord(transport) || (transport.kind !== 'stdio' && transport.kind !== 'http')) {
|
|
142
|
+
violations.push({
|
|
143
|
+
rule: 'MCP-X1',
|
|
144
|
+
path: at,
|
|
145
|
+
message: 'Transport must be { kind: "stdio" } or the declared HTTP opt-in { kind: "http" }.',
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (transport.kind === 'stdio')
|
|
150
|
+
return; // no port, no listener, nothing further to prove
|
|
151
|
+
const access = isRecord(transport.access) ? transport.access : undefined;
|
|
152
|
+
if (access?.cloudflareAccess !== true) {
|
|
153
|
+
violations.push({
|
|
154
|
+
rule: 'MCP-X2',
|
|
155
|
+
path: `${at}/access`,
|
|
156
|
+
message: 'An HTTP surface must sit behind Cloudflare Access (mcp.md MCP-X2: nothing is exposed beyond localhost without something authenticating in front of it).',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const rs = isRecord(transport.resourceServer) ? transport.resourceServer : undefined;
|
|
160
|
+
if (!rs) {
|
|
161
|
+
violations.push({
|
|
162
|
+
rule: 'MCP-X3',
|
|
163
|
+
path: `${at}/resourceServer`,
|
|
164
|
+
message: 'An HTTP surface must declare itself an OAuth 2.0 protected resource.',
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!isHttps(rs.canonicalUri)) {
|
|
169
|
+
violations.push({
|
|
170
|
+
rule: 'MCP-X3',
|
|
171
|
+
path: `${at}/resourceServer/canonicalUri`,
|
|
172
|
+
message: 'The canonical resource URI must be an https:// URL — it is the token audience.',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (!Array.isArray(rs.authorizationServers) || rs.authorizationServers.length === 0) {
|
|
176
|
+
violations.push({
|
|
177
|
+
rule: 'MCP-X3',
|
|
178
|
+
path: `${at}/resourceServer/authorizationServers`,
|
|
179
|
+
message: 'Declare at least one authorization server issuer.',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else if (!rs.authorizationServers.every(isHttps)) {
|
|
183
|
+
violations.push({
|
|
184
|
+
rule: 'MCP-X3',
|
|
185
|
+
path: `${at}/resourceServer/authorizationServers`,
|
|
186
|
+
message: 'Every authorization server issuer must be an https:// URL.',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (rs.resourceIndicators !== true) {
|
|
190
|
+
violations.push({
|
|
191
|
+
rule: 'MCP-X3',
|
|
192
|
+
path: `${at}/resourceServer/resourceIndicators`,
|
|
193
|
+
message: 'RFC 8707 resource indicators are mandatory — they are what binds a token to us.',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (rs.tokenPassthrough !== false) {
|
|
197
|
+
violations.push({
|
|
198
|
+
rule: 'MCP-X4',
|
|
199
|
+
path: `${at}/resourceServer/tokenPassthrough`,
|
|
200
|
+
message: 'Token passthrough is never allowed: the server MUST NOT accept a token that was not issued for its own canonical URI.',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The manifest-checkable subset of docs/standards/mcp.md. Source-level rules
|
|
206
|
+
* (MCP-P3 session-independence, MCP-S1 output redaction) are not decidable from
|
|
207
|
+
* a declaration and are enforced where the code is, not here.
|
|
208
|
+
*/
|
|
209
|
+
export function checkManifest(manifest) {
|
|
210
|
+
const violations = [];
|
|
211
|
+
if (!isRecord(manifest)) {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
violations: [{ rule: 'MCP-P2', path: '/', message: 'Manifest must be a JSON object.' }],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (manifest.protocolRevision !== MCP_PROTOCOL_REVISION) {
|
|
218
|
+
violations.push({
|
|
219
|
+
rule: 'MCP-P1',
|
|
220
|
+
path: '/protocolRevision',
|
|
221
|
+
message: `Pin the wire revision to ${MCP_PROTOCOL_REVISION} (what the Tier-1 SDK negotiates); got ${JSON.stringify(manifest.protocolRevision)}.`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (typeof manifest.name !== 'string' || manifest.name.length === 0) {
|
|
225
|
+
violations.push({
|
|
226
|
+
rule: 'MCP-P2',
|
|
227
|
+
path: '/name',
|
|
228
|
+
message: 'Manifest must name the server.',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (typeof manifest.version !== 'string' || !SEMVER.test(manifest.version)) {
|
|
232
|
+
violations.push({
|
|
233
|
+
rule: 'MCP-P2',
|
|
234
|
+
path: '/version',
|
|
235
|
+
message: `Server version must be semver; got ${JSON.stringify(manifest.version)}.`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
checkTransport(manifest.transport, violations);
|
|
239
|
+
if (!Array.isArray(manifest.tools)) {
|
|
240
|
+
violations.push({ rule: 'MCP-T5', path: '/tools', message: 'Manifest must declare tools[].' });
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
const seen = new Set();
|
|
244
|
+
manifest.tools.forEach((tool, index) => {
|
|
245
|
+
checkTool(tool, index, seen, violations);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return { ok: violations.length === 0, violations };
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Layers the MCP surface onto an existing project. Unlike scaffoldProject this
|
|
252
|
+
* REQUIRES the directory to already be a project (a package.json is the tell) —
|
|
253
|
+
* an MCP server with no project behind it has nothing to expose, and silently
|
|
254
|
+
* creating one would hide a mistyped project name.
|
|
255
|
+
*/
|
|
256
|
+
export function addMcpSurface(opts) {
|
|
257
|
+
validateProjectName(opts.projectName);
|
|
258
|
+
const templateDir = path.join(opts.templateDir ?? SCAFFOLD_DIR, 'mcp');
|
|
259
|
+
const targetDir = opts.targetDir ?? path.join(opts.projectsDir ?? PROJECTS_DIR, opts.projectName);
|
|
260
|
+
const manifestPath = path.join(targetDir, 'mcp.manifest.json');
|
|
261
|
+
if (!existsSync(path.join(targetDir, 'package.json'))) {
|
|
262
|
+
throw new McpError('mcp.unknown-project', `${targetDir} is not a scaffolded project (no package.json). Run \`smith new ${opts.projectName}\` first.`, { targetDir, projectName: opts.projectName });
|
|
263
|
+
}
|
|
264
|
+
if (existsSync(manifestPath)) {
|
|
265
|
+
throw new McpError('mcp.surface-exists', `${manifestPath} already exists. Edit the manifest in place; re-scaffolding would discard declared tools and their operator sign-off.`, { manifestPath, projectName: opts.projectName });
|
|
266
|
+
}
|
|
267
|
+
// Read the roadmap before writing anything, and throw here if it cannot be
|
|
268
|
+
// read. The registration is the LAST step below, so a roadmap this command
|
|
269
|
+
// cannot open used to fail after mcp.manifest.json existed — and the retry
|
|
270
|
+
// then hit `mcp.surface-exists` above, which refuses on purpose. A mistyped
|
|
271
|
+
// --roadmap-path left a half-initialised surface that the only command able
|
|
272
|
+
// to finish it would permanently decline to touch.
|
|
273
|
+
if (!opts.skipRoadmap)
|
|
274
|
+
readRoadmapText(opts.roadmapPath);
|
|
275
|
+
const filesWritten = [];
|
|
276
|
+
copyTemplateDir(templateDir, targetDir, opts.projectName, filesWritten, new Set([PACKAGE_FRAGMENT_NAME]));
|
|
277
|
+
mergePackageJson(targetDir, path.join(templateDir, PACKAGE_FRAGMENT_NAME));
|
|
278
|
+
const milestoneId = opts.skipRoadmap
|
|
279
|
+
? `${opts.projectName}-${MCP_MILESTONE_SUFFIX}`
|
|
280
|
+
: registerMcpMilestone(opts.projectName, opts.roadmapPath);
|
|
281
|
+
return {
|
|
282
|
+
targetDir,
|
|
283
|
+
manifestPath,
|
|
284
|
+
filesWritten,
|
|
285
|
+
milestoneId,
|
|
286
|
+
protocolRevision: MCP_PROTOCOL_REVISION,
|
|
287
|
+
transport: 'stdio',
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Appends the `<project> — mcp surface` milestone to the roadmap. This is what
|
|
292
|
+
* makes the surface *due*: docs/standards/mcp.md requires it to be closed
|
|
293
|
+
* before the project's final milestone, and `smith epic close` refuses while
|
|
294
|
+
* checkManifest is red.
|
|
295
|
+
*/
|
|
296
|
+
export function registerMcpMilestone(projectName, roadmapPath = ROADMAP_PATH) {
|
|
297
|
+
const id = `${projectName}-${MCP_MILESTONE_SUFFIX}`;
|
|
298
|
+
const text = readRoadmapText(roadmapPath);
|
|
299
|
+
// roadmapDeclaresId, not `text.includes('- id: ' + id)`: the substring is
|
|
300
|
+
// not what parseRoadmap means by a declaration, and appending a second copy
|
|
301
|
+
// of a milestone it already holds costs the whole file — milestoneId is the
|
|
302
|
+
// milestones table's primary key, so parseRoadmap refuses the roadmap from
|
|
303
|
+
// the duplicate onward, and `smith mcp init` cannot be run twice to undo it.
|
|
304
|
+
if (roadmapDeclaresId(text, id))
|
|
305
|
+
return id; // idempotent — already registered
|
|
306
|
+
const block = `\n## ${projectName} — mcp surface\n` +
|
|
307
|
+
`- id: ${id}\n` +
|
|
308
|
+
'- status: planned\n' +
|
|
309
|
+
`- project: ${projectName}\n` +
|
|
310
|
+
// Seeded, not empty: this list is what the epic-close gate reads to decide
|
|
311
|
+
// the surface is due (resolveMcpSurface/ownsEpic), and `[]` matched no epic
|
|
312
|
+
// at all — the gate shipped unreachable. Naming the epic here also states
|
|
313
|
+
// in the roadmap itself which epic closes the milestone, the way
|
|
314
|
+
// envkit-config-loader already does, instead of leaving it to convention.
|
|
315
|
+
`- epics: [${id}]\n` +
|
|
316
|
+
'- goal: Declare and harden the MCP surface — `smith mcp check` green against ' +
|
|
317
|
+
'docs/standards/mcp.md. Required before the final milestone can close.\n';
|
|
318
|
+
writeFileSync(roadmapPath, `${text.trimEnd()}\n${block}`, 'utf8');
|
|
319
|
+
return id;
|
|
320
|
+
}
|
|
321
|
+
const MANIFEST_NAME = 'mcp.manifest.json';
|
|
322
|
+
/** The project's own `<project>-mcp-surface` milestone, if the roadmap declares one. */
|
|
323
|
+
function findSurfaceMilestone(milestones, project) {
|
|
324
|
+
return (milestones.find((m) => m.project === project && m.milestoneId.endsWith(MCP_MILESTONE_SUFFIX)) ??
|
|
325
|
+
null);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The roadmap half of the standard — docs/standards/mcp.md Enforcement step 2,
|
|
329
|
+
* "the milestone is mandatory and closes before the project's final milestone".
|
|
330
|
+
* checkManifest can only judge a manifest that exists; a project that never
|
|
331
|
+
* registered the milestone has nothing to judge, which is the failure mode
|
|
332
|
+
* these two rules cover.
|
|
333
|
+
*/
|
|
334
|
+
export function checkRoadmapMcpMilestone(project, milestones) {
|
|
335
|
+
const surface = findSurfaceMilestone(milestones, project);
|
|
336
|
+
if (!surface) {
|
|
337
|
+
return [
|
|
338
|
+
{
|
|
339
|
+
rule: 'MCP-M1',
|
|
340
|
+
path: `/milestones/${project}`,
|
|
341
|
+
message: `Project "${project}" has no \`${project}-${MCP_MILESTONE_SUFFIX}\` milestone. Run \`smith mcp init ${project}\` to register it.`,
|
|
342
|
+
},
|
|
343
|
+
];
|
|
344
|
+
}
|
|
345
|
+
if (surface.status === 'completed')
|
|
346
|
+
return [];
|
|
347
|
+
// Only a violation once the rest of the project is done: while other work is
|
|
348
|
+
// still open the surface is legitimately pending, and firing here would make
|
|
349
|
+
// MCP-M2 a permanent red for every project from its first day.
|
|
350
|
+
const others = milestones.filter((m) => m.project === project && m.milestoneId !== surface.milestoneId);
|
|
351
|
+
if (others.length === 0 || !others.every((m) => m.status === 'completed'))
|
|
352
|
+
return [];
|
|
353
|
+
return [
|
|
354
|
+
{
|
|
355
|
+
rule: 'MCP-M2',
|
|
356
|
+
path: `/milestones/${surface.milestoneId}`,
|
|
357
|
+
message: `Every other "${project}" milestone is completed while ${surface.milestoneId} is "${surface.status}". A project reaching its last milestone without a closed MCP surface is not finished.`,
|
|
358
|
+
},
|
|
359
|
+
];
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Every checkout of `projectName` across every root: `<root>/<p>` plus each
|
|
363
|
+
* task worktree beside it. Roots are searched in order and the result is
|
|
364
|
+
* deduplicated, so overlapping roots enlarge the search without doubling a
|
|
365
|
+
* candidate in the list an ambiguity refusal prints.
|
|
366
|
+
*/
|
|
367
|
+
function projectCheckouts(projectName, roots) {
|
|
368
|
+
const found = [];
|
|
369
|
+
const seen = new Set();
|
|
370
|
+
const take = (dir) => {
|
|
371
|
+
if (seen.has(dir))
|
|
372
|
+
return;
|
|
373
|
+
seen.add(dir);
|
|
374
|
+
found.push(dir);
|
|
375
|
+
};
|
|
376
|
+
for (const root of roots) {
|
|
377
|
+
const main = path.join(root, projectName);
|
|
378
|
+
if (existsSync(main))
|
|
379
|
+
take(main);
|
|
380
|
+
const worktreeRoot = path.join(root, '.wt', projectName);
|
|
381
|
+
if (!existsSync(worktreeRoot))
|
|
382
|
+
continue;
|
|
383
|
+
for (const entry of readdirSync(worktreeRoot, { withFileTypes: true })
|
|
384
|
+
.filter((e) => e.isDirectory())
|
|
385
|
+
.map((e) => path.join(worktreeRoot, e.name))
|
|
386
|
+
.sort()) {
|
|
387
|
+
take(entry);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return found;
|
|
391
|
+
}
|
|
392
|
+
/** True when `cwd` is `dir` or lives under it — path-segment-wise, not by prefix. */
|
|
393
|
+
function isWithin(dir, cwd) {
|
|
394
|
+
const rel = path.relative(dir, cwd);
|
|
395
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* D-133. Which checkout `smith mcp check <project>` is about.
|
|
399
|
+
*
|
|
400
|
+
* The old answer was `<root>/<project>`, unconditionally. During an epic that
|
|
401
|
+
* is a *different valid checkout* from the one the caller is standing in —
|
|
402
|
+
* task work happens in `<root>/.wt/<project>/<task-id>` — so the command
|
|
403
|
+
* parsed a manifest the task had never touched and returned `ok: true`,
|
|
404
|
+
* `violations: []`, exit 0, both before and after the very edit the check
|
|
405
|
+
* existed to grade. A well-formed wrong answer is worse than a refusal, because
|
|
406
|
+
* nothing about it invites a second look.
|
|
407
|
+
*
|
|
408
|
+
* So: an explicit `--target-dir` wins; otherwise the caller's own checkout
|
|
409
|
+
* answers it; otherwise, if more than one checkout exists, refuse and name them
|
|
410
|
+
* all. The default root is only assumed when nothing else is a candidate — and
|
|
411
|
+
* "more than one" now spans every root, so a project that exists both beside
|
|
412
|
+
* this clone and under the legacy `workspaces/` is a refusal, not a coin flip.
|
|
413
|
+
*/
|
|
414
|
+
export function resolveMcpTarget(opts) {
|
|
415
|
+
if (opts.targetDir)
|
|
416
|
+
return { targetDir: path.resolve(opts.targetDir), source: 'flag' };
|
|
417
|
+
const roots = opts.projectRoots ?? [PROJECTS_DIR, WORKSPACES_DIR];
|
|
418
|
+
const cwd = path.resolve(opts.cwd ?? process.cwd());
|
|
419
|
+
const checkouts = projectCheckouts(opts.projectName, roots);
|
|
420
|
+
// Deepest match wins: nothing nests today, but a checkout inside a checkout
|
|
421
|
+
// should resolve to the inner one rather than to whichever came first.
|
|
422
|
+
const containing = checkouts
|
|
423
|
+
.filter((dir) => isWithin(dir, cwd))
|
|
424
|
+
.sort((a, b) => b.length - a.length);
|
|
425
|
+
if (containing[0])
|
|
426
|
+
return { targetDir: containing[0], source: 'cwd' };
|
|
427
|
+
if (checkouts.length > 1) {
|
|
428
|
+
throw new McpError('mcp.ambiguous-target', `"${opts.projectName}" has ${checkouts.length} checkouts and ${cwd} is not inside any of them, so there is nothing to infer the target from. Pass --target-dir with one of:\n${checkouts
|
|
429
|
+
.map((dir) => ` ${dir}`)
|
|
430
|
+
.join('\n')}`, { projectName: opts.projectName, cwd, candidates: checkouts });
|
|
431
|
+
}
|
|
432
|
+
// Exactly one checkout: that one, wherever it lives. Constructing
|
|
433
|
+
// `<first root>/<project>` instead would have named a directory that does not
|
|
434
|
+
// exist while a real one sat under the second root — the single-root code
|
|
435
|
+
// this replaced got away with it only because the two were always the same
|
|
436
|
+
// path. Nothing at all: the root a new project would land in, so the caller
|
|
437
|
+
// is told where the surface is expected rather than where it is missing from.
|
|
438
|
+
return {
|
|
439
|
+
targetDir: checkouts[0] ?? path.join(roots[0] ?? PROJECTS_DIR, opts.projectName),
|
|
440
|
+
source: 'default',
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* `smith mcp check` — the manifest verdict and the roadmap verdict in one
|
|
445
|
+
* report. Refuses (rather than reporting a red) when there is no manifest at
|
|
446
|
+
* all: "your surface is broken" and "you have no surface" are different
|
|
447
|
+
* answers, and collapsing them would let `smith mcp init` be skipped silently.
|
|
448
|
+
*/
|
|
449
|
+
export function runMcpCheck(opts) {
|
|
450
|
+
validateProjectName(opts.projectName);
|
|
451
|
+
const { targetDir, source } = resolveMcpTarget(opts);
|
|
452
|
+
const manifestPath = path.join(targetDir, MANIFEST_NAME);
|
|
453
|
+
const roadmapPath = opts.roadmapPath ?? ROADMAP_PATH;
|
|
454
|
+
if (!existsSync(manifestPath)) {
|
|
455
|
+
throw new McpError('mcp.no-surface', `${manifestPath} does not exist. Run \`smith mcp init ${opts.projectName}\` before checking the surface.`, { manifestPath, projectName: opts.projectName });
|
|
456
|
+
}
|
|
457
|
+
let manifest;
|
|
458
|
+
try {
|
|
459
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
460
|
+
}
|
|
461
|
+
catch (cause) {
|
|
462
|
+
throw new McpError('mcp.unreadable-manifest', `${manifestPath} is not valid JSON: ${cause.message}`, { manifestPath, projectName: opts.projectName });
|
|
463
|
+
}
|
|
464
|
+
const violations = [
|
|
465
|
+
...checkManifest(manifest).violations,
|
|
466
|
+
...checkRoadmapMcpMilestone(opts.projectName, loadRoadmap(roadmapPath)),
|
|
467
|
+
];
|
|
468
|
+
return {
|
|
469
|
+
ok: violations.length === 0,
|
|
470
|
+
violations,
|
|
471
|
+
projectName: opts.projectName,
|
|
472
|
+
targetDir,
|
|
473
|
+
targetSource: source,
|
|
474
|
+
manifestPath,
|
|
475
|
+
roadmapPath,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
/** The explicit "this epic owes no surface" value. Named so a skip is legible in a diff. */
|
|
479
|
+
export const MCP_SURFACE_NOT_REQUIRED = Object.freeze({
|
|
480
|
+
required: false,
|
|
481
|
+
milestoneId: null,
|
|
482
|
+
manifestPath: null,
|
|
483
|
+
check: null,
|
|
484
|
+
problem: null,
|
|
485
|
+
});
|
|
486
|
+
/**
|
|
487
|
+
* Reads the roadmap to decide whether this epic is under the mcp surface
|
|
488
|
+
* milestone, and if so renders checkManifest over the project's real manifest.
|
|
489
|
+
* A missing or unparseable manifest resolves to `check: null` rather than
|
|
490
|
+
* throwing — mcpBlockers turns that into a refusal, so the gate reports the
|
|
491
|
+
* problem instead of crashing the verdict that was meant to report it.
|
|
492
|
+
*
|
|
493
|
+
* `check` is checkManifest alone — deliberately not the roadmap rules that
|
|
494
|
+
* `smith mcp check` also runs. Both M rules are meaningless or harmful here:
|
|
495
|
+
*
|
|
496
|
+
* - MCP-M1 ("no `<project>-mcp-surface` milestone") cannot fire. Reaching this
|
|
497
|
+
* line at all means a surface milestone was found and claims this epic.
|
|
498
|
+
* - MCP-M2 fires when the surface milestone is not yet `completed` while every
|
|
499
|
+
* other milestone is. Nothing in this codebase ever writes a milestone
|
|
500
|
+
* status — registerMcpMilestone seeds `planned`, epic.ts never touches the
|
|
501
|
+
* roadmap, and only a human edit moves it to `completed`. So blocking the
|
|
502
|
+
* close on MCP-M2 would require the operator to mark the surface done before
|
|
503
|
+
* the gate would let them close the epic that makes it done, leaving the gate
|
|
504
|
+
* to rubber-stamp an assertion the operator had already been forced to write.
|
|
505
|
+
* That is the circularity D-108 described; it is unreal today only because
|
|
506
|
+
* the manifest rules are the ones wired here.
|
|
507
|
+
*
|
|
508
|
+
* MCP-M2's honest host is the *final* milestone's close — the standard's
|
|
509
|
+
* "closes before the project's final milestone" — not this epic's. Wiring it
|
|
510
|
+
* here is the tempting fix and it is the wrong one.
|
|
511
|
+
*/
|
|
512
|
+
export function resolveMcpSurface(opts) {
|
|
513
|
+
const milestone = loadRoadmap(opts.roadmapPath).find((m) => m.milestoneId.endsWith(MCP_MILESTONE_SUFFIX) && ownsEpic(m, opts.epicId));
|
|
514
|
+
if (!milestone)
|
|
515
|
+
return MCP_SURFACE_NOT_REQUIRED;
|
|
516
|
+
const manifestPath = path.join(opts.projectDir, MANIFEST_NAME);
|
|
517
|
+
let check = null;
|
|
518
|
+
let problem = 'missing';
|
|
519
|
+
if (existsSync(manifestPath)) {
|
|
520
|
+
problem = 'unreadable';
|
|
521
|
+
try {
|
|
522
|
+
check = checkManifest(JSON.parse(readFileSync(manifestPath, 'utf8')));
|
|
523
|
+
problem = null;
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
// The reason stays here on purpose (see McpManifestProblem). runMcpCheck
|
|
527
|
+
// is the caller that renders it, and it re-reads the file to do so.
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return { required: true, milestoneId: milestone.milestoneId, manifestPath, check, problem };
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* The hard gate, expressed as blockers rather than its own refusal path
|
|
534
|
+
* (operator decision of 2026-08-07). summarizeEpic folds these into
|
|
535
|
+
* EpicSummary.blockers, which runEpicVerdict already turns into
|
|
536
|
+
* hold/mechanical-blockers and closeEpic already refuses without
|
|
537
|
+
* --override-rationale — so the override records the overridden MCP rules by
|
|
538
|
+
* name, and there is exactly one override policy in the codebase.
|
|
539
|
+
*/
|
|
540
|
+
export function mcpBlockers(epicId, status) {
|
|
541
|
+
if (!status.required)
|
|
542
|
+
return [];
|
|
543
|
+
const milestone = status.milestoneId ?? MCP_MILESTONE_SUFFIX;
|
|
544
|
+
const manifest = status.manifestPath ?? MANIFEST_NAME;
|
|
545
|
+
// Fail closed on `check === null` exactly as before; `problem` only chooses
|
|
546
|
+
// the wording, so a status that somehow carries neither still blocks.
|
|
547
|
+
if (!status.check) {
|
|
548
|
+
return [
|
|
549
|
+
status.problem === 'unreadable'
|
|
550
|
+
? `epic ${epicId} is under milestone ${milestone} but its MCP surface at ${manifest} exists and could not be read, so no verdict could be rendered — fix the manifest in place and run \`smith mcp check\` for the parser's own message. Do not re-run \`smith mcp init\`: it refuses over an existing manifest, because re-scaffolding would discard the declared tools and their operator sign-off.`
|
|
551
|
+
: `epic ${epicId} is under milestone ${milestone} but has no MCP surface at ${manifest} — run \`smith mcp init\` and declare it.`,
|
|
552
|
+
];
|
|
553
|
+
}
|
|
554
|
+
return status.check.violations.map((v) => `mcp surface ${milestone}: ${v.rule} at ${v.path} — ${v.message}`);
|
|
555
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
// factory/orchestrator/src/paths.ts -> repo root is three levels up.
|
|
4
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
export const REPO_ROOT = path.resolve(here, '..', '..', '..');
|
|
6
|
+
/**
|
|
7
|
+
* The repo's own `.env`, read at CLI start by `loadDotEnv`. Gitignored, and
|
|
8
|
+
* the one place `docs/runbooks/providers.md` tells an operator to put a
|
|
9
|
+
* provider key. Anchored on this module, not on cwd: an agent runs `smith`
|
|
10
|
+
* from a worktree, and the key belongs to the clone.
|
|
11
|
+
*/
|
|
12
|
+
export const DOTENV_PATH = path.join(REPO_ROOT, '.env');
|
|
13
|
+
export const TAXONOMY_PATH = path.join(REPO_ROOT, 'factory', 'policies', 'taxonomy.yml');
|
|
14
|
+
export const SCHEMA_DIR = path.join(REPO_ROOT, 'factory', 'specs', 'schema');
|
|
15
|
+
export const SPECS_ACTIVE_DIR = path.join(REPO_ROOT, 'factory', 'specs', 'active');
|
|
16
|
+
export const STATE_EVENTS_DIR = path.join(REPO_ROOT, 'state', 'events');
|
|
17
|
+
/** Where a task's declared artifacts must live: `state/artifacts/<task-id>/` (P9-22). */
|
|
18
|
+
export const STATE_ARTIFACTS_DIR = path.join(REPO_ROOT, 'state', 'artifacts');
|
|
19
|
+
export const WORKTREE_POLICY_PATH = path.join(REPO_ROOT, 'factory', 'policies', 'worktree.yml');
|
|
20
|
+
export const STATE_DB_PATH = path.join(REPO_ROOT, 'state', 'smith.db');
|
|
21
|
+
/** The background watcher's lock and last tick: `state/daemon/{daemon.pid,status.json}`. */
|
|
22
|
+
export const STATE_DAEMON_DIR = path.join(REPO_ROOT, 'state', 'daemon');
|
|
23
|
+
export const DB_MIGRATIONS_DIR = path.join(REPO_ROOT, 'factory', 'orchestrator', 'drizzle');
|
|
24
|
+
export const ROADMAP_PATH = path.join(REPO_ROOT, 'factory', 'specs', 'roadmap.md');
|
|
25
|
+
export const SCAFFOLD_DIR = path.join(REPO_ROOT, 'factory', 'scaffold');
|
|
26
|
+
/**
|
|
27
|
+
* `workspaces/` inside this clone. Still a legal place to keep a project, and
|
|
28
|
+
* still searched for ones already there -- no longer where a new one goes.
|
|
29
|
+
*/
|
|
30
|
+
export const WORKSPACES_DIR = path.join(REPO_ROOT, 'workspaces');
|
|
31
|
+
/**
|
|
32
|
+
* Where a project lands when nothing says otherwise: beside this clone.
|
|
33
|
+
*
|
|
34
|
+
* A project this factory builds is not a part of it -- it takes no dependency
|
|
35
|
+
* on it and carries no mark of it -- and a project scaffolded under REPO_ROOT
|
|
36
|
+
* contradicted that from the first commit: inside this clone's git tree, its
|
|
37
|
+
* ignore rules and its lint roots, and read as factory code by every tool that
|
|
38
|
+
* walks up from a file inside it. Beside, not within. A task's worktrees follow
|
|
39
|
+
* it there on their own, since worktree.ts places them next to the project it
|
|
40
|
+
* is handed rather than at a path of its own choosing.
|
|
41
|
+
*/
|
|
42
|
+
export const PROJECTS_DIR = path.dirname(REPO_ROOT);
|
|
43
|
+
export const SCHEDULER_POLICY_PATH = path.join(REPO_ROOT, 'factory', 'policies', 'scheduler.yml');
|
|
44
|
+
export const LESSONS_MD_PATH = path.join(REPO_ROOT, 'factory', 'policies', 'lessons.md');
|
|
45
|
+
/** The shipped role templates — read at dispatch for their `<!-- LESSONS:<scope> -->` markers (P9-2). */
|
|
46
|
+
export const AGENTS_DIR = path.join(REPO_ROOT, '.claude', 'agents');
|
|
47
|
+
export const CROSSCHECK_POLICY_PATH = path.join(REPO_ROOT, 'factory', 'policies', 'crosscheck.yml');
|
|
48
|
+
/** Who may hold `Agent` — read by `smith delegation check` (delegation.ts, D13 step 3). */
|
|
49
|
+
export const DELEGATION_POLICY_PATH = path.join(REPO_ROOT, 'factory', 'policies', 'delegation.yml');
|
|
50
|
+
export const BUDGETS_POLICY_PATH = path.join(REPO_ROOT, 'factory', 'policies', 'budgets.yml');
|
|
51
|
+
/** How much judgment an epic buys — read by `smith effort show` (effort.ts). */
|
|
52
|
+
export const EFFORT_POLICY_PATH = path.join(REPO_ROOT, 'factory', 'policies', 'effort.yml');
|
|
53
|
+
/** The security-reviewer's dispatch triggers — read by `smith security triggers` (P9-4). */
|
|
54
|
+
export const SENSITIVE_PATHS_POLICY_PATH = path.join(REPO_ROOT, 'factory', 'policies', 'sensitive-paths.yml');
|
|
55
|
+
/** The guard hook's rule data — read by `smith policy check`/`smith policy hook` (policy.ts). */
|
|
56
|
+
export const GUARDRAILS_POLICY_PATH = path.join(REPO_ROOT, 'factory', 'policies', 'guardrails.yml');
|
|
57
|
+
/**
|
|
58
|
+
* The S1-S4 ladder and which levels block a merge (severity.ts).
|
|
59
|
+
*
|
|
60
|
+
* Declared here like every other policy file rather than inside severity.ts,
|
|
61
|
+
* which held its own `${REPO_ROOT}/factory/policies/severity.yml` — a second
|
|
62
|
+
* spelling of a path this module exists to spell once, and the only one built
|
|
63
|
+
* by interpolation instead of path.join.
|
|
64
|
+
*/
|
|
65
|
+
export const SEVERITY_POLICY_PATH = path.join(REPO_ROOT, 'factory', 'policies', 'severity.yml');
|
|
66
|
+
/**
|
|
67
|
+
* The operator's stack answers, recorded at install time (stack.ts).
|
|
68
|
+
*
|
|
69
|
+
* A declaration, not state: it is what this operator said they build with,
|
|
70
|
+
* and the scaffolder reads it instead of the prose in docs/standards/stack.md
|
|
71
|
+
* — which described one operator's stack as if it were everyone's.
|
|
72
|
+
*/
|
|
73
|
+
export const STACK_POLICY_PATH = path.join(REPO_ROOT, 'factory', 'policies', 'stack.yml');
|
|
74
|
+
/**
|
|
75
|
+
* Open judge sandbox leases, one file per worktree (sandbox.ts).
|
|
76
|
+
*
|
|
77
|
+
* Under `state/` because a lease is state, not a declaration — and in the
|
|
78
|
+
* main clone rather than the judge's own worktree, which is the point: a
|
|
79
|
+
* judge cannot revoke its own guard by deleting a file it is not allowed to
|
|
80
|
+
* reach.
|
|
81
|
+
*/
|
|
82
|
+
export const SANDBOX_LEASE_DIR = path.join(REPO_ROOT, 'state', 'sandboxes');
|