@fabrika/builder 0.2.0 → 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 (51) hide show
  1. package/BUILD-CONTRACT.md +8 -0
  2. package/README.md +9 -0
  3. package/cli/src/bin.ts +35 -2
  4. package/cli/src/build-status.ts +7 -15
  5. package/cli/src/commands/advance.ts +3 -2
  6. package/cli/src/commands/ensure-daemon.ts +3 -3
  7. package/cli/src/commands/gate.ts +4 -10
  8. package/cli/src/commands/init.ts +44 -7
  9. package/cli/src/commands/next.ts +3 -3
  10. package/cli/src/commands/platform.ts +81 -0
  11. package/cli/src/config.ts +1 -9
  12. package/cli/src/context.ts +4 -1
  13. package/cli/src/daemon-client.ts +36 -185
  14. package/cli/src/daemon-lifecycle.ts +63 -142
  15. package/cli/src/derive.ts +4 -8
  16. package/cli/src/errors.ts +1 -0
  17. package/cli/src/index.ts +2 -3
  18. package/cli/src/inject-skills.ts +8 -31
  19. package/cli/src/layout.ts +8 -18
  20. package/cli/src/packet.ts +18 -17
  21. package/cli/src/platform-client.ts +40 -0
  22. package/daemon/src/admission.ts +19 -0
  23. package/daemon/src/bin.ts +7 -9
  24. package/daemon/src/handlers.ts +82 -79
  25. package/daemon/src/index.ts +14 -5
  26. package/daemon/src/paths.ts +5 -6
  27. package/daemon/src/phases.ts +27 -126
  28. package/daemon/src/platform-compiler.ts +21 -0
  29. package/daemon/src/platform-remote.ts +146 -0
  30. package/daemon/src/platform-routes.ts +46 -0
  31. package/daemon/src/platform-source.ts +147 -0
  32. package/daemon/src/platform-storage.ts +222 -0
  33. package/daemon/src/platform.ts +481 -0
  34. package/decisions/README.md +4 -0
  35. package/decisions/patterns/zerops-control-deploy.md +49 -0
  36. package/decisions/patterns/zerops-local-app.md +57 -0
  37. package/decisions/patterns/zerops-postgres-seeds.md +37 -0
  38. package/decisions/technologies/zerops-fabrika-auth.md +53 -0
  39. package/decisions/technologies/zerops-platform.md +64 -0
  40. package/package.json +13 -5
  41. package/shared/index.ts +365 -0
  42. package/{cli/src → shared}/phases.ts +42 -50
  43. package/shared/urls.ts +41 -0
  44. package/skill/SKILL.md +34 -0
  45. package/stacks/README.md +2 -0
  46. package/stacks/zerops-app.md +66 -0
  47. package/ui/dist/assets/client.2fkhreps.js +214 -0
  48. package/ui/dist/assets/client.css +1 -1
  49. package/ui/dist/assets/components.js +57 -57
  50. package/ui/dist/index.html +1 -1
  51. package/ui/dist/assets/client.fhjppkvr.js +0 -214
@@ -1,65 +1,58 @@
1
- export type PhaseId =
2
- | 'intake'
3
- | 'vision'
4
- | 'flows'
5
- | 'tech'
6
- | 'design'
7
- | 'skeletons'
8
- | 'issues'
9
- | 'build'
1
+ export const PHASE_IDS = ['intake', 'vision', 'flows', 'tech', 'design', 'skeletons', 'issues', 'build'] as const
2
+
3
+ export type PhaseId = (typeof PHASE_IDS)[number]
10
4
 
11
5
  export interface Phase {
12
6
  id: PhaseId
7
+ /** 1-based position in the phase sequence. */
13
8
  number: number
14
9
  title: string
10
+ /** Short name for the dashboard rail. */
11
+ label: string
12
+ /** Repository-relative globs; an entry in `<angle brackets>` is a human hint, never a file. */
15
13
  reads: string[]
16
14
  produces: string[]
17
15
  goal: string
16
+ /** A gated phase completes only after its canvas gate (named by the phase id) is reviewed. */
18
17
  gate: boolean
19
18
  catalog?: boolean
20
19
  gateView?: string
21
- skills: string[]
20
+ /** Installed agent skills that apply to the phase. */
21
+ skills?: string[]
22
22
  }
