@theagilemonkeys/facility 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +68 -0
  3. package/bin/facility.mjs +10 -0
  4. package/modules/README.md +35 -0
  5. package/modules/ai-queryability/agents/queryability-reviewer.md +35 -0
  6. package/modules/ai-queryability/module.json +9 -0
  7. package/modules/ai-queryability/standard-section.md +22 -0
  8. package/modules/analytics/agents/analytics-reviewer.md +32 -0
  9. package/modules/analytics/commands/add-telemetry.md +23 -0
  10. package/modules/analytics/module.json +10 -0
  11. package/modules/analytics/standard-section.md +23 -0
  12. package/modules/database/agents/data-security-reviewer.md +38 -0
  13. package/modules/database/commands/new-migration.md +24 -0
  14. package/modules/database/guards/migration-versions.mjs +41 -0
  15. package/modules/database/guards/migrations-immutable.mjs +57 -0
  16. package/modules/database/hooks/protect-migrations.fragment.mjs +10 -0
  17. package/modules/database/module.json +25 -0
  18. package/modules/database/standard-section.md +20 -0
  19. package/modules/design-system/agents/design-reviewer.md +37 -0
  20. package/modules/design-system/module.json +9 -0
  21. package/modules/design-system/standard-section.md +15 -0
  22. package/package.json +42 -0
  23. package/src/add.mjs +77 -0
  24. package/src/cli.mjs +352 -0
  25. package/src/detect.mjs +127 -0
  26. package/src/doctor.mjs +582 -0
  27. package/src/init.mjs +572 -0
  28. package/src/instance.mjs +114 -0
  29. package/src/platform-admin.mjs +1542 -0
  30. package/src/platform-config.mjs +39 -0
  31. package/src/platform.mjs +1759 -0
  32. package/src/prompts.mjs +64 -0
  33. package/src/render.mjs +66 -0
  34. package/src/ui.mjs +30 -0
  35. package/templates/claude/agents/security-reviewer.md +41 -0
  36. package/templates/claude/agents/standards-reviewer.md +31 -0
  37. package/templates/claude/commands/open-pr.md +21 -0
  38. package/templates/claude/commands/verify.md +16 -0
  39. package/templates/claude/hooks/protect-branch.mjs +58 -0
  40. package/templates/claude/hooks/protect-files.mjs +35 -0
  41. package/templates/claude/settings.json +71 -0
  42. package/templates/claude/skills/maintainable-software/SKILL.md +67 -0
  43. package/templates/claude/skills/reviewing-to-standard/SKILL.md +49 -0
  44. package/templates/claude/skills/working-to-standard/SKILL.md +45 -0
  45. package/templates/delivery/verify.mjs +157 -0
  46. package/templates/doctor/resolve.mjs +144 -0
  47. package/templates/guards/README.md +30 -0
  48. package/templates/guards/_kit.mjs +81 -0
  49. package/templates/guards/actions-pinned.mjs +38 -0
  50. package/templates/guards/run.mjs +111 -0
  51. package/templates/guards/watchtower-locked.mjs +66 -0
  52. package/templates/prompts/address-review.md +14 -0
  53. package/templates/prompts/architect.md +62 -0
  54. package/templates/prompts/builder.md +71 -0
  55. package/templates/prompts/doctor.md +64 -0
  56. package/templates/prompts/review.md +14 -0
  57. package/templates/prompts/sweep.md +75 -0
  58. package/templates/receipts/collect.mjs +289 -0
  59. package/templates/review/finalize.mjs +38 -0
  60. package/templates/scripts/move-board-status.sh +155 -0
  61. package/templates/security/sync-findings.mjs +226 -0
  62. package/templates/standard/STANDARD.md +141 -0
  63. package/templates/standard/agents-block.md +25 -0
  64. package/templates/watchtower/budgets.json +12 -0
  65. package/templates/watchtower/canary.mjs +216 -0
  66. package/templates/watchtower/health.mjs +148 -0
  67. package/templates/watchtower/outcomes.mjs +188 -0
  68. package/templates/workflows/facility-address-review.yml +153 -0
  69. package/templates/workflows/facility-canary.yml +61 -0
  70. package/templates/workflows/facility-codex.yml +326 -0
  71. package/templates/workflows/facility-crew.yml +350 -0
  72. package/templates/workflows/facility-doctor.yml +155 -0
  73. package/templates/workflows/facility-review.yml +134 -0
  74. package/templates/workflows/facility-security-sweep.yml +204 -0
  75. package/templates/workflows/facility-watchtower.yml +87 -0
