@enderfga/claw-orchestrator 3.7.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/dist/bin/cli.js +30 -7
- package/dist/bin/cli.js.map +1 -1
- package/dist/src/autoloop/messages.d.ts +0 -2
- package/dist/src/autoloop/messages.js +0 -2
- package/dist/src/autoloop/messages.js.map +1 -1
- package/dist/src/autoloop/planner-tools.d.ts +3 -3
- package/dist/src/autoloop/planner-tools.js +3 -3
- package/dist/src/autoloop/runner.d.ts +1 -2
- package/dist/src/autoloop/runner.js +1 -2
- package/dist/src/autoloop/runner.js.map +1 -1
- package/dist/src/autoloop/types.d.ts +0 -2
- package/dist/src/autoloop/types.js +0 -2
- package/dist/src/autoloop/types.js.map +1 -1
- package/dist/src/council.js +1 -0
- package/dist/src/council.js.map +1 -1
- package/dist/src/dashboard/index.html +709 -8
- package/dist/src/embedded-server.js +172 -6
- package/dist/src/embedded-server.js.map +1 -1
- package/dist/src/index.js +222 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/session-manager.d.ts +82 -1
- package/dist/src/session-manager.js +262 -5
- package/dist/src/session-manager.js.map +1 -1
- package/dist/src/ultraapp/build-events.d.ts +46 -0
- package/dist/src/ultraapp/build-events.js +6 -0
- package/dist/src/ultraapp/build-events.js.map +1 -0
- package/dist/src/ultraapp/build.d.ts +39 -0
- package/dist/src/ultraapp/build.js +111 -0
- package/dist/src/ultraapp/build.js.map +1 -0
- package/dist/src/ultraapp/conventions.d.ts +8 -0
- package/dist/src/ultraapp/conventions.js +248 -0
- package/dist/src/ultraapp/conventions.js.map +1 -0
- package/dist/src/ultraapp/council-adapter.d.ts +49 -0
- package/dist/src/ultraapp/council-adapter.js +152 -0
- package/dist/src/ultraapp/council-adapter.js.map +1 -0
- package/dist/src/ultraapp/deploy.d.ts +45 -0
- package/dist/src/ultraapp/deploy.js +82 -0
- package/dist/src/ultraapp/deploy.js.map +1 -0
- package/dist/src/ultraapp/diff-apply.d.ts +15 -0
- package/dist/src/ultraapp/diff-apply.js +48 -0
- package/dist/src/ultraapp/diff-apply.js.map +1 -0
- package/dist/src/ultraapp/docker.d.ts +57 -0
- package/dist/src/ultraapp/docker.js +83 -0
- package/dist/src/ultraapp/docker.js.map +1 -0
- package/dist/src/ultraapp/feedback-classifier.d.ts +24 -0
- package/dist/src/ultraapp/feedback-classifier.js +85 -0
- package/dist/src/ultraapp/feedback-classifier.js.map +1 -0
- package/dist/src/ultraapp/files.d.ts +24 -0
- package/dist/src/ultraapp/files.js +79 -0
- package/dist/src/ultraapp/files.js.map +1 -0
- package/dist/src/ultraapp/fix-on-failure-session.d.ts +23 -0
- package/dist/src/ultraapp/fix-on-failure-session.js +48 -0
- package/dist/src/ultraapp/fix-on-failure-session.js.map +1 -0
- package/dist/src/ultraapp/fix-on-failure.d.ts +39 -0
- package/dist/src/ultraapp/fix-on-failure.js +69 -0
- package/dist/src/ultraapp/fix-on-failure.js.map +1 -0
- package/dist/src/ultraapp/host-strategy.d.ts +57 -0
- package/dist/src/ultraapp/host-strategy.js +205 -0
- package/dist/src/ultraapp/host-strategy.js.map +1 -0
- package/dist/src/ultraapp/interview-parser.d.ts +41 -0
- package/dist/src/ultraapp/interview-parser.js +83 -0
- package/dist/src/ultraapp/interview-parser.js.map +1 -0
- package/dist/src/ultraapp/interview-tools.d.ts +16 -0
- package/dist/src/ultraapp/interview-tools.js +36 -0
- package/dist/src/ultraapp/interview-tools.js.map +1 -0
- package/dist/src/ultraapp/json-patch.d.ts +6 -0
- package/dist/src/ultraapp/json-patch.js +78 -0
- package/dist/src/ultraapp/json-patch.js.map +1 -0
- package/dist/src/ultraapp/lifecycle.d.ts +38 -0
- package/dist/src/ultraapp/lifecycle.js +31 -0
- package/dist/src/ultraapp/lifecycle.js.map +1 -0
- package/dist/src/ultraapp/manager.d.ts +141 -0
- package/dist/src/ultraapp/manager.js +661 -0
- package/dist/src/ultraapp/manager.js.map +1 -0
- package/dist/src/ultraapp/narrator-prompt.d.ts +10 -0
- package/dist/src/ultraapp/narrator-prompt.js +54 -0
- package/dist/src/ultraapp/narrator-prompt.js.map +1 -0
- package/dist/src/ultraapp/narrator.d.ts +49 -0
- package/dist/src/ultraapp/narrator.js +83 -0
- package/dist/src/ultraapp/narrator.js.map +1 -0
- package/dist/src/ultraapp/patcher.d.ts +35 -0
- package/dist/src/ultraapp/patcher.js +159 -0
- package/dist/src/ultraapp/patcher.js.map +1 -0
- package/dist/src/ultraapp/router.d.ts +39 -0
- package/dist/src/ultraapp/router.js +139 -0
- package/dist/src/ultraapp/router.js.map +1 -0
- package/dist/src/ultraapp/spec-delta.d.ts +18 -0
- package/dist/src/ultraapp/spec-delta.js +35 -0
- package/dist/src/ultraapp/spec-delta.js.map +1 -0
- package/dist/src/ultraapp/spec.d.ts +82 -0
- package/dist/src/ultraapp/spec.js +125 -0
- package/dist/src/ultraapp/spec.js.map +1 -0
- package/dist/src/ultraapp/store.d.ts +67 -0
- package/dist/src/ultraapp/store.js +177 -0
- package/dist/src/ultraapp/store.js.map +1 -0
- package/dist/src/ultraapp/versions.d.ts +50 -0
- package/dist/src/ultraapp/versions.js +65 -0
- package/dist/src/ultraapp/versions.js.map +1 -0
- package/openclaw.plugin.json +15 -1
- package/package.json +3 -1
- package/skills/SKILL.md +2 -2
- package/skills/references/autoloop.md +1 -1
- package/skills/references/mcp.md +2 -2
- package/skills/references/tools.md +183 -0
- package/skills/references/ultraapp.md +203 -0
- 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
|