@enderfga/claw-orchestrator 3.7.1 → 4.0.3

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 (115) hide show
  1. package/README.md +45 -3
  2. package/configs/autoloop-coder-prompt.md +65 -28
  3. package/configs/autoloop-planner-prompt.md +103 -42
  4. package/configs/autoloop-reviewer-prompt.md +58 -30
  5. package/dist/bin/cli.js +30 -7
  6. package/dist/bin/cli.js.map +1 -1
  7. package/dist/src/autoloop/dispatcher.js +16 -2
  8. package/dist/src/autoloop/dispatcher.js.map +1 -1
  9. package/dist/src/autoloop/messages.d.ts +0 -2
  10. package/dist/src/autoloop/messages.js +0 -2
  11. package/dist/src/autoloop/messages.js.map +1 -1
  12. package/dist/src/autoloop/planner-tools.d.ts +11 -6
  13. package/dist/src/autoloop/planner-tools.js +24 -7
  14. package/dist/src/autoloop/planner-tools.js.map +1 -1
  15. package/dist/src/autoloop/runner.d.ts +1 -2
  16. package/dist/src/autoloop/runner.js +1 -2
  17. package/dist/src/autoloop/runner.js.map +1 -1
  18. package/dist/src/autoloop/types.d.ts +0 -2
  19. package/dist/src/autoloop/types.js +0 -2
  20. package/dist/src/autoloop/types.js.map +1 -1
  21. package/dist/src/council.js +1 -0
  22. package/dist/src/council.js.map +1 -1
  23. package/dist/src/dashboard/index.html +1066 -12
  24. package/dist/src/embedded-server.d.ts +7 -0
  25. package/dist/src/embedded-server.js +359 -17
  26. package/dist/src/embedded-server.js.map +1 -1
  27. package/dist/src/index.js +222 -1
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/session-manager.d.ts +98 -1
  30. package/dist/src/session-manager.js +340 -5
  31. package/dist/src/session-manager.js.map +1 -1
  32. package/dist/src/ultraapp/build-events.d.ts +46 -0
  33. package/dist/src/ultraapp/build-events.js +6 -0
  34. package/dist/src/ultraapp/build-events.js.map +1 -0
  35. package/dist/src/ultraapp/build.d.ts +39 -0
  36. package/dist/src/ultraapp/build.js +111 -0
  37. package/dist/src/ultraapp/build.js.map +1 -0
  38. package/dist/src/ultraapp/conventions.d.ts +8 -0
  39. package/dist/src/ultraapp/conventions.js +248 -0
  40. package/dist/src/ultraapp/conventions.js.map +1 -0
  41. package/dist/src/ultraapp/council-adapter.d.ts +49 -0
  42. package/dist/src/ultraapp/council-adapter.js +152 -0
  43. package/dist/src/ultraapp/council-adapter.js.map +1 -0
  44. package/dist/src/ultraapp/deploy.d.ts +45 -0
  45. package/dist/src/ultraapp/deploy.js +82 -0
  46. package/dist/src/ultraapp/deploy.js.map +1 -0
  47. package/dist/src/ultraapp/diff-apply.d.ts +15 -0
  48. package/dist/src/ultraapp/diff-apply.js +48 -0
  49. package/dist/src/ultraapp/diff-apply.js.map +1 -0
  50. package/dist/src/ultraapp/docker.d.ts +57 -0
  51. package/dist/src/ultraapp/docker.js +83 -0
  52. package/dist/src/ultraapp/docker.js.map +1 -0
  53. package/dist/src/ultraapp/feedback-classifier.d.ts +24 -0
  54. package/dist/src/ultraapp/feedback-classifier.js +85 -0
  55. package/dist/src/ultraapp/feedback-classifier.js.map +1 -0
  56. package/dist/src/ultraapp/files.d.ts +24 -0
  57. package/dist/src/ultraapp/files.js +79 -0
  58. package/dist/src/ultraapp/files.js.map +1 -0
  59. package/dist/src/ultraapp/fix-on-failure-session.d.ts +23 -0
  60. package/dist/src/ultraapp/fix-on-failure-session.js +48 -0
  61. package/dist/src/ultraapp/fix-on-failure-session.js.map +1 -0
  62. package/dist/src/ultraapp/fix-on-failure.d.ts +39 -0
  63. package/dist/src/ultraapp/fix-on-failure.js +69 -0
  64. package/dist/src/ultraapp/fix-on-failure.js.map +1 -0
  65. package/dist/src/ultraapp/host-strategy.d.ts +57 -0
  66. package/dist/src/ultraapp/host-strategy.js +205 -0
  67. package/dist/src/ultraapp/host-strategy.js.map +1 -0
  68. package/dist/src/ultraapp/interview-parser.d.ts +41 -0
  69. package/dist/src/ultraapp/interview-parser.js +83 -0
  70. package/dist/src/ultraapp/interview-parser.js.map +1 -0
  71. package/dist/src/ultraapp/interview-tools.d.ts +16 -0
  72. package/dist/src/ultraapp/interview-tools.js +36 -0
  73. package/dist/src/ultraapp/interview-tools.js.map +1 -0
  74. package/dist/src/ultraapp/json-patch.d.ts +6 -0
  75. package/dist/src/ultraapp/json-patch.js +78 -0
  76. package/dist/src/ultraapp/json-patch.js.map +1 -0
  77. package/dist/src/ultraapp/lifecycle.d.ts +38 -0
  78. package/dist/src/ultraapp/lifecycle.js +31 -0
  79. package/dist/src/ultraapp/lifecycle.js.map +1 -0
  80. package/dist/src/ultraapp/manager.d.ts +150 -0
  81. package/dist/src/ultraapp/manager.js +684 -0
  82. package/dist/src/ultraapp/manager.js.map +1 -0
  83. package/dist/src/ultraapp/narrator-prompt.d.ts +10 -0
  84. package/dist/src/ultraapp/narrator-prompt.js +54 -0
  85. package/dist/src/ultraapp/narrator-prompt.js.map +1 -0
  86. package/dist/src/ultraapp/narrator.d.ts +49 -0
  87. package/dist/src/ultraapp/narrator.js +83 -0
  88. package/dist/src/ultraapp/narrator.js.map +1 -0
  89. package/dist/src/ultraapp/patcher.d.ts +35 -0
  90. package/dist/src/ultraapp/patcher.js +159 -0
  91. package/dist/src/ultraapp/patcher.js.map +1 -0
  92. package/dist/src/ultraapp/router.d.ts +39 -0
  93. package/dist/src/ultraapp/router.js +139 -0
  94. package/dist/src/ultraapp/router.js.map +1 -0
  95. package/dist/src/ultraapp/spec-delta.d.ts +18 -0
  96. package/dist/src/ultraapp/spec-delta.js +35 -0
  97. package/dist/src/ultraapp/spec-delta.js.map +1 -0
  98. package/dist/src/ultraapp/spec.d.ts +82 -0
  99. package/dist/src/ultraapp/spec.js +125 -0
  100. package/dist/src/ultraapp/spec.js.map +1 -0
  101. package/dist/src/ultraapp/store.d.ts +67 -0
  102. package/dist/src/ultraapp/store.js +177 -0
  103. package/dist/src/ultraapp/store.js.map +1 -0
  104. package/dist/src/ultraapp/versions.d.ts +50 -0
  105. package/dist/src/ultraapp/versions.js +65 -0
  106. package/dist/src/ultraapp/versions.js.map +1 -0
  107. package/openclaw.plugin.json +15 -1
  108. package/package.json +3 -1
  109. package/skills/SKILL.md +2 -2
  110. package/skills/references/autoloop.md +7 -4
  111. package/skills/references/dashboard.md +153 -0
  112. package/skills/references/mcp.md +2 -2
  113. package/skills/references/tools.md +183 -0
  114. package/skills/references/ultraapp.md +203 -0
  115. package/skills/ultraapp/SKILL.md +112 -0