@@ -0,0 +1,114 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import postgres from "postgres";
3
+
4
+ export async function bootstrapInstance(flags, options = {}) {
5
+ if (flags.help) {
6
+ console.log("facility instance bootstrap --org-name <name> --org-slug <slug> --owner-email <email> --owner-name <name> --github-user-id <id> --github-login <login> --github-account-id <id> --github-account-login <login> --github-installation-id <id> [--github-account-type <organization|user>] [--json]");
7
+ return 0;
8
+ }
9
+ const databaseUrl = options.databaseUrl ?? process.env.DATABASE_URL;
10
+ if (!databaseUrl) return failure(flags, "DATABASE_URL is required");
11
+ const input = {
12
+ orgName: stringFlag(flags, "org-name"),
13
+ orgSlug: stringFlag(flags, "org-slug"),
14
+ ownerEmail: stringFlag(flags, "owner-email")?.toLowerCase(),
15
+ ownerName: stringFlag(flags, "owner-name"),
16
+ githubUserId: positiveInteger(flags, "github-user-id"),
17
+ githubLogin: stringFlag(flags, "github-login"),
18
+ githubAccountId: positiveInteger(flags, "github-account-id"),
19
+ githubInstallationId: positiveInteger(flags, "github-installation-id"),
20
+ githubAccountLogin: stringFlag(flags, "github-account-login"),
21
+ githubAccountType: (stringFlag(flags, "github-account-type") ?? "organization").toLowerCase(),
22
+ };
23
+ const missing = Object.entries(input).filter(([, value]) => value === undefined).map(([key]) => key);
24
+ if (missing.length) return failure(flags, `Missing required bootstrap values: ${missing.join(", ")}`);
25
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input.orgSlug)) return failure(flags, "--org-slug must be a lowercase URL slug");
26
+ if (!/^\S+@\S+\.\S+$/.test(input.ownerEmail)) return failure(flags, "--owner-email must be valid");
27
+ if (!new Set(["organization", "user"]).has(input.githubAccountType))
28
+ return failure(flags, "--github-account-type must be organization or user");
29
+ const targetType = input.githubAccountType === "user" ? "User" : "Organization";
30
+
31
+ const sql = (options.postgres ?? postgres)(databaseUrl, { max: 1 });
32
+ try {
33
+ const result = await sql.begin(async (tx) => {
34
+ await tx`SELECT pg_advisory_xact_lock(hashtext('facility-instance-bootstrap'))`;
35
+ const counts = (await tx`SELECT
36
+ (SELECT count(*)::int FROM orgs) AS orgs,
37
+ (SELECT count(*)::int FROM users) AS users,
38
+ (SELECT count(*)::int FROM org_members) AS members,
39
+ (SELECT count(*)::int FROM user_identities) AS identities,
40
+ (SELECT count(*)::int FROM github_installations) AS installations`)[0];
41
+ const hasInstanceData = Object.values(counts).some((value) => Number(value) > 0);
42
+ const existing = hasInstanceData ? await tx`SELECT
43
+ o.id AS org_id,
44
+ o.name AS org_name,
45
+ o.slug,
46
+ u.name AS owner_name,
47
+ u.email,
48
+ u.status,
49
+ i.provider_subject,
50
+ i.login AS github_login,
51
+ g.installation_id,
52
+ g.account_id,
53
+ g.account_login,
54
+ g.target_type
55
+ FROM orgs o
56
+ JOIN org_members m ON m.org_id = o.id
57
+ JOIN users u ON u.id = m.user_id
58
+ JOIN user_identities i ON i.user_id = u.id AND i.provider = 'github'
59
+ JOIN github_installations g ON g.org_id = o.id
60
+ LIMIT 2` : [];
61
+ if (hasInstanceData) {
62
+ const row = existing[0];
63
+ const exactlyOneBinding = existing.length === 1 && Object.values(counts).every((value) => Number(value) === 1);
64
+ const identical = exactlyOneBinding && row.org_name === input.orgName && row.slug === input.orgSlug &&
65
+ row.owner_name === input.ownerName && row.email.toLowerCase() === input.ownerEmail && row.status === "active" &&
66
+ row.provider_subject === String(input.githubUserId) && row.github_login === input.githubLogin &&
67
+ Number(row.installation_id) === input.githubInstallationId && Number(row.account_id) === input.githubAccountId &&
68
+ row.account_login === input.githubAccountLogin && row.target_type === targetType;
69
+ if (!identical) throw new Error("Database already contains a different Facility instance");
70
+ return { created: false, orgId: row.org_id };
71
+ }
72
+ const ownerRole = await tx`SELECT id FROM roles WHERE name = 'owner' AND org_id IS NULL LIMIT 1`;
73
+ if (!ownerRole[0]) throw new Error("Bundled roles are missing; run Facility migrations and seed first");
74
+ const orgId = id("org");
75
+ const userId = id("user");
76
+ await tx`INSERT INTO orgs (id, name, slug, settings) VALUES (${orgId}, ${input.orgName}, ${input.orgSlug}, ${tx.json({ githubAccountId: input.githubAccountId, githubInstallationId: input.githubInstallationId })})`;
77
+ await tx`INSERT INTO users (id, email, name, status) VALUES (${userId}, ${input.ownerEmail}, ${input.ownerName}, 'active')`;
78
+ await tx`INSERT INTO user_identities (id, user_id, provider, provider_subject, login, metadata)
79
+ VALUES (${id("user")}, ${userId}, 'github', ${String(input.githubUserId)}, ${input.githubLogin}, ${tx.json({ accountIds: [input.githubAccountId] })})`;
80
+ await tx`INSERT INTO org_members (id, org_id, user_id, role_id) VALUES (${id("member")}, ${orgId}, ${userId}, ${ownerRole[0].id})`;
81
+ await tx`INSERT INTO github_installations (id, org_id, installation_id, account_id, account_login, target_type)
82
+ VALUES (${id("int")}, ${orgId}, ${input.githubInstallationId}, ${input.githubAccountId}, ${input.githubAccountLogin}, ${targetType})`;
83
+ return { created: true, orgId };
84
+ });
85
+ const output = { ok: true, created: result.created, orgId: result.orgId, slug: input.orgSlug };
86
+ if (flags.json) console.log(JSON.stringify(output));
87
+ else console.log(result.created ? `Bootstrapped Facility instance ${input.orgSlug}.` : `Facility instance ${input.orgSlug} is already bootstrapped.`);
88
+ return 0;
89
+ } catch (error) {
90
+ return failure(flags, error instanceof Error ? error.message : String(error));
91
+ } finally {
92
+ await sql.end();
93
+ }
94
+ }
95
+
96
+ function stringFlag(flags, name) {
97
+ const value = flags[name];
98
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
99
+ }
100
+
101
+ function positiveInteger(flags, name) {
102
+ const value = Number(stringFlag(flags, name));
103
+ return Number.isSafeInteger(value) && value > 0 ? value : undefined;
104
+ }
105
+
106
+ function id(prefix) {
107
+ return `${prefix}_${randomUUID().replaceAll("-", "")}`;
108
+ }
109
+
110
+ function failure(flags, message) {
111
+ if (flags.json) console.log(JSON.stringify({ error: { code: "bootstrap_failed", message } }));
112
+ else console.error(message);
113
+ return 1;
114
+ }