@theagilemonkeys/facility 0.3.1
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/LICENSE +201 -0
- package/README.md +68 -0
- package/bin/facility.mjs +10 -0
- package/modules/README.md +35 -0
- package/modules/ai-queryability/agents/queryability-reviewer.md +35 -0
- package/modules/ai-queryability/module.json +9 -0
- package/modules/ai-queryability/standard-section.md +22 -0
- package/modules/analytics/agents/analytics-reviewer.md +32 -0
- package/modules/analytics/commands/add-telemetry.md +23 -0
- package/modules/analytics/module.json +10 -0
- package/modules/analytics/standard-section.md +23 -0
- package/modules/database/agents/data-security-reviewer.md +38 -0
- package/modules/database/commands/new-migration.md +24 -0
- package/modules/database/guards/migration-versions.mjs +41 -0
- package/modules/database/guards/migrations-immutable.mjs +57 -0
- package/modules/database/hooks/protect-migrations.fragment.mjs +10 -0
- package/modules/database/module.json +25 -0
- package/modules/database/standard-section.md +20 -0
- package/modules/design-system/agents/design-reviewer.md +37 -0
- package/modules/design-system/module.json +9 -0
- package/modules/design-system/standard-section.md +15 -0
- package/package.json +42 -0
- package/src/add.mjs +77 -0
- package/src/cli.mjs +352 -0
- package/src/detect.mjs +127 -0
- package/src/doctor.mjs +582 -0
- package/src/init.mjs +572 -0
- package/src/instance.mjs +114 -0
- package/src/platform-admin.mjs +1542 -0
- package/src/platform-config.mjs +39 -0
- package/src/platform.mjs +1759 -0
- package/src/prompts.mjs +64 -0
- package/src/render.mjs +66 -0
- package/src/ui.mjs +30 -0
- package/templates/claude/agents/security-reviewer.md +41 -0
- package/templates/claude/agents/standards-reviewer.md +31 -0
- package/templates/claude/commands/open-pr.md +21 -0
- package/templates/claude/commands/verify.md +16 -0
- package/templates/claude/hooks/protect-branch.mjs +58 -0
- package/templates/claude/hooks/protect-files.mjs +35 -0
- package/templates/claude/settings.json +71 -0
- package/templates/claude/skills/maintainable-software/SKILL.md +67 -0
- package/templates/claude/skills/reviewing-to-standard/SKILL.md +49 -0
- package/templates/claude/skills/working-to-standard/SKILL.md +45 -0
- package/templates/delivery/verify.mjs +157 -0
- package/templates/doctor/resolve.mjs +144 -0
- package/templates/guards/README.md +30 -0
- package/templates/guards/_kit.mjs +81 -0
- package/templates/guards/actions-pinned.mjs +38 -0
- package/templates/guards/run.mjs +111 -0
- package/templates/guards/watchtower-locked.mjs +66 -0
- package/templates/prompts/address-review.md +14 -0
- package/templates/prompts/architect.md +62 -0
- package/templates/prompts/builder.md +71 -0
- package/templates/prompts/doctor.md +64 -0
- package/templates/prompts/review.md +14 -0
- package/templates/prompts/sweep.md +75 -0
- package/templates/receipts/collect.mjs +289 -0
- package/templates/review/finalize.mjs +38 -0
- package/templates/scripts/move-board-status.sh +155 -0
- package/templates/security/sync-findings.mjs +226 -0
- package/templates/standard/STANDARD.md +141 -0
- package/templates/standard/agents-block.md +25 -0
- package/templates/watchtower/budgets.json +12 -0
- package/templates/watchtower/canary.mjs +216 -0
- package/templates/watchtower/health.mjs +148 -0
- package/templates/watchtower/outcomes.mjs +188 -0
- package/templates/workflows/facility-address-review.yml +153 -0
- package/templates/workflows/facility-canary.yml +61 -0
- package/templates/workflows/facility-codex.yml +326 -0
- package/templates/workflows/facility-crew.yml +350 -0
- package/templates/workflows/facility-doctor.yml +155 -0
- package/templates/workflows/facility-review.yml +134 -0
- package/templates/workflows/facility-security-sweep.yml +204 -0
- package/templates/workflows/facility-watchtower.yml +87 -0
package/src/init.mjs
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
// `facility init` — install the method into a repository.
|
|
2
|
+
//
|
|
3
|
+
// Detect the stack, ask six short questions, write the files, print the
|
|
4
|
+
// manual steps that only a human can do. Every generated file belongs to the
|
|
5
|
+
// target repo afterwards; facility is the installer, not a runtime dependency.
|
|
6
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { detect } from "./detect.mjs";
|
|
11
|
+
import { render, hasManagedBlock, appendManagedBlock } from "./render.mjs";
|
|
12
|
+
import { ask, confirm, closePrompts } from "./prompts.mjs";
|
|
13
|
+
import { addModule } from "./add.mjs";
|
|
14
|
+
import { accent, banner, bold, dim, heading, item, ok, skip, warn } from "./ui.mjs";
|
|
15
|
+
|
|
16
|
+
const CHECKOUT_SHA = "34e114876b0b11c390a56381ad16ebd13914f8d5"; // actions/checkout v4
|
|
17
|
+
const SETUP_NODE_SHA = "49933ea5288caeca8642d1e84afbd3f7d6820020"; // actions/setup-node v4
|
|
18
|
+
const PNPM_SHA = "b906affcce14559ad1aafd4ab0e942779e9f58b1"; // pnpm/action-setup v4
|
|
19
|
+
const AWS_AUTH_SHA = "517a711dbcd0e402f90c77e7e2f81e849156e31d"; // aws-actions/configure-aws-credentials v6.2.2
|
|
20
|
+
const GOOGLE_AUTH_SHA = "7c6bc770dae815cd3e89ee6cdf493a5fab2cc093"; // google-github-actions/auth v3
|
|
21
|
+
const AUTH_MODES = new Set(["api-key", "oauth", "wif", "bedrock", "vertex"]);
|
|
22
|
+
|
|
23
|
+
function toolchainSteps(packageManager, { conditional = false } = {}) {
|
|
24
|
+
const guard = conditional ? "\n if: steps.workflow-change.outputs.changed != 'true'" : "";
|
|
25
|
+
if (packageManager === "pnpm") {
|
|
26
|
+
return [
|
|
27
|
+
"",
|
|
28
|
+
` - uses: pnpm/action-setup@${PNPM_SHA} # v4${guard}`,
|
|
29
|
+
"",
|
|
30
|
+
` - uses: actions/setup-node@${SETUP_NODE_SHA} # v4${guard}`,
|
|
31
|
+
" with:",
|
|
32
|
+
" node-version: 22",
|
|
33
|
+
" cache: pnpm",
|
|
34
|
+
"",
|
|
35
|
+
` - run: pnpm install --frozen-lockfile${guard}`,
|
|
36
|
+
"",
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
|
39
|
+
if (packageManager === "yarn" || packageManager === "npm") {
|
|
40
|
+
const installCmd = packageManager === "yarn" ? "yarn install --immutable" : "npm ci";
|
|
41
|
+
return [
|
|
42
|
+
"",
|
|
43
|
+
` - uses: actions/setup-node@${SETUP_NODE_SHA} # v4${guard}`,
|
|
44
|
+
" with:",
|
|
45
|
+
" node-version: 22",
|
|
46
|
+
` cache: ${packageManager}`,
|
|
47
|
+
"",
|
|
48
|
+
` - run: ${installCmd}${guard}`,
|
|
49
|
+
"",
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
return [
|
|
53
|
+
"",
|
|
54
|
+
" # facility: no Node toolchain detected. Add your language setup steps",
|
|
55
|
+
" # here (compilers, package managers) so the crew can build and test.",
|
|
56
|
+
"",
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function boardStep(org, projectNumber) {
|
|
61
|
+
if (!projectNumber) return "";
|
|
62
|
+
return [
|
|
63
|
+
"",
|
|
64
|
+
" # Reflect the invoked agent on the org Project board:",
|
|
65
|
+
" # /architect -> Planning, /builder -> In Progress (accepting the plan).",
|
|
66
|
+
" # Forward-only; no-ops if PROJECTS_PAT is unset (the default",
|
|
67
|
+
" # GITHUB_TOKEN cannot write org Projects v2).",
|
|
68
|
+
" - name: Move Project board status",
|
|
69
|
+
" if: >",
|
|
70
|
+
" steps.requested-agent.outputs.run == 'true' &&",
|
|
71
|
+
" (github.event_name == 'issues' ||",
|
|
72
|
+
" (github.event_name == 'issue_comment' && github.event.issue.pull_request == null))",
|
|
73
|
+
" env:",
|
|
74
|
+
" GH_TOKEN: ${{ secrets.PROJECTS_PAT }}",
|
|
75
|
+
" MODE: ${{ steps.requested-agent.outputs.mode }}",
|
|
76
|
+
" ISSUE_NODE_ID: ${{ github.event.issue.node_id }}",
|
|
77
|
+
` ORG: '${org}'`,
|
|
78
|
+
` PROJECT_NUMBER: '${projectNumber}'`,
|
|
79
|
+
" run: bash .github/facility/move-board-status.sh",
|
|
80
|
+
"",
|
|
81
|
+
].join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function boardReviewStep(org, projectNumber) {
|
|
85
|
+
if (!projectNumber) return "";
|
|
86
|
+
return [
|
|
87
|
+
"",
|
|
88
|
+
" - name: Move delivered work to In Review",
|
|
89
|
+
" if: steps.delivery.outcome == 'success' && github.event.issue.pull_request == null",
|
|
90
|
+
" env:",
|
|
91
|
+
" GH_TOKEN: ${{ secrets.PROJECTS_PAT }}",
|
|
92
|
+
" MODE: review",
|
|
93
|
+
" ISSUE_NODE_ID: ${{ github.event.issue.node_id }}",
|
|
94
|
+
` ORG: '${org}'`,
|
|
95
|
+
` PROJECT_NUMBER: '${projectNumber}'`,
|
|
96
|
+
" run: bash .github/facility/move-board-status.sh",
|
|
97
|
+
"",
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function doctorWatch(workflowNames) {
|
|
102
|
+
const watched = [...new Set([...workflowNames, "facility-review"])];
|
|
103
|
+
const lines = watched.map((name) => ` - ${name}`);
|
|
104
|
+
if (workflowNames.length === 0) {
|
|
105
|
+
lines.unshift(" # facility: no existing check workflows detected at init time —");
|
|
106
|
+
lines.unshift(" # add your CI workflow names here so the doctor watches them.");
|
|
107
|
+
}
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function checksAllowJson(checks) {
|
|
112
|
+
if (!checks.length) return "";
|
|
113
|
+
return checks.map((c) => ` "Bash(${c})",`).join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function checksList(checks) {
|
|
117
|
+
const lines = checks.length
|
|
118
|
+
? checks.map((c) => `- \`${c}\``)
|
|
119
|
+
: ["- _No check commands configured yet — add your typecheck/lint/test/build commands here._"];
|
|
120
|
+
lines.push("- `node guards/run.mjs` — deterministic repo invariants. Always cheap; always run.");
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push(
|
|
123
|
+
"Escalate beyond this list when the change touches data, auth, or critical user flows — and say which extra checks ran."
|
|
124
|
+
);
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function checksRun(checks) {
|
|
129
|
+
if (checks.length) return checks.map((command) => ` ${command}`).join("\n");
|
|
130
|
+
return [
|
|
131
|
+
' echo "::error::No verification commands are configured in .facility.json."',
|
|
132
|
+
" exit 1",
|
|
133
|
+
].join("\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function previewStep(preview) {
|
|
137
|
+
if (!preview?.enabled) return "";
|
|
138
|
+
return [
|
|
139
|
+
"",
|
|
140
|
+
" - name: Request Facility SSO-protected preview",
|
|
141
|
+
" if: steps.delivery.outcome == 'success'",
|
|
142
|
+
" shell: bash",
|
|
143
|
+
" env:",
|
|
144
|
+
" FACILITY_API_URL: ${{ secrets.FACILITY_API_URL }}",
|
|
145
|
+
" FACILITY_PROJECT_ID: ${{ secrets.FACILITY_PROJECT_ID }}",
|
|
146
|
+
" FACILITY_PREVIEW_KEY: ${{ secrets.FACILITY_PREVIEW_KEY }}",
|
|
147
|
+
` PREVIEW_IMAGE: ${JSON.stringify(preview.image)}`,
|
|
148
|
+
` PREVIEW_COMMAND_JSON: ${JSON.stringify(JSON.stringify(preview.command ?? []))}`,
|
|
149
|
+
` PREVIEW_PORT: ${JSON.stringify(String(preview.port))}`,
|
|
150
|
+
` PREVIEW_READINESS_PATH: ${JSON.stringify(preview.readinessPath ?? "")}`,
|
|
151
|
+
` PREVIEW_TTL_HOURS: ${JSON.stringify(String(preview.ttlHours ?? 24))}`,
|
|
152
|
+
" run: |",
|
|
153
|
+
" set -euo pipefail",
|
|
154
|
+
' test -n "$FACILITY_API_URL" && test -n "$FACILITY_PROJECT_ID" && test -n "$FACILITY_PREVIEW_KEY"',
|
|
155
|
+
" payload=\"$(jq -nc --arg image \"$PREVIEW_IMAGE\" --argjson command \"$PREVIEW_COMMAND_JSON\" --argjson port \"$PREVIEW_PORT\" --arg readiness \"$PREVIEW_READINESS_PATH\" --argjson ttl \"$PREVIEW_TTL_HOURS\" --argjson pr '${{ steps.delivery.outputs.pr_number }}' --arg commit '${{ steps.delivery.outputs.head_sha }}' '{image:$image,command:$command,port:$port,ttlHours:$ttl,prNumber:$pr,commitSha:$commit} + if $readiness == \"\" then {} else {readinessPath:$readiness} end')\"",
|
|
156
|
+
' curl --fail-with-body --silent --show-error -X POST "$FACILITY_API_URL/v1/projects/$FACILITY_PROJECT_ID/previews" -H "authorization: Bearer $FACILITY_PREVIEW_KEY" -H "content-type: application/json" -H "idempotency-key: preview-${{ github.run_id }}-${{ github.run_attempt }}" --data "$payload"',
|
|
157
|
+
"",
|
|
158
|
+
].join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function anthropicAuthSetup(mode, condition = "") {
|
|
162
|
+
const conditional = condition ? [` if: ${condition}`] : [];
|
|
163
|
+
if (mode === "oauth") {
|
|
164
|
+
return [
|
|
165
|
+
" - name: Prepare Claude Code OAuth token",
|
|
166
|
+
" id: claude-auth",
|
|
167
|
+
...conditional,
|
|
168
|
+
" shell: bash",
|
|
169
|
+
" env:",
|
|
170
|
+
" RAW_CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}",
|
|
171
|
+
" run: |",
|
|
172
|
+
' token="$RAW_CLAUDE_CODE_OAUTH_TOKEN"',
|
|
173
|
+
" token=\"${token//$'\\r'/}\"",
|
|
174
|
+
" token=\"${token//$'\\n'/}\"",
|
|
175
|
+
' token="${token#"${token%%[![:space:]]*}"}"',
|
|
176
|
+
' token="${token%"${token##*[![:space:]]}"}"',
|
|
177
|
+
" token=\"${token#export CLAUDE_CODE_OAUTH_TOKEN=}\"",
|
|
178
|
+
" token=\"${token#CLAUDE_CODE_OAUTH_TOKEN=}\"",
|
|
179
|
+
' token="${token#\\\"}"',
|
|
180
|
+
' token="${token%\\\"}"',
|
|
181
|
+
" token=\"${token#\\'}\"",
|
|
182
|
+
" token=\"${token%\\'}\"",
|
|
183
|
+
"",
|
|
184
|
+
' if [ -z "$token" ]; then',
|
|
185
|
+
' echo "::error::CLAUDE_CODE_OAUTH_TOKEN is empty. Generate it with \'claude setup-token\' and store it as a repository or organization secret."',
|
|
186
|
+
" exit 1",
|
|
187
|
+
" fi",
|
|
188
|
+
` if printf '%s' "$token" | grep -q '[[:cntrl:]]'; then`,
|
|
189
|
+
' echo "::error::CLAUDE_CODE_OAUTH_TOKEN contains control characters after normalization."',
|
|
190
|
+
" exit 1",
|
|
191
|
+
" fi",
|
|
192
|
+
"",
|
|
193
|
+
' echo "::add-mask::$token"',
|
|
194
|
+
' echo "token=$token" >> "$GITHUB_OUTPUT"',
|
|
195
|
+
].join("\n");
|
|
196
|
+
}
|
|
197
|
+
if (mode === "bedrock") {
|
|
198
|
+
return [
|
|
199
|
+
" - name: Authenticate to AWS for Amazon Bedrock",
|
|
200
|
+
...conditional,
|
|
201
|
+
` uses: aws-actions/configure-aws-credentials@${AWS_AUTH_SHA} # v6.2.2`,
|
|
202
|
+
" with:",
|
|
203
|
+
" role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}",
|
|
204
|
+
" aws-region: ${{ vars.AWS_REGION }}",
|
|
205
|
+
].join("\n");
|
|
206
|
+
}
|
|
207
|
+
if (mode === "vertex") {
|
|
208
|
+
return [
|
|
209
|
+
" - name: Authenticate to Google Cloud for Vertex AI",
|
|
210
|
+
...conditional,
|
|
211
|
+
` uses: google-github-actions/auth@${GOOGLE_AUTH_SHA} # v3`,
|
|
212
|
+
" with:",
|
|
213
|
+
" workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}",
|
|
214
|
+
" service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}",
|
|
215
|
+
].join("\n");
|
|
216
|
+
}
|
|
217
|
+
return "";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function anthropicAuthInputs(mode) {
|
|
221
|
+
if (mode === "api-key") {
|
|
222
|
+
return " anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}";
|
|
223
|
+
}
|
|
224
|
+
if (mode === "oauth") {
|
|
225
|
+
return " claude_code_oauth_token: ${{ steps.claude-auth.outputs.token }}";
|
|
226
|
+
}
|
|
227
|
+
if (mode === "wif") {
|
|
228
|
+
return [
|
|
229
|
+
" anthropic_federation_rule_id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}",
|
|
230
|
+
" anthropic_organization_id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}",
|
|
231
|
+
" anthropic_service_account_id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}",
|
|
232
|
+
" anthropic_workspace_id: ${{ vars.ANTHROPIC_WORKSPACE_ID }}",
|
|
233
|
+
].join("\n");
|
|
234
|
+
}
|
|
235
|
+
if (mode === "bedrock") return ' use_bedrock: "true"';
|
|
236
|
+
return ' use_vertex: "true"';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function authOnboardingStep(mode) {
|
|
240
|
+
if (mode === "api-key") {
|
|
241
|
+
return "Add a dedicated, spend-capped Anthropic key: gh secret set ANTHROPIC_API_KEY";
|
|
242
|
+
}
|
|
243
|
+
if (mode === "oauth") {
|
|
244
|
+
return "Create the agent token: claude setup-token then gh secret set CLAUDE_CODE_OAUTH_TOKEN";
|
|
245
|
+
}
|
|
246
|
+
if (mode === "wif") {
|
|
247
|
+
return "Configure Anthropic WIF and set ANTHROPIC_FEDERATION_RULE_ID + ANTHROPIC_ORGANIZATION_ID as GitHub variables (service account/workspace are optional).";
|
|
248
|
+
}
|
|
249
|
+
if (mode === "bedrock") {
|
|
250
|
+
return "Configure GitHub OIDC for AWS, then set AWS_ROLE_TO_ASSUME as a secret and AWS_REGION as a variable.";
|
|
251
|
+
}
|
|
252
|
+
return "Configure Google Workload Identity Federation, then set GCP_WORKLOAD_IDENTITY_PROVIDER + GCP_SERVICE_ACCOUNT as GitHub variables.";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function formatManifest(manifest) {
|
|
256
|
+
let text = JSON.stringify(manifest, null, 2).replace(
|
|
257
|
+
' "engines": [\n "claude-code",\n "codex"\n ],',
|
|
258
|
+
' "engines": ["claude-code", "codex"],',
|
|
259
|
+
);
|
|
260
|
+
if (manifest.preview?.command?.length) {
|
|
261
|
+
const expanded = ` "command": [\n${manifest.preview.command
|
|
262
|
+
.map((part) => ` ${JSON.stringify(part)}`)
|
|
263
|
+
.join(",\n")}\n ],`;
|
|
264
|
+
text = text.replace(
|
|
265
|
+
expanded,
|
|
266
|
+
` "command": [${manifest.preview.command.map((part) => JSON.stringify(part)).join(", ")}],`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
const compactChecks = ` \"checks\": [${manifest.checks.map((check) => JSON.stringify(check)).join(", ")}],`;
|
|
270
|
+
if (compactChecks.length > 100 || manifest.checks.length === 0) return `${text}\n`;
|
|
271
|
+
const lines = text.split("\n");
|
|
272
|
+
const start = lines.findIndex((line) => line === ' "checks": [');
|
|
273
|
+
const end = lines.findIndex((line, index) => index > start && line === " ],");
|
|
274
|
+
if (start < 0 || end < 0) return `${text}\n`;
|
|
275
|
+
lines.splice(start, end - start + 1, compactChecks);
|
|
276
|
+
return `${lines.join("\n")}\n`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function init(flags, pkgRoot, version) {
|
|
280
|
+
const dir = flags.dir || process.cwd();
|
|
281
|
+
const interactive = !flags.yes;
|
|
282
|
+
|
|
283
|
+
banner(version);
|
|
284
|
+
|
|
285
|
+
const detected = detect(dir);
|
|
286
|
+
if (!detected.isGitRepo) {
|
|
287
|
+
warn(`${dir} is not a git repository. Run \`git init\` first — facility installs GitHub workflows.`);
|
|
288
|
+
if (interactive && !(await confirm("Continue anyway?", false))) {
|
|
289
|
+
closePrompts();
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
heading("Detected");
|
|
295
|
+
item(`package manager ${bold(detected.packageManager)}`);
|
|
296
|
+
item(`default branch ${bold(detected.defaultBranch)}`);
|
|
297
|
+
item(`checks ${detected.checks.length ? bold(detected.checks.join(", ")) : dim("none found")}`);
|
|
298
|
+
item(`check workflows ${detected.workflowNames.length ? bold(detected.workflowNames.join(", ")) : dim("none — the doctor watch list starts empty")}`);
|
|
299
|
+
item(`deployments ${detected.deploymentProviders.length ? bold(detected.deploymentProviders.join(", ")) : dim("none detected")}`);
|
|
300
|
+
item(`project board ${detected.board ? bold(`${detected.board.org} #${detected.board.project}`) : dim("none detected")}`);
|
|
301
|
+
item(`protected preview ${detected.previewConfigured ? bold("configured") : dim("not configured")}`);
|
|
302
|
+
if (detected.suggestedModules.length) item(`suggested modules ${bold(detected.suggestedModules.join(", "))}`);
|
|
303
|
+
|
|
304
|
+
heading("A few questions");
|
|
305
|
+
const defaultBranch = flags.branch || (interactive ? await ask("Default branch?", detected.defaultBranch) : detected.defaultBranch);
|
|
306
|
+
const provision =
|
|
307
|
+
flags.provision ??
|
|
308
|
+
(interactive
|
|
309
|
+
? await ask("Provision command (DB, seeds, browsers — what the crew runs before working)?", detected.provision)
|
|
310
|
+
: detected.provision);
|
|
311
|
+
const checksRaw =
|
|
312
|
+
flags.checks ??
|
|
313
|
+
(interactive
|
|
314
|
+
? await ask("Check commands, comma-separated?", detected.checks.join(", "))
|
|
315
|
+
: detected.checks.join(", "));
|
|
316
|
+
const checks = checksRaw.split(",").map((c) => c.trim()).filter(Boolean);
|
|
317
|
+
// Model tiering is an opinionated default, not a question: deep reasoning
|
|
318
|
+
// where it matters, volume where it doesn't. Override with
|
|
319
|
+
// --build-model / --review-model / --plan-model.
|
|
320
|
+
const models = {
|
|
321
|
+
build: flags["build-model"] || "opusplan",
|
|
322
|
+
review: flags["review-model"] || "claude-sonnet-4-6",
|
|
323
|
+
plan: flags["plan-model"] || "claude-opus-4-8",
|
|
324
|
+
codexBuild: flags["codex-build-model"] || "gpt-5.6-sol",
|
|
325
|
+
codexPlan: flags["codex-plan-model"] || "gpt-5.6-sol",
|
|
326
|
+
};
|
|
327
|
+
item(dim(`model tiering: ${models.build} build · ${models.review} review · ${models.plan} plan/repair/sweep · ${models.codexBuild} Codex build · ${models.codexPlan} Codex plan`));
|
|
328
|
+
const anthropicAuth = String(flags.auth || "api-key").toLowerCase();
|
|
329
|
+
if (!AUTH_MODES.has(anthropicAuth)) {
|
|
330
|
+
throw new Error(`Unsupported --auth=${anthropicAuth}. Use api-key, oauth, wif, bedrock, or vertex.`);
|
|
331
|
+
}
|
|
332
|
+
item(dim(`Anthropic auth: ${anthropicAuth}`));
|
|
333
|
+
const canaryBot = flags["canary-bot"] || "facility-canary[bot]";
|
|
334
|
+
const preview = flags["preview-image"]
|
|
335
|
+
? {
|
|
336
|
+
enabled: true,
|
|
337
|
+
image: flags["preview-image"],
|
|
338
|
+
command: flags["preview-command"] ? ["sh", "-lc", flags["preview-command"]] : undefined,
|
|
339
|
+
port: Number(flags["preview-port"] || 3000),
|
|
340
|
+
readinessPath: flags["preview-readiness-path"] || undefined,
|
|
341
|
+
ttlHours: Number(flags["preview-ttl-hours"] || 24),
|
|
342
|
+
}
|
|
343
|
+
: undefined;
|
|
344
|
+
if (
|
|
345
|
+
preview &&
|
|
346
|
+
(!Number.isInteger(preview.port) ||
|
|
347
|
+
preview.port < 1 ||
|
|
348
|
+
preview.port > 65_535 ||
|
|
349
|
+
(preview.readinessPath && !preview.readinessPath.startsWith("/")) ||
|
|
350
|
+
!Number.isInteger(preview.ttlHours) ||
|
|
351
|
+
preview.ttlHours < 1 ||
|
|
352
|
+
preview.ttlHours > 168)
|
|
353
|
+
) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
"Preview port must be 1-65535, readiness path must start with /, and preview TTL must be 1-168 hours.",
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const project =
|
|
359
|
+
flags.project ??
|
|
360
|
+
(interactive
|
|
361
|
+
? await ask(
|
|
362
|
+
"Org Project number for board moves (empty to skip)?",
|
|
363
|
+
detected.board ? String(detected.board.project) : "",
|
|
364
|
+
)
|
|
365
|
+
: detected.board
|
|
366
|
+
? String(detected.board.project)
|
|
367
|
+
: "");
|
|
368
|
+
const org = project
|
|
369
|
+
? flags.org ||
|
|
370
|
+
(interactive
|
|
371
|
+
? await ask("GitHub org for the Project board?", detected.board?.org ?? detected.org)
|
|
372
|
+
: (detected.board?.org ?? detected.org))
|
|
373
|
+
: "";
|
|
374
|
+
const modulesRaw =
|
|
375
|
+
flags.modules ??
|
|
376
|
+
(interactive
|
|
377
|
+
? await ask("Modules to install now (analytics, database, ai-queryability, design-system)?", detected.suggestedModules.join(", "))
|
|
378
|
+
: detected.suggestedModules.join(", "));
|
|
379
|
+
const modules = modulesRaw.split(",").map((m) => m.trim()).filter(Boolean);
|
|
380
|
+
|
|
381
|
+
const provisionCmd =
|
|
382
|
+
provision ||
|
|
383
|
+
'echo "facility: no provision command configured — the crew runs on a bare checkout. Set one in this workflow + .facility.json."';
|
|
384
|
+
const checksInline = checks.length ? checks.join(" ; ") : "the checks configured in STANDARD.md";
|
|
385
|
+
|
|
386
|
+
// The canary hash is derived from the canonical probe body so the crew
|
|
387
|
+
// workflow, the canary script, and the watchtower-locked guard can never
|
|
388
|
+
// drift apart at generation time.
|
|
389
|
+
const { CANARY_PROBE_BODY } = await import(
|
|
390
|
+
pathToFileURL(join(pkgRoot, "templates/watchtower/canary.mjs")).href
|
|
391
|
+
);
|
|
392
|
+
const canarySha256 = createHash("sha256").update(CANARY_PROBE_BODY.replace(/\r/g, ""), "utf8").digest("hex");
|
|
393
|
+
|
|
394
|
+
const vars = {
|
|
395
|
+
FACILITY_VERSION: version,
|
|
396
|
+
DEFAULT_BRANCH: defaultBranch,
|
|
397
|
+
BUILD_MODEL: models.build,
|
|
398
|
+
REVIEW_MODEL: models.review,
|
|
399
|
+
PLAN_MODEL: models.plan,
|
|
400
|
+
CODEX_BUILD_MODEL: models.codexBuild,
|
|
401
|
+
CODEX_PLAN_MODEL: models.codexPlan,
|
|
402
|
+
CODEX_EFFORT: "xhigh",
|
|
403
|
+
CODEX_VERSION: "0.144.6",
|
|
404
|
+
ARCHITECT_REPO_LANE: "true",
|
|
405
|
+
BUILDER_REPO_LANE: "true",
|
|
406
|
+
PROVISION_CMD: provisionCmd,
|
|
407
|
+
CHECKS_INLINE: checksInline,
|
|
408
|
+
CHECKS_RUN: checksRun(checks),
|
|
409
|
+
CHECKS_LIST: checksList(checks),
|
|
410
|
+
ALLOW_CHECKS_JSON: checksAllowJson(checks),
|
|
411
|
+
TOOLCHAIN_STEPS: toolchainSteps(detected.packageManager),
|
|
412
|
+
TOOLCHAIN_STEPS_CONDITIONAL: toolchainSteps(detected.packageManager, { conditional: true }),
|
|
413
|
+
BOARD_STEP: boardStep(org, project),
|
|
414
|
+
BOARD_REVIEW_STEP: boardReviewStep(org, project),
|
|
415
|
+
CANARY_BOT: canaryBot,
|
|
416
|
+
CANARY_SHA256: canarySha256,
|
|
417
|
+
DOCTOR_WATCH: doctorWatch(detected.workflowNames),
|
|
418
|
+
ANTHROPIC_AUTH_SETUP: anthropicAuthSetup(anthropicAuth),
|
|
419
|
+
ANTHROPIC_AUTH_SETUP_CREW: anthropicAuthSetup(
|
|
420
|
+
anthropicAuth,
|
|
421
|
+
"steps.requested-agent.outputs.run == 'true'",
|
|
422
|
+
),
|
|
423
|
+
ANTHROPIC_AUTH_SETUP_CONDITIONAL: anthropicAuthSetup(
|
|
424
|
+
anthropicAuth,
|
|
425
|
+
"steps.workflow-change.outputs.changed != 'true'",
|
|
426
|
+
),
|
|
427
|
+
ANTHROPIC_AUTH_INPUTS: anthropicAuthInputs(anthropicAuth),
|
|
428
|
+
PREVIEW_STEP: previewStep(preview),
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const template = (relPath) => readFileSync(join(pkgRoot, "templates", relPath), "utf8");
|
|
432
|
+
const plan = [
|
|
433
|
+
{ to: ".github/workflows/facility-crew.yml", content: render(template("workflows/facility-crew.yml"), vars) },
|
|
434
|
+
{ to: ".github/workflows/facility-codex.yml", content: render(template("workflows/facility-codex.yml"), vars) },
|
|
435
|
+
{ to: ".github/workflows/facility-review.yml", content: render(template("workflows/facility-review.yml"), vars) },
|
|
436
|
+
{ to: ".github/workflows/facility-address-review.yml", content: render(template("workflows/facility-address-review.yml"), vars) },
|
|
437
|
+
{ to: ".github/workflows/facility-doctor.yml", content: render(template("workflows/facility-doctor.yml"), vars) },
|
|
438
|
+
{ to: ".github/workflows/facility-security-sweep.yml", content: render(template("workflows/facility-security-sweep.yml"), vars) },
|
|
439
|
+
{ to: ".github/workflows/facility-watchtower.yml", content: render(template("workflows/facility-watchtower.yml"), vars) },
|
|
440
|
+
{ to: ".github/workflows/facility-canary.yml", content: render(template("workflows/facility-canary.yml"), vars) },
|
|
441
|
+
{ to: ".github/facility/architect.md", content: render(template("prompts/architect.md"), vars) },
|
|
442
|
+
{ to: ".github/facility/builder.md", content: render(template("prompts/builder.md"), vars) },
|
|
443
|
+
{ to: ".github/facility/doctor.md", content: render(template("prompts/doctor.md"), vars) },
|
|
444
|
+
{ to: ".github/facility/sweep.md", content: render(template("prompts/sweep.md"), vars) },
|
|
445
|
+
{ to: ".github/facility/doctor/resolve.mjs", content: template("doctor/resolve.mjs") },
|
|
446
|
+
{ to: ".github/facility/delivery/verify.mjs", content: template("delivery/verify.mjs") },
|
|
447
|
+
{ to: ".github/facility/receipts/collect.mjs", content: template("receipts/collect.mjs") },
|
|
448
|
+
{ to: ".github/facility/review/finalize.mjs", content: template("review/finalize.mjs") },
|
|
449
|
+
{ to: ".github/facility/security/sync-findings.mjs", content: template("security/sync-findings.mjs") },
|
|
450
|
+
{ to: ".github/facility/watchtower/outcomes.mjs", content: template("watchtower/outcomes.mjs") },
|
|
451
|
+
{ to: ".github/facility/watchtower/health.mjs", content: template("watchtower/health.mjs") },
|
|
452
|
+
{ to: ".github/facility/watchtower/canary.mjs", content: template("watchtower/canary.mjs") },
|
|
453
|
+
{ to: ".github/facility/watchtower/budgets.json", content: template("watchtower/budgets.json") },
|
|
454
|
+
{ to: ".github/facility/move-board-status.sh", content: template("scripts/move-board-status.sh"), executable: true },
|
|
455
|
+
{ to: "STANDARD.md", content: render(template("standard/STANDARD.md"), vars) },
|
|
456
|
+
{ to: ".claude/settings.json", content: render(template("claude/settings.json"), vars) },
|
|
457
|
+
{ to: ".claude/hooks/protect-branch.mjs", content: render(template("claude/hooks/protect-branch.mjs"), vars) },
|
|
458
|
+
{ to: ".claude/hooks/protect-files.mjs", content: template("claude/hooks/protect-files.mjs") },
|
|
459
|
+
{ to: ".claude/agents/standards-reviewer.md", content: template("claude/agents/standards-reviewer.md") },
|
|
460
|
+
{ to: ".claude/agents/security-reviewer.md", content: template("claude/agents/security-reviewer.md") },
|
|
461
|
+
{ to: ".claude/skills/working-to-standard/SKILL.md", content: template("claude/skills/working-to-standard/SKILL.md") },
|
|
462
|
+
{ to: ".claude/skills/reviewing-to-standard/SKILL.md", content: template("claude/skills/reviewing-to-standard/SKILL.md") },
|
|
463
|
+
{ to: ".claude/skills/maintainable-software/SKILL.md", content: template("claude/skills/maintainable-software/SKILL.md") },
|
|
464
|
+
{ to: ".claude/commands/verify.md", content: render(template("claude/commands/verify.md"), vars) },
|
|
465
|
+
{ to: ".claude/commands/open-pr.md", content: render(template("claude/commands/open-pr.md"), vars) },
|
|
466
|
+
{ to: "guards/run.mjs", content: template("guards/run.mjs") },
|
|
467
|
+
{ to: "guards/_kit.mjs", content: template("guards/_kit.mjs") },
|
|
468
|
+
{ to: "guards/actions-pinned.mjs", content: template("guards/actions-pinned.mjs") },
|
|
469
|
+
{ to: "guards/watchtower-locked.mjs", content: template("guards/watchtower-locked.mjs") },
|
|
470
|
+
{ to: "guards/README.md", content: template("guards/README.md") },
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
heading("Writing");
|
|
474
|
+
let written = 0;
|
|
475
|
+
for (const file of plan) {
|
|
476
|
+
const target = join(dir, file.to);
|
|
477
|
+
if (existsSync(target) && !flags.force) {
|
|
478
|
+
skip(`${file.to} exists — left untouched`);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
482
|
+
writeFileSync(target, file.content);
|
|
483
|
+
if (file.executable) chmodSync(target, 0o755);
|
|
484
|
+
ok(file.to);
|
|
485
|
+
written += 1;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// AGENTS.md / CLAUDE.md get a managed block, never an overwrite.
|
|
489
|
+
const agentsBlock = render(template("standard/agents-block.md"), vars);
|
|
490
|
+
for (const entry of ["AGENTS.md", "CLAUDE.md"]) {
|
|
491
|
+
const target = join(dir, entry);
|
|
492
|
+
if (!existsSync(target)) {
|
|
493
|
+
writeFileSync(target, entry === "CLAUDE.md" ? `Follow AGENTS.md.\n\n${agentsBlock}` : `# Agent instructions\n\n${agentsBlock}`);
|
|
494
|
+
ok(entry);
|
|
495
|
+
written += 1;
|
|
496
|
+
} else if (!hasManagedBlock(readFileSync(target, "utf8"))) {
|
|
497
|
+
writeFileSync(target, appendManagedBlock(readFileSync(target, "utf8"), agentsBlock));
|
|
498
|
+
ok(`${entry} (facility block appended)`);
|
|
499
|
+
written += 1;
|
|
500
|
+
} else {
|
|
501
|
+
skip(`${entry} already has a facility block`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Cross-tool: non-Claude agents discover the same skills via .agents/skills.
|
|
506
|
+
const agentsSkillsLink = join(dir, ".agents/skills");
|
|
507
|
+
try {
|
|
508
|
+
lstatSync(agentsSkillsLink);
|
|
509
|
+
skip(".agents/skills exists — left untouched");
|
|
510
|
+
} catch {
|
|
511
|
+
try {
|
|
512
|
+
mkdirSync(join(dir, ".agents"), { recursive: true });
|
|
513
|
+
symlinkSync("../.claude/skills", agentsSkillsLink, "dir");
|
|
514
|
+
ok(".agents/skills → .claude/skills");
|
|
515
|
+
} catch {
|
|
516
|
+
warn(".agents/skills symlink could not be created (filesystem without symlink support) — skipped.");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const manifest = {
|
|
521
|
+
facility: version,
|
|
522
|
+
engine: "claude-code",
|
|
523
|
+
engines: ["claude-code", "codex"],
|
|
524
|
+
auth: {
|
|
525
|
+
provider: "anthropic",
|
|
526
|
+
mode: anthropicAuth,
|
|
527
|
+
codex: { provider: "openai", mode: "api-key" },
|
|
528
|
+
},
|
|
529
|
+
defaultBranch,
|
|
530
|
+
provision: provision || null,
|
|
531
|
+
checks,
|
|
532
|
+
models,
|
|
533
|
+
preview,
|
|
534
|
+
canaryBot,
|
|
535
|
+
board: project ? { org, project: Number(project) } : null,
|
|
536
|
+
modules: [],
|
|
537
|
+
};
|
|
538
|
+
writeFileSync(join(dir, ".facility.json"), formatManifest(manifest));
|
|
539
|
+
ok(".facility.json");
|
|
540
|
+
|
|
541
|
+
for (const moduleName of modules) {
|
|
542
|
+
await addModule(moduleName, { dir, pkgRoot, banner: false });
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
heading("Done. The steps only you can do:");
|
|
546
|
+
const steps = [
|
|
547
|
+
authOnboardingStep(anthropicAuth),
|
|
548
|
+
`Store a spend-capped ${bold("OPENAI_API_KEY")} in the ${bold("facility-codex")} Environment to enable /codex-architect and /codex-builder.`,
|
|
549
|
+
`Install the Claude GitHub App on the repo (github.com/apps/claude) so crew pushes re-trigger CI.`,
|
|
550
|
+
`Protect ${bold(defaultBranch)}: require a PR and one human review. The crew never merges; this makes it structural.`,
|
|
551
|
+
];
|
|
552
|
+
if (project) {
|
|
553
|
+
steps.push(`Add a ${bold("PROJECTS_PAT")} secret (org Projects read+write) so the crew moves the board. It no-ops until then.`);
|
|
554
|
+
}
|
|
555
|
+
steps.push(
|
|
556
|
+
`If your tests need provider keys, put TEST-tier, spend-capped keys in a ${bold("facility-crew")} Environment — never production keys.`
|
|
557
|
+
);
|
|
558
|
+
steps.push(
|
|
559
|
+
`Optional, for the weekly canary: create a small GitHub App, install it on the repo, set ${bold("CANARY_APP_ID")} + ${bold("CANARY_APP_PRIVATE_KEY")} in the facility-crew Environment, and re-run init with --canary-bot=<your-app>[bot] (comments posted with GITHUB_TOKEN trigger nothing). It skips politely until then.`
|
|
560
|
+
);
|
|
561
|
+
steps.push(`Commit, push, open an issue, and comment ${accent("/architect")} on it. That's the whole onboarding.`);
|
|
562
|
+
steps.push(
|
|
563
|
+
`The watchtower starts reporting on its own: nightly outcomes as telemetry/artifacts, daily health as one incident issue while unhealthy, with budgets from .github/facility/watchtower/budgets.json.`
|
|
564
|
+
);
|
|
565
|
+
steps.forEach((step, index) => item(`${bold(String(index + 1) + ".")} ${step}`));
|
|
566
|
+
console.log("");
|
|
567
|
+
item(dim(`${written} files written. Read STANDARD.md next — it is yours now.`));
|
|
568
|
+
console.log("");
|
|
569
|
+
|
|
570
|
+
closePrompts();
|
|
571
|
+
return 0;
|
|
572
|
+
}
|