@@ -0,0 +1,248 @@
1
+ /**
2
+ * The architectural conventions every council-generated codebase must satisfy.
3
+ * Embedded in the council super-task prompt by council-adapter.ts.
4
+ *
5
+ * Single source of truth: any change here flows to all builds, including
6
+ * v0.5 spec-delta reruns and v1.0 reference-trace assertions.
7
+ */
8
+ export const ARCHITECTURAL_CONVENTIONS = `
9
+ You are building a deployable web app. Your codebase MUST satisfy the following
10
+ architectural conventions. They are non-negotiable; non-compliant codebases
11
+ will be rejected at fix-on-failure or deploy time.
12
+
13
+ ## 1. Path-based deploy
14
+
15
+ The app will be served behind a reverse proxy at /forge/<slug>/ on port 19000.
16
+ The slug equals AppSpec.meta.name and is provided to your container as the
17
+ BASE_PATH environment variable at "docker run" time.
18
+
19
+ You MUST configure your framework's base path setting from BASE_PATH:
20
+ - Next.js: next.config.js → basePath: process.env.BASE_PATH ?? ''
21
+ - Vite: vite.config.ts → base: process.env.BASE_PATH ?? '/'
22
+ - SvelteKit: paths.base via $env/dynamic/private
23
+ - Hono/express: prefix routes with process.env.BASE_PATH
24
+
25
+ All in-app links and fetches MUST be relative to base; no hardcoded absolute
26
+ paths. <base href> tag in HTML must be set from BASE_PATH.
27
+
28
+ ## 2. Async file-queue runtime
29
+
30
+ The app MUST expose exactly these HTTP endpoints. No more, no fewer (apart from
31
+ framework-required static asset routes):
32
+
33
+ GET / — render the input form derived from AppSpec.inputs
34
+ POST /run — multipart upload of inputs. Returns
35
+ { jobId, statusUrl, resultUrl }. Synchronously
36
+ writes inputs to /data/jobs/<jobId>/inputs/ and
37
+ returns immediately; pipeline runs in a background
38
+ worker.
39
+ GET /status/:jobId — { progress: 0..1, currentStep: string,
40
+ state: 'queued'|'running'|'done'|'failed',
41
+ error?: string }
42
+ GET /result/:jobId — returns output file(s) per AppSpec.outputs.
43
+ Returns 404 until state === 'done'.
44
+ GET /health — plain "200 OK" for the deploy health check.
45
+
46
+ Persistence layout under the data dir:
47
+
48
+ $DATA_DIR/
49
+ jobs/
50
+ <jobId>/
51
+ inputs/<name> — uploaded files
52
+ outputs/<name> — pipeline outputs
53
+ state.json — { progress, currentStep, state, error }
54
+ log.txt — step-by-step log
55
+
56
+ **Data path MUST be controlled by an env var, not hard-coded.** Use
57
+ \`process.env.DATA_DIR ?? '/data'\` (the '/data' default is for Docker
58
+ mode where it's a mounted volume; in host mode the orchestrator passes a
59
+ per-run writable path via DATA_DIR). Never hard-code '/data' in source —
60
+ it'll fail with EACCES on host runtime since '/data' isn't writable.
61
+
62
+ NO database. NO SQLite. File-based queue only. Job retention: 7 days, then
63
+ GC by a background sweeper that runs once per hour.
64
+
65
+ ## 3. BYOK (only if AppSpec.runtime.needsLLM is true)
66
+
67
+ If pipelines call an LLM provider, the API key MUST come from the user's
68
+ browser localStorage and be passed in fetch requests directly from browser to
69
+ provider. The server MUST NEVER receive the key.
70
+
71
+ Frontend:
72
+ - On first load, read localStorage for byok.<provider> keys.
73
+ - If missing, show a modal: "This app uses <Provider>. Enter your API key.
74
+ Stored in your browser only."
75
+ - Store via localStorage.setItem('byok.<provider>', key).
76
+
77
+ Server:
78
+ - MUST NOT read process.env.<*>_API_KEY in any non-test file.
79
+ - MUST NOT accept an "Authorization" or "x-api-key" header on /run.
80
+ - A custom ESLint rule (ship as eslint-plugin-no-server-keys with the
81
+ skeleton) enforces this. Smoke test asserts /run rejects requests carrying
82
+ any auth headers. The server must never receive the key.
83
+
84
+ If AppSpec.runtime.needsLLM is false, omit the BYOK panel entirely.
85
+
86
+ ## 4. Dockerfile + smoke test
87
+
88
+ Single Dockerfile at the repo root. Multi-stage. Final stage exposes port 3000
89
+ internally. ENTRYPOINT runs the production server. Build args:
90
+ - BASE_PATH (passed by deployer at run time, defaulted at build time to '')
91
+
92
+ Smoke test: package.json must expose "scripts.smoke" that drives ONE complete
93
+ job using AppSpec.inputs[].examples[0].ref files copied into the image at
94
+ build time under $DATA_DIR/_smoke/. The smoke test asserts pipeline completes and
95
+ output matches AppSpec.outputs[].type. fix-on-failure runs "npm run smoke" and
96
+ gates build-success on its passing.
97
+
98
+ Smoke test must complete in < 90 seconds for the reference-trace inputs
99
+ provided. If your pipeline genuinely takes longer, design the smoke test to
100
+ use a degenerate-small input (e.g., 5-second clip vs 5-minute) but still
101
+ exercise every step of the DAG.
102
+
103
+ ## 5. Council voting protocol
104
+
105
+ This is a 3-agent council collaborating in separate git worktrees. Each round:
106
+ - Read the spec + the architectural conventions above + the current codebase
107
+ state on the shared 'main' branch (other agents' work).
108
+ - Make changes in your worktree, commit, merge to main with a clear commit
109
+ message.
110
+ - At the end of your turn, vote with the literal marker:
111
+ [CONSENSUS: YES] — if codebase fully implements spec + all conventions met
112
+ AND every agent has executed the §7g frontend gate
113
+ [CONSENSUS: NO] — otherwise; explain what's still wrong/missing
114
+
115
+ Collaboration ends only when ALL 3 agents vote YES, or after maxRounds=8.
116
+
117
+ ## 6. Tech stack
118
+
119
+ You may pick any modern TypeScript or JavaScript framework that satisfies the
120
+ above. Recommended choices: Next.js 15+, Vite + Hono, SvelteKit. AVOID:
121
+ Python (this is a Node ecosystem app), pure static-site generators (need
122
+ server endpoints).
123
+
124
+ ## 7. Frontend quality (MANDATORY — not optional)
125
+
126
+ This is a one-click app generator. The output must look like a real product
127
+ that a startup could put in front of users tomorrow. "Functional minimum"
128
+ is a NO vote. Bare HTML with browser-default styling is a NO vote.
129
+ Hand-rolled CSS without a system is a NO vote. The bar is professional
130
+ visual polish, not "it works".
131
+
132
+ ### 7a. Styling system (pick one — none of these are optional)
133
+ - Tailwind CSS v4+ with a configured theme (preferred), OR
134
+ - A modern component library that brings its own styling (shadcn/ui +
135
+ Tailwind, daisyUI, Mantine, Chakra UI v3+), OR
136
+ - CSS Modules + a design-token file with explicit color/spacing/typography
137
+ scales — NO inline \`style=\` attributes, NO ad-hoc per-component CSS
138
+ files.
139
+
140
+ Icons: lucide-react / @heroicons/react / phosphor-react. NO emoji as UI.
141
+ NO unicode glyphs (✓ ✗ →) standing in for icons.
142
+
143
+ ### 7b. Layout & typography
144
+ - Centered max-width container (≤ 1280px on desktop). NO full-bleed walls
145
+ of text.
146
+ - Type hierarchy: h1 / h2 / body MUST be visually distinct (size + weight).
147
+ Body line-height ≥ 1.5. Pick a deliberate font stack (Inter, system-ui,
148
+ or a Google Font); browser default is a NO vote.
149
+ - Generous whitespace. ≥ 16px padding around interactive elements.
150
+ Sections separated by ≥ 32px vertical rhythm.
151
+ - Mobile-responsive: layout MUST work at 375px viewport width with no
152
+ horizontal scrollbar. Test in browser devtools (or check media-query
153
+ breakpoints) before voting.
154
+ - Real favicon (not the default Vite/Next placeholder) and meaningful
155
+ <title> derived from AppSpec.meta.title.
156
+
157
+ ### 7c. State coverage (every async surface)
158
+ For every page/panel that loads or submits data, ALL FOUR states MUST be
159
+ explicitly designed:
160
+ - Empty: friendly copy + neutral illustration or icon (e.g. "No jobs yet
161
+ — submit one above").
162
+ - Loading: skeleton screens or spinners with context ("Resizing 12
163
+ images…"), NEVER raw "Loading…" text.
164
+ - Error: human-readable message + retry action, NEVER raw error JSON or
165
+ stack traces.
166
+ - Success: visible confirmation (toast, inline check, or transition to
167
+ result view).
168
+
169
+ ### 7d. Form quality (the /run input form)
170
+ The input form auto-derived from AppSpec.inputs MUST have:
171
+ - Labels positioned above inputs (NOT placeholder-only).
172
+ - File uploads via drag-and-drop zone with file preview, file name, file
173
+ size, and remove-button per file.
174
+ - Inline validation errors (red border + helper text under the field).
175
+ - Submit button disabled while submitting + showing a spinner with
176
+ context ("Uploading…" → "Processing…").
177
+ - For 'files' input type: thumbnail grid for image MIME types; list with
178
+ type-icon for other types.
179
+
180
+ ### 7e. Result presentation
181
+ The result page MUST present outputs in a way appropriate to their type:
182
+ - Image outputs: gallery view with lightbox-on-click, plus per-image
183
+ download.
184
+ - File outputs: clear download button with file size and file type label.
185
+ - Text outputs: rendered (markdown if applicable), monospace block for
186
+ code, with copy-to-clipboard.
187
+ - ZIP outputs: list contents (filename + size) before the download CTA.
188
+ NO "Click here to download result.bin" with no preview or context.
189
+
190
+ ### 7f. Theme
191
+ Pick ONE deliberate theme:
192
+ - Light only (modern, clean — Linear/Notion aesthetic), OR
193
+ - Dark only (developer-tool aesthetic), OR
194
+ - Both with a toggle (toggle MUST persist in localStorage).
195
+ Browser-default styling is a NO vote.
196
+
197
+ Brand colors: define as CSS variables or Tailwind theme extension. Use a
198
+ single accent color consistently across primary actions.
199
+ AppSpec.ui.accentColor (if present) is the source of truth.
200
+
201
+ ### 7g. Council frontend gate (enforcement — screenshots required)
202
+
203
+ **Reading the source code or the rendered HTML is NOT sufficient.** Meta
204
+ tags, class names, and \`@media\` queries don't tell you what the page
205
+ actually looks like. You MUST capture screenshots and look at the pixels
206
+ before voting.
207
+
208
+ Before voting [CONSENSUS: YES], EVERY agent MUST:
209
+
210
+ 1. Start the dev/preview server in their worktree (\`npm run preview\` or
211
+ \`npm run dev\` and wait for "ready").
212
+ 2. **Capture screenshots at TWO viewports** using Chrome headless:
213
+ \`\`\`
214
+ chrome --headless=new --hide-scrollbars --window-size=1440,900 \\
215
+ --virtual-time-budget=4000 --screenshot=/tmp/ua-shot-desktop.png <URL>
216
+ chrome --headless=new --hide-scrollbars --window-size=375,812 \\
217
+ --virtual-time-budget=4000 --screenshot=/tmp/ua-shot-mobile.png <URL>
218
+ \`\`\`
219
+ (\`chrome\` may be \`google-chrome\`, \`chromium\`, or
220
+ \`/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\`.)
221
+ 3. **Open the PNG files and look at the rendering.** Confirm by eye:
222
+ - Desktop: layout matches §7a–§7f. Centered, generous whitespace,
223
+ readable type hierarchy, real icons (not emoji), polished form,
224
+ consistent accent colour.
225
+ - **Mobile (375px): NO horizontal overflow.** Every text block wraps
226
+ at word boundaries, not letter boundaries. The form card and all
227
+ inner content fit within the viewport. Section header hints
228
+ either fit or are hidden on mobile. The h1 fits the viewport at
229
+ its mobile font-size.
230
+ 4. **For each of the four states (§7c)** — empty, loading, error, success
231
+ — drive the app to that state (programmatically or via a saved
232
+ fixture) and screenshot it. Visual verification only; "the code path
233
+ exists" is not enough.
234
+ 5. If the screenshots reveal any §7a–§7f gap, vote NO and cite the
235
+ specific screenshot region and criterion. Iterate until the
236
+ screenshots are publishable.
237
+
238
+ **Refining via meta tags / class names alone is insufficient.** A
239
+ \`text-3xl sm:text-4xl\` h1 may still overflow on 375px if the words are
240
+ long; only the rendered screenshot tells you that. A \`flex
241
+ justify-between\` row with two long children may overflow even if both
242
+ children have the right text classes. Trust the pixels.
243
+
244
+ The frontend gate is as binding as the smoke-test gate. A green smoke
245
+ test with a bare-bones UI — or a UI that overflows on mobile — is still
246
+ a NO vote.
247
+ `.trim();
248
+ //# sourceMappingURL=conventions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conventions.js","sourceRoot":"","sources":["../../../src/ultraapp/conventions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,yBAAyB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+OxC,CAAC,IAAI,EAAE,CAAC"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Council super-task adapter.
3
+ *
4
+ * Wraps the existing Council class (src/council.ts) for ultraapp's purpose:
5
+ * given an AppSpec, prepare a fresh git project dir, run a council session
6
+ * inside it, and return the consensus codebase path on success.
7
+ *
8
+ * The "projectDir" given to Council is a brand-new empty git repo — NOT the
9
+ * claw-orchestrator repo. Council creates its own git worktrees inside that
10
+ * project dir and merges to its main branch. After consensus, we copy the
11
+ * main HEAD into versions/v1/codebase as the buildable artifact.
12
+ */
13
+ import type { AppSpec } from './spec.js';
14
+ import type { CouncilSession, CouncilConfig } from '../types.js';
15
+ interface SessionManagerLike {
16
+ startSession(c: {
17
+ name?: string;
18
+ engine?: string;
19
+ model?: string;
20
+ cwd?: string;
21
+ systemPrompt?: string;
22
+ permissionMode?: string;
23
+ }): Promise<{
24
+ name: string;
25
+ }>;
26
+ sendMessage(name: string, msg: string): Promise<{
27
+ output: string;
28
+ }>;
29
+ stopSession(name: string): Promise<void>;
30
+ }
31
+ export interface CouncilSynthArgs {
32
+ spec: AppSpec;
33
+ runId: string;
34
+ /** Per-run dir under ~/.claw-orchestrator/ultraapps/<runId>/. Adapter creates
35
+ `<runDir>/council-project/` (fresh git repo) and `<runDir>/versions/v1/codebase/`. */
36
+ runDir: string;
37
+ sessionManager: SessionManagerLike;
38
+ /** Injectable for tests. In production, defaults to running the real Council. */
39
+ councilRun?: (cfg: CouncilConfig, sm: SessionManagerLike, task: string) => Promise<CouncilSession>;
40
+ }
41
+ export interface CouncilSynthResult {
42
+ ok: boolean;
43
+ worktreePath?: string;
44
+ reason?: string;
45
+ rounds: number;
46
+ }
47
+ export declare function composeCouncilPrompt(spec: AppSpec): string;
48
+ export declare function runCouncilSynth(args: CouncilSynthArgs): Promise<CouncilSynthResult>;
49
+ export {};
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Council super-task adapter.
3
+ *
4
+ * Wraps the existing Council class (src/council.ts) for ultraapp's purpose:
5
+ * given an AppSpec, prepare a fresh git project dir, run a council session
6
+ * inside it, and return the consensus codebase path on success.
7
+ *
8
+ * The "projectDir" given to Council is a brand-new empty git repo — NOT the
9
+ * claw-orchestrator repo. Council creates its own git worktrees inside that
10
+ * project dir and merges to its main branch. After consensus, we copy the
11
+ * main HEAD into versions/v1/codebase as the buildable artifact.
12
+ */
13
+ import * as fs from 'node:fs';
14
+ import * as fsp from 'node:fs/promises';
15
+ import * as path from 'node:path';
16
+ import { spawnSync } from 'node:child_process';
17
+ import { ARCHITECTURAL_CONVENTIONS } from './conventions.js';
18
+ export function composeCouncilPrompt(spec) {
19
+ return `
20
+ You are one of three Claude Opus agents collaborating to build a complete web
21
+ application from an AppSpec. Each agent works in its own git worktree and
22
+ merges to the shared 'main' branch. Convergence is by 3-way YES vote.
23
+
24
+ # AppSpec
25
+
26
+ \`\`\`json
27
+ ${JSON.stringify(spec, null, 2)}
28
+ \`\`\`
29
+
30
+ The slug is "${spec.meta.name}" — use it for container name, BASE_PATH, and
31
+ all routing.
32
+
33
+ # Architectural conventions (NON-NEGOTIABLE)
34
+
35
+ ${ARCHITECTURAL_CONVENTIONS}
36
+
37
+ # Voting
38
+
39
+ Refer to the voting protocol in section 5 of the conventions above. Vote
40
+ [CONSENSUS: YES] only when (a) every AppSpec field is implemented, (b) every
41
+ architectural convention is met, (c) "npm run build && npm test && docker
42
+ build . && npm run smoke" all pass green in your worktree, AND (d) you have
43
+ personally executed the §7g frontend gate by capturing Chrome headless
44
+ screenshots at BOTH 1440×900 and 375×812 and visually inspecting the PNGs
45
+ — not by reading source code or meta tags. A green smoke test with a
46
+ bare-bones UI, or a UI that overflows on mobile, is still a NO vote.
47
+ `.trim();
48
+ }
49
+ export async function runCouncilSynth(args) {
50
+ const projectDir = path.join(args.runDir, 'council-project');
51
+ await initFreshGitRepo(projectDir);
52
+ const cfg = buildCouncilConfig(projectDir, args.spec.meta.name);
53
+ const task = composeCouncilPrompt(args.spec);
54
+ const runner = args.councilRun ?? defaultCouncilRun;
55
+ let session;
56
+ try {
57
+ session = await runner(cfg, args.sessionManager, task);
58
+ }
59
+ catch (e) {
60
+ return { ok: false, reason: `council threw: ${e.message}`, rounds: 0 };
61
+ }
62
+ const rounds = session.responses.length > 0 ? Math.max(...session.responses.map((r) => r.round)) : 0;
63
+ // Council's lifecycle on success is: round-with-all-YES → status='awaiting_user'
64
+ // (waiting for the human-review tools council_review/council_accept). For
65
+ // ultraapp the human-review step is unwanted — once consensus is reached the
66
+ // codebase is shipped automatically. So both 'consensus' and 'awaiting_user'
67
+ // (and 'accepted', for symmetry if a caller has already accepted) count as
68
+ // success.
69
+ const successStatuses = new Set(['consensus', 'awaiting_user', 'accepted']);
70
+ if (!successStatuses.has(session.status)) {
71
+ if (session.status === 'max_rounds') {
72
+ return {
73
+ ok: false,
74
+ reason: `council reached max rounds (${rounds}) without consensus`,
75
+ rounds,
76
+ };
77
+ }
78
+ return { ok: false, reason: `council ended in status '${session.status}'`, rounds };
79
+ }
80
+ // Snapshot main HEAD into versions/v1/codebase
81
+ const codebaseDir = path.join(args.runDir, 'versions', 'v1', 'codebase');
82
+ await snapshotMainBranch(projectDir, codebaseDir);
83
+ return { ok: true, worktreePath: codebaseDir, rounds };
84
+ }
85
+ function buildCouncilConfig(projectDir, slug) {
86
+ const personaIntro = `You are one of three Claude Opus agents collaborating to build the "${slug}" web app from a detailed AppSpec. You work in your own git worktree and merge to a shared 'main' branch. Convergence is by 3-way YES vote.`;
87
+ return {
88
+ name: `ultraapp-${slug}`,
89
+ projectDir,
90
+ maxRounds: 8,
91
+ agents: [
92
+ {
93
+ name: 'agent-A',
94
+ emoji: '🔵',
95
+ persona: `${personaIntro} You lead with architecture and the framework choice; scaffold the project, define folder layout, set up basic /health and BYOK handling. Bias to forward progress.`,
96
+ engine: 'claude',
97
+ model: 'claude-opus-4-7',
98
+ },
99
+ {
100
+ name: 'agent-B',
101
+ emoji: '🟠',
102
+ persona: `${personaIntro} You implement the pipeline DAG: input ingestion, step execution, output assembly, persistence under /data. You make sure /run, /status, /result wire to real work.`,
103
+ engine: 'claude',
104
+ model: 'claude-opus-4-7',
105
+ },
106
+ {
107
+ name: 'agent-C',
108
+ emoji: '🟢',
109
+ persona: `${personaIntro} You verify and harden: smoke test, Dockerfile multi-stage, base path correctness, ESLint rule for no-server-keys, AND the §7g frontend gate. The §7g gate is binding: you MUST capture Chrome headless screenshots at 1440×900 AND 375×812, open the PNGs, and verify by eye that the rendering matches §7a–§7f at BOTH viewports — particularly that mobile has no horizontal overflow and h1 wraps at word (not letter) boundaries. Reading source code or meta tags is insufficient evidence; only the screenshots count. You vote NO until "npm run build && npm test && docker build . && npm run smoke" all pass green AND your screenshots are publishable. A working but ugly app, or one that overflows on mobile, is a NO vote.`,
110
+ engine: 'claude',
111
+ model: 'claude-opus-4-7',
112
+ },
113
+ ],
114
+ defaultPermissionMode: 'bypassPermissions',
115
+ };
116
+ }
117
+ async function initFreshGitRepo(dir) {
118
+ await fsp.mkdir(dir, { recursive: true });
119
+ if (fs.existsSync(path.join(dir, '.git')))
120
+ return;
121
+ for (const args of [
122
+ ['init', '-b', 'main'],
123
+ ['config', 'user.email', 'ultraapp@example.local'],
124
+ ['config', 'user.name', 'ultraapp'],
125
+ ['commit', '--allow-empty', '-m', 'init'],
126
+ ]) {
127
+ const r = spawnSync('git', args, { cwd: dir });
128
+ if (r.status !== 0)
129
+ throw new Error(`git ${args.join(' ')} failed: ${r.stderr.toString()}`);
130
+ }
131
+ }
132
+ async function snapshotMainBranch(projectDir, dest) {
133
+ await fsp.mkdir(dest, { recursive: true });
134
+ const r = spawnSync('git', ['-C', projectDir, 'archive', 'main'], { encoding: 'buffer' });
135
+ if (r.status !== 0)
136
+ throw new Error(`git archive failed: ${r.stderr?.toString()}`);
137
+ const t = spawnSync('tar', ['-x', '-C', dest], { input: r.stdout });
138
+ if (t.status !== 0)
139
+ throw new Error(`tar -x failed: ${t.stderr?.toString()}`);
140
+ // Init dest as its own git repo so fix-on-failure can commit fixes
141
+ spawnSync('git', ['init', '-b', 'main'], { cwd: dest });
142
+ spawnSync('git', ['config', 'user.email', 'ultraapp@example.local'], { cwd: dest });
143
+ spawnSync('git', ['config', 'user.name', 'ultraapp'], { cwd: dest });
144
+ spawnSync('git', ['add', '.'], { cwd: dest });
145
+ spawnSync('git', ['commit', '--allow-empty', '-m', 'consensus snapshot'], { cwd: dest });
146
+ }
147
+ async function defaultCouncilRun(cfg, sm, task) {
148
+ const { Council } = await import('../council.js');
149
+ const council = new Council(cfg, sm);
150
+ return council.run(task);
151
+ }
152
+ //# sourceMappingURL=council-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"council-adapter.js","sourceRoot":"","sources":["../../../src/ultraapp/council-adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Deploy orchestrator: docker build → allocate port → docker run →
3
+ * register with router → wait for /health to pass → return URL.
4
+ *
5
+ * All side-effecting collaborators (docker, router, fetch) are injected so
6
+ * unit tests can drive the state machine without real docker.
7
+ */
8
+ import type { BuildResult, DockerRunResult } from './docker.js';
9
+ export interface DeployArgs {
10
+ runId: string;
11
+ version: string;
12
+ worktreePath: string;
13
+ slug: string;
14
+ hostDataDir: string;
15
+ dockerBuild: (a: {
16
+ tag: string;
17
+ cwd: string;
18
+ buildArgs: Record<string, string>;
19
+ }) => Promise<BuildResult>;
20
+ dockerRun: (a: {
21
+ image: string;
22
+ name: string;
23
+ hostPort: number;
24
+ env: Record<string, string>;
25
+ volumes: Record<string, string>;
26
+ }) => Promise<DockerRunResult>;
27
+ router: {
28
+ register: (slug: string, port: number) => void;
29
+ port: () => number;
30
+ };
31
+ fetchFn: typeof fetch;
32
+ takenPorts: Set<number>;
33
+ healthTimeoutMs?: number;
34
+ healthIntervalMs?: number;
35
+ }
36
+ export interface DeployResult {
37
+ ok: boolean;
38
+ url?: string;
39
+ port?: number;
40
+ containerName?: string;
41
+ imageTag?: string;
42
+ reason?: string;
43
+ }
44
+ export declare function allocatePort(taken: Set<number>): number;
45
+ export declare function deployArtifact(args: DeployArgs): Promise<DeployResult>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Deploy orchestrator: docker build → allocate port → docker run →
3
+ * register with router → wait for /health to pass → return URL.
4
+ *
5
+ * All side-effecting collaborators (docker, router, fetch) are injected so
6
+ * unit tests can drive the state machine without real docker.
7
+ */
8
+ import * as path from 'node:path';
9
+ export function allocatePort(taken) {
10
+ for (let p = 19100; p <= 19999; p++) {
11
+ if (!taken.has(p))
12
+ return p;
13
+ }
14
+ throw new Error('no free port in [19100, 19999]');
15
+ }
16
+ export async function deployArtifact(args) {
17
+ const tag = `ultraapp/${args.slug}:${args.version}`;
18
+ const containerName = `ultraapp-${args.slug}-${args.version}`;
19
+ const basePath = `/forge/${args.slug}`;
20
+ const built = await args.dockerBuild({
21
+ tag,
22
+ cwd: args.worktreePath,
23
+ buildArgs: { BASE_PATH: basePath },
24
+ });
25
+ if (!built.ok)
26
+ return { ok: false, reason: `docker build failed: ${built.error ?? 'unknown'}` };
27
+ let hostPort;
28
+ try {
29
+ hostPort = allocatePort(args.takenPorts);
30
+ }
31
+ catch (e) {
32
+ return { ok: false, reason: e.message };
33
+ }
34
+ const run = await args.dockerRun({
35
+ image: tag,
36
+ name: containerName,
37
+ hostPort,
38
+ env: { BASE_PATH: basePath },
39
+ volumes: { [path.join(args.hostDataDir, 'data')]: '/data' },
40
+ });
41
+ if (!run.ok)
42
+ return { ok: false, reason: `docker run failed: ${run.error ?? 'unknown'}` };
43
+ args.router.register(args.slug, hostPort);
44
+ const ok = await waitForHealth({
45
+ url: `http://localhost:${args.router.port()}/forge/${args.slug}/health`,
46
+ fetchFn: args.fetchFn,
47
+ timeoutMs: args.healthTimeoutMs ?? 30000,
48
+ intervalMs: args.healthIntervalMs ?? 2000,
49
+ });
50
+ if (!ok) {
51
+ return {
52
+ ok: false,
53
+ reason: 'health check did not pass within timeout',
54
+ containerName,
55
+ port: hostPort,
56
+ imageTag: tag,
57
+ };
58
+ }
59
+ return {
60
+ ok: true,
61
+ url: `http://localhost:${args.router.port()}/forge/${args.slug}/`,
62
+ port: hostPort,
63
+ containerName,
64
+ imageTag: tag,
65
+ };
66
+ }
67
+ async function waitForHealth(opts) {
68
+ const deadline = Date.now() + opts.timeoutMs;
69
+ while (Date.now() < deadline) {
70
+ try {
71
+ const r = await opts.fetchFn(opts.url);
72
+ if (r.ok)
73
+ return true;
74
+ }
75
+ catch {
76
+ /* keep retrying */
77
+ }
78
+ await new Promise((r) => setTimeout(r, opts.intervalMs));
79
+ }
80
+ return false;
81
+ }
82
+ //# sourceMappingURL=deploy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploy.js","sourceRoot":"","sources":["../../../src/ultraapp/deploy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Minimal unified-diff applier — wraps the well-tested `diff` npm package
3
+ * (BSD-2). Used by the patcher for cosmetic post-deploy changes.
4
+ *
5
+ * Handles multi-file diffs, --- /dev/null create, +++ /dev/null delete,
6
+ * and reports per-file conflicts when context lines don't match.
7
+ */
8
+ export interface ApplyResult {
9
+ ok: boolean;
10
+ conflicts?: Array<{
11
+ file: string;
12
+ reason: string;
13
+ }>;
14
+ }
15
+ export declare function applyUnifiedDiff(diff: string, cwd: string): ApplyResult;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Minimal unified-diff applier — wraps the well-tested `diff` npm package
3
+ * (BSD-2). Used by the patcher for cosmetic post-deploy changes.
4
+ *
5
+ * Handles multi-file diffs, --- /dev/null create, +++ /dev/null delete,
6
+ * and reports per-file conflicts when context lines don't match.
7
+ */
8
+ import * as Diff from 'diff';
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ export function applyUnifiedDiff(diff, cwd) {
12
+ const patches = Diff.parsePatch(diff);
13
+ const conflicts = [];
14
+ for (const patch of patches) {
15
+ const oldFile = patch.oldFileName ?? '';
16
+ const newFile = patch.newFileName ?? '';
17
+ const isCreate = oldFile === '/dev/null';
18
+ const isDelete = newFile === '/dev/null';
19
+ const targetRel = (isDelete ? oldFile : newFile).replace(/^[ab]\//, '');
20
+ const target = path.join(cwd, targetRel);
21
+ if (isDelete) {
22
+ try {
23
+ fs.unlinkSync(target);
24
+ }
25
+ catch (e) {
26
+ conflicts.push({ file: targetRel, reason: e.message });
27
+ }
28
+ continue;
29
+ }
30
+ let original;
31
+ try {
32
+ original = isCreate ? '' : fs.readFileSync(target, 'utf8');
33
+ }
34
+ catch (e) {
35
+ conflicts.push({ file: targetRel, reason: `read failed: ${e.message}` });
36
+ continue;
37
+ }
38
+ const next = Diff.applyPatch(original, patch);
39
+ if (next === false) {
40
+ conflicts.push({ file: targetRel, reason: 'context mismatch' });
41
+ continue;
42
+ }
43
+ fs.mkdirSync(path.dirname(target), { recursive: true });
44
+ fs.writeFileSync(target, next);
45
+ }
46
+ return conflicts.length === 0 ? { ok: true } : { ok: false, conflicts };
47
+ }
48
+ //# sourceMappingURL=diff-apply.js.map