ucode-agent 1.39.0 → 1.41.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 CHANGED
@@ -27,7 +27,7 @@ corner:
27
27
  add a dark mode toggle that remembers the choice
28
28
 
29
29
 
30
- v1.36.0
30
+ v1.41.0
31
31
  ```
32
32
 
33
33
  A light crosses the wordmark once as it opens, and the three lines under the box
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucode-agent",
3
- "version": "1.39.0",
3
+ "version": "1.41.0",
4
4
  "description": "ucode - a terminal coding agent that reads, edits and runs your code, on NVIDIA and Cohere models.",
5
5
  "type": "module",
6
6
  "main": "ucode.js",
@@ -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
- ## 2b. Do not type what already exists
48
-
49
- `add_block` has the pieces every app needs, written for whichever starter this
50
- one uses: a list you can add to, tick off, rename and remove; a filter row; a
51
- localStorage store; a dialog; toasts; a theme toggle; a table; an empty state.
52
- Call it before writing any of those by hand. Each is a hundred lines you skip,
53
- and typing is the slowest part of a build — a page assembled from blocks is
54
- done minutes before the same page typed out. Call `add_block` with no name to
55
- see what fits this app.
56
-
57
- ## 3. Structure
58
-
59
- One component per file, named for what it is, not a 600-line `page.tsx`. In
60
- Next.js: `src/app` (routes, `globals.css`, `api/<name>/route.ts`),
61
- `src/components/<feature>/`, `src/lib/` for outside services and schemas.
62
- Server components by default, `"use client"` only where it is interactive.
63
- Types at every boundary; parse external data rather than trusting its shape.
64
-
65
- ## 4. Secrets and outside services
66
-
67
- - **A key never reaches the browser.** It lives in a server route. Anything
68
- imported by a `"use client"` file ships to every visitor, including a
69
- "hardcoded for now" key — put it in a server-only module and say where.
70
- - Every outbound call gets a timeout (`AbortSignal.timeout(60_000)`), a status
71
- check, and an error that says what failed — surfaced as a real message,
72
- never a silent `catch {}`.
73
- - Calling a model: ask for JSON and parse it defensively (extract the first
74
- `{...}`, validate, clamp numbers), put the judgement rules in the prompt
75
- explicitly, and make the route timeout longer than the model takes.
76
-
77
- ## 5. Build order
78
-
79
- Skeleton and design tokens first, so everything after is styled correctly the
80
- first time; then the server route with the real integration; then the core
81
- loop UI wired to it; then every state — empty, loading, success, error,
82
- invalid input; then polish: motion, responsive, copy, title and metadata.
83
-
84
- ## 6. Prove it works, then report
85
-
86
- `npm run build` type-checks and lints — a build that fails is not done. Start
87
- it (`npm run dev` backgrounds itself and returns the URL; do not start it
88
- twice), then `look_at_app` on every page. A clean build proves it compiles,
89
- not that it works. Fix what you find and check again.
90
-
91
- Done means: the core loop works end to end, no TODO, no placeholder copy, no
92
- dead buttons, no console errors, every async action has its states, secrets
93
- server-side, build passes. Then say what you built, how to run it, and — in
94
- one sentence — anything you did not finish or could not test.
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
@@ -103,6 +103,9 @@ const SILENT = new Set(['update_plan']);
103
103
  const MAX_FIX_ROUNDS = 3;
104
104
 
105
105
  /** Files worth checking after they change. */
106
+ /** A file with a page in it — something a browser can be pointed at. */
107
+ const PAGE = /\.(?:html?|tsx|jsx)$/i;
108
+
106
109
  const CHECKABLE = /\.(?:[cm]?[jt]sx?|py|html?)$/i;
107
110
 
108
111
  /** Where TypeScript keeps what it learned, so the next check is a quick one. */
@@ -631,6 +634,24 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
631
634
  '',
632
635
 
633
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.',
634
655
  'Before you guess at an API, ask: type_of gives the exact signature from the',
635
656
  'TypeScript this project has installed, and find_symbol says where something is declared without',
636
657
  'reading five files to find it. Rename with rename_symbol rather than edit_file — a',
@@ -1082,6 +1103,8 @@ export class Agent {
1082
1103
  // /undo can put the whole turn back.
1083
1104
  beginTurn();
1084
1105
  this.lookedThisTurn = false;
1106
+ this.reads = new Map();
1107
+ this.declines = 0;
1085
1108
  forgetReviews(); // a new request: its apps get a fresh design review
1086
1109
  const images = await this.attachImages(input);
1087
1110
  this.push(images.length
@@ -1641,7 +1664,14 @@ export class Agent {
1641
1664
  // changed so far; the automatic check at the end would only repeat it.
1642
1665
  if (call.name === 'run_command' && out.exitCode === 0 &&
1643
1666
  /(?:next build|npm run build|pnpm (?:run )?build|tsc)/.test(call.args?.command ?? '')) {
1667
+ // The pages stay. A build proves the project compiles, which is not the
1668
+ // same claim as the page working — and clearing everything here meant a
1669
+ // Next.js app, where a passing `npm run build` is part of every turn,
1670
+ // never reached the look at all. That is the one check that opens the
1671
+ // thing and presses its buttons, skipped on the whole framework.
1672
+ const pages = [...(this.sinceCheck ?? [])].filter((f) => PAGE.test(f));
1644
1673
  this.sinceCheck?.clear();
1674
+ for (const p of pages) this.sinceCheck?.add(p);
1645
1675
  }
1646
1676
  if (!QUIET.has(call.name)) this.ui.toolResult(out.summary);
1647
1677
  // The change as its two numbers, not as a copy of the file. The diff rows
@@ -1670,11 +1700,32 @@ export class Agent {
1670
1700
  // user made, and anything that actually failed, still shows.
1671
1701
  if (err instanceof Declined) this.ui.toolFailed('declined');
1672
1702
  else if (err.kind !== 'bad_args') this.ui.toolFailed(`${err.kind}: ${err.failed}`);
1703
+
1704
+ // Declines that keep coming are not decisions, they are a wall.
1705
+ //
1706
+ // A traced build ran 250 steps and 32 minutes before dying on the step
1707
+ // limit because every command it tried was declined — a piped session
1708
+ // cannot answer "go ahead? [y/N]", so the answer is always no. The model
1709
+ // read each refusal as being about that command, picked a different one,
1710
+ // and was refused again. Nothing ever told it the refusals were the room
1711
+ // rather than the request.
1712
+ //
1713
+ // Two are a person saying no to two things. The third is the wall, and it
1714
+ // is worth naming, because the way out is to finish without commands
1715
+ // rather than to keep hunting for one that is allowed.
1716
+ this.declines = err instanceof Declined ? (this.declines ?? 0) + 1 : 0;
1673
1717
  this.push({
1674
1718
  role: 'tool',
1675
1719
  toolCallId: call.id,
1676
1720
  name: call.name,
1677
- content: err.forModel() + (err instanceof Declined ? '' : this.stuckNote(call, { err })),
1721
+ content: err.forModel()
1722
+ + (err instanceof Declined ? '' : this.stuckNote(call, { err }))
1723
+ + (this.declines >= 3
1724
+ ? '\n\nThat is the third command declined in a row. They are not being refused one by one — '
1725
+ + 'nothing can be run in this session at all, and the next one will be declined too. '
1726
+ + 'Stop trying to run things: finish the work with the files you have, and say plainly '
1727
+ + 'in your reply which steps someone will need to run themselves.'
1728
+ : ''),
1678
1729
  });
1679
1730
  return err.kind === 'bad_args';
1680
1731
  }
@@ -1705,6 +1756,34 @@ export class Agent {
1705
1756
  });
1706
1757
  }
1707
1758
 
1759
+ // Reading the same unchanged file over and over.
1760
+ //
1761
+ // A traced build of a one-page app spent 53 of its 80 tool calls on
1762
+ // read_file, most of them the same handful of files again and again — six
1763
+ // seconds and a few thousand tokens each, for text that was already in the
1764
+ // conversation. The prompt has asked it not to since 1.27; asking is not
1765
+ // working, so the third read of a file nothing has written to since says
1766
+ // so instead of sending the file a third time.
1767
+ //
1768
+ // Two reads are left alone: the first is the work, and the second is
1769
+ // usually a fair re-check after an edit. Only the third is a loop. And it
1770
+ // is keyed on the file being untouched since — the moment anything writes
1771
+ // to it, the count starts again and a real re-read goes through.
1772
+ if (call.name === 'read_file' && call.args?.path) {
1773
+ const key = String(call.args.path);
1774
+ const seen = (this.reads ??= new Map()).get(key) ?? 0;
1775
+ if (seen >= 2 && !this.sinceCheck?.has(key)) {
1776
+ this.reads.set(key, seen + 1);
1777
+ return {
1778
+ content: `You have already read ${key} ${seen} times this turn and nothing has written to it since, ` +
1779
+ 'so its contents are unchanged and already above. Use what is there. If you need a part ' +
1780
+ 'you have lost, say which and read the files you still need together in one read_files.',
1781
+ summary: 'unchanged since you last read it',
1782
+ };
1783
+ }
1784
+ this.reads.set(key, seen + 1);
1785
+ }
1786
+
1708
1787
  if (call.name === 'load_skill') return this.loadSkill(call.args?.name);
1709
1788
  if (call.name === 'update_plan') return this.updatePlan(call.args?.items);
1710
1789
  if (call.name === 'delegate') return this.delegate(call.args?.tasks);
@@ -2036,7 +2115,7 @@ export class Agent {
2036
2115
  async lookOnceThisTurn(root, changed) {
2037
2116
  if (this.lookedThisTurn) return null;
2038
2117
 
2039
- const pages = [...changed].filter((f) => /\.(?:html?|tsx|jsx)$/i.test(f));
2118
+ const pages = [...changed].filter((f) => PAGE.test(f));
2040
2119
  if (!pages.length) return null;
2041
2120
  this.lookedThisTurn = true;
2042
2121
 
@@ -2056,7 +2135,12 @@ export class Agent {
2056
2135
  const html = pages.find((f) => /\.html?$/i.test(f));
2057
2136
  if (!html) return null;
2058
2137
  return await withStaticServer(path.dirname(path.resolve(root, html)), look);
2059
- } catch {
2138
+ } catch (err) {
2139
+ // Say so, quietly, rather than skipping in silence. A browser that will
2140
+ // not start is a reason to carry on without the look — but a check that
2141
+ // stops running and never mentions it is worse than one that was never
2142
+ // written, because everything downstream still believes it ran.
2143
+ this.ui.note(`could not open the app to check it: ${String(err?.failed ?? err?.message ?? err).split('\n')[0]}`);
2060
2144
  return null;
2061
2145
  }
2062
2146
  }
@@ -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