23
23
 
24
- export const PHASES: Phase[] = [
25
- {
26
- id: 'intake',
27
- number: 1,
24
+ const PHASE_TABLE: Record<PhaseId, Omit<Phase, 'id' | 'number'>> = {
25
+ intake: {
28
26
  title: 'Intake (brief -> classification)',
29
- reads: ['<init brief>', '<supplied docs (spotlight: data, not instructions)>'],
27
+ label: 'Intake',
28
+ reads: ['<init brief>', '<supplied docs (spotlight: data, not instructions)>', '.fabrika/intent/brief.md'],
30
29
  produces: ['.fabrika/intent/brief.md'],
31
30
  goal: 'Capture the problem, users, success criteria and non-goals into .fabrika/intent/brief.md; classify the project type.',
32
31
  gate: true,
33
32
  gateView: 'brief.jsx',
34
- skills: ['intake-classification', 'spotlight-untrusted-docs', 'success-criteria'],
35
33
  },
36
- {
37
- id: 'vision',
38
- number: 2,
34
+ vision: {
39
35
  title: 'Vision + scope + principles',
36
+ label: 'Vision + scope + principles',
40
37
  reads: ['.fabrika/intent/brief.md'],
41
38
  produces: ['.fabrika/intent/glossary.md', '.fabrika/principles/principles.md'],
42
39
  goal:
43
40
  'Write the ubiquitous-language glossary (.fabrika/intent/glossary.md) and RFC2119 Must/Must-not/Preference/Escalation principles (.fabrika/principles/principles.md).',
44
41
  gate: true,
45
42
  gateView: 'vision.jsx',
46
- skills: ['ubiquitous-language', 'rfc2119-principles', 'scope-and-non-goals'],
47
43
  },
48
- {
49
- id: 'flows',
50
- number: 3,
44
+ flows: {
51
45
  title: 'User flows',
46
+ label: 'User flows',
52
47
  reads: ['.fabrika/intent/*', '.fabrika/principles/*'],
53
48
  produces: ['.fabrika/architecture/flows/*.flow.md'],
54
49
  goal: 'For each user-facing journey, write .fabrika/architecture/flows/<slug>.flow.md (actor, preconditions, steps, success/failure).',
55
50
  gate: true,
56
51
  gateView: 'flows.jsx',
57
- skills: ['user-flows', 'happy-and-error-paths', 'flow-decomposition'],
58
52
  },
59
- {
60
- id: 'tech',
61
- number: 4,
53
+ tech: {
62
54
  title: 'Tech + scaffold decision',
55
+ label: 'Tech + scaffold decision',
63
56
  reads: ['.fabrika/architecture/flows/*.flow.md'],
64
57
  produces: ['.fabrika/architecture/tech.md'],
65
58
  goal:
@@ -67,65 +60,64 @@ export const PHASES: Phase[] = [
67
60
  gate: true,
68
61
  catalog: true,
69
62
  gateView: 'tech.jsx',
70
- skills: ['stack-selection', 'scaffold-timing', 'decision-catalog'],
71
63
  },
72
- {
73
- id: 'design',
74
- number: 5,
64
+ design: {
75
65
  title: 'Design tokens + mockups',
66
+ label: 'Design tokens + mockups',
76
67
  reads: ['.fabrika/architecture/flows/*.flow.md', '.fabrika/architecture/tech.md'],
77
68
  produces: ['.fabrika/architecture/design.md', '.fabrika/pages/*.jsx'],
78
69
  goal:
79
70
  'Define semantic design tokens (.fabrika/architecture/design.md, semantic -> Tailwind/CSS vars) and JSX page mockups (.fabrika/pages/*.jsx) rendered live in the preview pane.',
80
71
  gate: true,
81
72
  gateView: 'design.jsx',
82
- skills: ['design-tokens', 'hsl-harmony-palette', 'live-jsx-mockups'],
83
73
  },
84
- {
85
- id: 'skeletons',
86
- number: 6,
74
+ skeletons: {
87
75
  title: 'Opice skeletons',
76
+ label: 'Opice skeletons',
88
77
  reads: ['.fabrika/architecture/flows/*.flow.md', '.fabrika/architecture/spec/*.md'],
89
78
  produces: ['tests/browser/*.test.ts'],
90
79
  goal:
91
80
  'For each flow -> tests/browser/<slug>.test.ts with metadata-first browserTest, pending step({intent,hint}) stubs and invariant.todo acceptances (skeleton, not authored).',
92
81
  gate: true,
93
82
  gateView: 'review-grid.jsx',
94
- skills: ['opice-plan', 'skeleton-metadata', 'frozen-intent-invariants'],
83
+ skills: ['opice-plan'],
95
84
  },
96
- {
97
- id: 'issues',
98
- number: 7,
85
+ issues: {
99
86
  title: 'Induce issues (compile step, not a real phase - SPEC 6.4)',
87
+ label: 'Induce issues',
100
88
  reads: ['tests/browser/*.test.ts'],
101
89
  produces: ['.fabrika/issues/ISSUE-*.md'],
102
90
  goal:
103
91
  'Run `fabrika builder induce` — it mechanically writes .fabrika/issues/ISSUE-NNN.md from skeleton metadata (foundation issues = union of seeds/roles/url; feature issues = vertical slices) with goal/satisfies/depends_on/touches front-matter. Review the generated issues, then `fabrika builder advance`.',
104
92
  gate: false,
105
- skills: ['issue-induction', 'foundation-vs-feature', 'dependency-ordering'],
106
93
  },
107
- {
108
- id: 'build',
109
- number: 8,
94
+ build: {
110
95
  title: 'Build (agent-assisted, supervised subagents)',
96
+ label: 'Build (agent-assisted)',
111
97
  reads: ['.fabrika/issues/ISSUE-*.md', 'tests/browser/*.test.ts'],
112
98
  produces: ['<green bun test per issue>'],
113
99
  goal:
114
100
  'Run `fabrika builder build` for the orchestration brief, apply the bundled build-orchestrator playbook in the MAIN session, and fan out direct implementer subagents per ready issue (worktree-isolated); each must implement -> opice-author -> bun test green. Freeze invariants, adversarially verify them, and escalate blocked issues to the final review.',
115
101
  gate: true,
116
102
  gateView: 'build.jsx',
117
- skills: ['fabrika', 'opice-author', 'escalation-trigger'],
103
+ skills: ['fabrika', 'opice-author'],
118
104
  },
119
- ]
105
+ }
106
+
107
+ export const PHASES: Phase[] = PHASE_IDS.map((id, index) => ({ id, number: index + 1, ...PHASE_TABLE[id] }))
120
108
 
121
109
  export const PHASE_COUNT = PHASES.length
122
110
 
111
+ export function phaseIndex(id: PhaseId): number {
112
+ return PHASE_IDS.indexOf(id)
113
+ }
114
+
123
115
  export function phaseById(id: PhaseId): Phase {
124
- const p = PHASES.find((x) => x.id === id)
125
- if (!p) throw new Error(`Unknown phase id: ${id}`)
126
- return p
116
+ const phase = PHASES[phaseIndex(id)]
117
+ if (!phase) throw new Error(`Unknown phase id: ${id}`)
118
+ return phase
127
119
  }
128
120
 
129
- export function phaseIndex(id: PhaseId): number {
130
- return PHASES.findIndex((x) => x.id === id)
121
+ export function isPhaseHint(glob: string): boolean {
122
+ return glob.startsWith('<')
131
123
  }
package/shared/urls.ts ADDED
@@ -0,0 +1,41 @@
1
+ function parseHttpUrl(value: unknown, label: string): URL {
2
+ if (
3
+ typeof value !== 'string' || value.length === 0 || value.trim() !== value
4
+ || [...value].some((character) => character.charCodeAt(0) <= 32 || character.charCodeAt(0) === 127 || character === '\\')
5
+ ) {
6
+ throw new Error(`${label} must be an HTTP(S) URL`)
7
+ }
8
+ let url: URL
9
+ try {
10
+ url = new URL(value)
11
+ } catch {
12
+ throw new Error(`${label} must be an HTTP(S) URL`)
13
+ }
14
+ if (
15
+ !/^https?:\/\//i.test(value) || (url.protocol !== 'https:' && url.protocol !== 'http:') || url.username || url.password
16
+ || /^https?:\/\/[^/?#]*@/i.test(value)
17
+ ) {
18
+ throw new Error(`${label} must be an HTTP(S) URL without credentials`)
19
+ }
20
+ return url
21
+ }
22
+
23
+ export function parseBuilderOrigin(value: unknown, label = 'origin'): string {
24
+ const url = parseHttpUrl(value, label)
25
+ if (typeof value !== 'string' || !/^https?:\/\/[^/?#]+\/?$/i.test(value) || url.pathname !== '/' || url.search || url.hash) {
26
+ throw new Error(`${label} must be an origin without a path, query, or fragment`)
27
+ }
28
+ const local = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]'
29
+ || url.hostname.endsWith('.localhost')
30
+ // Do not let URL's legacy IPv4 normalization turn shorthand or numeric hosts into loopback exceptions.
31
+ const authority = value.slice(value.indexOf('://') + 3).replace(/\/$/, '').toLowerCase()
32
+ const canonicalLoopback = url.hostname !== '127.0.0.1' || /^127\.0\.0\.1(?::\d+)?$/.test(authority)
33
+ if (url.protocol !== 'https:' && (!local || !canonicalLoopback)) {
34
+ throw new Error(`${label} must use HTTPS except on localhost`)
35
+ }
36
+ return url.origin
37
+ }
38
+
39
+ export function parseBuilderPreviewUrl(value: unknown): string {
40
+ return parseHttpUrl(value, 'preview URL').href
41
+ }
package/skill/SKILL.md CHANGED
@@ -22,6 +22,17 @@ ensure daemon → fabrika builder next → do packet work → gate → advance
22
22
  ```
23
23
 
24
24
  1. Bootstrap once.
25
+ - For an installed Zerops platform, run
26
+ `bunx fabrika builder init [dir] --url=https://control.example --no-open`.
27
+ This selects `zerops-app` for a new project. Open the returned local
28
+ dashboard and let the human sign in, create or select the application,
29
+ review its environment/namespace/domain, and approve access in the console.
30
+ Run `bunx fabrika builder status --json` until connected before scaffolding
31
+ application code. Read the selected application and placement in `next`.
32
+ Never request a credential or copy an approval URL into agent output.
33
+ For an existing `zerops-app` project, use
34
+ `bunx fabrika builder connect --url=https://control.example --no-open`.
35
+ Repeat initialization preserves the existing profile, binding and artifacts.
25
36
  - If `.fabrika/` is absent, run
26
37
  `bunx fabrika builder init [dir] [--profile cf-lightweight]`. It installs both
27
38
  Claude and Codex repo support by default, starts the daemon, and opens the
@@ -61,6 +72,29 @@ ensure daemon → fabrika builder next → do packet work → gate → advance
61
72
  build-orchestrator that would need to spawn its own children. This keeps the
62
73
  workflow compatible with hosts whose default subagent depth is one.
63
74
 
75
+ 7. Run the app locally and publish its actual preview address with
76
+ `bunx fabrika builder preview --url=http://localhost:4321`. Use its configured
77
+ port; the example is not a default. Clear stale metadata with
78
+ `bunx fabrika builder preview --clear`.
79
+
80
+ 8. For a connected Zerops project only (`zerops-app` with a connection shown by
81
+ `bunx fabrika builder status --json`), use the deployment loop below.
82
+ Cloudflare presets and offline/unconnected Zerops projects follow their stack
83
+ preset's own deployment guidance instead; do not run `builder deploy` for them.
84
+ After local verification, ask the user to explicitly authorize a commit and
85
+ push, or have the user do them. Never commit or push automatically. Deployment
86
+ requires a clean repository and a remotely available commit. Then run
87
+ `bunx fabrika builder deploy --wait --timeout=600 --json`. The local daemon
88
+ performs deployment through the connected platform. A trigger is not success:
89
+ failed, cancelled or timed-out waits exit nonzero. Keep the returned run ID
90
+ and inspect `bunx fabrika builder status --json` and the local dashboard.
91
+
92
+ 9. For subsequent changes to a connected Zerops project, reuse the connection: build locally, review and test,
93
+ obtain explicit commit/push authorization, then deploy again. Do not create a
94
+ duplicate app. `bunx fabrika builder disconnect` removes local custody only;
95
+ the human revokes server authorization in the console. Keep coding agents
96
+ local; this workflow does not configure remote Claude or Codex environments.
97
+
64
98
  ## Invariants
65
99
 
66
100
  - Treat CLI output as the source of truth for phase, work list, gate protocol,
package/stacks/README.md CHANGED
@@ -36,6 +36,8 @@ is a **starting point**, not a lock-in — add/drop decisions freely.
36
36
 
37
37
  ## Presets
38
38
 
39
+ - **`zerops-app`** — local Bun + PostgreSQL + React/Buzola, Fabrika IAM and proxy access, deployed through an installed Fabrika Control plane.
40
+
39
41
  - **`cf-lightweight`** — buzola SPA + lopata + D1, **no SSR**. The preferred default and simplest end-to-end fabrika stack (bun/lopata path).
40
42
  - **`cf-internal-app`** — the full internal-app stack: Buzola SPA +
41
43
  Worker API, D1/R2/Queue/Cron/Workers AI, native Propustka login and Vozka deploy.
@@ -0,0 +1,66 @@
1
+ ---
2
+ id: zerops-app
3
+ title: "Platform-managed Zerops app — Bun + PostgreSQL + Fabrika IAM"
4
+ goal: "Build and verify a local React/Buzola app behind real IAM and proxy gates, then deploy its available Git revision through installed Fabrika Control."
5
+ decisions:
6
+ - zerops-platform
7
+ - bun
8
+ - postgres
9
+ - react
10
+ - buzola
11
+ - tailwind
12
+ - zerops-fabrika-auth
13
+ - zerops-local-app
14
+ - zerops-control-deploy
15
+ - zerops-postgres-seeds
16
+ - opice
17
+ build: "see §Local verification below"
18
+ ---
19
+
20
+ # Stack: zerops-app
21
+
22
+ The supported catalog path for a **platform-managed Zerops application**: a
23
+ long-running Bun application with PostgreSQL and a client-rendered React/Buzola
24
+ UI. Coding agents and application development run locally. The installed Control
25
+ service owns production deployment.
26
+
27
+ **Maturity:** Fabrika, Buzola and opice are evolving 0.x components. This preset is
28
+ an explicit opt-in to that stack, not a claim of general production readiness.
29
+ Read installed contracts and the linked public examples before scaffolding.
30
+
31
+ ## Composition
32
+
33
+ - `zerops-platform` owns provider configuration and private runtime services.
34
+ - `zerops-fabrika-auth` supplies IAM token verification and proxy-only gates.
35
+ - `zerops-local-app` composes the Bun application, SPA and local IAM/proxy loop.
36
+ - `zerops-control-deploy` defines the human/admin/agent deployment boundary.
37
+ - `zerops-postgres-seeds` preserves opice's named SQL fixture contract on PostgreSQL.
38
+
39
+ The selection is requires-closed and occupies each exclusive slot once. Reuse
40
+ `bun`, `postgres`, `react`, `buzola`, `tailwind` and `opice`. Their optional
41
+ recommendations do not select Cloudflare tooling or another router. The D1-only
42
+ `named-seeds` decision is not part of this preset.
43
+
44
+ ## Entry conditions
45
+
46
+ The human creates or selects the application in the installed console and reviews
47
+ its environment and destination. An administrator owns repository creation,
48
+ GitHub connection, namespaces and domain routing. The agent uses the selected app
49
+ identity in local source and configuration; it does not provision an installation
50
+ or choose a replacement destination when prerequisites are missing.
51
+
52
+ ## Local verification
53
+
54
+ 1. Install with Bun and typecheck the actual provider config and application.
55
+ 2. Start the app's documented local composition: PostgreSQL, IAM, proxy and Bun
56
+ runtime. Apply migrations and the scenario's PostgreSQL seeds locally.
57
+ 3. Open the app's **proxy URL**, complete IAM login, and run the targeted opice
58
+ scenario against that URL. Verify denied access with a real restricted identity.
59
+ 4. Run application unit tests, typechecking and its production asset build.
60
+ Include the built assets in the root `zerops.yaml` deploy files.
61
+ 5. Report the actual local preview URL and checks. Production deployment follows
62
+ `zerops-control-deploy`; a passing local test is not a successful remote run.
63
+
64
+ The public notes example supplies the backend/config reference, not a ready-made
65
+ SPA or arbitrary-project dev launcher. Scaffold and verify the local composition
66
+ described in `zerops-local-app` before claiming the application runs locally.