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