@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.
- package/README.md +61 -47
- package/package.json +3 -4
- package/src/cli.mjs +27 -176
- package/src/detect.mjs +24 -94
- package/src/doctor.mjs +54 -559
- package/src/init.mjs +92 -535
- package/templates/agents/address-review.md +53 -0
- package/templates/agents/architect.md +50 -0
- package/templates/agents/builder.md +58 -0
- package/templates/agents/ci-doctor.md +55 -0
- package/templates/agents/pr-reviewer.md +52 -0
- package/templates/agents/security-audit.md +54 -0
- package/modules/README.md +0 -35
- package/modules/ai-queryability/agents/queryability-reviewer.md +0 -35
- package/modules/ai-queryability/module.json +0 -9
- package/modules/ai-queryability/standard-section.md +0 -22
- package/modules/analytics/agents/analytics-reviewer.md +0 -32
- package/modules/analytics/commands/add-telemetry.md +0 -23
- package/modules/analytics/module.json +0 -10
- package/modules/analytics/standard-section.md +0 -23
- package/modules/database/agents/data-security-reviewer.md +0 -38
- package/modules/database/commands/new-migration.md +0 -24
- package/modules/database/guards/migration-versions.mjs +0 -41
- package/modules/database/guards/migrations-immutable.mjs +0 -57
- package/modules/database/hooks/protect-migrations.fragment.mjs +0 -10
- package/modules/database/module.json +0 -25
- package/modules/database/standard-section.md +0 -20
- package/modules/design-system/agents/design-reviewer.md +0 -37
- package/modules/design-system/module.json +0 -9
- package/modules/design-system/standard-section.md +0 -15
- package/src/add.mjs +0 -77
- package/src/platform-admin.mjs +0 -1552
- package/src/platform-config.mjs +0 -39
- package/src/platform.mjs +0 -1759
- package/src/render.mjs +0 -66
- package/templates/claude/settings.json +0 -71
- package/templates/delivery/verify.mjs +0 -157
- package/templates/doctor/resolve.mjs +0 -572
- package/templates/guards/README.md +0 -30
- package/templates/guards/_kit.mjs +0 -81
- package/templates/guards/actions-pinned.mjs +0 -38
- package/templates/guards/run.mjs +0 -111
- package/templates/guards/watchtower-locked.mjs +0 -66
- package/templates/prompts/address-review.md +0 -14
- package/templates/prompts/architect.md +0 -63
- package/templates/prompts/builder.md +0 -79
- package/templates/prompts/doctor.md +0 -69
- package/templates/prompts/review.md +0 -14
- package/templates/prompts/sweep.md +0 -75
- package/templates/receipts/collect.mjs +0 -297
- package/templates/review/finalize.mjs +0 -38
- package/templates/scripts/move-board-status.sh +0 -155
- package/templates/security/sync-findings.mjs +0 -226
- package/templates/standard/STANDARD.md +0 -141
- package/templates/standard/agents-block.md +0 -25
- package/templates/watchtower/budgets.json +0 -12
- package/templates/watchtower/canary.mjs +0 -216
- package/templates/watchtower/health.mjs +0 -148
- package/templates/watchtower/outcomes.mjs +0 -188
- package/templates/workflows/facility-address-review.yml +0 -154
- package/templates/workflows/facility-canary.yml +0 -61
- package/templates/workflows/facility-codex.yml +0 -327
- package/templates/workflows/facility-crew.yml +0 -351
- package/templates/workflows/facility-doctor.yml +0 -174
- package/templates/workflows/facility-review.yml +0 -135
- package/templates/workflows/facility-security-sweep.yml +0 -204
- package/templates/workflows/facility-watchtower.yml +0 -87
package/src/init.mjs
CHANGED
|
@@ -1,297 +1,29 @@
|
|
|
1
|
-
// `facility init`
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
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 {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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.
|
|
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(`
|
|
303
|
-
item(`
|
|
304
|
-
item(`
|
|
305
|
-
item(`
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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("
|
|
51
|
+
? await ask("Setup command (optional, reruns when the environment changes)?", detected.provision)
|
|
317
52
|
: detected.provision);
|
|
318
|
-
const
|
|
319
|
-
flags.
|
|
53
|
+
const start =
|
|
54
|
+
flags.start ??
|
|
320
55
|
(interactive
|
|
321
|
-
? await ask("
|
|
322
|
-
: detected.
|
|
323
|
-
|
|
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
|
-
"
|
|
60
|
+
"A development environment start command is required. Pass --start, for example --start='docker compose up -d'.",
|
|
363
61
|
);
|
|
364
62
|
}
|
|
365
|
-
const
|
|
366
|
-
flags
|
|
367
|
-
(interactive
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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 = (
|
|
81
|
+
const template = (relativePath) =>
|
|
82
|
+
readFileSync(join(pkgRoot, "templates", relativePath), "utf8");
|
|
442
83
|
const plan = [
|
|
443
|
-
{
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
578
|
-
console.log("");
|
|
579
|
-
item(dim(`${written} files written. Read STANDARD.md next — it is yours now.`));
|
|
580
|
-
console.log("");
|
|
122
|
+
}
|
|
581
123
|
|
|
582
|
-
|
|
583
|
-
return
|
|
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
|
}
|