ucode-agent 1.40.0 → 1.41.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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/build-app/DIGEST.md +121 -94
- package/src/core/loop.js +67 -0
- package/templates/plain-html/TEMPLATE.md +15 -0
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,94 +1,121 @@
|
|
|
1
|
-
# Building something from nothing — the short form
|
|
2
|
-
|
|
3
|
-
The failure mode is not bad code. It is a folder of files that has never been
|
|
4
|
-
run, handed over as if it works.
|
|
5
|
-
|
|
6
|
-
## 1. Decide the shape before any file exists
|
|
7
|
-
|
|
8
|
-
One line each: **what it does**, **the core loop** (the one path that must work
|
|
9
|
-
perfectly), **the stack**, **the file list**.
|
|
10
|
-
|
|
11
|
-
| Need | Choose |
|
|
12
|
-
| --- | --- |
|
|
13
|
-
| One page, no secrets, no server | `create_app` with `plain-html` |
|
|
14
|
-
| Interactive client app, no secrets | `plain-html` still, unless it truly needs a build |
|
|
15
|
-
| Pages plus a server, secrets, API routes, SEO | `create_app` with `next-shadcn` |
|
|
16
|
-
| An API on its own | Node (Hono/Express) or Python (FastAPI) |
|
|
17
|
-
|
|
18
|
-
Pick the smallest one that does the job and mean it: a tasks app, a
|
|
19
|
-
calculator, a timer, a game, a visualisation — all one page. Next.js costs an
|
|
20
|
-
install and a build, minutes the user waits through, and buys nothing an app
|
|
21
|
-
with no server needs.
|
|
22
|
-
|
|
23
|
-
## 2. Start from the starter — and finish in the same call
|
|
24
|
-
|
|
25
|
-
`create_app` takes `files`, so for a one-page app the scaffold and the whole
|
|
26
|
-
app are one call:
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
create_app({ folder: "tide", name: "Tide", files: [
|
|
30
|
-
{ path: "tide/index.html", content: "…" },
|
|
31
|
-
{ path: "tide/styles.css", content: "…" },
|
|
32
|
-
{ path: "tide/app.js", content: "…" },
|
|
33
|
-
]})
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Every round trip is ten to forty seconds of the user's time, so one call
|
|
37
|
-
instead of four is most of how long the build takes.
|
|
38
|
-
|
|
39
|
-
- `plain-html` is the default: three files, no install, no build. Its files
|
|
40
|
-
come back in full inside the result — **never read them back**.
|
|
41
|
-
- `next-shadcn` installs in the background; commands in that folder wait for
|
|
42
|
-
it on their own, so start writing components at once. Re-tint `globals.css`
|
|
43
|
-
for the app's direction rather than shipping the slate default.
|
|
44
|
-
- Never run `create-next-app` or `shadcn init`. Nothing you run has a
|
|
45
|
-
keyboard: every scaffolder needs its answers as flags up front.
|
|
46
|
-
|
|
47
|
-
##
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
##
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
1
|
+
# Building something from nothing — the short form
|
|
2
|
+
|
|
3
|
+
The failure mode is not bad code. It is a folder of files that has never been
|
|
4
|
+
run, handed over as if it works.
|
|
5
|
+
|
|
6
|
+
## 1. Decide the shape before any file exists
|
|
7
|
+
|
|
8
|
+
One line each: **what it does**, **the core loop** (the one path that must work
|
|
9
|
+
perfectly), **the stack**, **the file list**.
|
|
10
|
+
|
|
11
|
+
| Need | Choose |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| One page, no secrets, no server | `create_app` with `plain-html` |
|
|
14
|
+
| Interactive client app, no secrets | `plain-html` still, unless it truly needs a build |
|
|
15
|
+
| Pages plus a server, secrets, API routes, SEO | `create_app` with `next-shadcn` |
|
|
16
|
+
| An API on its own | Node (Hono/Express) or Python (FastAPI) |
|
|
17
|
+
|
|
18
|
+
Pick the smallest one that does the job and mean it: a tasks app, a
|
|
19
|
+
calculator, a timer, a game, a visualisation — all one page. Next.js costs an
|
|
20
|
+
install and a build, minutes the user waits through, and buys nothing an app
|
|
21
|
+
with no server needs.
|
|
22
|
+
|
|
23
|
+
## 2. Start from the starter — and finish in the same call
|
|
24
|
+
|
|
25
|
+
`create_app` takes `files`, so for a one-page app the scaffold and the whole
|
|
26
|
+
app are one call:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
create_app({ folder: "tide", name: "Tide", files: [
|
|
30
|
+
{ path: "tide/index.html", content: "…" },
|
|
31
|
+
{ path: "tide/styles.css", content: "…" },
|
|
32
|
+
{ path: "tide/app.js", content: "…" },
|
|
33
|
+
]})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Every round trip is ten to forty seconds of the user's time, so one call
|
|
37
|
+
instead of four is most of how long the build takes.
|
|
38
|
+
|
|
39
|
+
- `plain-html` is the default: three files, no install, no build. Its files
|
|
40
|
+
come back in full inside the result — **never read them back**.
|
|
41
|
+
- `next-shadcn` installs in the background; commands in that folder wait for
|
|
42
|
+
it on their own, so start writing components at once. Re-tint `globals.css`
|
|
43
|
+
for the app's direction rather than shipping the slate default.
|
|
44
|
+
- Never run `create-next-app` or `shadcn init`. Nothing you run has a
|
|
45
|
+
keyboard: every scaffolder needs its answers as flags up front.
|
|
46
|
+
|
|
47
|
+
## 2a. Paths in `files` are not the paths in the page
|
|
48
|
+
|
|
49
|
+
The two are relative to different things, and getting them confused is the
|
|
50
|
+
commonest way a finished build comes up as bare markup. `files` paths are
|
|
51
|
+
relative to the **project root**, so they carry the app folder. A link inside a
|
|
52
|
+
page is relative to **that page**, so it must not.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
create_app({ folder: "tide", name: "Tide", files: [
|
|
56
|
+
{ path: "tide/index.html", content: "... <link rel=stylesheet href=\"styles.css\">
|
|
57
|
+
<script type=module src=\"app.js\"></script> ..." },
|
|
58
|
+
{ path: "tide/styles.css", content: "..." },
|
|
59
|
+
{ path: "tide/app.js", content: "..." },
|
|
60
|
+
]})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Wrong, and it 404s: `href="tide/styles.css"` inside `tide/index.html` — the
|
|
64
|
+
browser resolves that to `tide/tide/styles.css`, so no stylesheet and no script
|
|
65
|
+
load, and the page is unstyled markup with dead buttons.
|
|
66
|
+
|
|
67
|
+
## 2b. Look before you create
|
|
68
|
+
|
|
69
|
+
`create_app` refuses a folder that already has something in it unless you pass
|
|
70
|
+
the app in `files` — and a folder you half-made on an earlier attempt counts.
|
|
71
|
+
One `list_dir` in front of it costs a second and tells you whether you are
|
|
72
|
+
starting or resuming. The same goes for any command that makes a directory.
|
|
73
|
+
|
|
74
|
+
## 2c. Do not type what already exists
|
|
75
|
+
|
|
76
|
+
`add_block` has the pieces every app needs, written for whichever starter this
|
|
77
|
+
one uses: a list you can add to, tick off, rename and remove; a filter row; a
|
|
78
|
+
localStorage store; a dialog; toasts; a theme toggle; a table; an empty state.
|
|
79
|
+
Call it before writing any of those by hand. Each is a hundred lines you skip,
|
|
80
|
+
and typing is the slowest part of a build — a page assembled from blocks is
|
|
81
|
+
done minutes before the same page typed out. Call `add_block` with no name to
|
|
82
|
+
see what fits this app.
|
|
83
|
+
|
|
84
|
+
## 3. Structure
|
|
85
|
+
|
|
86
|
+
One component per file, named for what it is, not a 600-line `page.tsx`. In
|
|
87
|
+
Next.js: `src/app` (routes, `globals.css`, `api/<name>/route.ts`),
|
|
88
|
+
`src/components/<feature>/`, `src/lib/` for outside services and schemas.
|
|
89
|
+
Server components by default, `"use client"` only where it is interactive.
|
|
90
|
+
Types at every boundary; parse external data rather than trusting its shape.
|
|
91
|
+
|
|
92
|
+
## 4. Secrets and outside services
|
|
93
|
+
|
|
94
|
+
- **A key never reaches the browser.** It lives in a server route. Anything
|
|
95
|
+
imported by a `"use client"` file ships to every visitor, including a
|
|
96
|
+
"hardcoded for now" key — put it in a server-only module and say where.
|
|
97
|
+
- Every outbound call gets a timeout (`AbortSignal.timeout(60_000)`), a status
|
|
98
|
+
check, and an error that says what failed — surfaced as a real message,
|
|
99
|
+
never a silent `catch {}`.
|
|
100
|
+
- Calling a model: ask for JSON and parse it defensively (extract the first
|
|
101
|
+
`{...}`, validate, clamp numbers), put the judgement rules in the prompt
|
|
102
|
+
explicitly, and make the route timeout longer than the model takes.
|
|
103
|
+
|
|
104
|
+
## 5. Build order
|
|
105
|
+
|
|
106
|
+
Skeleton and design tokens first, so everything after is styled correctly the
|
|
107
|
+
first time; then the server route with the real integration; then the core
|
|
108
|
+
loop UI wired to it; then every state — empty, loading, success, error,
|
|
109
|
+
invalid input; then polish: motion, responsive, copy, title and metadata.
|
|
110
|
+
|
|
111
|
+
## 6. Prove it works, then report
|
|
112
|
+
|
|
113
|
+
`npm run build` type-checks and lints — a build that fails is not done. Start
|
|
114
|
+
it (`npm run dev` backgrounds itself and returns the URL; do not start it
|
|
115
|
+
twice), then `look_at_app` on every page. A clean build proves it compiles,
|
|
116
|
+
not that it works. Fix what you find and check again.
|
|
117
|
+
|
|
118
|
+
Done means: the core loop works end to end, no TODO, no placeholder copy, no
|
|
119
|
+
dead buttons, no console errors, every async action has its states, secrets
|
|
120
|
+
server-side, build passes. Then say what you built, how to run it, and — in
|
|
121
|
+
one sentence — anything you did not finish or could not test.
|
package/src/core/loop.js
CHANGED
|
@@ -634,6 +634,24 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
634
634
|
'',
|
|
635
635
|
|
|
636
636
|
'',
|
|
637
|
+
'',
|
|
638
|
+
'NEVER ASSUME A LIBRARY IS THERE. Not React, not lodash, not a component kit,',
|
|
639
|
+
'however well known it is. Check what this project already uses before you reach',
|
|
640
|
+
'for it: its package.json, the file next to the one you are writing, what the',
|
|
641
|
+
'starter actually shipped. The plain-html starter has no build step and no',
|
|
642
|
+
'packages at all, so JSX, TypeScript and bare module imports are not syntax it',
|
|
643
|
+
'can run — a JSX tag in a plain module is a page that renders nothing and one',
|
|
644
|
+
'unexpected-token error in the console. Write what the project can execute.',
|
|
645
|
+
'',
|
|
646
|
+
'CALL TOOLS TOGETHER WHEN THEY DO NOT DEPEND ON EACH OTHER. Several can go in',
|
|
647
|
+
'one reply and they run at the same time. Six files is one read_files in one',
|
|
648
|
+
'message, never six read_file calls in six — each of those is a round trip you',
|
|
649
|
+
'and the user sit through. Same for independent searches, same for commands',
|
|
650
|
+
'that do not feed each other.',
|
|
651
|
+
'',
|
|
652
|
+
'POINT AT CODE AS file:line. The filter is built in src/app.js:42 — not in the',
|
|
653
|
+
'app file somewhere near the filter. One of those the reader can open; the',
|
|
654
|
+
'other they have to go hunting from.',
|
|
637
655
|
'Before you guess at an API, ask: type_of gives the exact signature from the',
|
|
638
656
|
'TypeScript this project has installed, and find_symbol says where something is declared without',
|
|
639
657
|
'reading five files to find it. Rename with rename_symbol rather than edit_file — a',
|
|
@@ -1087,6 +1105,7 @@ export class Agent {
|
|
|
1087
1105
|
this.lookedThisTurn = false;
|
|
1088
1106
|
this.reads = new Map();
|
|
1089
1107
|
this.declines = 0;
|
|
1108
|
+
this.apps = [];
|
|
1090
1109
|
forgetReviews(); // a new request: its apps get a fresh design review
|
|
1091
1110
|
const images = await this.attachImages(input);
|
|
1092
1111
|
this.push(images.length
|
|
@@ -1664,6 +1683,13 @@ export class Agent {
|
|
|
1664
1683
|
// a designer's review, and now the app actually driven — and it was the
|
|
1665
1684
|
// quietest line on screen, saying only that it had happened. Its verdict
|
|
1666
1685
|
// goes on the same line, the way a change carries its two numbers.
|
|
1686
|
+
// Which app folders this turn actually made, so a second one can be
|
|
1687
|
+
// refused before it is built and any leftovers can be counted at the end.
|
|
1688
|
+
if (call.name === 'create_app' && call.args?.folder) {
|
|
1689
|
+
const made = path.resolve(this.cwd, String(call.args.folder));
|
|
1690
|
+
if (!(this.apps ??= []).includes(made)) this.apps.push(made);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1667
1693
|
if (call.name === 'look_at_app') {
|
|
1668
1694
|
const found = /^(\d+) problem/.exec(out.summary ?? '');
|
|
1669
1695
|
this.ui.runStat?.(found ? `${found[1]} to fix` : 'clean');
|
|
@@ -1766,6 +1792,30 @@ export class Agent {
|
|
|
1766
1792
|
this.reads.set(key, seen + 1);
|
|
1767
1793
|
}
|
|
1768
1794
|
|
|
1795
|
+
// Starting a second app instead of fixing the first.
|
|
1796
|
+
//
|
|
1797
|
+
// A traced build hit a problem in todo/, abandoned it and made todo-fixed/
|
|
1798
|
+
// — three create_app calls, two folders, one broken, the app typed twice.
|
|
1799
|
+
// Starting over is never the cheap way out of a problem in a file, and it
|
|
1800
|
+
// leaves the user to work out which folder is the real one.
|
|
1801
|
+
if (call.name === 'create_app' && call.args?.folder && (this.apps ?? []).length) {
|
|
1802
|
+
const wanted = path.resolve(this.cwd, String(call.args.folder));
|
|
1803
|
+
const already = this.apps.filter((f) => f !== wanted);
|
|
1804
|
+
if (already.length && !this.apps.includes(wanted)) {
|
|
1805
|
+
const show = already.map((f) => path.basename(f)).join(', ');
|
|
1806
|
+
throw new ToolFailure({
|
|
1807
|
+
kind: 'already_building',
|
|
1808
|
+
attempted: `creating ${call.args.folder}`,
|
|
1809
|
+
failed: `You already made ${show} this turn, and it is still there.`,
|
|
1810
|
+
fix:
|
|
1811
|
+
`Fix ${show} instead of starting again — whatever is wrong with it is a smaller `
|
|
1812
|
+
+ 'job than writing the whole app a second time, and a half-finished folder left '
|
|
1813
|
+
+ `beside the real one is worse than either. If ${show} genuinely cannot be saved, `
|
|
1814
|
+
+ 'delete it first, then create this one.',
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1769
1819
|
if (call.name === 'load_skill') return this.loadSkill(call.args?.name);
|
|
1770
1820
|
if (call.name === 'update_plan') return this.updatePlan(call.args?.items);
|
|
1771
1821
|
if (call.name === 'delegate') return this.delegate(call.args?.tasks);
|
|
@@ -2064,6 +2114,23 @@ export class Agent {
|
|
|
2064
2114
|
const live = await this.liveErrors();
|
|
2065
2115
|
if (live) problems.push(live);
|
|
2066
2116
|
|
|
2117
|
+
// Two app folders, one of them abandoned.
|
|
2118
|
+
//
|
|
2119
|
+
// A build hit a problem in todo/, started todo-fixed/, then said in its
|
|
2120
|
+
// reply that it had removed the duplicate — and had not. Both were still
|
|
2121
|
+
// on disk for the user to sort out, and the claim that they were not is
|
|
2122
|
+
// the failure this whole checking pass exists to catch: saying a thing is
|
|
2123
|
+
// done when it is not. So it is checked rather than believed.
|
|
2124
|
+
const apps = this.apps ?? [];
|
|
2125
|
+
if (apps.length > 1) {
|
|
2126
|
+
const names = apps.map((f) => path.basename(f));
|
|
2127
|
+
problems.push(
|
|
2128
|
+
`There are ${apps.length} app folders here now: ${names.join(', ')}. Only one of them is `
|
|
2129
|
+
+ 'the app. Delete the ones you are not shipping — actually delete them, do not just say '
|
|
2130
|
+
+ 'you have — and make sure the one you keep is the one that works.',
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2067
2134
|
// Then look at it, in the same pass that type-checks — not when the model
|
|
2068
2135
|
// remembers to. A check that runs only when it is asked for reports
|
|
2069
2136
|
// nothing on exactly the builds that needed it, and this is the only one
|
|
@@ -15,6 +15,21 @@ Open `index.html` in a browser, or serve the folder if the app fetches
|
|
|
15
15
|
anything: `python -m http.server 8000`. There is nothing to install and
|
|
16
16
|
nothing to compile, so a change is visible on refresh.
|
|
17
17
|
|
|
18
|
+
## What this starter cannot run
|
|
19
|
+
|
|
20
|
+
There is no build step, so there is no compiler to turn anything into browser
|
|
21
|
+
JavaScript. That rules out, in `app.js` or any module it loads:
|
|
22
|
+
|
|
23
|
+
- **JSX** — `render(<App />, root)` is a syntax error in a plain module. The
|
|
24
|
+
page renders nothing and the console says an unexpected token was found.
|
|
25
|
+
- **TypeScript** — no types, no `interface`, no `as`.
|
|
26
|
+
- **Bare imports** — `import React from "react"` has nowhere to resolve from.
|
|
27
|
+
Only relative paths (`./store.js`) and full URLs work.
|
|
28
|
+
|
|
29
|
+
Write plain ES modules and DOM calls. If the app genuinely needs a framework,
|
|
30
|
+
it needed `next-shadcn` instead, and that decision belongs before the first
|
|
31
|
+
file, not after the page comes up blank.
|
|
32
|
+
|
|
18
33
|
## Conventions worth keeping
|
|
19
34
|
|
|
20
35
|
- **One state object, one render.** Patching the DOM from several places is
|