@theagilemonkeys/facility 0.11.3 → 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/README.md CHANGED
@@ -1,69 +1,83 @@
1
1
  # @theagilemonkeys/facility
2
2
 
3
- The command-line half of [Facility](https://github.com/theam/facility)
4
- open-source, self-hosted tooling for running AI coding agents as part of a
5
- reviewable delivery process, with the humans, the gates and the evidence in one
6
- place.
3
+ The Facility CLI installs and validates the small repository contract used by
4
+ Facility 0.12. Day-to-day story work happens through the Facility MCP server or
5
+ web application.
7
6
 
8
- One binary with two jobs, and you rarely need both:
7
+ ```bash
8
+ cd your-repository
9
+ npx @theagilemonkeys/facility init
10
+ npx @theagilemonkeys/facility doctor
11
+ ```
12
+
13
+ Node.js 24 LTS is recommended; Node.js 22 is supported from 22.13.0.
14
+
15
+ ## Initialize a repository
9
16
 
10
- - **A client for a Facility platform.** Dispatch agents, follow sessions, decide
11
- proposals, inspect spend, manage projects and repositories everything the
12
- web application does, from a terminal or a script.
13
- - **An installer for a repository.** When a team wants the process running in
14
- its own CI, invoked from issue comments, `facility init` writes the workflows,
15
- the standard, the skills and the guards into the repository.
17
+ Interactive init detects the GitHub remote, package manager, setup command,
18
+ development command, and application port. For automation, provide the values
19
+ explicitly:
16
20
 
17
21
  ```bash
18
- npx @theagilemonkeys/facility --help
22
+ npx @theagilemonkeys/facility init --yes \
23
+ --repo=acme/application \
24
+ --provision='pnpm install --frozen-lockfile' \
25
+ --start='docker compose up -d' \
26
+ --preview-readiness-command='curl --fail http://localhost:3000/health' \
27
+ --service-port=3000
19
28
  ```
20
29
 
21
- Node.js 24 LTS recommended; Node.js 22 supported from 22.13.0. Zero
22
- configuration to read; one dependency.
30
+ `init` writes exactly two kinds of project-owned configuration:
23
31
 
24
- **Early software.** Facility is published while it is still being built: the
25
- API is young, generated files change shape between `0.x` releases, and no
26
- upgrade path is promised across them. Apache-2.0 means what it says — no
27
- warranty, use at your own risk.
32
+ - `.facility.yml` describes the repositories, setup command, development
33
+ command, readiness check, and exposed services for a persistent workspace.
34
+ - `.agents/*.md` describes each agent's prompt, engine, model, and manual, MCP,
35
+ UI, scheduled, or GitHub triggers.
28
36
 
29
- ## Talking to a platform
37
+ Existing files are preserved unless `--force` is explicit. Agent manifests do
38
+ not contain permission profiles: every enabled agent receives the same full
39
+ workspace and GitHub maintainer capability for the connected project.
30
40
 
31
- ```bash
32
- npx @theagilemonkeys/facility login --url https://facility.example.com --key fak_…
33
- npx @theagilemonkeys/facility status # runs, approvals, issues and spend at a glance
34
- npx @theagilemonkeys/facility inbox # review and decide pending proposals
35
- npx @theagilemonkeys/facility sessions # trigger, watch, steer or cancel agent runs
41
+ Init creates `architect`, `builder`, `pr-reviewer`, `address-review`,
42
+ `ci-doctor`, and `security-audit`. Model flags customize the initial templates:
43
+
44
+ ```text
45
+ --build-model=<id> --review-model=<id>
46
+ --plan-model=<id> --codex-build-model=<id>
47
+ --codex-plan-model=<id>
36
48
  ```
37
49
 
38
- Platform commands take a global `--json`, `--profile <name>` and
39
- `--timeout <seconds>`, so the CLI is as usable from a script as from a prompt.
40
- It speaks the same versioned REST API and obeys the same permissions as the web
41
- application: what your key cannot do, the CLI will not do either.
50
+ Use `--dir=<path>` to configure another checkout. Existing targets are
51
+ preserved independently; `--force` overwrites only `.facility.yml` and those
52
+ six agent files. Review them before using force because they are project-owned
53
+ configuration.
54
+
55
+ ## Validate locally
42
56
 
43
- ## Installing the process into a repository
57
+ `doctor` checks the seven-file contract locally. It never connects to a
58
+ Facility instance and supports machine-readable output with `--json`.
44
59
 
45
60
  ```bash
46
- cd your-repository
47
- npx @theagilemonkeys/facility init
48
- npx @theagilemonkeys/facility doctor --run-guards --github
61
+ npx @theagilemonkeys/facility doctor --json
49
62
  ```
50
63
 
51
- `init` asks about the package manager, the default branch, the commands that
52
- provision an environment and verify a change, then writes GitHub workflows for
53
- planning, building, reviewing and repairing, plus `STANDARD.md`, agent
54
- instructions, skills, deterministic guards, and a `.facility.json` recording the
55
- answers. It never overwrites an existing generated file unless you pass
56
- `--force`, and it appends to `AGENTS.md` and `CLAUDE.md` inside a delimited
57
- managed block rather than replacing them.
64
+ Doctor provides fast local feedback; the server's strict manifest parsers and
65
+ a real disposable workspace remain authoritative for runtime acceptance.
66
+
67
+ ## Bootstrap an instance
58
68
 
59
- This is the second step, not the entry price: a repository connected to a
60
- Facility platform is operated entirely from the platform, without a single file
61
- being added to it.
69
+ Instance operators can create the first organization, owner, and GitHub App
70
+ installation with `facility instance bootstrap`. See the
71
+ [Facility documentation](https://github.com/theam/facility/tree/main/apps/docs/docs)
72
+ for deployment and MCP setup.
62
73
 
63
- ## Documentation
74
+ Bootstrap requires `DATABASE_URL` and explicit organization, owner GitHub
75
+ identity, account, and installation values. It is safe to repeat only with the
76
+ exact same binding and refuses a different binding when instance data exists.
77
+ Run `facility instance bootstrap --help` for the full command.
64
78
 
65
- - [CLI reference](https://github.com/theam/facility/blob/main/apps/docs/docs/reference/cli.md)
66
- - [Self-hosting guide](https://github.com/theam/facility/blob/main/apps/docs/docs/self-host/quickstart.md)
67
- - [Repository and issues](https://github.com/theam/facility)
79
+ Facility 0.x is early software and does not promise in-place database upgrades
80
+ between minor versions. Facility 0.12 specifically requires a new database
81
+ rather than a 0.11 in-place migration.
68
82
 
69
- Apache-2.0 · an initiative by [The Agile Monkeys](https://theagilemonkeys.com)
83
+ Apache-2.0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@theagilemonkeys/facility",
3
- "version": "0.11.3",
4
- "description": "Run an AI crew on your GitHub repo: agents that plan before building, build to your standard, verify on a provisioned environment, and never merge their own work.",
3
+ "version": "0.12.0",
4
+ "description": "Create and validate Facility 0.12 repository workspace and agent manifests.",
5
5
  "keywords": [
6
6
  "ai",
7
7
  "agents",
@@ -27,8 +27,7 @@
27
27
  "files": [
28
28
  "bin",
29
29
  "src",
30
- "templates",
31
- "modules"
30
+ "templates"
32
31
  ],
33
32
  "engines": {
34
33
  "node": "^22.13.0 || ^24.0.0"
package/src/cli.mjs CHANGED
@@ -1,12 +1,10 @@
1
- // facility CLI entry: init · add · doctor.
1
+ // Facility 0.12 CLI entry: repository kickstart, validation, and instance bootstrap.
2
2
  import { readFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { init } from "./init.mjs";
6
- import { addModule } from "./add.mjs";
7
6
  import { doctor } from "./doctor.mjs";
8
7
  import { bootstrapInstance } from "./instance.mjs";
9
- import { runPlatformCommand } from "./platform.mjs";
10
8
  import { banner, bold, dim, item } from "./ui.mjs";
11
9
 
12
10
  const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -20,7 +18,7 @@ function parseFlags(args) {
20
18
  if (arg === "--yes" || arg === "-y") flags.yes = true;
21
19
  else if (arg === "--json") flags.json = true;
22
20
  else if (arg === "--force") flags.force = true;
23
- else if (["--help", "--local", "--platform"].includes(arg)) flags[arg.slice(2)] = true;
21
+ else if (arg === "--help") flags.help = true;
24
22
  else if (arg.startsWith("--") && arg.includes("=")) {
25
23
  const [key, ...rest] = arg.slice(2).split("=");
26
24
  flags[key] = rest.join("=");
@@ -38,63 +36,21 @@ function parseFlags(args) {
38
36
 
39
37
  function help() {
40
38
  banner(version);
41
- helpGroup("Build your repo", [
42
- ["init", "install the Facility method"],
43
- ["add <module>", "add a quality module"],
44
- ["doctor", "verify local and platform readiness"],
39
+ helpGroup("Configure a repository", [
40
+ ["init", "write .facility.yml and the .agents catalog"],
41
+ ["doctor", "verify the workspace contract"],
45
42
  ]);
46
- helpGroup("Operate", [
47
- ["status", "runs, approvals, issues, and spend at a glance"],
48
- ["projects", "create, inspect, update, or archive projects"],
49
- ["repos", "connect or create GitHub repositories"],
50
- ["agents", "configure agent definitions and schedules"],
51
- ["sessions", "trigger, watch, steer, or cancel governed sessions"],
52
- ["conversations", "continue durable agent conversations"],
53
- ["github", "discover repositories and act on GitHub issues"],
54
- ["inbox", "review and decide proposals"],
55
- ["issues", "acknowledge and resolve platform issues"],
56
- ["kickstart", "preview and open a governed installation PR"],
57
- ["upgrade", "open a governed template upgrade PR"],
58
- ]);
59
- helpGroup("Knowledge & policy", [
60
- ["kb", "manage the project knowledge base"],
61
- ["tasks", "manage the product-owner task queue"],
62
- ["registry", "version contracts, harnesses, and policies"],
63
- ["proposals", "create, inspect, and execute approval gates"],
64
- ["action-types", "discover proposal payload contracts"],
65
- ["budgets", "enforce org, project, and agent budgets"],
66
- ["health", "inspect project readiness"],
67
- ["outcomes", "inspect pull-request delivery outcomes"],
68
- ["catalog", "discover engines, models, permissions, and triggers"],
69
- ]);
70
- helpGroup("Platform administration", [
71
- ["instance bootstrap", "bootstrap a dedicated Facility instance"],
72
- ["org", "organization settings"],
73
- ["members", "organization membership"],
74
- ["roles", "permission policy"],
75
- ["keys", "control-plane API credentials"],
76
- ["virtual-keys", "project-scoped model credentials"],
77
- ["providers", "model-provider credentials"],
78
- ["sandboxes", "execution profiles and isolation"],
79
- ["integrations", "inbound and outbound webhooks"],
80
- ["analytics", "run and reliability trends"],
81
- ["spend", "cost attribution"],
82
- ["audit", "tamper-evident change ledger"],
83
- ["llm-requests", "inspect metering and request envelopes"],
84
- ]);
85
- helpGroup("Account", [
86
- ["login", "verify and save an environment profile"],
87
- ["logout", "remove saved profile credentials"],
88
- ["profiles", "switch between Facility environments"],
43
+ item(dim("Run stories through Facility's MCP server or web UI."));
44
+ console.log("");
45
+ helpGroup("Instance administration", [
46
+ ["instance bootstrap", "create the first organization, owner, and GitHub installation"],
89
47
  ]);
90
48
  console.log("");
91
- item(dim("init flags: --yes --force --dir=<path> --branch=<name> --provision=<cmd>"));
92
- item(dim(' --checks="cmd1, cmd2" --auth=<api-key|oauth|wif|bedrock|vertex>'));
49
+ item(dim("init flags: --yes --force --dir=<path> --repo=<owner/name> --provision=<cmd> --start=<cmd>"));
50
+ item(dim(" --service-port=<n> --preview-readiness-command=<cmd>"));
93
51
  item(dim(' --build-model=<id> --review-model=<id> --plan-model=<id>'));
94
- item(dim(' --codex-build-model=<id> --codex-plan-model=<id> --org=<org> --project=<n>'));
95
- item(dim(' --preview-image=<image> --preview-command=<cmd> --preview-port=<n> --preview-readiness-path=</health> --preview-ttl-hours=<n>'));
52
+ item(dim(' --codex-build-model=<id> --codex-plan-model=<id>'));
96
53
  item(dim("Run facility <command> --help for precise usage."));
97
- item(dim("Global platform flags: --profile <name> --json --timeout <seconds>"));
98
54
  console.log("");
99
55
  item(dim("Docs: https://github.com/theam/facility"));
100
56
  console.log("");
@@ -121,7 +77,7 @@ export async function main(argv) {
121
77
  }
122
78
  if (flags.dir) flags.dir = resolve(flags.dir);
123
79
 
124
- if (flags.help && ["init", "add", "doctor"].includes(command)) {
80
+ if (flags.help && ["init", "doctor"].includes(command)) {
125
81
  localHelp(command);
126
82
  return 0;
127
83
  }
@@ -129,56 +85,12 @@ export async function main(argv) {
129
85
  switch (command) {
130
86
  case "init":
131
87
  return init(flags, pkgRoot, version);
132
- case "add": {
133
- if (!positional[0]) {
134
- help();
135
- return 1;
136
- }
137
- return addModule(positional[0], { dir: flags.dir || process.cwd(), pkgRoot });
138
- }
139
88
  case "doctor":
140
89
  return doctor(flags, version);
141
90
  case "instance":
142
91
  if (positional[0] === "bootstrap") return bootstrapInstance(flags);
143
92
  console.error("Usage: facility instance bootstrap [options]");
144
93
  return 1;
145
- case "login":
146
- case "logout":
147
- case "profiles":
148
- case "status":
149
- case "projects":
150
- case "sessions":
151
- case "runs":
152
- case "conversations":
153
- case "github":
154
- case "outcomes":
155
- case "catalog":
156
- case "inbox":
157
- case "issues":
158
- case "kickstart":
159
- case "upgrade":
160
- case "keys":
161
- case "llm-requests":
162
- case "org":
163
- case "members":
164
- case "roles":
165
- case "repos":
166
- case "agents":
167
- case "providers":
168
- case "budgets":
169
- case "registry":
170
- case "sandboxes":
171
- case "tasks":
172
- case "virtual-keys":
173
- case "kb":
174
- case "analytics":
175
- case "audit":
176
- case "integrations":
177
- case "spend":
178
- case "proposals":
179
- case "action-types":
180
- case "health":
181
- return runPlatformCommand(command, rest);
182
94
  case "--version":
183
95
  case "-v":
184
96
  console.log(version);
@@ -198,40 +110,20 @@ function validateLocalFlags(command, flags) {
198
110
  "yes",
199
111
  "force",
200
112
  "dir",
201
- "branch",
202
113
  "provision",
203
- "checks",
204
- "project",
114
+ "start",
115
+ "repo",
116
+ "service-port",
117
+ "preview-readiness-command",
205
118
  "org",
206
- "modules",
207
- "auth",
208
119
  "build-model",
209
120
  "review-model",
210
121
  "plan-model",
211
122
  "codex-build-model",
212
123
  "codex-plan-model",
213
- "preview-image",
214
- "preview-command",
215
- "preview-port",
216
- "preview-readiness-path",
217
- "preview-ttl-hours",
218
- "help",
219
- ]),
220
- add: new Set(["dir", "help"]),
221
- doctor: new Set([
222
- "dir",
223
- "local",
224
- "platform",
225
- "profile",
226
- "url",
227
- "key",
228
- "allow-insecure",
229
- "timeout",
230
- "run-guards",
231
- "github",
232
- "json",
233
124
  "help",
234
125
  ]),
126
+ doctor: new Set(["dir", "json", "help"]),
235
127
  instance: new Set([
236
128
  "org-name",
237
129
  "org-slug",
@@ -254,29 +146,23 @@ function validateLocalFlags(command, flags) {
254
146
  }
255
147
  const valueNames =
256
148
  command === "doctor"
257
- ? ["dir", "profile", "url", "key", "timeout"]
149
+ ? ["dir"]
258
150
  : command === "instance"
259
151
  ? [...allowed].filter((name) => !["json", "help"].includes(name))
260
152
  : command === "init"
261
153
  ? [
262
154
  "dir",
263
- "branch",
264
155
  "provision",
265
- "checks",
266
- "project",
156
+ "start",
157
+ "repo",
158
+ "service-port",
159
+ "preview-readiness-command",
267
160
  "org",
268
- "modules",
269
- "auth",
270
161
  "build-model",
271
162
  "review-model",
272
163
  "plan-model",
273
164
  "codex-build-model",
274
165
  "codex-plan-model",
275
- "preview-image",
276
- "preview-command",
277
- "preview-port",
278
- "preview-readiness-path",
279
- "preview-ttl-hours",
280
166
  ]
281
167
  : ["dir"];
282
168
  for (const name of valueNames) {
@@ -284,29 +170,10 @@ function validateLocalFlags(command, flags) {
284
170
  return `--${name} requires a value`;
285
171
  }
286
172
  }
287
- if (
288
- command === "doctor" &&
289
- flags.local &&
290
- (flags.platform || flags.profile || flags.url || flags.key)
291
- ) {
292
- return "--local cannot be combined with platform target options";
293
- }
294
- if (
295
- command === "doctor" &&
296
- (flags.platform || flags.profile || flags.url || flags.key) &&
297
- (flags["run-guards"] || flags.github)
298
- ) {
299
- return "--run-guards and --github are local-doctor options";
300
- }
301
173
  for (const name of [
302
174
  "yes",
303
175
  "force",
304
176
  "help",
305
- "local",
306
- "platform",
307
- "allow-insecure",
308
- "run-guards",
309
- "github",
310
177
  "json",
311
178
  ]) {
312
179
  if (name in flags && flags[name] !== true) return `--${name} does not take a value`;
@@ -315,34 +182,18 @@ function validateLocalFlags(command, flags) {
315
182
  }
316
183
 
317
184
  function extractInvocation(argv) {
318
- const leading = [];
319
- const valueFlags = new Set(["--profile", "--timeout", "--dir", "--url", "--key"]);
320
- let index = 0;
321
- while (index < argv.length && argv[index]?.startsWith("-") && !["--help", "--version", "-v"].includes(argv[index])) {
322
- const flag = argv[index];
323
- leading.push(flag);
324
- index += 1;
325
- if (valueFlags.has(flag) && index < argv.length) {
326
- leading.push(argv[index]);
327
- index += 1;
328
- }
329
- }
330
- return { command: argv[index], rest: [...leading, ...argv.slice(index + 1)] };
185
+ return { command: argv[0], rest: argv.slice(1) };
331
186
  }
332
187
 
333
188
  function localHelp(command) {
334
189
  const lines = {
335
190
  init: [
336
191
  "facility init [--dir <path>] [--yes] [--force]",
337
- "Detect the repository and install Facility workflows, guards, agents, and policy.",
338
- ],
339
- add: [
340
- "facility add <module> [--dir <path>]",
341
- "Add a Facility quality module to an existing installation.",
192
+ "Detect the repository and install .facility.yml plus the shared .agents catalog.",
342
193
  ],
343
194
  doctor: [
344
- "facility doctor [--dir <path>] [--run-guards] [--github] [--json] | --platform [--profile <name>] [--timeout <seconds>] [--json] | --url <url> --key <key> [--allow-insecure] [--timeout <seconds>] [--json]",
345
- "Verify a local installation or an explicitly selected platform deployment; JSON works in either mode.",
195
+ "facility doctor [--dir <path>] [--json]",
196
+ "Verify the repository's .facility.yml and .agents catalog.",
346
197
  ],
347
198
  };
348
199
  const [usage, description] = lines[command];
package/src/detect.mjs CHANGED
@@ -1,7 +1,5 @@
1
- // Stack detection: read the target repo and propose sensible defaults so
2
- // `init` asks six short questions instead of twenty.
3
- import { existsSync, readFileSync, readdirSync } from "node:fs";
4
1
  import { execFileSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
5
3
  import { join } from "node:path";
6
4
 
7
5
  function readJson(path) {
@@ -14,7 +12,11 @@ function readJson(path) {
14
12
 
15
13
  function git(cwd, args) {
16
14
  try {
17
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
15
+ return execFileSync("git", args, {
16
+ cwd,
17
+ encoding: "utf8",
18
+ stdio: ["ignore", "pipe", "ignore"],
19
+ }).trim();
18
20
  } catch {
19
21
  return "";
20
22
  }
@@ -23,105 +25,33 @@ function git(cwd, args) {
23
25
  export function detect(dir) {
24
26
  const pkg = readJson(join(dir, "package.json"));
25
27
  const scripts = pkg?.scripts ?? {};
26
-
27
28
  const packageManager = existsSync(join(dir, "pnpm-lock.yaml"))
28
29
  ? "pnpm"
29
30
  : existsSync(join(dir, "yarn.lock"))
30
31
  ? "yarn"
31
- : existsSync(join(dir, "package-lock.json"))
32
+ : existsSync(join(dir, "package-lock.json")) || pkg
32
33
  ? "npm"
33
- : pkg
34
- ? "npm"
35
- : "none";
36
-
37
- const runner = packageManager === "none" ? null : packageManager === "npm" ? "npm run" : `${packageManager} run`;
38
- const checks = [];
39
- if (runner) {
40
- for (const name of ["typecheck", "lint", "test", "build"]) {
41
- if (scripts[name]) checks.push(`${runner} ${name}`);
42
- }
43
- }
44
-
45
- const provision = runner && scripts["setup"] ? `${runner} setup` : "";
46
-
47
- const isGitRepo = git(dir, ["rev-parse", "--is-inside-work-tree"]) === "true";
48
- let defaultBranch = "main";
49
- const originHead = git(dir, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
50
- if (originHead) defaultBranch = originHead.replace("refs/remotes/origin/", "");
51
- else {
52
- const current = git(dir, ["branch", "--show-current"]);
53
- if (current) defaultBranch = current;
54
- }
55
-
56
- let org = "";
34
+ : "none";
35
+ const runner =
36
+ packageManager === "none" ? null : packageManager === "npm" ? "npm run" : `${packageManager} run`;
37
+ const provision = runner && scripts.setup ? `${runner} setup` : "";
38
+ const start =
39
+ existsSync(join(dir, "compose.yml")) || existsSync(join(dir, "docker-compose.yml"))
40
+ ? "docker compose up -d"
41
+ : runner && scripts.dev
42
+ ? `${runner} dev`
43
+ : runner && scripts.start
44
+ ? `${runner} start`
45
+ : "";
57
46
  const remote = git(dir, ["remote", "get-url", "origin"]);
58
- const remoteMatch = remote.match(/[/:]([^/:]+)\/([^/]+?)(\.git)?$/);
59
- if (remoteMatch) org = remoteMatch[1];
60
-
61
- const migrationDirs = ["migrations", "supabase/migrations", "db/migrations", "prisma/migrations"].filter((d) =>
62
- existsSync(join(dir, d))
63
- );
64
-
65
- // Existing check workflows (by their `name:`), so the doctor knows what to
66
- // watch. Facility's own workflows are excluded — the watchtower covers them.
67
- const workflowNames = [];
68
- const deploymentProviders = new Set();
69
- try {
70
- for (const file of readdirSync(join(dir, ".github/workflows"))) {
71
- if (!/\.ya?ml$/.test(file) || file.startsWith("facility-")) continue;
72
- const workflow = readFileSync(join(dir, ".github/workflows", file), "utf8");
73
- const nameMatch = workflow.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m);
74
- if (nameMatch) workflowNames.push(nameMatch[1].trim());
75
- detectDeploymentProvider(workflow, deploymentProviders);
76
- }
77
- } catch {}
78
- if (existsSync(join(dir, "vercel.json"))) deploymentProviders.add("vercel");
79
- if (existsSync(join(dir, "netlify.toml"))) deploymentProviders.add("netlify");
80
- if (existsSync(join(dir, "fly.toml"))) deploymentProviders.add("fly.io");
81
- if (existsSync(join(dir, "render.yaml"))) deploymentProviders.add("render");
82
- if (existsSync(join(dir, "Dockerfile"))) deploymentProviders.add("container-image");
83
- const board = detectBoard(dir, org);
84
- const facility = readJson(join(dir, ".facility.json"));
47
+ const match = remote.match(/[/:]([^/:]+)\/([^/]+?)(\.git)?$/);
85
48
 
86
49
  return {
87
- isGitRepo,
88
- defaultBranch,
50
+ isGitRepo: git(dir, ["rev-parse", "--is-inside-work-tree"]) === "true",
89
51
  packageManager,
90
- checks,
91
52
  provision,
92
- org,
93
- workflowNames,
94
- deploymentProviders: [...deploymentProviders].sort(),
95
- board,
96
- previewConfigured: facility?.preview?.enabled === true,
97
- suggestedModules: migrationDirs.length ? ["database"] : [],
98
- existing: {
99
- agentsMd: existsSync(join(dir, "AGENTS.md")),
100
- claudeMd: existsSync(join(dir, "CLAUDE.md")),
101
- claudeSettings: existsSync(join(dir, ".claude/settings.json")),
102
- standard: existsSync(join(dir, "STANDARD.md")),
103
- },
53
+ start,
54
+ org: match?.[1] ?? "",
55
+ repository: match ? `${match[1]}/${match[2]}` : "",
104
56
  };
105
57
  }
106
-
107
- function detectDeploymentProvider(content, providers) {
108
- const lower = content.toLowerCase();
109
- if (lower.includes("vercel")) providers.add("vercel");
110
- if (lower.includes("netlify")) providers.add("netlify");
111
- if (lower.includes("flyctl") || lower.includes("fly.io")) providers.add("fly.io");
112
- if (lower.includes("cloudflare") || lower.includes("wrangler")) providers.add("cloudflare");
113
- if (lower.includes("amazon-ecr") || lower.includes("amazon-ecs")) providers.add("aws");
114
- if (lower.includes("render.com")) providers.add("render");
115
- }
116
-
117
- function detectBoard(dir, fallbackOrg) {
118
- for (const path of ["AGENTS.md", "CLAUDE.md", "README.md"]) {
119
- try {
120
- const match = readFileSync(join(dir, path), "utf8").match(
121
- /github\.com\/orgs\/([^/\s]+)\/projects\/(\d+)/i,
122
- );
123
- if (match?.[2]) return { org: match[1] || fallbackOrg, project: Number(match[2]) };
124
- } catch {}
125
- }
126
- return null;
127
- }